@skill-map/spec 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +680 -1
  2. package/README.md +6 -6
  3. package/architecture.md +244 -41
  4. package/cli-contract.md +48 -20
  5. package/conformance/README.md +2 -2
  6. package/conformance/cases/kernel-empty-boot.json +2 -2
  7. package/conformance/cases/orphan-markdown-fallback.json +22 -0
  8. package/conformance/cases/plugin-missing-ui-rejected.json +2 -1
  9. package/conformance/cases/sidecar-end-to-end.json +3 -4
  10. package/conformance/coverage.md +8 -6
  11. package/conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md +6 -0
  12. package/conformance/fixtures/orphan-markdown/ARCHITECTURE.md +10 -0
  13. package/conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm +2 -2
  14. package/conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.sm +1 -1
  15. package/conformance/fixtures/sidecar-example/agent-example.md +1 -1
  16. package/conformance/fixtures/sidecar-example/agent-example.sm +1 -1
  17. package/db-schema.md +68 -23
  18. package/index.json +47 -42
  19. package/interfaces/security-scanner.md +2 -2
  20. package/job-events.md +12 -12
  21. package/job-lifecycle.md +1 -1
  22. package/package.json +1 -1
  23. package/plugin-author-guide.md +374 -69
  24. package/plugin-kv-api.md +5 -5
  25. package/prompt-preamble.md +1 -1
  26. package/schemas/annotations.schema.json +5 -9
  27. package/schemas/api/rest-envelope.schema.json +55 -11
  28. package/schemas/conformance-case.schema.json +2 -2
  29. package/schemas/extensions/analyzer.schema.json +43 -0
  30. package/schemas/extensions/base.schema.json +14 -4
  31. package/schemas/extensions/extractor.schema.json +3 -10
  32. package/schemas/extensions/hook.schema.json +6 -4
  33. package/schemas/extensions/provider.schema.json +1 -1
  34. package/schemas/frontmatter/base.schema.json +6 -1
  35. package/schemas/input-types.schema.json +260 -0
  36. package/schemas/issue.schema.json +6 -6
  37. package/schemas/link.schema.json +2 -2
  38. package/schemas/node.schema.json +1 -19
  39. package/schemas/plugins-registry.schema.json +14 -2
  40. package/schemas/project-config.schema.json +25 -0
  41. package/schemas/sidecar.schema.json +6 -6
  42. package/schemas/summaries/agent.schema.json +1 -1
  43. package/schemas/summaries/command.schema.json +1 -1
  44. package/schemas/summaries/hook.schema.json +1 -1
  45. package/schemas/summaries/markdown.schema.json +1 -1
  46. package/schemas/view-slots.schema.json +335 -0
  47. package/schemas/extensions/rule.schema.json +0 -43
package/plugin-kv-api.md CHANGED
@@ -102,7 +102,7 @@ Errors MUST NOT leak backend-specific details (SQL strings, file paths) to plugi
102
102
 
103
103
  ## Mode B: dedicated tables
104
104
 
105
- Mode B is governed by [`db-schema.md`](./db-schema.md) (catalog rules + triple protection). This section restates the API surface.
105
+ Mode B is governed by [`db-schema.md`](./db-schema.md) (catalog analyzers + triple protection). This section restates the API surface.
106
106
 
107
107
  ### Declaration
108
108
 
@@ -147,7 +147,7 @@ Mode B plugins MAY call `db.transaction(async (tx) => { ... })`. The kernel prov
147
147
  - Index and constraint prefixes are similarly injected.
148
148
  - A failing plugin migration disables only that plugin (`status: load-error`); other plugins and the kernel continue.
149
149
 
150
- See [`db-schema.md`](./db-schema.md) for the normative migration rules.
150
+ See [`db-schema.md`](./db-schema.md) for the normative migration analyzers.
151
151
 
152
152
  ---
153
153
 
@@ -172,7 +172,7 @@ A plugin MUST declare **exactly one** storage mode. Mixing modes in the same plu
172
172
 
173
173
  ---
174
174
 
175
- ## Visibility rules
175
+ ## Visibility analyzers
176
176
 
177
177
  - A plugin MUST NOT read or write rows outside its scope. Mode A: the accessor is scoped. Mode B: the validator enforces the prefix.
178
178
  - The kernel MAY expose read-only introspection for diagnostics (e.g., `sm plugins show <id> --storage` lists key counts). This is authoritative, not a plugin-level API.
@@ -203,8 +203,8 @@ Post-v1.0 work: signed manifest, sandboxed worker-thread isolation, per-plugin D
203
203
 
204
204
  ## See also
205
205
 
206
- - [`db-schema.md`](./db-schema.md) — table catalog, migration rules, triple protection for mode B.
207
- - [`architecture.md`](./architecture.md) — extension contract rules and `ctx.store` injection via the kernel.
206
+ - [`db-schema.md`](./db-schema.md) — table catalog, migration analyzers, triple protection for mode B.
207
+ - [`architecture.md`](./architecture.md) — extension contract analyzers and `ctx.store` injection via the kernel.
208
208
 
209
209
  ---
210
210
 
@@ -143,7 +143,7 @@ This preamble is a **mitigation**, not a guarantee. A determined attacker can st
143
143
  2. It gives the model a structured place to report suspected injections, so consumers can act (flag the node, re-run with a different model, refuse to summarize).
144
144
  3. It makes injection attempts visible (via the `safety` field in reports) so that deterministic rules can surface patterns over the graph.
