返回 DeepSeek-Reasonix
catalog.go
根目录 / internal / extension / catalog.go
1 package extension
2
3 import (
4 "fmt"
5 "sort"
6 )
7
8 // ContributionKind names one category of runtime capability. The wire names
9 // are stable for the same reason as Scope's.
10 type ContributionKind string
11
12 const (
13 KindTool ContributionKind = "tool"
14 KindSkill ContributionKind = "skill"
15 KindCommand ContributionKind = "command"
16 KindPrompt ContributionKind = "prompt"
17 KindHook ContributionKind = "hook"
18 KindMCPServer ContributionKind = "mcp_server"
19 KindProvider ContributionKind = "provider"
20 KindTheme ContributionKind = "theme"
21 KindUIAction ContributionKind = "ui_action"
22 KindInterceptor ContributionKind = "interceptor"
23 KindStrategy ContributionKind = "strategy"
24 )
25
26 // knownKind reports whether k is a declared kind; the builder rejects unknown
27 // kinds so a misspelled manifest cannot silently drop a capability.
28 func knownKind(k ContributionKind) bool {
29 switch k {
30 case KindTool, KindSkill, KindCommand, KindPrompt, KindHook, KindMCPServer,
31 KindProvider, KindTheme, KindUIAction, KindInterceptor, KindStrategy:
32 return true
33 default:
34 return false
35 }
36 }
37
38 // additiveKind reports whether every contribution of this kind survives
39 // resolution. Hooks and interceptors accumulate across sources by design —
40 // shadowing them would let one package silently disable another package's
41 // safety hook, which is the opposite of what hooks are for.
42 func additiveKind(k ContributionKind) bool {
43 return k == KindHook || k == KindInterceptor
44 }
45
46 // Contribution is one capability offered to the kernel.
47 type Contribution struct {
48 // Kind is the capability category.
49 Kind ContributionKind
50 // ID is the canonical identifier within the kind: the tool name, the
51 // command's full (package-qualified) name, the skill's slash-or-bare name,
52 // a hook's "event#n" sequence, the MCP server name, the provider ref
53 // "provider/model", "plugin:<plugin>:<theme>" for themes, and so on.
54 // Winner rules key on (Kind, ID).
55 ID string
56 // Source is the provenance; its Scope drives shadowing.
57 Source ContributionSource
58 // Priority orders interceptors (see SortInterceptors) and breaks catalog
59 // ties within one tier for other kinds. Interceptor priorities must pass
60 // ValidatePriority.
61 Priority int
62 // Payload is the existing concrete value — skill.Skill, command.Command,
63 // plugin.Spec, provider.Descriptor, tool.Tool, ... — deliberately not
64 // re-wrapped so downstream consumers keep working with the types they
65 // already know.
66 Payload any
67 // Order is the per-contributor registration sequence: the index of this
68 // contribution inside its contributor's returned slice, stamped by the
69 // Builder. It makes otherwise-identical contributions from one source
70 // deterministic (first registration wins) without depending on map
71 // iteration or discovery order.
72 Order int
73 }
74
75 // Catalog is the collected set of contributions. It is appended to during
76 // discovery and frozen into the snapshot at Freeze; adding to a frozen
77 // catalog panics, because mutating a published snapshot's contents would
78 // invalidate its CacheHash and every consumer's assumption of stability.
79 type Catalog struct {
80 contribs []Contribution
81 frozen bool
82 }
83
84 // NewCatalog returns an empty, mutable catalog.
85 func NewCatalog() *Catalog { return &Catalog{} }
86
87 // Add appends contributions. It panics on a frozen catalog: freeze marks the
88 // point where the effective set became observable, and growth past that point
89 // is a wiring bug, not data.
90 func (c *Catalog) Add(contribs ...Contribution) {
91 if c.frozen {
92 panic("extension: Add on frozen catalog")
93 }
94 c.contribs = append(c.contribs, contribs...)
95 }
96
97 // Frozen reports whether the catalog has been sealed into a snapshot.
98 func (c *Catalog) Frozen() bool { return c.frozen }
99
100 // freeze seals the catalog. Only the Builder calls it.
101 func (c *Catalog) freeze() { c.frozen = true }
102
103 // Len returns the number of contributions.
104 func (c *Catalog) Len() int { return len(c.contribs) }
105
106 // All returns every contribution in deterministic order. The returned slice
107 // is a copy; mutating it cannot affect the catalog.
108 func (c *Catalog) All() []Contribution {
109 out := make([]Contribution, len(c.contribs))
110 copy(out, c.contribs)
111 SortContributions(out)
112 return out
113 }
114
115 // ByKind returns the contributions of one kind in deterministic order (a
116 // copy, like All).
117 func (c *Catalog) ByKind(kind ContributionKind) []Contribution {
118 out := make([]Contribution, 0, len(c.contribs))
119 for _, ct := range c.contribs {
120 if ct.Kind == kind {
121 out = append(out, ct)
122 }
123 }
124 SortContributions(out)
125 return out
126 }
127
128 // Get returns the contributions matching (kind, id) in deterministic order.
129 // Additive kinds can legitimately return several entries.
130 func (c *Catalog) Get(kind ContributionKind, id string) []Contribution {
131 out := make([]Contribution, 0, 1)
132 for _, ct := range c.contribs {
133 if ct.Kind == kind && ct.ID == id {
134 out = append(out, ct)
135 }
136 }
137 SortContributions(out)
138 return out
139 }
140
141 // SortContributions orders contributions deterministically for display and
142 // assembly: kind first so categories stay together, then shadowing tier
143 // (highest first, so the effective winner reads first), priority (higher
144 // first), then the identity fields. The final Source tiebreaks make the order
145 // total — without them, identical (Kind, ID) pairs from one contributor would
146 // inherit input order, which depends on contributor registration order.
147 func SortContributions(cs []Contribution) {
148 sort.SliceStable(cs, func(i, j int) bool {
149 a, b := cs[i], cs[j]
150 if a.Kind != b.Kind {
151 return a.Kind < b.Kind
152 }
153 if ra, rb := tierRank(a.Source.Scope), tierRank(b.Source.Scope); ra != rb {
154 return ra > rb
155 }
156 if a.Priority != b.Priority {
157 return a.Priority > b.Priority
158 }
159 if a.Source.PluginID != b.Source.PluginID {
160 return a.Source.PluginID < b.Source.PluginID
161 }
162 if a.ID != b.ID {
163 return a.ID < b.ID
164 }
165 if a.Order != b.Order {
166 return a.Order < b.Order
167 }
168 if a.Source.Origin != b.Source.Origin {
169 return a.Source.Origin < b.Source.Origin
170 }
171 return a.Source.Path < b.Source.Path
172 })
173 }
174
175 // ConflictError reports same-tier duplicates of one canonical ID from
176 // different sources for a shadowed kind. The kernel makes these hard failures
177 // because today's alternative — a silent last-writer-wins — lets an installed
178 // package override another package's capability with no trace.
179 type ConflictError struct {
180 Kind ContributionKind
181 ID string
182 Sources []ContributionSource
183 }
184
185 func (e *ConflictError) Error() string {
186 labels := make([]string, 0, len(e.Sources))
187 for _, s := range e.Sources {
188 labels = append(labels, s.label())
189 }
190 return fmt.Sprintf("extension: conflicting %s %q claimed by %v", e.Kind, e.ID, labels)
191 }
192
193 // Conflicts reports every same-tier multi-source duplicate of a canonical ID
194 // among shadowed (non-additive) kinds. Hooks and interceptors are additive
195 // and never appear here. The result is deterministically ordered.
196 func (c *Catalog) Conflicts() []ConflictError {
197 groups := map[ContributionKind]map[string][]Contribution{}
198 for _, ct := range c.contribs {
199 if additiveKind(ct.Kind) {
200 continue
201 }
202 byID := groups[ct.Kind]
203 if byID == nil {
204 byID = map[string][]Contribution{}
205 groups[ct.Kind] = byID
206 }
207 byID[ct.ID] = append(byID[ct.ID], ct)
208 }
209 var out []ConflictError
210 for kind, byID := range groups {
211 for id, cs := range byID {
212 sources, ok := conflictingSources(cs)
213 if !ok {
214 continue
215 }
216 out = append(out, ConflictError{Kind: kind, ID: id, Sources: sources})
217 }
218 }
219 sort.Slice(out, func(i, j int) bool {
220 if out[i].Kind != out[j].Kind {
221 return out[i].Kind < out[j].Kind
222 }
223 return out[i].ID < out[j].ID
224 })
225 return out
226 }
227
228 // conflictingSources returns the distinct sources tied at the highest tier
229 // among cs, when more than one source sits there. Sources are deduped by
230 // their identity key and returned in deterministic order.
231 func conflictingSources(cs []Contribution) ([]ContributionSource, bool) {
232 best := -1
233 for _, ct := range cs {
234 if r := tierRank(ct.Source.Scope); r > best {
235 best = r
236 }
237 }
238 seen := map[string]bool{}
239 var sources []ContributionSource
240 for _, ct := range cs {
241 if tierRank(ct.Source.Scope) != best {
242 continue
243 }
244 key := ct.Source.key()
245 if seen[key] {
246 continue
247 }
248 seen[key] = true
249 sources = append(sources, ct.Source)
250 }
251 sort.Slice(sources, func(i, j int) bool { return sources[i].key() < sources[j].key() })
252 return sources, len(sources) > 1
253 }
254
254 lines GO