| 1 | package extension |
| 2 | |
| 3 | // Scope records which tier contributed a capability. The wire names are |
| 4 | // stable: they appear in manifests, diagnostics, and conflict reports, so a |
| 5 | // rename is a protocol break. |
| 6 | type Scope string |
| 7 | |
| 8 | const ( |
| 9 | // ScopeBuiltin is compiled into the binary (built-in tools, shipped |
| 10 | // skills). It is the lowest tier: anything user-provided shadows it. |
| 11 | ScopeBuiltin Scope = "builtin" |
| 12 | // ScopeGlobal is user-level configuration under the Reasonix home dir. |
| 13 | ScopeGlobal Scope = "global" |
| 14 | // ScopeProject is configuration rooted at the current project. It is the |
| 15 | // highest tier, matching the existing skill and command discovery where |
| 16 | // project roots always win. |
| 17 | ScopeProject Scope = "project" |
| 18 | // ScopePlugin is contributed by an installed plugin package. Plugins sit |
| 19 | // below project config but above global user config: a project may |
| 20 | // override a plugin, a plugin may override a user default. |
| 21 | ScopePlugin Scope = "plugin" |
| 22 | // ScopeClaudeCompat marks contributions imported from Claude conventions |
| 23 | // (.claude dirs, Claude-style settings). Compat tiers rank just below |
| 24 | // their native project counterpart so imported config never silently |
| 25 | // overrides native Reasonix config. |
| 26 | ScopeClaudeCompat Scope = "claude_compat" |
| 27 | // ScopeCodexCompat marks contributions imported from Codex conventions. |
| 28 | ScopeCodexCompat Scope = "codex_compat" |
| 29 | ) |
| 30 | |
| 31 | // tierRank orders the scopes for shadowing: a higher rank wins the same |
| 32 | // canonical ID. Compat scopes sit between project and plugin because imported |
| 33 | // project-adjacent config (.claude under a project root) is closer to project |
| 34 | // intent than to an installed package, but must never outrank native config. |
| 35 | // Unknown scopes rank below builtin so they lose every shadow race instead of |
| 36 | // accidentally winning one. |
| 37 | func tierRank(s Scope) int { |
| 38 | switch s { |
| 39 | case ScopeProject: |
| 40 | return 60 |
| 41 | case ScopeClaudeCompat: |
| 42 | return 50 |
| 43 | case ScopeCodexCompat: |
| 44 | return 40 |
| 45 | case ScopePlugin: |
| 46 | return 30 |
| 47 | case ScopeGlobal: |
| 48 | return 20 |
| 49 | case ScopeBuiltin: |
| 50 | return 10 |
| 51 | default: |
| 52 | return 0 |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // knownScope reports whether s is one of the declared wire values. The builder |
| 57 | // rejects unknown scopes at validation time; silently accepting a typo'd scope |
| 58 | // would rank it 0 and change shadowing outcomes invisibly. |
| 59 | func knownScope(s Scope) bool { |
| 60 | switch s { |
| 61 | case ScopeBuiltin, ScopeGlobal, ScopeProject, ScopePlugin, ScopeClaudeCompat, ScopeCodexCompat: |
| 62 | return true |
| 63 | default: |
| 64 | return false |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // ContributionSource is the provenance of one contribution. It answers "who |
| 69 | // put this here" for diagnostics and conflict reports, and its Scope drives |
| 70 | // shadow resolution. |
| 71 | type ContributionSource struct { |
| 72 | // PluginID is the installed plugin package name when the contribution came |
| 73 | // from one; empty for non-plugin sources. |
| 74 | PluginID string |
| 75 | // Version is the contributor's version (plugin package version, contract |
| 76 | // version). Optional; empty means unspecified. |
| 77 | Version string |
| 78 | // Origin is a short stable label for the discovery path: "builtin", |
| 79 | // "user", "project", "plugin", a manifest path, and so on. |
| 80 | Origin string |
| 81 | // Scope is the shadowing tier. |
| 82 | Scope Scope |
| 83 | // Priority is provenance-level priority metadata carried for display and |
| 84 | // tie-breaking. Per-contribution priority lives on Contribution.Priority; |
| 85 | // the two are deliberately separate so a source descriptor can be shared |
| 86 | // by contributions with different priorities. |
| 87 | Priority int |
| 88 | // Path is the file or directory the contribution was loaded from, when one |
| 89 | // exists. Diagnostic only; it never influences winner rules. |
| 90 | Path string |
| 91 | } |
| 92 | |
| 93 | // key identifies a source for conflict detection. Two contributions share a |
| 94 | // source only when scope, plugin, and origin all agree — path is excluded so |
| 95 | // several files discovered by the same root count as one source (mirroring |
| 96 | // today's first-root-wins behavior inside a single discovery pass). |
| 97 | func (s ContributionSource) key() string { |
| 98 | return string(s.Scope) + "|" + s.PluginID + "|" + s.Origin |
| 99 | } |
| 100 | |
| 101 | // label renders the source for error messages, preferring the plugin identity |
| 102 | // because cross-plugin conflicts are the ones users must act on. |
| 103 | func (s ContributionSource) label() string { |
| 104 | if s.PluginID != "" { |
| 105 | return "plugin " + s.PluginID |
| 106 | } |
| 107 | if s.Origin != "" { |
| 108 | return string(s.Scope) + " (" + s.Origin + ")" |
| 109 | } |
| 110 | return string(s.Scope) |
| 111 | } |
| 112 |