145
145
 
146
- Defense-in-depth: the deterministic rule `injection-pattern` (shipped as a built-in rule in the default plugin pack) scans node bodies for known injection patterns independently of the LLM. Neither layer is sufficient alone.
146
+ Defense-in-depth: the deterministic analyzer `injection-pattern` (shipped as a built-in analyzer in the default plugin pack) scans node bodies for known injection patterns independently of the LLM. Neither layer is sufficient alone.
147
147
 
148
148
  ---
149
149
 
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/annotations.schema.json",
4
4
  "title": "Annotations",
5
- "description": "Catalog of conventional annotation fields skill-map ships out of the box, written into the `annotations:` block of a sidecar (`<basename>.sm`). Every field is OPTIONAL — a sidecar with an empty `annotations: {}` is valid. Schema is `additionalProperties: true` so users / plugins can add custom keys without coordination; the built-in `unknown-field` rule emits a warning on unrecognized keys (typo guard). The curated catalog is the load-bearing 14 fields below — versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`, `requires`, `conflictsWith`, `related`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), display (`hidden`), docs (`docsUrl`). The activity timestamp lives in the reserved `audit:` block (`audit.lastBumpedAt`), not in `annotations:`. Plugins that want first-class custom keys with their own validation declare `annotationContributions` in their manifest (see Step 9.6.6).",
5
+ "description": "Catalog of conventional annotation fields skill-map ships out of the box, written into the `annotations:` block of a sidecar (`<basename>.sm`). Every field is OPTIONAL — a sidecar with an empty `annotations: {}` is valid. Schema is `additionalProperties: true` so users / plugins can add custom keys without coordination; the built-in `unknown-field` analyzer emits a warning on unrecognized keys (typo guard). The curated catalog is the load-bearing 13 fields below — versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`, `requires`, `conflictsWith`, `related`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), docs (`docsUrl`). The activity timestamp lives in the reserved `audit:` block (`audit.lastBumpedAt`), not in `annotations:`. Plugins that want first-class custom keys with their own validation declare `annotationContributions` in their manifest (see Step 9.6.6).",
6
6
  "type": "object",
7
7
  "additionalProperties": true,
8
8
  "properties": {
@@ -19,7 +19,7 @@
19
19
  "supersedes": {
20
20
  "type": "array",
21
21
  "items": { "type": "string", "minLength": 1 },
22
- "description": "Paths (relative to scope root) of nodes this node replaces. Consumed by the built-in `superseded` rule and surfaces in `sm list --superseded`."
22
+ "description": "Paths (relative to scope root) of nodes this node replaces. Consumed by the built-in `superseded` analyzer and surfaces in `sm list --superseded`."
23
23
  },
24
24
  "supersededBy": {
25
25
  "type": "string",
@@ -29,7 +29,7 @@
29
29
  "requires": {
30
30
  "type": "array",
31
31
  "items": { "type": "string", "minLength": 1 },
32
- "description": "Paths (relative to scope root) of nodes this node depends on. Surfaces in the dependency graph; the `broken-ref` rule flags missing targets."
32
+ "description": "Paths (relative to scope root) of nodes this node depends on. Surfaces in the dependency graph; the `broken-ref` analyzer flags missing targets."
33
33
  },
34
34
  "conflictsWith": {
35
35
  "type": "array",
@@ -39,7 +39,7 @@
39
39
  "related": {
40
40
  "type": "array",
41
41
  "items": { "type": "string", "minLength": 1 },
42
- "description": "Paths (relative to scope root) of conceptually related nodes. Soft link for navigation; no strong semantics, no rule enforcement."
42
+ "description": "Paths (relative to scope root) of conceptually related nodes. Soft link for navigation; no strong semantics, no analyzer enforcement."
43
43
  },
44
44
  "authors": {
45
45
  "type": "array",
@@ -64,11 +64,7 @@
64
64
  "tags": {
65
65
  "type": "array",
66
66
  "items": { "type": "string", "minLength": 1 },
67
- "description": "Free-form taxonomy tags. Consumed by `sm list --tag <name>` and UI faceted search."
68
- },
69
- "hidden": {
70
- "type": "boolean",
71
- "description": "When true, the node is excluded from default listings (still appears under `--all` / `--include-hidden`). Useful for in-progress nodes the author does not want surfaced yet."
67
+ "description": "**User-supplied** taxonomy tags. Skill-map's tag system is dual-source: this field carries the user's post-hoc tags (curator's view of the node from the `.sm` sidecar); `frontmatter.tags` carries the author's tags (intrinsic categories written in the `.md`). Both surfaces are first-class — `sm list --tag <name>` and UI faceted search match the union and the UI distinguishes them visually (`sm list --tag-source author|user` filters one source). Empty array and missing field are equivalent."
72
68
  },
73
69
  "docsUrl": {
74
70
  "type": "string",
@@ -24,7 +24,8 @@
24
24
  "health",
25
25
  "scan",
26
26
  "sidecar.bumped",
27
- "annotations.registered"
27
+ "annotations.registered",
28
+ "contributions.registered"
28
29
  ],
29
30
  "description": "Discriminator. List kinds (`nodes`, `links`, `issues`, `plugins`) carry `items` + `filters` + `counts` + `kindRegistry`. The `node` kind carries `item` + `kindRegistry`. The `config` kind carries `value` + `kindRegistry`. The `sidecar.bumped` kind (Step 9.6.5, BFF half) carries `value` + `elapsedMs` (no `filters` / `counts` / `kindRegistry` — it's an action-result projection orthogonal to the kindRegistry surface). The `annotations.registered` kind (Step 9.6.6, BFF half) carries `items` + `counts.total` (no `filters` / `kindRegistry` — pure read-only catalog projection). The `health` / `scan` / `graph` values are reserved for documentation parity with the routes that DON'T use this envelope."
30
31
  },
@@ -103,6 +104,26 @@
103
104
  }
104
105
  }
105
106
  },
