@skill-map/spec 0.27.0 → 0.28.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,239 @@
1
1
  # Spec changelog
2
2
 
3
+ ## 0.28.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e21216e: Simplify plugin manifest fields beyond the file-layout refactor. The
8
+ previous `structure-as-truth-plugins` changeset moved bundle / kind /
9
+ id discovery onto the filesystem; this one extends the same principle
10
+ into the manifest schemas themselves so the only fields that survive
11
+ are the ones the kernel cannot derive from disk.
12
+
13
+ **Plugin manifest (`plugin.json`):**
14
+
15
+ - Drop `id` (the directory name is the id; AJV rejects manifests that
16
+ declare it).
17
+ - `description` and `catalogCompat` are now required (were optional).
18
+ - `granularity` is now optional with a default of `'extension'` (was
19
+ required). Most plugins drop the field entirely.
20
+ - Drop `settings` at the plugin level; settings move to the extension
21
+ manifests that actually consume them.
22
+
23
+ **Extension base (every kind):**
24
+
25
+ - Drop `id`, `kind`, `stability`, `preconditions` (free-form). The
26
+ loader injects `id` / `kind` / `pluginId` from the folder layout;
27
+ the other two were display-only and free-form respectively, and
28
+ the kernel never consumed them.
29
+ - `description` is now required.
30
+ - Rename `annotationContributions` (map) to `annotation` (singular):
31
+ one extension contributes at most one annotation key, and the key
32
+ is the extension's folder name. Use multiple extensions to
33
+ contribute multiple keys.
34
+ - Rename `viewContributions` to `ui` on the manifest. The
35
+ runtime-aggregated catalog (`Kernel.getRegisteredViewContributions()`,
36
+ `IPluginRuntimeBundle.viewContributions`) keeps its name.
37
+ - Add `settings: Record<id, ISettingDeclaration>` (moved from the
38
+ plugin manifest).
39
+
40
+ **Provider:**
41
+
42
+ - Drop the inline `kinds` map. The kind catalog now lives under
43
+ `<plugin>/kinds/<kindName>/` with two files per kind:
44
+ `schema.json` (frontmatter schema) and `kind.json` carrying the
45
+ `{ ui }` block. The loader walks the directory and projects each
46
+ entry into the runtime `kinds` descriptor.
47
+ - New schema `extensions/provider-kind.schema.json` validates the
48
+ `kind.json` shape.
49
+ - Drop `defaultRefreshAction`. The UI's `🧠 prob` refresh button is
50
+ retired; a replacement UX is TBD.
51
+ - `roots` is enforcement-grade: a Provider with declared `roots`
52
+ only sees files matching at least one glob; a Provider without
53
+ `roots` acts as the fallback for files unmatched by any other
54
+ Provider. Supported patterns: `prefix/**` (deep), `prefix/*`
55
+ (shallow), exact path. Two Providers whose roots both match the
56
+ same file produce `provider-ambiguous` (already in spec) and the
57
+ file stays unclassified.
58
+
59
+ **Extractor:**
60
+
61
+ - Drop `emitsLinkKinds` (the global closed enum of link kinds is the
62
+ contract; off-enum emissions drop with `extension.error`).
63
+ - Drop `defaultConfidence` (declare confidence per-emit on
64
+ `ctx.emitLink({ ..., confidence })`).
65
+ - Drop `applicableKinds` (array). Use `precondition.kind` instead with
66
+ qualified ids like `'claude/agent'`. The same `precondition` shape
67
+ is shared with Analyzer and Action.
68
+
69
+ **Analyzer:**
70
+
71
+ - Drop `emitsAnalyzerIds` (the qualified extension id is the default
72
+ `analyzer_id`).
73
+ - Drop `defaultSeverity` (declare severity per-emit on
74
+ `ctx.emitIssue({ ..., severity })`).
75
+ - Drop `consumes`, `configurable`, `recommendedActions`. The
76
+ analyzer↔action relationship is now declared from the Action side
77
+ via `precondition.analyzerIds` (Modelo B): one Action says "I
78
+ resolve these analyzer findings", instead of one Analyzer saying
79
+ "these actions help".
80
+ - Add `precondition: { kind?, provider? }` (same shape as Extractor).
81
+
82
+ **Action:**
83
+
84
+ - Drop `reportSchemaRef` and `promptTemplateRef`. The kernel now
85
+ resolves these by convention from the action folder:
86
+ `<action-dir>/report.schema.json` (always required) and
87
+ `<action-dir>/prompt.md` (required when `mode='probabilistic'`,
88
+ forbidden when `mode='deterministic'`).
89
+ - Drop `expectedTools`, `fanOutPolicy`, `precondition.stability`,
90
+ `precondition.custom`.
91
+ - Add `precondition.analyzerIds` (Modelo B).
92
+ - Rename `expectedDurationSeconds` to `probExpectedDurationSeconds`
93
+ to mark it as probabilistic-only via the `prob*` prefix convention.
94
+ - `mode` is now optional with default `'deterministic'` (was
95
+ required).
96
+
97
+ **Formatter:**
98
+
99
+ - Drop `formatId` (comes from the folder name; the loader injects it
100
+ into the runtime instance).
101
+ - Drop `supportsFilter` (every formatter supports `--filter`).
102
+
103
+ **Hook:**
104
+
105
+ - Drop `mode`. Hooks are deterministic-only; LLM-dependent reactions
106
+ are modeled as a deterministic hook that enqueues a probabilistic
107
+ Action via `ctx.queue('<plugin>/<action>', payload)`.
108
+
109
+ **Loader changes (`src/kernel/adapters/plugin-loader/`):**
110
+
111
+ - The exported manifest is stripped of any `id` / `kind` / `pluginId`
112
+ / `kinds` / `formatId` keys before AJV validation; the loader
113
+ injects the canonical values from the folder layout. Legacy
114
+ manifests that still inline these fields load cleanly.
115
+ - New `discoverProviderKinds(...)` reads `<plugin>/kinds/<k>/{schema.json,
116
+ kind.json}` and merges the result into the runtime Provider
117
+ instance. Failure modes: missing or unparseable `schema.json`
118
+ → `load-error`; missing, unparseable, or AJV-invalid `kind.json`
119
+ → `invalid-manifest`.
120
+ - New `validateActionFileConventions(...)` enforces the
121
+ `report.schema.json` / `prompt.md` conventions.
122
+ - New `matchesAnyRoot(...)` powers Provider `roots` enforcement
123
+ inside `processRawNode`.
124
+
125
+ **Spec docs:**
126
+
127
+ - `architecture.md` §Extension kinds table, §Provider · `kinds`
128
+ catalog, §View contribution system updated.
129
+ - `plugin-author-guide.md` §Manifest section rewritten (id from
130
+ folder; description/catalogCompat required), §Extractor section
131
+ reworked around `precondition.kind`, drop guidance for
132
+ `emitsLinkKinds` / `defaultConfidence`.
133
+ - `view-slots.md` references `ui` map.
134
+
135
+ **Built-ins migration:**
136
+
137
+ - `core/bump/report.schema.json` and `core/mark-superseded/report.schema.json`
138
+ added (file conventions).
139
+ - `core/tools-count` extractor uses
140
+ `precondition: { kind: ['claude/agent'] }`.
141
+ - All built-in extensions drop `stability`, `preconditions`,
142
+ `emitsLinkKinds`, `defaultConfidence`, `emitsAnalyzerIds`,
143
+ `defaultSeverity`, `consumes`, `configurable`, `recommendedActions`,
144
+ `defaultRefreshAction`, `formatId` (formatter), `supportsFilter`,
145
+ `mode` (hook).
146
+ - `scripts/generate-built-ins.js` updated: `id` from bundle folder,
147
+ `granularity ?? 'extension'`, `toExtensionRow` drops the retired
148
+ display fields.
149
+
150
+ **Testkit:**
151
+
152
+ - `makeExtractorContext` populates `settings: {}` so test fixtures
153
+ satisfy the new required field on `IExtractorContext`.
154
+
155
+ ## User-facing
156
+
157
+ **Plugin manifests are smaller.** `plugin.json` drops `id`; every extension declares only `version` + `description` plus kind-specific fields. View contributions move to `ui:`. Provider kinds live under `kinds/<kindName>/`. Run `sm plugins doctor` after upgrading.
158
+
159
+ - 8b7abbf: Structure-as-truth refactor for plugin extensions. The filesystem
160
+ layout (rather than declarative manifest fields) is now the single
161
+ source of truth for bundle / kind / extension id.
162
+
163
+ **Schema changes:**
164
+
165
+ - `PluginManifest` drops the required `extensions: string[]` array;
166
+ the kernel now auto-discovers extensions by walking
167
+ `<plugin-dir>/<kind>s/<name>/index.{js,mjs,ts}` for each known
168
+ kind. `granularity` is now required (no implicit default).
169
+ - `ExtensionBase` drops the `entry` field (it was an override for
170
+ the now-gone `extensions[]` path; the loader computes the entry
171
+ path from the discovered file).
172
+ - `viewContributions` moves from `base.schema.json` to
173
+ `extractor.schema.json` and `analyzer.schema.json`. Runtime
174
+ `ctx.emitContribution` only exists for those two kinds; declaring
175
+ view contributions on other kinds used to be a silent no-op and
176
+ is now rejected at manifest load.
177
+
178
+ **Loader changes:**
179
+
180
+ - `<plugin-dir>/<kind>s/<name>/` is the unit of discovery; the
181
+ loader walks `providers`, `extractors`, `analyzers`, `actions`,
182
+ `formatters`, `hooks` in canonical order, looking for an
183
+ `index.{js,mjs,ts}` inside each name directory.
184
+ - A manifest whose `kind` disagrees with the folder it lives under
185
+ (e.g. an `extractor` placed under `analyzers/`) is rejected as
186
+ `invalid-manifest` with a directed reason.
187
+ - Containment is enforced by construction: the loader never reads
188
+ paths the manifest could redirect, so `..`-escape and
189
+ absolute-path lanes are closed without runtime checks.
190
+
191
+ **Built-ins reorganization:**
192
+
193
+ - Source tree renamed `src/built-in-plugins/` → `src/plugins/` and
194
+ reorganized to `src/plugins/<bundle>/<kind>s/<name>/index.ts`.
195
+ Each bundle (`core`, `claude`, `gemini`, `agent-skills`) gains a
196
+ `plugin.json` with its metadata.
197
+ - `src/plugins/built-ins.ts` is now generated by
198
+ `scripts/generate-built-ins.js` (runs as `prebuild`, checked for
199
+ drift by `built-ins:check` in CI). The generator walks the
200
+ filesystem, reads each bundle's `plugin.json`, and emits static
201
+ imports + the legacy API surface (`builtIns()`,
202
+ `listBuiltIns()`, `builtInBundles`).
203
+
204
+ **Scaffolder (`sm plugins create`):**
205
+
206
+ - Emits the new layout: `extractors/<plugin-id>-extractor/index.js`
207
+ plus a `plugin.json` without `extensions` and without
208
+ `pluginId` on the extension export (the loader injects it from
209
+ `plugin.json#/id`). The legacy `mode: 'deterministic'` field on
210
+ the extractor stub was a no-op holdover from when extractors had
211
+ a mode and has been removed.
212
+
213
+ **Per-extension co-located files convention:**
214
+
215
+ Files that share the extension's folder with `index.{js,mjs,ts}`
216
+ are author-owned siblings. Two blessed names so consumers know
217
+ where to look:
218
+
219
+ - `text.ts` for externalised user-facing strings (one per
220
+ extension, imported by `index.ts` as `./text.js`).
221
+ - `<extension-name>.test.{ts,mjs,js}` for the colocated test
222
+ suite (picked up by the workspace's `plugins/**/*.test.ts`
223
+ glob).
224
+
225
+ Both are optional; the loader ignores anything that is not
226
+ `index.{js,mjs,ts}`, so future schemas / fixtures / conformance
227
+ scopes can live next to the code without manifest plumbing. The
228
+ in-tree built-ins under `src/plugins/` were migrated to this
229
+ shape: each analyzer's user-facing strings now live at
230
+ `<bundle>/analyzers/<name>/text.ts` instead of a centralised
231
+ `i18n/` directory.
232
+
233
+ ## User-facing
234
+
235
+ **Plugin layout changed.** Extensions now live at `<kind>s/<name>/index.js` (e.g. `extractors/keyword-counter/index.js`); `plugin.json` no longer lists `extensions[]` and requires `granularity`. Run `sm plugins doctor` after migrating, or use `sm plugins create` for new plugins.
236
+
3
237
  ## 0.27.0