107
+ "contributionsRegistry": {
108
+ "type": "object",
109
+ "description": "Catalog of registered view contributions active in the current scope, keyed by qualified contribution id `<pluginId>/<extensionId>/<contributionId>`. Built once per server boot from every enabled extension's `viewContributions` map and embedded into every payload-bearing envelope so the UI can fetch the catalog without a separate request. Sentinel envelopes (`health`, `scan`, `graph`), action-result envelopes (`sidecar.bumped`), and the catalog envelopes themselves (`annotations.registered`, `contributions.registered`) are exempt. Mirror of `kindRegistry`, parallel surface. Each entry references a slot by name from the closed catalog at `view-slots.schema.json#/$defs/SlotName`; the slot fixes both the renderer and the payload shape. Per-node payloads are delivered separately on `node` envelopes (single via `item.contributions`) or list envelopes (`items[].contributions`, only when `limit ≤ bff.maxBulkContributions`, default 200).",
110
+ "additionalProperties": {
111
+ "type": "object",
112
+ "required": ["pluginId", "extensionId", "contributionId", "slot"],
113
+ "additionalProperties": false,
114
+ "properties": {
115
+ "pluginId": { "type": "string", "minLength": 1 },
116
+ "extensionId": { "type": "string", "minLength": 1 },
117
+ "contributionId": { "type": "string", "minLength": 1 },
118
+ "slot": { "$ref": "../view-slots.schema.json#/$defs/SlotName" },
119
+ "label": { "type": "string", "maxLength": 64 },
120
+ "tooltip": { "type": "string", "maxLength": 256 },
121
+ "icon": { "type": "string", "maxLength": 64 },
122
+ "emptyText": { "type": "string", "maxLength": 128 },
123
+ "emitWhenEmpty": { "type": "boolean", "default": false }
124
+ }
125
+ }
126
+ },
106
127
  "counts": {
107
128
  "type": "object",
108
129
  "required": ["total"],
@@ -142,8 +163,8 @@
142
163
  },
143
164
  "oneOf": [
144
165
  {
145
- "description": "List envelope — `items` payload + `filters` + `counts` (with `returned`) + `kindRegistry`. Used by `/api/nodes`, `/api/links`, `/api/issues`, `/api/plugins`.",
146
- "required": ["items", "counts", "filters", "kindRegistry"],
166
+ "description": "List envelope — `items` payload + `filters` + `counts` (with `returned`) + `kindRegistry` + `contributionsRegistry`. Used by `/api/nodes`, `/api/links`, `/api/issues`, `/api/plugins`.",
167
+ "required": ["items", "counts", "filters", "kindRegistry", "contributionsRegistry"],
147
168
  "properties": {
148
169
  "kind": { "enum": ["nodes", "links", "issues", "plugins"] },
149
170
  "counts": { "required": ["total", "returned"] }
@@ -157,8 +178,8 @@
157
178
  }
158
179
  },
159
180
  {
160
- "description": "Single-resource envelope — `item` payload + `kindRegistry`, no `counts` / `filters`. Used by `/api/nodes/:pathB64`.",
161
- "required": ["item", "kindRegistry"],
181
+ "description": "Single-resource envelope — `item` payload + `kindRegistry` + `contributionsRegistry`, no `counts` / `filters`. Used by `/api/nodes/:pathB64`.",
182
+ "required": ["item", "kindRegistry", "contributionsRegistry"],
162
183
  "properties": {
163
184
  "kind": { "const": "node" }
164
185
  },
@@ -171,8 +192,8 @@
171
192
  }
172
193
  },
173
194
  {
174
- "description": "Value envelope — `value` payload + `kindRegistry`, no `counts` / `filters`. Used by `/api/config`.",
175
- "required": ["value", "kindRegistry"],
195
+ "description": "Value envelope — `value` payload + `kindRegistry` + `contributionsRegistry`, no `counts` / `filters`. Used by `/api/config`.",
196
+ "required": ["value", "kindRegistry", "contributionsRegistry"],
176
197
  "properties": {
177
198
  "kind": { "const": "config" }
178
199
  },
@@ -185,7 +206,7 @@
185
206
  }
186
207
  },
187
208
  {
188
- "description": "Action-result envelope — `value` + `elapsedMs` siblings, no `filters` / `counts` / `kindRegistry`. Used by `POST /api/sidecar/bump` (Step 9.6.5, BFF half) where the response carries the bump report (`{ nodePath, version, status }`) plus the wall-clock duration. The kindRegistry is intentionally absent — the action result is orthogonal to the kind catalog and the SPA already has it cached from a prior list call.",
209
+ "description": "Action-result envelope — `value` + `elapsedMs` siblings, no `filters` / `counts` / `kindRegistry` / `contributionsRegistry`. Used by `POST /api/sidecar/bump` (Step 9.6.5, BFF half) where the response carries the bump report (`{ nodePath, version, status }`) plus the wall-clock duration. The registries are intentionally absent — the action result is orthogonal to both catalogs and the SPA already has them cached from a prior list call.",
189
210
  "required": ["value", "elapsedMs"],
190
211
  "properties": {
191
212
  "kind": { "const": "sidecar.bumped" }
@@ -196,12 +217,13 @@
196
217
  { "required": ["item"] },
197
218
  { "required": ["filters"] },
198
219
  { "required": ["counts"] },
199
- { "required": ["kindRegistry"] }
220
+ { "required": ["kindRegistry"] },
221
+ { "required": ["contributionsRegistry"] }
200
222
  ]
201
223
  }
202
224
  },
203
225
  {
204
- "description": "Catalog envelope — `items` + `counts.total` only, no `filters` / `kindRegistry` / `returned`. Used by `GET /api/annotations/registered` (Step 9.6.6, BFF half). The catalog is small (typically 0–50 entries), ships in its entirety on every response, and does not paginate; `counts.total` doubles as `items.length`.",
226
+ "description": "Annotation-catalog envelope — `items` + `counts.total` only, no `filters` / `kindRegistry` / `contributionsRegistry` / `returned`. Used by `GET /api/annotations/registered` (Step 9.6.6, BFF half). The catalog is small (typically 0–50 entries), ships in its entirety on every response, and does not paginate; `counts.total` doubles as `items.length`.",
205
227
  "required": ["items", "counts"],
206
228
  "properties": {
207
229
  "kind": { "const": "annotations.registered" },
@@ -215,12 +237,33 @@
215
237
  { "required": ["value"] },
216
238
  { "required": ["filters"] },
217
239
  { "required": ["kindRegistry"] },
240
+ { "required": ["contributionsRegistry"] },
241
+ { "required": ["elapsedMs"] }
242
+ ]
243
+ }
244
+ },
245
+ {
246
+ "description": "View-contributions-catalog envelope — `items` + `counts.total` only, no `filters` / `kindRegistry` / `contributionsRegistry` / `returned`. Used by `GET /api/contributions/registered`. Mirror of `annotations.registered`. The catalog ships in entirety; `counts.total` doubles as `items.length`. Each item is an `IRegisteredViewContribution` shape: `{ pluginId, extensionId, contributionId, slot, label?, tooltip?, icon?, emptyText?, emitWhenEmpty? }`.",
247
+ "required": ["items", "counts"],
248
+ "properties": {
249
+ "kind": { "const": "contributions.registered" },
250
+ "counts": {
251
+ "not": { "required": ["returned"] }
252
+ }
253
+ },
254
+ "not": {
255
+ "anyOf": [
256
+ { "required": ["item"] },
257
+ { "required": ["value"] },
258
+ { "required": ["filters"] },
259
+ { "required": ["kindRegistry"] },
260
+ { "required": ["contributionsRegistry"] },
218
261
  { "required": ["elapsedMs"] }
219
262
  ]
220
263
  }
221
264
  },
222
265
  {
223
- "description": "Sentinel kinds — reserved for routes that do NOT carry an envelope payload at the wire level (`health`, `scan`, `graph`). They do not carry `kindRegistry` either; clients that need it must call any payload-bearing endpoint at boot.",
266
+ "description": "Sentinel kinds — reserved for routes that do NOT carry an envelope payload at the wire level (`health`, `scan`, `graph`). They do not carry `kindRegistry` or `contributionsRegistry` either; clients that need either must call any payload-bearing endpoint at boot.",
224
267
  "properties": {
225
268
  "kind": { "enum": ["health", "scan", "graph"] }
226
269
  },
@@ -230,6 +273,7 @@
230
273
  { "required": ["item"] },
231
274
  { "required": ["value"] },
232
275
  { "required": ["kindRegistry"] },
276
+ { "required": ["contributionsRegistry"] },
233
277
  { "required": ["elapsedMs"] }
234
278
  ]
235
279
  }
@@ -39,9 +39,9 @@
39
39
  "type": "boolean",
40
40
  "description": "When true, the runner injects `SKILL_MAP_DISABLE_ALL_EXTRACTORS=1` into the child process environment, dropping every Extractor extension before scan composition."
41
41
  },