4
238
 
5
239
  ### Minor Changes
package/architecture.md CHANGED
@@ -173,12 +173,12 @@ Six kinds, all first-class, all loaded through the same registry. Each kind has
173
173
 
174
174
  | Kind | Role | Input | Output |
175
175
  |---|---|---|---|
176
- | **Provider** | Recognizes a platform. Declares the catalog of node `kind`s it emits via the `kinds` map; each map entry pairs the kind's frontmatter schema (path relative to the Provider's package directory) with its `defaultRefreshAction` (a qualified action id that drives the probabilistic-refresh surface). The Provider's walker hardcodes the paths it scans within the project (e.g. `.claude/`, `.gemini/`); it does NOT extend the scan into the user's HOME. Deterministic-only. | Filesystem walk results, candidate path. | `{ kind, provider } \| null`. |
177
- | **Extractor** | Extracts signals from a node body. Deterministic-only: runs synchronously inside `sm scan`. Output flows through three context callbacks (no return value): `ctx.emitLink(link)` for the kernel's `links` table, `ctx.enrichNode(partial)` for the kernel's enrichment layer (separate from the author's frontmatter), `ctx.store` for the plugin's own KV / dedicated tables. | Parsed node (frontmatter + body) + callbacks. | `void` (output via callbacks). |
178
- | **Analyzer** | Evaluates the graph. Dual-mode: `deterministic` runs in `sm check`, `probabilistic` runs in jobs. | Full graph (nodes + links). | `Issue[]`. |
179
- | **Action** | Operates on one or more nodes. Dual-mode: `deterministic` (in-process code) or `probabilistic` (rendered prompt the runner executes). | Node(s), optional args. | Deterministic: report JSON. Probabilistic: rendered prompt that a runner executes. |
180
- | **Formatter** | Serializes the graph. Deterministic-only. | Graph + optional filter. | String (ASCII / Mermaid / DOT / JSON / user-defined). |
181
- | **Hook** | Reacts declaratively to one of ten curated lifecycle events, eight pipeline-driven (`scan.started`, `scan.completed`, `extractor.completed`, `analyzer.completed`, `action.completed`, `job.spawning`, `job.completed`, `job.failed`) plus two CLI-process-driven (`boot` before verb routing, `shutdown` after the verb's exit code resolves). Dual-mode: `deterministic` runs in-process during the dispatch, `probabilistic` is enqueued as a job. Hooks REACT to events; they cannot block, mutate, or steer the pipeline. | A curated event payload (run-scoped, scan-scoped, job-scoped, or process-scoped) plus an optional declarative `filter` map. | `void` (reactions are side effects). |
176
+ | **Provider** | Recognizes a platform. The kind catalog lives on disk under `<plugin>/kinds/<kindName>/{schema.json, kind.json}` (structure-as-truth); the loader projects it onto the runtime descriptor. The Provider's walker hardcodes the paths it scans within the project (e.g. `.claude/`, `.gemini/`); it does NOT extend the scan into the user's HOME. `Provider.roots` is enforcement-grade: a Provider with declared roots only sees matching files; a Provider without `roots` acts as the fallback. Deterministic-only. | Filesystem walk results, candidate path. | `{ kind, provider } \| null`. |
177
+ | **Extractor** | Extracts signals from a node body. Deterministic-only: runs synchronously inside `sm scan`. Output flows through context callbacks (no return value): `ctx.emitLink(link)` for the kernel's `links` table (validated against the global closed enum of link kinds; per-extractor allowlist was retired with the structure-as-truth refactor), `ctx.enrichNode(partial)` for the kernel's enrichment layer (separate from the author's frontmatter), `ctx.emitContribution(id, payload)` for view contributions, `ctx.store` for the plugin's own KV / dedicated tables. | Parsed node (frontmatter + body) + callbacks. | `void` (output via callbacks). |
178
+ | **Analyzer** | Evaluates the graph. Dual-mode: `deterministic` runs in `sm check`, `probabilistic` runs in jobs. The analyzer↔action relationship is declared from the Action side via `precondition.analyzerIds` (Modelo B). | Full graph (nodes + links). | `Issue[]`. |
179
+ | **Action** | Operates on one or more nodes. Dual-mode: `deterministic` (in-process code) or `probabilistic` (rendered prompt the runner executes). Files-by-convention: every Action carries `<action-dir>/report.schema.json`; probabilistic Actions additionally carry `<action-dir>/prompt.md`. The retired `reportSchemaRef` / `promptTemplateRef` / `expectedTools` / `fanOutPolicy` manifest fields were replaced by these conventions and the simplified `precondition` block. | Node(s), optional args. | Deterministic: report JSON. Probabilistic: rendered prompt that a runner executes. |
180
+ | **Formatter** | Serializes the graph. Deterministic-only. The `formatId` consumed by `sm graph --format <name>` comes from the formatter's folder name. | Graph + optional filter. | String (ASCII / Mermaid / DOT / JSON / user-defined). |
181
+ | **Hook** | Reacts declaratively to one of ten curated lifecycle events, eight pipeline-driven (`scan.started`, `scan.completed`, `extractor.completed`, `analyzer.completed`, `action.completed`, `job.spawning`, `job.completed`, `job.failed`) plus two CLI-process-driven (`boot` before verb routing, `shutdown` after the verb's exit code resolves). **Deterministic-only** since the structure-as-truth refactor: LLM-dependent reactions are modeled as a deterministic Hook that enqueues a probabilistic Action via `ctx.queue('<plugin>/<action>', payload)`. Hooks REACT to events; they cannot block, mutate, or steer the pipeline. | A curated event payload (run-scoped, scan-scoped, job-scoped, or process-scoped) plus an optional declarative `filter` map. | `void` (reactions are side effects). |
182
182
 
183
183
  ### IO discipline, extensions never write to the filesystem
184
184
 
@@ -194,13 +194,14 @@ This invariant is what makes the consent gate at the kernel boundary sufficient:
194
194
 
195
195
  ### Provider · `kinds` catalog
196
196
 
197
- Every `Provider` MUST declare a non-empty map `kinds: { <kind>: { schema, defaultRefreshAction, ui } }` covering every `kind` it classifies into. Each entry carries three required fields:
197
+ Every `Provider` declares its kind catalog via the filesystem (structure-as-truth): each kind lives under `<plugin>/kinds/<kindName>/` and ships exactly two files:
198
198
 
199
- - **`schema`**, path (relative to the Provider package) to the kind's frontmatter JSON Schema. The schema MUST extend [`frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) via `allOf` + `$ref` to base's `$id`. The kernel registers it with AJV at boot and validates every node's frontmatter against the entry matching its classified kind.
200
- - **`defaultRefreshAction`**, qualified action id (`<plugin-id>/<action-id>`) the UI's probabilistic-refresh surface (`🧠 prob`) dispatches for nodes of this kind. The action MUST exist in the registry; a dangling reference disables the Provider with status `invalid-manifest`. Plugins MAY override per-node via `metadata.refreshAction`; the Provider default is normative.
201
- - **`ui`**, presentation block: `{ label, color, colorDark?, emoji?, icon? }`. See §Provider · `ui` presentation below.
199
+ - **`schema.json`**, the kind's frontmatter JSON Schema. MUST extend [`frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) via `allOf` + `$ref` to base's `$id`. The kernel reads it once at boot, registers it with AJV, and validates every node's frontmatter against the entry matching its classified kind.
200
+ - **`kind.json`**, the per-kind metadata, today just `{ ui: { label, color, colorDark?, emoji?, icon? } }`. See §Provider · `ui` presentation below. Validated against [`schemas/extensions/provider-kind.schema.json`](./schemas/extensions/provider-kind.schema.json) at load time.
202
201
 
203
- The catalog is the single source of truth for "which kinds does this Provider emit", the `IProvider` runtime contract derives the kind set from `Object.keys(kinds)`.
202
+ The loader's discovery (`discoverProviderKinds`) projects every `kinds/<kindName>/` directory into the runtime descriptor `instance.kinds[<kindName>] = { schema, schemaJson, ui }`. The `IProvider` runtime contract derives the kind set from `Object.keys(kinds)`; authors do not write the map by hand any more.
203
+
204
+ The retired manifest field `defaultRefreshAction` (the qualified action id the UI's `🧠 prob` button dispatched) was removed alongside the button. A replacement UX is TBD; until then, the kernel does not surface a Provider-declared "default refresh" path.
204
205
 
205
206
  ### Provider · `ui` presentation
206
207
 
@@ -588,7 +589,7 @@ Two schemas describe the wire shape:
588
589
 
589
590
  ### Identity
590
591
 
591
- Each view contribution is identified by the qualified id `<pluginId>/<extensionId>/<contributionId>`. The plugin author declares contributions in the extension manifest under `viewContributions: Record<string, IViewContribution>`; the loader composes the qualified id from the plugin id, the extension id, and the Record key.
592
+ Each view contribution is identified by the qualified id `<pluginId>/<extensionId>/<contributionId>`. The plugin author declares contributions in the extension manifest under `ui: Record<string, IViewContribution>` (renamed from `viewContributions` with the structure-as-truth refactor); the loader composes the qualified id from the plugin id, the extension id, and the Record key. The runtime catalog aggregated by `Kernel.getRegisteredViewContributions()` keeps the original `viewContributions` name, only the manifest-side field changed.
592
593
 
593
594
  ### Manifest
594
595
 
@@ -596,7 +597,7 @@ Each entry picks a `slot` name from the closed catalog and supplies presentation
596
597
 
597
598
  ```jsonc
598
599
  {
599
- "viewContributions": {
600
+ "ui": {
600
601
  "breakdown": {
601
602
  "slot": "inspector.body.panel.breakdown",
602
603
  "label": "Keyword hits",
@@ -616,13 +617,13 @@ The plugin author picks ONE slot per contribution; that single decision determin
616
617
 
617
618
  ### Settings
618
619
 
619
- Plugin user-configurable settings live at the manifest root in `settings: Record<string, ISettingDeclaration>` (see [`schemas/plugins-registry.schema.json`](./schemas/plugins-registry.schema.json)). Each setting picks an input-type from the closed catalog at [`schemas/input-types.schema.json`](./schemas/input-types.schema.json) (`string-list`, `single-string`, `boolean-flag`, `integer`, `enum-pick`, `enum-multipick`, `path-glob`, `regex`, `secret`, `key-value-list`). The kernel exposes resolved settings to extractors via `ctx.settings.<settingId>`; the UI generates a form per declaration; the CLI's `sm plugins config <id>` exposes the same surface.
620
+ Plugin user-configurable settings live **on each extension's manifest** (structure-as-truth) in `settings: Record<string, ISettingDeclaration>` (see [`schemas/extensions/base.schema.json`](./schemas/extensions/base.schema.json) and [`schemas/input-types.schema.json`](./schemas/input-types.schema.json)). Each setting picks an input-type from the closed catalog (`string-list`, `single-string`, `boolean-flag`, `integer`, `enum-pick`, `enum-multipick`, `path-glob`, `regex`, `secret`, `key-value-list`). The kernel exposes resolved settings via `ctx.settings.<settingId>` to the extension's runtime methods (`extract`, `evaluate`, `invoke`, etc.); the UI generates a form per declaration; the CLI's `sm plugins config <plugin>/<extension>` exposes the same surface. Plugin-level settings are no longer supported; the field was moved from `plugin.json` to each extension that consumes it.
620
621
 
621
- Settings are read once at extractor invocation; changing a setting requires `sm scan` to re-emit affected contributions. The UI surfaces a "settings changed, rescan needed" indicator when the manifest detects mismatch; live re-emission is explicitly out of scope (rescan-required is a stability decision per `ROADMAP.md` §UI contribution system D4).
622
+ Settings are read once at extension invocation; changing a setting requires `sm scan` to re-emit affected contributions. The UI surfaces a "settings changed, rescan needed" indicator when the manifest detects mismatch; live re-emission is explicitly out of scope (rescan-required is a stability decision per `ROADMAP.md` §UI contribution system D4).
622
623
 
623
624
  ### Runtime catalog
624
625
 
625
- The kernel exposes a runtime catalog (`Kernel.getRegisteredViewContributions()`) listing every plugin-contributed view contribution with its `pluginId`, `extensionId`, `contributionId`, `slot`, and the manifest-declared `label` / `tooltip` / `icon` / `emptyText` / `emitWhenEmpty`. The catalog is built once at boot from every loaded extension's `viewContributions` map, AJV-validated, and frozen, same lifecycle as `getRegisteredAnnotationKeys()`.
626
+ The kernel exposes a runtime catalog (`Kernel.getRegisteredViewContributions()`) listing every plugin-contributed view contribution with its `pluginId`, `extensionId`, `contributionId`, `slot`, and the manifest-declared `label` / `tooltip` / `icon` / `emptyText` / `emitWhenEmpty`. The catalog is built once at boot from every loaded extension's `ui` map (renamed from `viewContributions` with the structure-as-truth refactor), AJV-validated, and frozen, same lifecycle as `getRegisteredAnnotationKeys()`.
626
627
 
627
628
  Analyzers see the catalog through `IAnalyzerContext.viewContributions` so cross-cutting checks (`core/unknown-slot`, `core/contribution-orphan`) can reason about emissions.
628
629
 
@@ -712,7 +713,7 @@ Same honest-note posture as [`plugin-kv-api.md`](./plugin-kv-api.md): isolated a
712
713
 
713
714
  Two built-ins ship with the system to cover catalog evolution and rename edge cases:
714
715
 
715
- - **`core/unknown-slot`**, walks every loaded plugin's `viewContributions[*].slot`; emits an `Issue` of severity `warn` for any slot not in the current kernel catalog. Parallel to `core/unknown-field` for annotations. Note: AJV at manifest load already rejects unknown slots as `invalid-manifest`; this analyzer covers the soft-warning path when a plugin remains loaded across a catalog version bump.
716
+ - **`core/unknown-slot`**, walks every loaded plugin's `ui[*].slot`; emits an `Issue` of severity `warn` for any slot not in the current kernel catalog. Parallel to `core/unknown-field` for annotations. Note: AJV at manifest load already rejects unknown slots as `invalid-manifest`; this analyzer covers the soft-warning path when a plugin remains loaded across a catalog version bump.
716
717
  - **`core/contribution-orphan`**, joins `scan_contributions` against the live `scan_nodes` set; emits an `Issue` of severity `warn` for emissions whose `node_path` no longer exists (post-rename heuristic miss).
717
718
 
718
719
  ### Catalog versioning
@@ -16,10 +16,10 @@
16
16
  { "type": "json-path", "path": "$.nodes[0].sidecar.status", "matches": "^stale-(body|frontmatter|both)$" },
17
17
  { "type": "json-path", "path": "$.nodes[0].sidecar.annotations.version", "equals": 7 },
18
18
  { "type": "json-path", "path": "$.stats.issuesCount", "equals": 2 },
19
- { "type": "json-path", "path": "$.issues[0].analyzerId", "equals": "annotation-stale" },
19
+ { "type": "json-path", "path": "$.issues[0].analyzerId", "equals": "annotation-orphan" },
20
20
  { "type": "json-path", "path": "$.issues[0].severity", "equals": "warn" },
21
- { "type": "json-path", "path": "$.issues[1].analyzerId", "equals": "annotation-orphan" },
22
- { "type": "json-path", "path": "$.issues[1].severity", "equals": "warn" },
23
- { "type": "json-path", "path": "$.issues[1].data.expectedMdPath", "equals": ".claude/agents/orphan.md" }
21
+ { "type": "json-path", "path": "$.issues[0].data.expectedMdPath", "equals": ".claude/agents/orphan.md" },
22
+ { "type": "json-path", "path": "$.issues[1].analyzerId", "equals": "annotation-stale" },
23
+ { "type": "json-path", "path": "$.issues[1].severity", "equals": "warn" }
24
24
  ]
25
25
  }
@@ -25,13 +25,14 @@ This file is hand-maintained. A CI check before spec release compares the schema
25
25
  | 15 | `summaries/hook.schema.json` |, | 🔴 missing | Blocked by Step 11. |
26
26
  | 16 | `summaries/markdown.schema.json` |, | 🔴 missing | Blocked by Step 11. |
27
27
  | 17 | `extensions/base.schema.json` |, | 🔴 missing | Meta-case: every manifest under `src/extensions/` validates against the appropriate kind schema (which extends base via `allOf`). |
28
- | 18 | `extensions/provider.schema.json` | `plugin-missing-ui-rejected` | 🟡 partial | A drop-in Provider whose `kinds[*]` entry omits the required `ui` block fails AJV validation with `invalid-manifest` while the rest of the pipeline keeps running (built-in Claude Provider, exit 0). The complementary positive case (canonical Claude Provider manifest validates) lives in `provider:claude` conformance. Direct case for missing `kinds` rejection still pending. |
29
- | 19 | `extensions/extractor.schema.json` |, | 🔴 missing | Case: `frontmatter` + `slash` + `at-directive` extractor manifests validate; an extractor emitting a disallowed `emitsLinkKinds` value fails. |
28
+ | 18 | `extensions/provider.schema.json` | `plugin-missing-ui-rejected` | 🟡 partial | A Provider plugin whose `kinds/<kindName>/kind.json` omits the required `ui` block fails AJV validation with `invalid-manifest` while the rest of the pipeline keeps running (built-in Claude Provider, exit 0). Since the structure-as-truth refactor, Providers no longer carry a `kinds` map in the manifest; per-kind metadata lives under `kinds/<kindName>/kind.json` and frontmatter schemas under `kinds/<kindName>/schema.json`. The complementary positive case (canonical Claude Provider validates) lives in `provider:claude` conformance. Direct case for missing `kinds/` directory rejection still pending. |
29
+ | 19 | `extensions/extractor.schema.json` |, | 🔴 missing | Case: `frontmatter` + `slash` + `at-directive` extractor manifests validate; an extractor declaring a `precondition.kind` against an unknown qualified kind emits `precondition-kind-unknown` in `sm plugins doctor`. |
30
30
  | 20 | `extensions/analyzer.schema.json` |, | 🔴 missing | Case: `trigger-collision`, `broken-ref`, `superseded` manifests validate. |
31
- | 21 | `extensions/action.schema.json` |, | 🔴 missing | Case: a `deterministic` action manifest validates; a `probabilistic` action WITHOUT `promptTemplateRef` fails. |
31
+ | 21 | `extensions/action.schema.json` |, | 🔴 missing | Case: a `deterministic` action manifest validates; a `probabilistic` action without `<action-dir>/prompt.md` surfaces as `load-error` (structure-as-truth: prompt template lives on disk by convention, no `promptTemplateRef` field). |
32
32
  | 22 | `extensions/formatter.schema.json` |, | 🔴 missing | Case: `ascii` formatter manifest validates. |
33
33
  | 23 | `history-stats.schema.json` |, | 🔴 missing | Blocked by Step 5 (history). Case: seed `state_executions` with a deterministic fixture, run `sm history stats --json --since <T0> --until <T1> --period month --top 5`, assert the document validates and that `totals.executionsCount == sum(perAction.executionsCount)` and `errorRates.global == totals.failedCount / totals.executionsCount`. Percentiles (`p95`/`p99`) intentionally omitted in v1, add later as a minor bump without breaking consumers. |
34
- | 24 | `extensions/hook.schema.json` |, | 🔴 missing | Case: a `deterministic` hook manifest with `triggers: ['scan.completed']` validates; a hook declaring an unknown trigger (e.g. `scan.progress`) fails with `invalid-manifest` at load time. |
34
+ | 24 | `extensions/hook.schema.json` |, | 🔴 missing | Case: a hook manifest with `triggers: ['scan.completed']` validates; a hook declaring an unknown trigger (e.g. `scan.progress`) fails with `invalid-manifest` at load time. Hooks are deterministic-only since structure-as-truth; manifests carrying a `mode` field are rejected. |
35
+ | 36 | `extensions/provider-kind.schema.json` |, | 🔴 missing | Case: `<plugin>/kinds/<kindName>/kind.json` carrying a valid `{ ui }` block validates; a kind folder missing `kind.json` or `schema.json` surfaces as `invalid-manifest`; a `kind.json` whose `ui.color` is not `#RRGGBB` fails AJV. |
35
36
  | 25 | `api/rest-envelope.schema.json` |, | 🔴 missing | Step 14.2 BFF list-envelope shape (`{ schemaVersion, kind, items \| item \| value, filters, counts }`). Case: hit `GET /api/nodes` against a primed scope, validate the response against the schema; assert the `oneOf` rejects an envelope that carries both `items` and `item`. Implementation-side coverage exists today (`src/test/server-endpoints.test.ts`) but a kernel-agnostic conformance case is required before v1.0.0 ships. |
36
37
  | 26 | `sidecar.schema.json` | `sidecar-end-to-end` | 🟢 covered | Co-located YAML sidecar (`<basename>.sm`) root shape: reserved blocks `for` / `annotations` / `settings` / `audit` plus opt-in plugin namespacing. Step 9.6.2 (2026-05-05) shipped the kernel reader; Step 9.6.3 (2026-05-05) formalised the `audit:` sub-shape populated by the built-in `bump` Action; Step 9.6.6 (2026-05-06) flips this row 🟢 with the end-to-end `sidecar-end-to-end` case (fixture `sidecar-end-to-end/`): a scan over a stale-`.sm` + orphan-`.sm` corpus produces a populated `Node.sidecar` overlay with `present: true` and `status: stale-*`, denormalises `annotations.version` into the node row, and emits both `annotation-stale` and `annotation-orphan` issues from the built-in core analyzers. Structural sample (untouched) at `fixtures/sidecar-example/agent-example.sm`. |
37
38
  | 27 | `annotations.schema.json` | `sidecar-end-to-end` | 🟢 covered | Curated catalog of 15 conventional skill-map annotation fields (versioning, supersession, provenance, lifecycle, taxonomy, display, docs). `additionalProperties: true` so users / plugins extend without coordination; the `unknown-field` Tier-1 analyzer shipped in Step 9.6.6 emits warnings on truly unrecognized keys. Step 9.6.2 (2026-05-05) wired the kernel reader; Step 9.6.6 (2026-05-06) flips this row 🟢 via `sidecar-end-to-end`, which asserts that an `annotations.version: 7` value round-trips through `state_scan_nodes.annotations_json` and surfaces in the node's `sidecar.annotations` overlay AND in the denormalised `Node.version` column. Structural sample at `fixtures/sidecar-example/agent-example.sm`. Catalog trimmed from 31 to 15 fields on 2026-05-07 after UX review. |
@@ -0,0 +1,3 @@
1
+ {
2
+ "_comment": "Conformance fixture: this kind.json deliberately omits the required `ui` block; the loader MUST reject the plugin as invalid-manifest."
3
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:test:bad-provider/markdown",
4
+ "type": "object",
5
+ "additionalProperties": true
6
+ }
@@ -1,6 +1,7 @@
1
1
  {
2
- "id": "bad-provider",
3
2
  "version": "0.1.0",
4
3
  "specCompat": "*",
5
- "extensions": ["provider.js"]
4
+ "catalogCompat": "*",
5
+ "description": "Conformance fixture: provider missing `ui` block.",
6
+ "granularity": "bundle"
6
7
  }
@@ -0,0 +1,17 @@
1
+ // Conformance fixture: provider whose `kinds/markdown/kind.json`
2
+ // deliberately omits the required `ui` block (structure-as-truth
3
+ // refactor moved the kind catalog from the manifest map to per-kind
4
+ // folders on disk). The plugin loader MUST reject this manifest with
5
+ // a clear "missing required property 'ui'" diagnostic and the plugin
6
+ // MUST end up in `invalid-manifest` status. The companion case
7
+ // `plugin-missing-ui-rejected.json` asserts the stderr text and that
8
+ // `sm scan` survives (the loader degrades the bad plugin and lets the
9
+ // rest of the pipeline continue).
10
+ export default {
11
+ version: '0.1.0',
12
+ description: 'provider whose markdown kind is missing the ui block',
13
+ async *walk() {},
14
+ classify() {
15
+ return 'markdown';
16
+ },
17
+ };
package/index.json CHANGED
@@ -174,25 +174,27 @@
174
174
  }
175
175
  ]
176
176
  },
177
- "specPackageVersion": "0.27.0",
177
+ "specPackageVersion": "0.28.0",
178
178
  "integrity": {
179
179
  "algorithm": "sha256",
180
180
  "files": {
181
- "CHANGELOG.md": "609e04963f4a7302161bc01718643fa60c358b62cf564cd683e68892be6a65de",
181
+ "CHANGELOG.md": "9d49970b380a997e8321ea9e7da326284c4b3bb2ac56bc26633e5108a96d8486",
182
182
  "README.md": "54c4649fa9742bf2f74423ea78788a7474ce09649cbe1e72a270b606cf16a0a5",
183
- "architecture.md": "7c735b2d305798d610760d23b03a450361131927109d7271ef78257fbf36b1f4",
183
+ "architecture.md": "d40423d3df102c31744186f3b5e91446e57fb78839e67c010501fb210db6d545",
184
184
  "cli-contract.md": "c22f7c82d460714efaf34a04a2d2367d21eb04985100aef1291071e6726cbc64",
185
185
  "conformance/README.md": "6871dde25b5770ed945284c9e0f749e0768ec3f5ba4966bdb215985789e43887",
186
186
  "conformance/cases/kernel-empty-boot.json": "2a5be9c93143d07a16d998df09dcc8fa4ea2d2f9a0bff6417573ed5a770352c1",
187
187
  "conformance/cases/no-global-scope.json": "1284763988026d924c0bd78ba8a9f417dc88f5b7e9f4c2b642ae0c447758bfd4",
188
188
  "conformance/cases/orphan-markdown-fallback.json": "8ef6e49b7e6532bd845d9f54974a16e537cf98d355f0c5e4f4fb06abac3adcc5",
189
189
  "conformance/cases/plugin-missing-ui-rejected.json": "bdebee810436e6be88edf2fe38ddc6939fd3f53e6a12dc1d66da051c4922f1e9",
190
- "conformance/cases/sidecar-end-to-end.json": "24a73e7c857709d001cf7013b8fe5ccad4027e064b39533dda33697d80b56e7a",
191
- "conformance/coverage.md": "0d202baf257f6690c107e1e20aff0c7db0399a53fcd72536e2d87ab7641510ab",
190
+ "conformance/cases/sidecar-end-to-end.json": "dbb3640f95769a36b881855a261f918481edadea13a7eb0765c6090f2417a142",
191
+ "conformance/coverage.md": "7c02052750ee5fca711218cbf993313dd1ed7c68bcac000b2cb23b5f688a4a2c",
192
192
  "conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md": "7f062731106f2d9811e4fffcf6ab44b8dfff4cfb16536a469514cc0664e832bf",
193
193
  "conformance/fixtures/orphan-markdown/ARCHITECTURE.md": "d6b6e18d4b963b26a292de73348c3396fd4710ab4c4bdd6cf094e581f99ec8d6",
194
- "conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json": "4d78af6f12faa9d131e2a19f1dbb8f250baacc525978f3a8c858932b95da4ff6",
195
- "conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/provider.js": "45843e8e1a679482a17e01dcd43e1800e1d3148f4ac2cd6ab8a29ef0430b6152",
194
+ "conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/kind.json": "6676a89bae5197e23cf50f1c11d596db558ac80f7334a7208fe57d8b92422251",
195
+ "conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/schema.json": "42795e7f1759fa25115a426edf5cd1b0c91b091b408aeee3f4f9fbc8f89f32bc",
196
+ "conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json": "fb3f52f82f1635d0e5de74788eb5f640d0e36b19464a46f0b2812f6aa9db435f",
197
+ "conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/providers/bad-provider/index.js": "6eac0555de4194c3266c2abef89e39d833159990e1822ae1d4895df67c31d18f",
196
198
  "conformance/fixtures/plugin-missing-ui/notes/example.md": "55767f0aa1b6774546a99f28c58e7b732aa9cfa5dfce8d0326470f7f622f577e",
197
199
  "conformance/fixtures/preamble-v1.txt": "1e0aeef224b64477bdc13a949c3ad402e68249caf499ecdba1302371677c068b",
198
200
  "conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm": "3102ff10a0f08f60c014f82409d45ad4faf2cefa04d652a87676d3557ad64944",
@@ -204,7 +206,7 @@
204
206
  "interfaces/security-scanner.md": "e8049712b9cf7a07c786bf19f8f775f8ef9638f063f7fba5c7a8b1431b92f38e",
205
207
  "job-events.md": "84206168ac12b536d34470d62f8c8cba95dab181fee66d23203c2cf5dfbee716",
206
208
  "job-lifecycle.md": "9c429121f98a07c8795f8979ed1abc5e5334e3f89db51585a8da55c527ef855b",
207
- "plugin-author-guide.md": "03c2f666d7856eae76594484a1580968e1292f247fb10c8abcff5d99c9d38f01",
209
+ "plugin-author-guide.md": "02a040a873c51843a4c3c667a40eea3ad4c7f343e08396d7de666010ed833c88",
208
210
  "plugin-kv-api.md": "1acc69ed82433a74e35ada61d63a6d7379fb61046ff83de1e0facbe884c64704",
209
211
  "prompt-preamble.md": "9dd4f6d1bc6a425f8782fcee10cbe75909e8d64e28781fda56c2fae909b02f40",
210
212
  "schemas/annotations.schema.json": "e39990d47f53e25a1b3a5587a5714486d0b819b8eeaac10d42783a675296aee1",
@@ -213,13 +215,14 @@
213
215
  "schemas/conformance-case.schema.json": "f6d4c9fb92e79cb516eeeb9d042223572a3bd5ff8e7871a0becce13916f20cf6",
214
216
  "schemas/conformance-result.schema.json": "426998e4f5cb079778ca7d0233634667d4fbc5a7e399cc41211fabd768db8ee0",
215
217
  "schemas/execution-record.schema.json": "0d61e33f2dc1aaa4cc7337b5eac4ea8b9034022ce24bae9156c3c9f33204c250",
216
- "schemas/extensions/action.schema.json": "262272175c06a2e33c08f819a45c3ef8260276c91a9d0542fdffc932aeb32db7",
217
- "schemas/extensions/analyzer.schema.json": "c3c5fc7de89318c178d170629dc9aecd6bcce9418e9ce7cc2f8d4f5ea8db99a6",
218
- "schemas/extensions/base.schema.json": "5a593f74678485b2e2dc5b92ba9a345d6290ef7381921758a852934e09db7a64",
219
- "schemas/extensions/extractor.schema.json": "38000aea61c4d06cd136ccbcf740d3608b2c5586c7a0ff93a75e75f0df09f018",
220
- "schemas/extensions/formatter.schema.json": "816136b9b933a93fde6859110d1d68dafe39099dea589786282b90627b464f7c",
221
- "schemas/extensions/hook.schema.json": "abfa75881a4bd20c367f1eada29fb577b89752920c9f9351f8247e1340f5f2ca",
222
- "schemas/extensions/provider.schema.json": "e9f026b7c6c3a167a55c581ebb92425e10563cc3b1864b18f48462b813d01ff3",
218
+ "schemas/extensions/action.schema.json": "9f6c2427ce3f0d6fa329adf0f13129821116ab79a1d2a53f96464513ff044ebe",
219
+ "schemas/extensions/analyzer.schema.json": "f9bed3ba1305b2b64da277dccfbe760f7c058c4bb62a2d845af9c75787f159f6",
220
+ "schemas/extensions/base.schema.json": "8aaf1f8f1693d401e32feb91d4e064ff80ec7d4b0e3f15eff4202c708febaef4",
221
+ "schemas/extensions/extractor.schema.json": "5994088bf669321d2a7b8262c07cc94e05e5e2f49a235ae5389b7c66ecc1b2e1",
222
+ "schemas/extensions/formatter.schema.json": "d6d417df20260e5ddfe71f104b11a45873869706f86372c3c3c78c583e06e8d5",
223
+ "schemas/extensions/hook.schema.json": "76bf2c07f9e689b3fd1c67cbad4516a4df10604f07103759e82670e5213ddcdf",
224
+ "schemas/extensions/provider-kind.schema.json": "add3c5648721e67887eb971a76b39319628effac6315cffd51f7dcf679810740",
225
+ "schemas/extensions/provider.schema.json": "ae528d6ce1e083a2b5e3e7c6c701fbfaa8d58c79fb1f71616dc2d00c1a841cb7",
223
226
  "schemas/frontmatter/base.schema.json": "df0056a9478514a0db7a705e59868fa4f67673ac1cc9c9da979de4237cdd62a1",
224
227
  "schemas/history-stats.schema.json": "5170dec0299f3d04382a38079a27b1f26300a6b95fdb1ea0fae11050ad9f0574",
225
228
  "schemas/input-types.schema.json": "c713b768d0b0e3d0c764afb401189f7fb624a82b4e988b73aab015cf9c67c01f",
@@ -228,7 +231,7 @@
228
231
  "schemas/link.schema.json": "7fc429d03aca7e4c0b9a28241712c1aa2a5275870cea5ed938c2f97e8cccb081",
229
232
  "schemas/node.schema.json": "e5da06c9262cc0f2f7584d5733ebc1c08acd75487952ed7b4d6035fb417aaa4b",
230
233
  "schemas/plugins-doctor.schema.json": "c1d92f30fdb0080e8cd8f7dc5d43e01aae02a16640bc5eb04811c337a275de58",
231
- "schemas/plugins-registry.schema.json": "3d8a29aa045d46d70be127aabbc59831da0c71a54f18eae752676e452577ae3d",
234
+ "schemas/plugins-registry.schema.json": "cca7ae65f0c22510ea27ea5ae34e0074f5beb5871a57b005b6b831e6ceaff5c0",
232
235
  "schemas/project-config.schema.json": "7bb695476015b6b43026db78208aedf67350f4bc2c796c822fa87d0c9093b13f",
233
236
  "schemas/refresh-report.schema.json": "54519b8caf86ba84c182f9565be9b5084bc1631ae05e9217ee18f34c0039fff3",
234
237
  "schemas/report-base-deterministic.schema.json": "9d318d0181d121097c906ef3af1c52d71c782740bd04cf23418d7627ce2c3ed5",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-map/spec",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "JSON Schemas, prose contracts, and conformance suite for the skill-map specification.",
5
5
  "license": "MIT",
6
6
  "type": "module",