42
- "disableAllRules": {
42
+ "disableAllAnalyzers": {
43
43
  "type": "boolean",
44
- "description": "When true, the runner injects `SKILL_MAP_DISABLE_ALL_RULES=1` into the child process environment, dropping every Rule extension before scan composition."
44
+ "description": "When true, the runner injects `SKILL_MAP_DISABLE_ALL_ANALYZERS=1` into the child process environment, dropping every Analyzer extension before scan composition."
45
45
  },
46
46
  "priorScans": {
47
47
  "type": "array",
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://skill-map.dev/spec/v0/extensions/analyzer.schema.json",
4
+ "title": "ExtensionAnalyzer",
5
+ "description": "Manifest shape for an `Analyzer` extension. An analyzer consumes the full graph (nodes + links) after all extractors have run, emits `Issue[]`, and MAY emit view contributions to project findings into the UI. Analyzers are dual-mode: `deterministic` analyzers MUST be byte-for-byte reproducible (same graph in → same issues out; time, random, and network are forbidden) and run synchronously inside `sm check` / `sm scan`. `probabilistic` analyzers invoke an LLM through the kernel's `RunnerPort` and execute only as queued jobs (`sm job submit analyzer:<id>`); their output MAY vary across runs and they NEVER participate in `sm scan`. See `architecture.md` §Execution modes for the full contract.",
6
+ "allOf": [
7
+ { "$ref": "base.schema.json" }
8
+ ],
9
+ "type": "object",
10
+ "required": ["id", "kind", "version", "emitsAnalyzerIds", "defaultSeverity"],
11
+ "unevaluatedProperties": false,
12
+ "properties": {
13
+ "kind": { "const": "analyzer" },
14
+ "mode": {
15
+ "type": "string",
16
+ "enum": ["deterministic", "probabilistic"],
17
+ "default": "deterministic",
18
+ "description": "`deterministic` (default): pure code, byte-for-byte reproducible, runs during `sm check` and `sm scan`. `probabilistic`: invokes an LLM via `ctx.runner` and runs only as a queued job; never participates in scan-time pipelines. The kernel rejects probabilistic analyzers that try to register scan-time hooks at load time. Omitting the field is equivalent to declaring `deterministic`."
19
+ },
20
+ "emitsAnalyzerIds": {
21
+ "type": "array",
22
+ "description": "List of `analyzer_id` values this analyzer may emit on issues. Typically a singleton (`trigger-collision` → emits `trigger-collision`). An analyzer emitting an `analyzer_id` not in this list → kernel logs `analyzer-id-violation` but keeps the issue (forward compatibility).",
23
+ "minItems": 1,
24
+ "items": { "type": "string" }
25
+ },
26
+ "defaultSeverity": {
27
+ "type": "string",
28
+ "enum": ["error", "warn", "info"],
29
+ "description": "Severity attached by default to emitted issues. Analyzers MAY override per-issue."
30
+ },
31
+ "consumes": {
32
+ "type": "string",
33
+ "enum": ["nodes", "links", "both"],
34
+ "default": "both",
35
+ "description": "Which slices of the graph the analyzer reads. The kernel MAY pass a restricted view when this is a strict subset."
36
+ },
37
+ "configurable": {
38
+ "type": "boolean",
39
+ "default": false,
40
+ "description": "If true, the analyzer reads its own config from `config_preferences` under the key `analyzers.<id>.<field>`. Implementations MAY surface these in `sm config`."
41
+ }
42
+ }
43
+ }
@@ -2,18 +2,18 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/base.schema.json",
4
4
  "title": "ExtensionBase",
5
- "description": "Base manifest shape common to every extension kind. Kind-specific schemas (`provider`, `extractor`, `rule`, `action`, `formatter`, `hook`) extend this via `allOf` and add a discriminant `kind` literal plus kind-specific fields. camelCase keys throughout. Closed-content enforcement (unknown keys = bug) lives on the kind schemas via `unevaluatedProperties: false`; those see base's evaluated keys through the `allOf` composition. Adding closure here too would force every kind schema to re-list every base key, which is the footgun the spec used to trip on before 2026-04-22.",
5
+ "description": "Base manifest shape common to every extension kind. Kind-specific schemas (`provider`, `extractor`, `analyzer`, `action`, `formatter`, `hook`) extend this via `allOf` and add a discriminant `kind` literal plus kind-specific fields. camelCase keys throughout. Closed-content enforcement (unknown keys = bug) lives on the kind schemas via `unevaluatedProperties: false`; those see base's evaluated keys through the `allOf` composition. Adding closure here too would force every kind schema to re-list every base key, which is the footgun the spec used to trip on before 2026-04-22.",
6
6
  "type": "object",
7
7
  "required": ["id", "kind", "version"],
8
8
  "properties": {
9
9
  "id": {
10
10
  "type": "string",
11
11
  "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
12
- "description": "Kebab-case identifier unique within the extension's kind across the same plugin. The kernel registers every extension under the **qualified** id `<plugin-id>/<id>` (e.g. `core/frontmatter`, `claude/slash`, `hello-world/greet`) — see `architecture.md` §Extension kinds and `plugin-author-guide.md` §Qualified extension ids. Authors declare only the short id here; the qualifier is composed by the loader from the manifest's `id` (or, for built-ins, from the bundle declaration in `built-ins.ts`). The pattern is intentionally constrained to a single kebab-case segment without the `/` separator: the qualifier always lives in the plugin id, never in the extension id."
12
+ "description": "Kebab-case identifier unique within the extension's kind across the same plugin. The kernel registers every extension under the **qualified** id `<plugin-id>/<id>` (e.g. `core/annotations`, `core/slash`, `hello-world/greet`) — see `architecture.md` §Extension kinds and `plugin-author-guide.md` §Qualified extension ids. Authors declare only the short id here; the qualifier is composed by the loader from the manifest's `id` (or, for built-ins, from the bundle declaration in `built-ins.ts`). The pattern is intentionally constrained to a single kebab-case segment without the `/` separator: the qualifier always lives in the plugin id, never in the extension id."
13
13
  },
14
14
  "kind": {
15
15
  "type": "string",
16
- "enum": ["provider", "extractor", "rule", "action", "formatter", "hook"],
16
+ "enum": ["provider", "extractor", "analyzer", "action", "formatter", "hook"],
17
17
  "description": "Discriminant. MUST match the file exporting this manifest; kind mismatch → load-error."
18
18
  },
19
19
  "version": {
@@ -62,7 +62,17 @@
62
62
  }
63
63
  }
64
64
  },
65
- "description": "Plugin-contributed annotation keys. Each entry declares an inline JSON Schema for the value the extension writes into a sidecar. Keys default to the plugin's `<plugin-id>:` namespace; opt-in to top-level via `location: 'root'` (requires `ownership: 'exclusive'`). The kernel exposes the runtime catalog via `kernel.getRegisteredAnnotationKeys()` for UI autocomplete; the built-in `core/unknown-field` Rule emits a warning on truly unrecognized keys (typo guard). See `plugin-author-guide.md` §Annotation contributions for the worked examples."
65
+ "description": "Plugin-contributed annotation keys. Each entry declares an inline JSON Schema for the value the extension writes into a sidecar. Keys default to the plugin's `<plugin-id>:` namespace; opt-in to top-level via `location: 'root'` (requires `ownership: 'exclusive'`). The kernel exposes the runtime catalog via `kernel.getRegisteredAnnotationKeys()` for UI autocomplete; the built-in `core/unknown-field` Analyzer emits a warning on truly unrecognized keys (typo guard). See `plugin-author-guide.md` §Annotation contributions for the worked examples."
66
+ },
67
+ "viewContributions": {
68
+ "type": "object",
69
+ "additionalProperties": {
70
+ "$ref": "../view-slots.schema.json#/$defs/IViewContribution"
71
+ },
72
+ "propertyNames": {
73
+ "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
74
+ },
75
+ "description": "Plugin-contributed view contributions. Each entry declares one rendering surface in the UI by picking a `slot` name from the closed catalog at `view-slots.schema.json#/$defs/SlotName`. The kernel validates the manifest at load (`invalid-manifest` on unknown slot); the plugin emits per-node payloads via `ctx.emitContribution(<contributionId>, payload)` during scan; the runtime validates payloads against the slot's payload schema in `view-slots.schema.json#/$defs/payloads/<slot>`; off-shape payloads emit `extension.error` and drop silently (mirror of `emitLink` off-contract drop). The kernel exposes the runtime catalog via `kernel.getRegisteredViewContributions()`; the BFF surfaces it at `GET /api/contributions/registered`. The plugin author picks ONE slot — that is the only choice; the slot fixes both the renderer and the payload shape. See `plugin-author-guide.md` §View contributions for worked examples."
66
76
  }
67
77
  }
68
78
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/extractor.schema.json",
4
4
  "title": "ExtensionExtractor",
5
- "description": "Manifest shape for an `Extractor` extension. An extractor consumes a parsed node (frontmatter + body) and emits output through three context-supplied callbacks rather than returning a value: `ctx.emitLink(link)` writes to the kernel's `links` table (validated against `emitsLinkKinds` before persistence), `ctx.enrichNode(partial)` merges author-canonical properties into the kernel's enrichment layer (separate from the author-supplied frontmatter), and `ctx.store` persists into the plugin's own KV namespace or dedicated tables. The runtime method is `extract(ctx) → void`. Extractors run in isolation: they MUST NOT read other nodes, the graph, or the DB. Cross-node reasoning lives in Rules. Extractors are dual-mode: `deterministic` extractors run synchronously inside `sm scan`; `probabilistic` extractors invoke an LLM through the kernel's `RunnerPort` (exposed as `ctx.runner`) and execute only as queued jobs (never during scan). See `architecture.md` §Execution modes for the full contract. Renamed from `detector` in spec 0.8.x — the FindBugs/SpotBugs lineage of \"detector\" connoted bug finding, while this kind extracts signals (relations, enrichments, custom data); ENRE (Entity Relationship Extractor) is the closer precedent.",
5
+ "description": "Manifest shape for an `Extractor` extension. An extractor consumes a parsed node (frontmatter + body) and emits output through three context-supplied callbacks rather than returning a value: `ctx.emitLink(link)` writes to the kernel's `links` table (validated against `emitsLinkKinds` before persistence), `ctx.enrichNode(partial)` merges author-canonical properties into the kernel's enrichment layer (separate from the author-supplied frontmatter), and `ctx.store` persists into the plugin's own KV namespace or dedicated tables. The runtime method is `extract(ctx) → void`. Extractors run in isolation: they MUST NOT read other nodes, the graph, or the DB. Cross-node reasoning lives in Analyzers. Extractors are deterministic-only: pure code, runs synchronously inside `sm scan`, same input same output every run. LLM-driven enrichment of a node is an Action concern (queued as a job), not an Extractor concern. See `architecture.md` §Execution modes for the full contract. Renamed from `detector` in spec 0.8.x — the FindBugs/SpotBugs lineage of \"detector\" connoted bug finding, while this kind extracts signals (relations, enrichments, custom data); ENRE (Entity Relationship Extractor) is the closer precedent.",
6
6
  "allOf": [
7
7
  { "$ref": "base.schema.json" }
8
8
  ],
@@ -11,16 +11,9 @@
11
11
  "unevaluatedProperties": false,
12
12
  "properties": {
13
13
  "kind": { "const": "extractor" },
14
- "mode": {
15
- "type": "string",
16
- "enum": ["deterministic", "probabilistic"],
17
- "default": "deterministic",
18
- "description": "`deterministic` (default): pure code, runs synchronously during `sm scan`. Same input → same output, every run. `probabilistic`: invokes an LLM via `ctx.runner` and runs only as a queued job (`sm job submit extractor:<id>`); never participates in `sm scan`. The kernel rejects probabilistic extractors that try to register scan-time hooks at load time. Omitting the field is equivalent to declaring `deterministic`."
19
- },
20
14
  "emitsLinkKinds": {
21
15
  "type": "array",
22
- "description": "Subset of `Link.kind` values this extractor is allowed to emit through `ctx.emitLink(...)`. Emitting an unlisted kind at runtime → kernel rejects the link and logs `extractor-kind-violation`.",
23
- "minItems": 1,
16
+ "description": "Subset of `Link.kind` values this extractor is allowed to emit through `ctx.emitLink(...)`. Emitting an unlisted kind at runtime → kernel rejects the link and logs `extractor-kind-violation`. Empty array (`[]`) is the honest declaration for pure-contributions extractors that emit only via `ctx.emitContribution(...)` and never call `ctx.emitLink(...)` — Phase 3 of the View contribution system relaxed `minItems: 1` to admit this case (`{ kind: 'extractor', emitsLinkKinds: [], viewContributions: { ... } }`).",
24
17
  "items": {
25
18
  "type": "string",
26
19
  "enum": ["invokes", "references", "mentions", "supersedes"]
@@ -42,7 +35,7 @@
42
35
  "minItems": 1,
43
36
  "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
44
37
  "uniqueItems": true,
45
- "description": "Optional opt-in filter. If declared, the extractor runs only on nodes whose kind is in this list. Absent = applies to all kinds (default). No wildcards — the absence of the field already means \"every kind\". Empty array is invalid (`minItems: 1`). Unknown kinds (not declared by any installed Provider) load OK but emit a warning in `sm plugins doctor` — the Provider may arrive later. The kernel filters fail-fast: nodes whose kind is excluded never see `extract()`, so a probabilistic extractor wastes zero LLM cost on inapplicable nodes."
38
+ "description": "Optional opt-in filter. If declared, the extractor runs only on nodes whose kind is in this list. Absent = applies to all kinds (default). No wildcards — the absence of the field already means \"every kind\". Empty array is invalid (`minItems: 1`). Unknown kinds (not declared by any installed Provider) load OK but emit a warning in `sm plugins doctor` — the Provider may arrive later. The kernel filters fail-fast: nodes whose kind is excluded never see `extract()`, so an extractor wastes zero CPU on inapplicable nodes."
46
39
  }
47
40
  }
48
41
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/hook.schema.json",
4
4
  "title": "ExtensionHook",
5
- "description": "Manifest shape for a `Hook` extension. Subscribes declaratively to a curated set of kernel lifecycle events. Dual-mode (deterministic / probabilistic). Hooks react to events; they cannot block or alter the main pipeline. Probabilistic hooks are deferred to the job subsystem and never run in-scan. The set of hookable triggers is intentionally small — eight events out of the full job-events catalog. Other events (per-node `scan.progress`, `model.delta`, `run.*`, internal job lifecycle) are deliberately not hookable: too verbose for a reactive surface, internal to the runner, or covered elsewhere. Declaring a trigger outside the hookable set yields `invalid-manifest` at load time. See `architecture.md` §Hook · curated trigger set for the per-trigger payload contracts.",
5
+ "description": "Manifest shape for a `Hook` extension. Subscribes declaratively to a curated set of kernel lifecycle events. Dual-mode (deterministic / probabilistic). Hooks react to events; they cannot block or alter the main pipeline. Probabilistic hooks are deferred to the job subsystem and never run in-scan. The set of hookable triggers is intentionally small — ten events out of the full job-events catalog. Eight are pipeline-driven (emitted from inside `runScan`); two (`boot`, `shutdown`) are CLI-process-driven (emitted by the driving binary before / after the verb runs, fire-and-forget so `process.exit` is never blocked). Other events (per-node `scan.progress`, `model.delta`, `run.*`, internal job lifecycle) are deliberately not hookable: too verbose for a reactive surface, internal to the runner, or covered elsewhere. Declaring a trigger outside the hookable set yields `invalid-manifest` at load time. See `architecture.md` §Hook · curated trigger set for the per-trigger payload contracts.",
6
6
  "type": "object",
7
7
  "required": ["id", "kind", "version", "triggers"],
8
8
  "unevaluatedProperties": false,
@@ -22,20 +22,22 @@
22
22
  "items": {
23
23
  "type": "string",
24
24
  "enum": [
25
+ "boot",
25
26
  "scan.started",
26
27
  "scan.completed",
27
28
  "extractor.completed",
28
- "rule.completed",
29
+ "analyzer.completed",
29
30
  "action.completed",
30
31
  "job.spawning",
31
32
  "job.completed",
32
- "job.failed"
33
+ "job.failed",
34
+ "shutdown"
33
35
  ]
34
36
  }
35
37
  },
36
38
  "filter": {
37
39
  "type": "object",
38
- "description": "Optional declarative filter applied to the event payload before invoking `on(ctx)`. Keys are payload field paths (e.g. `extractorId`, `ruleId`, `actionId`); values are the literal expected match. Cross-field validation against the declared `triggers` is performed at load time when the host implementation supports it; an unknown field for every declared trigger yields `invalid-manifest`. Absence of `filter` means \"invoke on every event of every declared trigger\"."
40
+ "description": "Optional declarative filter applied to the event payload before invoking `on(ctx)`. Keys are payload field paths (e.g. `extractorId`, `analyzerId`, `actionId`); values are the literal expected match. Cross-field validation against the declared `triggers` is performed at load time when the host implementation supports it; an unknown field for every declared trigger yields `invalid-manifest`. Absence of `filter` means \"invoke on every event of every declared trigger\"."
39
41
  }
40
42
  },
41
43
  "allOf": [
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/provider.schema.json",
4
4
  "title": "ExtensionProvider",
5
- "description": "Manifest shape for a `Provider` extension. A Provider declares its own universe: the platform it recognises (Claude Code, Codex, Gemini, Obsidian vault, generic MD), the catalog of node `kind`s it emits, the per-kind frontmatter schema each kind follows, and the filesystem directory (`explorationDir`) where its content lives. The catalog lives in the `kinds` map, keyed by kind name. Each map entry declares the relative path to the kind's frontmatter schema (resolved against the Provider's directory) and the qualified `defaultRefreshAction` id the UI's probabilistic-refresh surface dispatches for that kind. Spec only ships `frontmatter/base.schema.json` (universal); per-kind schemas live with their owning Provider so that adding a new platform is purely additive — no spec bump needed to introduce kinds. Exactly zero or one Provider MUST match any given file; multiple matches → the kernel emits an issue `provider-ambiguous` and the file is left unclassified. Providers are deterministic-only — they sit at the filesystem boundary and run during boot; probabilistic classification would make boot slow, costly, and non-reproducible. The `mode` field MUST NOT appear in Provider manifests. If you need LLM-assisted classification, write a probabilistic Extractor that emits classification hints via `ctx.emitLink`. Distinct from the **hexagonal-architecture** 'adapter' (`RunnerPort.adapter`, `StoragePort.adapter`, etc.), which is an internal driven-adapter implementing a port — Providers live in the extension surface, hexagonal adapters live in `src/kernel/adapters/`. Stability: stable as of spec v1.0.0 except where noted.",
5
+ "description": "Manifest shape for a `Provider` extension. A Provider declares its own universe: the platform it recognises (Claude Code, Codex, Gemini, Obsidian vault, generic MD), the catalog of node `kind`s it emits, the per-kind frontmatter schema each kind follows, and the filesystem directory (`explorationDir`) where its content lives. The catalog lives in the `kinds` map, keyed by kind name. Each map entry declares the relative path to the kind's frontmatter schema (resolved against the Provider's directory) and the qualified `defaultRefreshAction` id the UI's probabilistic-refresh surface dispatches for that kind. Spec only ships `frontmatter/base.schema.json` (universal); per-kind schemas live with their owning Provider so that adding a new platform is purely additive — no spec bump needed to introduce kinds. Exactly zero or one Provider MUST match any given file; multiple matches → the kernel emits an issue `provider-ambiguous` and the file is left unclassified. Providers are deterministic-only — they sit at the filesystem boundary and run during boot; probabilistic classification would make boot slow, costly, and non-reproducible. The `mode` field MUST NOT appear in Provider manifests. If you need LLM-assisted classification, write a probabilistic Action that runs as a queued job and writes back through the enrichment layer; Extractors are deterministic-only and Providers stay on the deterministic boot path. Distinct from the **hexagonal-architecture** 'adapter' (`RunnerPort.adapter`, `StoragePort.adapter`, etc.), which is an internal driven-adapter implementing a port — Providers live in the extension surface, hexagonal adapters live in `src/kernel/adapters/`. Stability: stable as of spec v1.0.0 except where noted.",
6
6
  "allOf": [
7
7
  { "$ref": "base.schema.json" }
8
8
  ],
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/frontmatter/base.schema.json",
4
4
  "title": "FrontmatterBase",
5
- "description": "Truly universal frontmatter shape `name` + `description` are the only fields every node, on every Provider, MUST carry. Per-vendor schemas (Anthropic Claude, Cursor, Obsidian, …) live in the Provider that emits the kind (declared via `provider.kinds[<kind>].schema`) and extend this base via `allOf` + `$ref` to its `$id`. `additionalProperties: true` is intentional: skill-map AGGREGATES vendor specs, it does not curate them. Vendor-specific fields (`tools`, `allowedTools`, `model`, `metadata`, etc.) flow through validation silently because the per-kind extension declares them. The future home for skill-map-only annotation fields (provenance, cross-vendor metadata) is a deferred decision — tracked separately, not in this schema.",
5
+ "description": "Universal frontmatter shape every Provider's per-kind schema extends via `allOf` + `$ref` to this `$id`. `name` and `description` are the two universally required fields; `tags` is a universally accepted optional field for author-supplied taxonomy. Per-vendor schemas (Anthropic Claude, Cursor, Obsidian, …) declare additional fields on top via the per-kind extension. `additionalProperties: true` is intentional: skill-map AGGREGATES vendor specs, it does not curate them. Vendor-specific fields (`tools`, `allowedTools`, `model`, etc.) flow through validation silently because the per-kind extension declares them.",
6
6
  "type": "object",
7
7
  "required": ["name", "description"],
8
8
  "additionalProperties": true,
@@ -16,6 +16,11 @@
16
16
  "type": "string",
17
17
  "minLength": 1,
18
18
  "description": "One-to-three-sentence description. REQUIRED."
19
+ },
20
+ "tags": {
21
+ "type": "array",
22
+ "items": { "type": "string", "minLength": 1 },
23
+ "description": "**Author-supplied** taxonomy tags written in the markdown frontmatter — what the file's author considers the node's intrinsic categories. Skill-map's tag system is **dual-source**: this field carries author tags; `sidecar.annotations.tags` carries user tags (post-hoc, written by whoever curates the project from their `.sm` sidecar). Both surfaces are first-class — searches and listings (`sm list --tag`, UI faceted search) match the union and the UI distinguishes them visually so the attribution stays explicit. Empty array and missing field are equivalent."
19
24
  }
20
25
  }
21
26
  }