@skill-map/spec 0.7.1 → 0.9.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 (50) hide show
  1. package/CHANGELOG.md +777 -3
  2. package/README.md +11 -13
  3. package/architecture.md +118 -35
  4. package/cli-contract.md +38 -25
  5. package/conformance/README.md +43 -14
  6. package/conformance/cases/kernel-empty-boot.json +3 -3
  7. package/conformance/coverage.md +20 -24
  8. package/db-schema.md +61 -6
  9. package/index.json +35 -77
  10. package/interfaces/security-scanner.md +1 -1
  11. package/job-events.md +75 -1
  12. package/package.json +1 -1
  13. package/plugin-author-guide.md +409 -51
  14. package/schemas/conformance-case.schema.json +14 -5
  15. package/schemas/execution-record.schema.json +8 -8
  16. package/schemas/extensions/action.schema.json +2 -2
  17. package/schemas/extensions/base.schema.json +5 -5
  18. package/schemas/extensions/extractor.schema.json +48 -0
  19. package/schemas/extensions/formatter.schema.json +29 -0
  20. package/schemas/extensions/hook.schema.json +44 -0
  21. package/schemas/extensions/provider.schema.json +51 -0
  22. package/schemas/extensions/rule.schema.json +1 -1
  23. package/schemas/frontmatter/base.schema.json +2 -2
  24. package/schemas/link.schema.json +4 -4
  25. package/schemas/node.schema.json +4 -4
  26. package/schemas/plugins-registry.schema.json +19 -4
  27. package/schemas/project-config.schema.json +2 -2
  28. package/schemas/scan-result.schema.json +3 -3
  29. package/conformance/cases/basic-scan.json +0 -17
  30. package/conformance/cases/orphan-detection.json +0 -22
  31. package/conformance/cases/rename-high.json +0 -21
  32. package/conformance/fixtures/minimal-claude/agents/reviewer.md +0 -16
  33. package/conformance/fixtures/minimal-claude/commands/status.md +0 -17
  34. package/conformance/fixtures/minimal-claude/hooks/pre-commit.md +0 -13
  35. package/conformance/fixtures/minimal-claude/notes/architecture.md +0 -11
  36. package/conformance/fixtures/minimal-claude/skills/hello.md +0 -22
  37. package/conformance/fixtures/orphan-after/skills/keep.md +0 -13
  38. package/conformance/fixtures/orphan-before/skills/keep.md +0 -13
  39. package/conformance/fixtures/orphan-before/skills/lonely.md +0 -13
  40. package/conformance/fixtures/rename-high-after/skills/bar.md +0 -14
  41. package/conformance/fixtures/rename-high-before/skills/foo.md +0 -14
  42. package/schemas/extensions/adapter.schema.json +0 -40
  43. package/schemas/extensions/audit.schema.json +0 -47
  44. package/schemas/extensions/detector.schema.json +0 -41
  45. package/schemas/extensions/renderer.schema.json +0 -29
  46. package/schemas/frontmatter/agent.schema.json +0 -17
  47. package/schemas/frontmatter/command.schema.json +0 -39
  48. package/schemas/frontmatter/hook.schema.json +0 -29
  49. package/schemas/frontmatter/note.schema.json +0 -11
  50. package/schemas/frontmatter/skill.schema.json +0 -37
package/README.md CHANGED
@@ -7,7 +7,7 @@ This document is the **source of truth**. The reference implementation under `..
7
7
  ## What this spec defines
8
8
 
9
9
  - The **domain model**: nodes, links, issues, scan results.
10
- - The **extension contract**: six extension kinds (detector, adapter, rule, action, audit, renderer) with their input/output shapes.
10
+ - The **extension contract**: six extension kinds (provider, extractor, rule, action, formatter, hook) with their input/output shapes.
11
11
  - The **CLI contract**: verb set, flags, exit codes, JSON introspection.
12
12
  - The **persistence contract**: table catalog owned by the kernel, plugin key-value API.
13
13
  - The **job contract**: lifecycle states, event stream, prompt preamble, submit/claim/record semantics.
@@ -76,20 +76,18 @@ spec/ ← published as @skill-map/spec
76
76
  │ │
77
77
  │ ├── extensions/ ← one per extension kind; validated at plugin load
78
78
  │ │ ├── base.schema.json ┐
79
- │ │ ├── adapter.schema.json
80
- │ │ ├── detector.schema.json 7 extension schemas
81
- │ │ ├── rule.schema.json │ (base + 6 kinds)
79
+ │ │ ├── provider.schema.json
80
+ │ │ ├── extractor.schema.json 6 extension schemas
81
+ │ │ ├── rule.schema.json │ (base + 5 kinds)
82
82
  │ │ ├── action.schema.json │
83
- │ │ ├── audit.schema.json
84
- │ │ └── renderer.schema.json ┘
83
+ │ │ └── formatter.schema.json
85
84
  │ │
86
85
  │ ├── frontmatter/ ← user-authored; additionalProperties: true
87
- │ │ ├── base.schema.json
88
- │ │ ├── skill.schema.json │
89
- │ │ ├── agent.schema.json │ 6 frontmatter schemas
90
- │ │ ├── command.schema.json │ (base + 5 kinds; each kind
91
- │ │ ├── hook.schema.json │ extends base via allOf)
92
- │ │ └── note.schema.json ┘
86
+ │ │ └── base.schema.json ← universal shape; per-kind schemas live with
87
+ │ │ the Provider that emits the kind (e.g. the
88
+ │ │ built-in Claude Provider's `skill / agent /
89
+ │ │ command / hook / note` schemas live in
90
+ │ │ `src/extensions/providers/claude/schemas/`)
93
91
  │ │
94
92
  │ └── summaries/ ← kernel-controlled; additionalProperties: false
95
93
  │ ├── skill.schema.json ┐
@@ -110,7 +108,7 @@ spec/ ← published as @skill-map/spec
110
108
  ## How to read this spec
111
109
 
112
110
  - **Building a tool or plugin that consumes skill-map output?** Start with [`schemas/scan-result.schema.json`](./schemas/scan-result.schema.json) and [`schemas/node.schema.json`](./schemas/node.schema.json).
113
- - **Building a custom detector, rule, or renderer?** Read [`architecture.md`](./architecture.md), then the relevant schema under [`schemas/extensions/`](./schemas/extensions/).
111
+ - **Building a custom extractor, rule, or formatter?** Read [`architecture.md`](./architecture.md), then the relevant schema under [`schemas/extensions/`](./schemas/extensions/).
114
112
  - **Building an alternative CLI implementation?** Read [`cli-contract.md`](./cli-contract.md) and run [`conformance/`](./conformance/README.md).
115
113
  - **Integrating a new platform (adapter)?** Read [`architecture.md`](./architecture.md) §adapters, then the Claude adapter source in `../src/extensions/adapters/claude/` as a worked example.
116
114
  - **Shipping a job-running runner?** Read [`job-events.md`](./job-events.md), [`job-lifecycle.md`](./job-lifecycle.md), [`prompt-preamble.md`](./prompt-preamble.md).
package/architecture.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Architecture
2
2
 
3
- Normative description of skill-map's internal boundaries: the **kernel**, the **ports** it exposes, the **adapters** that drive and serve it, and the six **extension kinds** that live outside the kernel.
3
+ Normative description of skill-map's internal boundaries: the **kernel**, the **ports** it exposes, the **adapters** that drive and serve it, and the five **extension kinds** that live outside the kernel.
4
4
 
5
- Any conforming implementation — reference or third-party — MUST respect these boundaries. The conformance suite under [`conformance/`](./conformance/README.md) enforces them.
5
+ Any conforming implementation — reference or third-party — MUST respect these boundaries. The conformance suite under [`conformance/`](./conformance/README.md) enforces the kernel-agnostic invariants; per-Provider suites (e.g. `src/extensions/providers/claude/conformance/` for the reference impl's Claude Provider) enforce the kind-catalog cases. Both are driven via `sm conformance run`.
6
6
 
7
7
  ---
8
8
 
@@ -67,6 +67,15 @@ Discovers plugin directories, reads `plugin.json`, checks `specCompat`, dynamica
67
67
 
68
68
  Operations: `discover(scopes)`, `load(pluginPath)`, `validateManifest(json)`.
69
69
 
70
+ The loader enforces two id-uniqueness rules during discovery (see [`plugin-author-guide.md` §Plugin id uniqueness](./plugin-author-guide.md#plugin-id-uniqueness) for the author-facing summary):
71
+
72
+ 1. **Directory name == manifest id.** A plugin lives at `<root>/<id>/plugin.json`. A mismatch surfaces as status `invalid-manifest`. This rule eliminates same-root collisions by construction.
73
+ 2. **Cross-root id collision blocks both sides.** Two plugins reachable from different roots (project + global, or any `--plugin-dir` combination) that declare the same `id` BOTH receive status `id-collision`. No precedence rule applies — coherent with §Boot invariant ("no extension is privileged"). The user resolves by renaming one of them.
74
+
75
+ In addition, the loader **qualifies every extension** with its owning plugin id before registering it. The registry stores extensions under the qualified id `<plugin-id>/<extension-id>` (e.g. `claude/slash`, `core/broken-ref`, `hello-world/greet`). Authors continue to declare the short `id` in each extension manifest; the loader composes the qualified form from `manifest.id` at load time. Built-in extensions bundled with the reference impl declare their `pluginId` directly in `built-ins.ts` — `core/` for kernel-internal primitives (rules, the formatter, the external-url-counter extractor) and `claude/` for the Claude provider bundle (the Provider and its kind-aware extractors). If a plugin author injects a `pluginId` field on an extension that disagrees with `plugin.json`'s `id`, the loader emits `invalid-manifest` with a directed reason.
76
+
77
+ Each plugin (and each built-in bundle) declares a **granularity** that controls how its extensions are toggled. `granularity: 'bundle'` (the default) means the plugin id is the only enable/disable key; `granularity: 'extension'` means each extension is independently toggle-able under its qualified id. The loader's pre-import `resolveEnabled(pluginId)` short-circuit is always coarse (bundle level) — when a granularity=`extension` bundle is partially enabled, the import work proceeds and the runtime composer (the CLI's `composeScanExtensions` / `composeFormatters` in `src/cli/util/plugin-runtime.ts`) drops the disabled extensions before they reach the orchestrator. The two built-in bundles split deliberately: `claude` is granularity=`bundle` (provider-level toggle), `core` is granularity=`extension` (every kernel built-in is removable, satisfying §Boot invariant: "no extension is privileged"). See [`plugin-author-guide.md` §Granularity — bundle vs extension](./plugin-author-guide.md#granularity--bundle-vs-extension) for the author-facing summary.
78
+
70
79
  ### `RunnerPort`
71
80
 
72
81
  Executes an action against a job file. Returns a report reference (or an error) plus runner-side metrics (duration, tokens, exit code).
@@ -103,15 +112,15 @@ The kernel is the only component that MAY:
103
112
  - Dispatch extension hooks.
104
113
 
105
114
  The kernel MUST NOT:
106
- - Know which adapter produced an event.
107
- - Know which platform a node belongs to (that is the `Adapter` extension's job).
115
+ - Know which Provider produced an event.
116
+ - Know which platform a node belongs to (that is the `Provider` extension's job).
108
117
  - Contain any platform-specific branching (e.g., `if (platform === 'claude')`).
109
118
 
110
119
  ### Boot invariant
111
120
 
112
121
  **With all extensions removed, the kernel MUST boot and return an empty graph.** This is enforced by the conformance suite case `kernel-empty-boot`.
113
122
 
114
- No extension is privileged. The Claude adapter ships bundled with the reference impl but is removable, same as any third-party plugin.
123
+ No extension is privileged. The Claude Provider ships bundled with the reference impl but is removable, same as any third-party plugin.
115
124
 
116
125
  ---
117
126
 
@@ -128,25 +137,16 @@ Mode is a property of the extension as a whole, not of an individual call. **An
128
137
 
129
138
  | Kind | Modes | How mode is set |
130
139
  |---|---|---|
131
- | **Detector** | deterministic / probabilistic | declared in manifest (`mode` field, optional; defaults to `deterministic`) |
140
+ | **Extractor** | deterministic / probabilistic | declared in manifest (`mode` field, optional; defaults to `deterministic`) |
132
141
  | **Rule** | deterministic / probabilistic | declared in manifest (`mode` field, optional; defaults to `deterministic`) |
133
142
  | **Action** | deterministic / probabilistic | declared in manifest (`mode` field, **required** — no default) |
134
- | **Audit** | deterministic / probabilistic | derived from `composes[]` (see below) |
135
- | **Adapter** | deterministic-only | implicit; `mode` field MUST NOT appear |
136
- | **Renderer** | deterministic-only | implicit; `mode` field MUST NOT appear |
137
-
138
- Adapter and Renderer are locked to deterministic because they sit at the **boundaries** of the system. An adapter resolves `path → kind` during boot; probabilistic classification would make the boot phase slow, costly, and non-reproducible. A renderer must produce diffable output (`sm scan` snapshots round-trip in CI). Probabilistic narrators of the graph are a valid product but they live in jobs and emit Findings, not in renderers.
143
+ | **Hook** | deterministic / probabilistic | declared in manifest (`mode` field, optional; defaults to `deterministic`) |
144
+ | **Provider** | deterministic-only | implicit; `mode` field MUST NOT appear |
145
+ | **Formatter** | deterministic-only | implicit; `mode` field MUST NOT appear |
139
146
 
140
- ### Audit · derived mode
147
+ Provider and Formatter are locked to deterministic because they sit at the **boundaries** of the system. A Provider resolves `path → kind` during boot; probabilistic classification would make the boot phase slow, costly, and non-reproducible. A formatter must produce diffable output (`sm scan` snapshots round-trip in CI). Probabilistic narrators of the graph are a valid product but they live in jobs and emit Findings, not in formatters.
141
148
 
142
- An audit is a **composer**: it declares which primitives it runs and the kernel handles dispatch. The audit manifest does NOT carry a `mode` field. Instead it declares `composes[]` the rule and action references the audit executes in sequence. At load time the kernel resolves each entry and computes the audit's **effective mode**:
143
-
144
- - If every composed primitive is `deterministic` → the audit's effective mode is `deterministic`. Runs synchronously inside `sm audit <id>`.
145
- - If any composed primitive is `probabilistic` → the audit's effective mode is `probabilistic`. Dispatches as a job via `sm job submit audit:<id>`.
146
-
147
- A dangling reference in `composes[]` (the id doesn't resolve, the kind is wrong, or the primitive is disabled) is a **load-time error**. The audit is rejected with status `invalid-manifest`, not silently skipped. This matches the rule already in place for `defaultRefreshAction`. Declaring `mode` directly on an audit manifest is also a load-time error.
148
-
149
- The effective mode is exposed to the UI and to `sm audit show <id>` so consumers can preview cost before invoking.
149
+ > **Naming note `Provider` vs hexagonal `adapter`.** The extension kind formerly named `Adapter` is now `Provider`. The hexagonal-architecture term `adapter` (driving / driven adapters that implement ports — `RunnerPort.adapter`, `StoragePort.adapter`, `FilesystemPort.adapter`, `PluginLoaderPort.adapter`) is unchanged: those live in `kernel/adapters/` and are internal to the impl. A `Provider` is an **extension** authored by plugins; an **adapter** in the hexagonal sense is a **port implementation** internal to the kernel package. The two concepts share an architectural lineage (both bridge two worlds) but live in deliberately disjoint namespaces so plugin authors and impl maintainers never confuse them.
150
150
 
151
151
  ### When each mode runs
152
152
 
@@ -169,22 +169,76 @@ Six kinds, all first-class, all loaded through the same registry. Each kind has
169
169
 
170
170
  | Kind | Role | Input | Output |
171
171
  |---|---|---|---|
172
- | **Adapter** | Recognizes a platform. Decides which files are nodes and what kind they are. Declares per-kind `defaultRefreshAction` (an action id that drives the probabilistic-refresh surface). Deterministic-only. | Filesystem walk results, candidate path. | `{ kind, adapter } \| null`. |
173
- | **Detector** | Extracts signals from a node body. Dual-mode: `deterministic` runs in scan, `probabilistic` runs in jobs. | Parsed node (frontmatter + body). | `Link[]`. |
172
+ | **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). Also declares the filesystem `explorationDir` where its content lives. Deterministic-only. | Filesystem walk results, candidate path. | `{ kind, provider } \| null`. |
173
+ | **Extractor** | Extracts signals from a node body. Dual-mode: `deterministic` runs in scan, `probabilistic` runs in jobs. 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). |
174
174
  | **Rule** | Evaluates the graph. Dual-mode: `deterministic` runs in `sm check`, `probabilistic` runs in jobs. | Full graph (nodes + links). | `Issue[]`. |
175
175
  | **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. |
176
- | **Audit** | Workflow that composes rules and actions. Effective mode is derived from `composes[]` — deterministic if all composed primitives are deterministic, probabilistic otherwise. Produces a structured report. | Graph + optional scope filter. | Audit report (hardcoded shape, kind-specific). |
177
- | **Renderer** | Serializes the graph. Deterministic-only. | Graph + optional filter. | String (ASCII / Mermaid / DOT / JSON / user-defined). |
176
+ | **Formatter** | Serializes the graph. Deterministic-only. | Graph + optional filter. | String (ASCII / Mermaid / DOT / JSON / user-defined). |
177
+ | **Hook** | Reacts declaratively to one of eight curated lifecycle events (`scan.started`, `scan.completed`, `extractor.completed`, `rule.completed`, `action.completed`, `job.spawning`, `job.completed`, `job.failed`). 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, or job-scoped) plus an optional declarative `filter` map. | `void` (reactions are side effects). |
178
+
179
+ ### Provider · `kinds` catalog
180
+
181
+ Every `Provider` extension MUST declare a map `kinds: { <kind>: { schema: string, defaultRefreshAction: string } }` covering every `kind` it can classify into. Each entry has two required fields:
182
+
183
+ - **`schema`** — path to the kind's frontmatter JSON Schema, relative to the Provider's package directory. The schema MUST extend the spec's universal [`frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) via `allOf` + `$ref` to base's `$id` so cross-package resolution works without copying base into every Provider. The kernel registers each Provider's schemas with AJV at scan boot and validates each node's frontmatter against the entry that matches its classified kind.
184
+ - **`defaultRefreshAction`** — qualified action id (`<plugin-id>/<action-id>`) the UI's probabilistic-refresh surface (`🧠 prob`) dispatches for nodes of this kind. The referenced action MUST exist in the registry by the time the graph is queried; a dangling reference is a load-time error for the Provider (status `invalid-manifest`). Consumers dispatch `sm job submit <defaultRefreshAction> -n <nodePath>` when the user asks for a probabilistic refresh. Implementations MAY allow plugins to override the default per-node via `metadata.refreshAction`, but the Provider default is normative.
185
+
186
+ 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)`. Spec 0.8.0 (Phase 3 of plug-in model overhaul) replaced two earlier fields (`emits: string[]` and a flat `defaultRefreshAction: { <kind>: actionId }`) with this richer map; the catalog also subsumes per-kind frontmatter schemas, which previously lived in spec under `schemas/frontmatter/<kind>.schema.json`.
187
+
188
+ ### Provider · `explorationDir`
189
+
190
+ Every `Provider` extension MUST declare an `explorationDir: string` naming the filesystem directory (relative to user home or project root) where its content lives. Examples: `'~/.claude'` for the Claude Provider, `'~/.cursor'` for a hypothetical Cursor Provider. The kernel walks this directory during boot/scan to discover nodes; the Provider's `globs` (if declared) refines what to match inside. `sm doctor` (and `sm plugins doctor`) validates the directory exists; missing directory yields a non-blocking warning so the user sees the gap without the load failing — the Provider may legitimately precede installation of its platform.
191
+
192
+ ### Extractor · output callbacks
193
+
194
+ The `Extractor` runtime contract is `extract(ctx) → void`. The extractor emits its work through three callbacks the kernel binds onto `ctx`:
195
+
196
+ - `ctx.emitLink(link)` — append a `Link` to the kernel's `links` table. The kernel validates the link against the extractor's declared `emitsLinkKinds` before persistence; off-contract links are dropped and surface as `extension.error` events. URL-shaped targets (`http(s)://…`) are partitioned out into `node.externalRefsCount` and never persisted.
197
+ - `ctx.enrichNode(partial)` — merge canonical, kernel-curated properties onto the current node's enrichment layer (persisted into [`node_enrichments`](./db-schema.md#node_enrichments)). **Strictly separate from the author-supplied frontmatter** (the latter remains immutable across scans). The enrichment layer is the right home for kernel-derived facts (e.g. computed titles, summaries, signals from probabilistic extractors) without polluting what the user wrote on disk. See §Enrichment layer below for the full lifecycle (per-extractor attribution, stale tracking, refresh verbs).
198
+ - `ctx.store` — plugin-scoped persistence. Optional, present only when the plugin declares `storage.mode` in `plugin.json`. Shape depends on the mode (`KvStore` for mode A, scoped `Database` for mode B). See [`plugin-kv-api.md`](./plugin-kv-api.md). The plugin author MAY opt into shape validation for their own writes by declaring `storage.schema` (Mode A) or `storage.schemas` (Mode B) in the manifest — JSON Schemas the kernel AJV-compiles at load time and runs against every `ctx.store.set(key, value)` / `ctx.store.write(table, row)` call. Absent = permissive (status quo). `emitLink` and `enrichNode` keep their universal validation against `link.schema.json` / `node.schema.json` regardless of this opt-in. See [`plugin-author-guide.md` §`outputSchema`](./plugin-author-guide.md#outputschema--opt-in-correctness-for-custom-storage-writes).
199
+
200
+ Probabilistic extractors additionally receive `ctx.runner` (the `RunnerPort`) for LLM dispatch.
201
+
202
+ ### Extractor · enrichment layer
203
+
204
+ `ctx.enrichNode(partial)` is the only writable surface the Extractor pipeline has on a node. The author's frontmatter on `scan_nodes.frontmatter_json` is read-only from any Extractor — that contract holds for both deterministic and probabilistic extractors. Implementations MUST:
178
205
 
179
- ### Adapter · `defaultRefreshAction`
206
+ - Persist enrichments into a per-`(node, extractor)` table (the reference impl uses [`node_enrichments`](./db-schema.md#node_enrichments)) so attribution survives across scans.
207
+ - Preserve the author frontmatter byte-for-byte through every scan and refresh; the enrichment overlay is a SEPARATE store.
208
+ - Track stale state for probabilistic rows: when the scan loop detects `body_hash_at_enrichment != node.body_hash` for a probabilistic enrichment, mark the row stale (NOT delete it — the LLM cost is preserved). Deterministic enrichments do not need stale tracking — they regenerate via the §Extractor · fine-grained scan cache contract.
180
209
 
181
- Every `Adapter` extension MUST declare a map `defaultRefreshAction: { <kind>: <actionId> }` covering every `kind` it emits. The referenced action MUST exist in the registry by the time the graph is queried; a dangling reference is a load-time error for the adapter. Consumers (CLI `🧠 prob` buttons in `sm show`, Web UI inspector) dispatch `sm job submit <defaultRefreshAction[kind]> -n <nodePath>` when the user asks for a probabilistic refresh on a node. Implementations MAY allow plugins to override the default per-node via `metadata.refreshAction`, but the adapter default is normative.
210
+ Read-side merge (`mergeNodeWithEnrichments` in the reference impl):
182
211
 
183
- ### Detector · trigger normalization
212
+ 1. Filter to non-stale enrichments for the target node.
213
+ 2. Sort by `enriched_at` ASC.
214
+ 3. Spread-merge each `value` over the author frontmatter (last-write-wins per field).
184
215
 
185
- Detectors that emit invocation-style links (slashes, at-directives, command names) populate the `link.trigger` block defined in [`schemas/link.schema.json`](./schemas/link.schema.json):
216
+ Rules / `sm check` / `sm export` consume `node.frontmatter` directly (deterministic CI-safe baseline); enrichment consumption is opt-in by the caller. Stale visibility is also opt-in (`includeStale: true` in the merge helper) so the UI can render a "stale (last value: …)" marker without polluting the deterministic merge.
186
217
 
187
- - `originalTrigger` the exact source text the detector saw, byte-for-byte. Used only for display.
218
+ Refresh verbs (`sm refresh <node>` and `sm refresh --stale`) re-run the Extractor pipeline against a node or the stale set and upsert fresh enrichment rows — see [`cli-contract.md` §Scan](./cli-contract.md#scan).
219
+
220
+ ### Extractor · `applicableKinds` filter
221
+
222
+ Extractors MAY declare an optional `applicableKinds: string[]` on their manifest. When declared, the kernel filters fail-fast: `extract()` is invoked **only** for nodes whose `kind` appears in the list. The skip happens BEFORE the extractor context is built so a probabilistic extractor wastes zero LLM cost — and a deterministic extractor zero CPU — on inapplicable nodes. Absent (`undefined`) is the default and means "applies to every kind"; there is no wildcard syntax. An empty array (`[]`) is invalid (`minItems: 1` in the schema). Unknown kinds (no installed Provider declares them in its `kinds` catalog) are non-blocking: the extractor keeps `loaded` status and `sm plugins doctor` surfaces an informational warning so the author sees typos and missing-Provider cases, but the doctor's exit code is NOT promoted by this warning. See [`plugin-author-guide.md` §Extractor `applicableKinds`](./plugin-author-guide.md#extractor-applicablekinds--narrow-the-pipeline) for the full author-side contract.
223
+
224
+ ### Extractor · fine-grained scan cache
225
+
226
+ Implementations MAY maintain a per-`(node, extractor)` cache so that on `sm scan --changed` the orchestrator can skip rerunning an Extractor against an unchanged body when that specific Extractor already ran against the same body hash. The reference impl persists the cache in [`scan_extractor_runs`](./db-schema.md#scan_extractor_runs).
227
+
228
+ The contract the cache MUST satisfy (engine-agnostic):
229
+
230
+ - A node-level cache hit (body+frontmatter unchanged) is upgraded to a full skip ONLY when every currently-registered Extractor that applies to the node's kind has a recorded run against the prior body hash.
231
+ - A new Extractor registered between scans MUST run on the cached node — its absence from the cache is the canonical signal. The rest of the cache (existing Extractors against the same body) is preserved.
232
+ - An Extractor uninstalled between scans MUST have its cache rows removed and its sole-source links dropped. Links whose `sources` mix the uninstalled Extractor's short id with a still-cached Extractor's short id MUST be reshaped: the obsolete short id is stripped from the array and the link survives with the cached attribution intact. The persisted audit trail therefore never references a removed contributor.
233
+ - The cache is transparent to plugin authors. An Extractor cannot opt out and cannot inspect the cache; its only obligation is to be deterministic for a given body input (probabilistic Extractors run as jobs, never in scan).
234
+
235
+ This invariant is the difference between a free and a paid scan for the probabilistic Extractor model: re-running an LLM Extractor against an unchanged body would be both expensive and non-reproducible.
236
+
237
+ ### Extractor · trigger normalization
238
+
239
+ Extractors that emit invocation-style links (slashes, at-directives, command names) populate the `link.trigger` block defined in [`schemas/link.schema.json`](./schemas/link.schema.json):
240
+
241
+ - `originalTrigger` — the exact source text the extractor saw, byte-for-byte. Used only for display.
188
242
  - `normalizedTrigger` — the output of the pipeline below. Used for equality and collision detection — the built-in `trigger-collision` rule keys on this field.
189
243
 
190
244
  Both fields MUST be present whenever `link.trigger` is non-null. Implementations MUST produce byte-identical `normalizedTrigger` output for byte-identical input across platforms and locales.
@@ -200,7 +254,7 @@ Applied in exactly this order:
200
254
  5. **Collapse whitespace** — runs of two or more spaces become one.
201
255
  6. **Trim** — strip leading and trailing whitespace.
202
256
 
203
- Characters outside the separator set that are not letters or digits (e.g. `/`, `@`, `:`, `.`) are **preserved**. Stripping them is the detector's concern, not the normalizer's — the normalizer operates on whatever the detector classifies as "the trigger text". This keeps namespaced invocations like `/skill-map:explore` or `@my-plugin/foo` comparable in their intended form.
257
+ Characters outside the separator set that are not letters or digits (e.g. `/`, `@`, `:`, `.`) are **preserved**. Stripping them is the extractor's concern, not the normalizer's — the normalizer operates on whatever the extractor classifies as "the trigger text". This keeps namespaced invocations like `/skill-map:explore` or `@my-plugin/foo` comparable in their intended form.
204
258
 
205
259
  #### Examples
206
260
 
@@ -212,9 +266,38 @@ Characters outside the separator set that are not letters or digits (e.g. `/`, `
212
266
  | ` hacer review ` | `hacer review` |
213
267
  | `Clúster` | `cluster` |
214
268
  | `/MyCommand` | `/mycommand` |
215
- | `@FooDetector` | `@foodetector` |
269
+ | `@FooExtractor` | `@fooextractor` |
216
270
  | `skill-map:explore` | `skill map:explore` |
217
271
 
272
+ ### Hook · curated trigger set
273
+
274
+ Hooks subscribe declaratively to a curated set of kernel lifecycle events and react to them. Reaction-only by design: a hook cannot mutate the pipeline, block emission, or alter outputs. The hookable trigger set is intentionally small — eight events out of the full [`job-events.md`](./job-events.md) catalog. Other events (per-node `scan.progress`, `model.delta`, `run.*`, `job.claimed`, `job.callback.received`) are deliberately NOT hookable: too verbose for a reactive surface, internal to the runner, or covered elsewhere. Declaring a trigger outside the curated set yields `invalid-manifest` at load time.
275
+
276
+ | Trigger | When it fires | Payload (key fields) | Hook scope |
277
+ |---|---|---|---|
278
+ | `scan.started` | Once at the start of every `sm scan` invocation. | `roots: string[]`. | Pre-scan setup (cache warm-up, telemetry init). |
279
+ | `scan.completed` | Once at the end of every `sm scan` invocation. | `stats: { filesWalked, nodesCount, linksCount, issuesCount, durationMs }`. | Post-scan reaction (Slack notification, CI gate, summary). |
280
+ | `extractor.completed` | Once per registered Extractor, after the full walk completes. Aggregated, NOT per-node. | `extractorId: string` (qualified). | Per-Extractor metrics, audit. |
281
+ | `rule.completed` | Once per Rule, after every issue has been validated. | `ruleId: string` (qualified). | Per-Rule alerting, downstream tooling. |
282
+ | `action.completed` | Once per Action invocation, after the report has been recorded. | `actionId: string` (qualified), `node`, `jobResult`. | Per-Action notification, integration glue. |
283
+ | `job.spawning` | Pre-spawn of a runner subprocess (job subsystem; Step 10). | `jobId`, `actionId`, spawn metadata. | Pre-flight checks, audit logging. |
284
+ | `job.spawning`, `job.completed`, `job.failed` | The three job-lifecycle hookables; same payload shapes as the [`job-events.md`](./job-events.md) entries of the same name. | See [`job-events.md` §Event catalog](./job-events.md#event-catalog). | Most common Hook surface (notifications, retries, billing). |
285
+
286
+ A hook MAY narrow further with an optional declarative `filter` map: keys are payload field paths (top-level only in v0.x); values are the literal expected match. The dispatcher walks `event.data` for each declared key and short-circuits the invocation when any value disagrees. Examples:
287
+
288
+ - `filter: { extractorId: 'core/external-url-counter' }` — invoke only when THIS extractor finishes.
289
+ - `filter: { actionId: 'claude/skill-summarizer' }` — invoke only for one Action.
290
+ - `filter: { reason: 'runner-error' }` (on `job.failed`) — invoke only when the runner crashed.
291
+
292
+ #### Mode semantics
293
+
294
+ - **Deterministic** (default): the hook's `on(ctx)` runs in-process during the dispatch of the matching event, synchronously between the event's emission and the next pipeline step. Errors are caught by the dispatcher (logged through a synthetic `extension.error` event with kind `hook-error`) and NEVER block the main pipeline. A buggy hook degrades gracefully — the scan continues.
295
+ - **Probabilistic**: the hook is enqueued as a job. Until the job subsystem ships at Step 10, probabilistic hooks load but skip dispatch with a stderr advisory. The hook still surfaces in `sm plugins list` / `sm plugins doctor`; it just does not fire today.
296
+
297
+ #### Cross-extension impact
298
+
299
+ Hooks introduce no new persisted state and do NOT participate in the deterministic scan cache (A.9). A scan that re-runs against an unchanged corpus dispatches `scan.started` / `scan.completed` exactly as before; subscribed hooks fire on every scan regardless of cache hit / miss. Hooks that need cache-aware behaviour MUST inspect their own state via `ctx.store` (declared in their plugin's manifest).
300
+
218
301
  ### Contract rules
219
302
 
220
303
  1. An extension declares its kind in its module export and its manifest. Kind mismatch → load-error.
@@ -226,7 +309,7 @@ Characters outside the separator set that are not letters or digits (e.g. `/`, `
226
309
  ### Locality
227
310
 
228
311
  - **Drop-in**: extensions live inside plugins, discovered at boot from `.skill-map/plugins/<id>/` and `~/.skill-map/plugins/<id>/`.
229
- - **Built-in**: the reference impl bundles a default extension set (one adapter, three detectors, three rules, one audit, one renderer). These are loaded from `src/extensions/` and are indistinguishable from plugin-supplied extensions from the kernel's point of view.
312
+ - **Built-in**: the reference impl bundles a default extension set (one Provider, four extractors, five rules, one formatter, zero hooks). The fifth rule, `core/validate-all`, replays every scanned node and link through the authoritative spec schemas via AJV — the kernel-side guard against persisting non-conforming graph rows. The Hook kind has no built-ins at this bump; the kind exists so plugins can subscribe (concrete built-in hooks land separately when demand surfaces). These are loaded from `src/extensions/` and are indistinguishable from plugin-supplied extensions from the kernel's point of view.
230
313
 
231
314
  ---
232
315
 
@@ -313,10 +396,10 @@ This is what makes "CLI-first" a coherent rule: every CLI verb is a kernel funct
313
396
 
314
397
  The **port list** is stable as of spec v1.0.0. Adding a sixth port is a major bump.
315
398
 
316
- The **extension kind list** (6 kinds) is stable as of spec v1.0.0. Adding a seventh kind is a major bump.
399
+ The **extension kind list** (5 kinds) is stable as of spec v1.0.0. Adding a sixth kind is a major bump.
317
400
 
318
- The **execution modes** (`deterministic` / `probabilistic`) and the per-kind mode capability matrix above are stable as of spec v1.0.0. Adding a third mode, changing which kinds are dual-mode, or changing the audit's mode-derivation rule is a major bump. Renaming or repurposing the mode enum values is a major bump.
401
+ The **execution modes** (`deterministic` / `probabilistic`) and the per-kind mode capability matrix above are stable as of spec v1.0.0. Adding a third mode or changing which kinds are dual-mode is a major bump. Renaming or repurposing the mode enum values is a major bump.
319
402
 
320
403
  The **dependency rules** above are stable as of spec v1.0.0. Relaxing any is a major bump; tightening (forbidding an allowed import) is a minor bump.
321
404
 
322
- The **Detector · trigger normalization** pipeline (six steps, in order) is stable from the next spec release. Adding a new step at the end is a minor bump; reordering, removing, or changing any existing step (including the character classes in step 4) is a major bump. Implementations that produce different `normalizedTrigger` output for equivalent input are non-conforming.
405
+ The **Extractor · trigger normalization** pipeline (six steps, in order) is stable from the next spec release. Adding a new step at the end is a minor bump; reordering, removing, or changing any existing step (including the character classes in step 4) is a major bump. Implementations that produce different `normalizedTrigger` output for equivalent input are non-conforming.
package/cli-contract.md CHANGED
@@ -62,8 +62,8 @@ All verbs use this shared table. Additional codes MAY be defined per-verb (docum
62
62
  | Code | Meaning | When emitted |
63
63
  |---|---|---|
64
64
  | `0` | OK | Command completed, no issues at or above the configured severity threshold. |
65
- | `1` | Issues found | Command completed, but deterministic issues at `error` severity exist. Applies to `sm scan`, `sm check`, `sm audit run`, `sm doctor`. |
66
- | `2` | Operational error | Bad flags, missing DB, unreadable file, corrupt config, unhandled exception. Accompanied by an error message on stderr. |
65
+ | `1` | Issues found | Command completed, but deterministic issues at `error` severity exist. Applies to `sm scan`, `sm check`, `sm doctor`. |
66
+ | `2` | Operational error | Bad flags, missing DB, unreadable file, corrupt config, runtime / environment mismatch (e.g. wrong Node version, missing native dependency), unhandled exception. Accompanied by an error message on stderr. |
67
67
  | `3` | Duplicate conflict | Job submission refused because an active duplicate exists (same `action + version + node + contentHash`). Returned by `sm job submit`. |
68
68
  | `4` | Nonce mismatch | `sm record` called with an `id`/`nonce` pair that does not match. |
69
69
  | `5` | Not found | A named resource does not exist (node id, job id, plugin id, config key). |
@@ -72,6 +72,20 @@ Codes 6–15 are reserved. Codes ≥ 16 are free for verb-specific use.
72
72
 
73
73
  ---
74
74
 
75
+ ## Dry-run
76
+
77
+ A verb that exposes `-n` / `--dry-run` MUST honour the following contract:
78
+
79
+ - **No observable side effects.** The command MUST NOT mutate the database, the filesystem, the config, the network, or spawn external processes. Read-only operations needed to compute the preview (e.g. loading the prior `ScanResult`, reading existing config files, listing FS entries) ARE permitted.
80
+ - **No auto-provisioning.** A dry-run MUST NOT create directories, schema files, or DBs that would not exist after the command. If the operation would create a `.skill-map/` scope, dry-run only previews the creation; the directory must NOT appear on disk.
81
+ - **Output mirrors the live mode** — same shape, same fields, same `--json` schema — except that human-readable output explicitly indicates the dry-run state ("would persist …", "would create …", "would delete …", or a clear "(dry-run)" suffix) and machine-readable output sets a top-level `dryRun: true` field where applicable.
82
+ - **Exit codes mirror the live mode.** Same exit code table; the dry-run posture does not introduce new codes. A dry-run that surfaces an error severity (e.g. "scan would emit an error-severity issue") still exits `1`; a dry-run that fails to read the input still exits `2`.
83
+ - **Dry-run MUST NOT depend on `--yes` / `--force`.** Verbs that offer interactive confirmation for destructive operations MUST allow `--dry-run` to bypass the prompt entirely (no confirmation needed when nothing is being destroyed).
84
+
85
+ Dry-run is **per-verb opt-in**. The flag is not global; verbs that do not declare it MUST reject `--dry-run` as an unknown option (exit `2`), the same as any other unknown flag. The verb catalog below names every verb that exposes the flag and what its preview looks like.
86
+
87
+ ---
88
+
75
89
  ## Verb catalog
76
90
 
77
91
  ### Setup & state
@@ -85,7 +99,7 @@ Bootstrap the current scope.
85
99
  - Runs migrations.
86
100
  - Runs a first scan.
87
101
 
88
- Flags: `--no-scan` (skip the first scan), `--force` (rewrite an existing config).
102
+ Flags: `--no-scan` (skip the first scan), `--force` (rewrite an existing config), `-n` / `--dry-run` (preview the scope provisioning — would-create lines for every directory and file the live invocation would write — without touching the filesystem; respects `--force` for the "would-overwrite" preview).
89
103
 
90
104
  Exit: 0 on success, 2 on failure.
91
105
 
@@ -112,7 +126,7 @@ Diagnostic report:
112
126
  - Orphan job files (count).
113
127
  - Plugins in error state (list).
114
128
  - LLM runner availability (`claude` binary on PATH, version).
115
- - Detected platform adapters that matched nothing.
129
+ - Detected Providers that matched nothing, or whose `explorationDir` does not exist on disk (non-blocking warning).
116
130
 
117
131
  Exit: 0 if all green, 1 if warnings, 2 if any `error`-level problem.
118
132
 
@@ -167,8 +181,10 @@ Keys are dot-paths (`jobs.minimumTtlSeconds`, `scan.tokenize`). Unknown keys →
167
181
  | `sm scan -n <node.path>` | Partial scan: one node. |
168
182
  | `sm scan --changed` | Incremental: only files changed since last scan (mtime heuristic). |
169
183
  | `sm scan --watch` | Long-running: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`. |
170
- | `sm scan --compare-with <path>` | Delta report: compare current state with a saved scan dump. Does not modify the DB. |
184
+ | `sm scan compare-with <dump> [roots...]` | Delta report: run a fresh scan in memory and compare against the saved `ScanResult` dump at `<dump>`. Read-only — does not modify the DB. Exit `0` on empty delta, `1` on any drift, `2` on operational error (missing or malformed dump, schema violation). |
171
185
  | `sm watch [roots...]` | Long-running watcher. Same semantics as `sm scan --watch`, exposed as a top-level verb because the watcher is a loop, not a one-shot scan. |
186
+ | `sm refresh <node.path>` | Re-run Extractors against a single node and upsert their outputs into the universal enrichment layer (`node_enrichments`, see [`db-schema.md`](./db-schema.md#node_enrichments)). Stub state until the job subsystem ships at Step 10: deterministic Extractors run for real and persist; probabilistic Extractors emit a stderr advisory and skip without touching their stale rows. Exit `0` on success (with possible stub advisory), `2` on failure, `5` if the node is not in the persisted scan. |
187
+ | `sm refresh --stale` | Batch form of `sm refresh <node>` — refreshes every node carrying at least one stale probabilistic enrichment row. Same stub caveat: deterministic Extractors persist; probabilistic Extractors skip with a stderr advisory. Exit `0` (including when the stale set is empty — prints a "nothing to do" advisory). |
172
188
 
173
189
  `--json` output conforms to `schemas/scan-result.schema.json`. `sm watch` (and `sm scan --watch`) emit one ScanResult per batch — under `--json` this is an `ndjson` stream of ScanResult documents.
174
190
 
@@ -183,10 +199,10 @@ Exit: 0 on clean (or clean watcher shutdown), 1 if error-severity issues exist (
183
199
  | Command | Purpose |
184
200
  |---|---|
185
201
  | `sm list [--kind <k>] [--issue] [--sort-by ...] [--limit N]` | Tabular listing. `--json` emits an array conforming to `node.schema.json`. |
186
- | `sm show <node.path>` | Node detail: weight (bytes/tokens triple-split), frontmatter, links in/out, issues, findings, summary. `--json` emits a detail object with the raw link rows. Pretty output groups identical-shape links (same endpoint, kind, normalized trigger) onto one line and lists the union of detector ids in a `sources:` field; the section header reports both the raw row count and the unique-after-grouping count, e.g. `Links out (12, 9 unique)`. Storage keeps one row per detector (`scan_links` is unchanged) — the grouping is purely a read-time presentation choice. |
187
- | `sm check` | Print all current issues. Equivalent to `sm scan --json \| jq '.issues'` but faster (reads from DB). |
202
+ | `sm show <node.path>` | Node detail: weight (bytes/tokens triple-split), frontmatter, links in/out, issues, findings, summary. `--json` emits a detail object with the raw link rows. Pretty output groups identical-shape links (same endpoint, kind, normalized trigger) onto one line and lists the union of extractor ids in a `sources:` field; the section header reports both the raw row count and the unique-after-grouping count, e.g. `Links out (12, 9 unique)`. Storage keeps one row per extractor (`scan_links` is unchanged) — the grouping is purely a read-time presentation choice. |
203
+ | `sm check [-n <node.path>] [--rules <ids>] [--include-prob] [--async]` | Print all current issues. Equivalent to `sm scan --json \| jq '.issues'` but faster (reads from DB). `-n` restricts to issues whose `nodeIds` include the path; `--rules <ids>` accepts a comma-separated list of qualified or short rule ids and restricts the issue read accordingly. Default behaviour is deterministic-only (CI-safe, status quo). `--include-prob` is the opt-in flag for probabilistic Rule dispatch (spec § A.7): the verb loads the plugin runtime, finds Rules with `mode === 'probabilistic'` (filtered by `--rules` if set), and emits a stderr advisory naming the rule ids. Full prob dispatch requires the job subsystem (Step 10); until then `--include-prob` is a stub — prob rules never produce issues, never alter the exit code, and `--async` (reserved companion: returns job ids without waiting once jobs land) is a no-op the advisory simply mentions. The flag does NOT extend to `sm scan` or `sm list`. |
188
204
  | `sm findings [--kind ...] [--since ...] [--threshold <n>]` | Probabilistic findings (injection, stale summaries, low confidence). `--json` emits an array of finding objects. |
189
- | `sm graph [--format ascii\|mermaid\|dot]` | Render the full graph via the named renderer. |
205
+ | `sm graph [--format ascii\|mermaid\|dot]` | Render the full graph via the named formatter. |
190
206
  | `sm export <query> --format json\|md\|mermaid` | Filtered export. Query syntax is implementation-defined pre-1.0. |
191
207
  | `sm orphans` | History rows whose target node is missing. |
192
208
  | `sm orphans reconcile <orphan.path> --to <new.path>` | Migrate history rows from the old path to the new one after a rename. Use case: the scan's rename heuristic missed a match (semantic-only rename, body rewrite) and the user wants to stitch history manually. |
@@ -277,33 +293,22 @@ Authentication: the nonce is the sole credential. An implementation MUST reject
277
293
 
278
294
  ---
279
295
 
280
- ### Audits
281
-
282
- | Command | Purpose |
283
- |---|---|
284
- | `sm audit list` | Registered audits. |
285
- | `sm audit run <id>` | Execute. `--json` emits the audit report per the audit's declared shape. |
286
-
287
- Exit: 0 if audit returns "pass"; 1 if audit returns "fail" with at least one error-severity finding; 2 on operational error.
288
-
289
- ---
290
-
291
296
  ### Database
292
297
 
293
298
  See `db-schema.md` for the table catalog.
294
299
 
295
300
  | Command | Purpose |
296
301
  |---|---|
297
- | `sm db reset` | Drop `scan_*` only. Keep `state_*` and `config_*`. Non-destructive — no confirmation required. |
298
- | `sm db reset --state` | Drop `scan_*` AND `state_*` (including `state_plugin_kvs` and every `plugin_<id>_*` table). Keep `config_*`. Destructive. |
299
- | `sm db reset --hard` | Delete the DB file entirely. Keep the plugins folder so the next boot re-discovers them. Destructive. |
302
+ | `sm db reset [-n / --dry-run]` | Drop `scan_*` only. Keep `state_*` and `config_*`. Non-destructive — no confirmation required. `--dry-run` prints the row counts that would be deleted per `scan_*` table without touching the DB. |
303
+ | `sm db reset --state [-n / --dry-run]` | Drop `scan_*` AND `state_*` (including `state_plugin_kvs` and every `plugin_<id>_*` table). Keep `config_*`. Destructive. `--dry-run` previews the deletion without touching the DB. |
304
+ | `sm db reset --hard [-n / --dry-run]` | Delete the DB file entirely. Keep the plugins folder so the next boot re-discovers them. Destructive. `--dry-run` reports the file path and size that would be deleted without unlinking it. |
300
305
  | `sm db backup [--out <path>]` | WAL checkpoint + file copy. |
301
- | `sm db restore <path>` | Swap the DB. |
306
+ | `sm db restore <path> [-n / --dry-run]` | Swap the DB. Destructive. `--dry-run` validates the source file (existence, header, schema version) and reports what would be overwritten without touching the live DB. |
302
307
  | `sm db shell` | Interactive SQL shell (implementations backed by SQLite use `sqlite3`; others use equivalent). |
303
308
  | `sm db dump [--tables ...]` | SQL dump. |
304
309
  | `sm db migrate [--dry-run \| --status \| --to <n> \| --kernel-only \| --plugin <id> \| --no-backup]` | Migration controls. |
305
310
 
306
- Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interactive confirmation unless `--yes` (non-interactive mode for scripts) or `--force` (alias, kept for backward compatibility) is passed. `sm db reset` without a modifier is non-destructive and never prompts.
311
+ Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interactive confirmation unless `--yes` (non-interactive mode for scripts) or `--force` (alias, kept for backward compatibility) is passed. `sm db reset` without a modifier is non-destructive and never prompts. **`--dry-run` short-circuits the confirmation prompt entirely** (per §Dry-run rule: dry-run MUST NOT depend on `--yes` / `--force`).
307
312
 
308
313
  ---
309
314
 
@@ -322,6 +327,14 @@ Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interacti
322
327
 
323
328
  These two formats are NORMATIVE: any change to verbs, flags, or exit codes MUST reflect in `--format json` output immediately. Third-party consumers rely on this.
324
329
 
330
+ ### Conformance
331
+
332
+ | Command | Purpose |
333
+ |---|---|
334
+ | `sm conformance run [--scope spec\|provider:<id>\|all]` | Run the conformance suite. `--scope spec` runs only the kernel-agnostic cases bundled with `@skill-map/spec` (default fixture: `preamble-v1.txt`, case: `kernel-empty-boot`). `--scope provider:<id>` runs only the named built-in Provider's suite (today: `provider:claude`). `--scope all` (default) runs every visible scope in registry order. Exit 0 on a clean sweep; exit 1 if any case failed; exit 2 on a configuration error (unknown scope, missing binary). |
335
+
336
+ Per-Provider conformance suites live next to the Provider's manifest under `<plugin-dir>/conformance/{cases,fixtures}/`. The verb discovers them by walking the built-in Provider directory (and, post-job-subsystem, the plugin loader's discovery output). External consumers — alt-impl authors, Provider authors validating their own work — drive the same suite via this verb without reaching into bespoke scripts.
337
+
325
338
  ---
326
339
 
327
340
  ## Machine-readable output rules
@@ -354,7 +367,7 @@ Every verb that does non-trivial work MUST report its own wall-clock duration. C
354
367
 
355
368
  ### Scope
356
369
 
357
- **In scope**: any verb that walks the filesystem, hits the DB, spawns a subprocess, or renders a report. Examples: `sm scan`, `sm check`, `sm list`, `sm show`, `sm findings`, `sm history`, `sm history stats`, `sm graph`, `sm export`, `sm audit run`, `sm job submit`, `sm job run`, `sm job claim`, `sm job preview`, `sm record`, `sm doctor`, `sm db backup`, `sm db restore`, `sm db dump`, `sm db migrate`, `sm plugins list`, `sm plugins doctor`, `sm init`.
370
+ **In scope**: any verb that walks the filesystem, hits the DB, spawns a subprocess, or renders a report. Examples: `sm scan`, `sm check`, `sm list`, `sm show`, `sm findings`, `sm history`, `sm history stats`, `sm graph`, `sm export`, `sm job submit`, `sm job run`, `sm job claim`, `sm job preview`, `sm record`, `sm doctor`, `sm db backup`, `sm db restore`, `sm db dump`, `sm db migrate`, `sm plugins list`, `sm plugins doctor`, `sm init`, `sm conformance run`.
358
371
 
359
372
  **Exempt**: informational verbs that return in well under a millisecond and would clutter the output — `sm --version`, `sm --help`, `sm version`, `sm help`, `sm config get`, `sm config list`, `sm config show`.
360
373
 
@@ -2,7 +2,22 @@
2
2
 
3
3
  Language-neutral test suite the specification demands. A conforming implementation passes every case; failing any case is a conformance bug.
4
4
 
5
- This directory is **stub-level** as of spec v0.1.0. Two cases ship (`basic-scan`, `kernel-empty-boot`) with a single shared fixture (`minimal-claude`). The shape below is normative; the case count expands before spec-v1.0.0 (see [`../versioning.md`](../versioning.md)). See [`coverage.md`](./coverage.md) for the full schema-to-case matrix.
5
+ The suite splits across two ownership boundaries (Phase 5 / A.13 of spec 0.8.0):
6
+
7
+ - **Spec-owned cases** — kernel-agnostic. They live in this directory and ship with `@skill-map/spec`. Today: `kernel-empty-boot` (boot invariant) and the `preamble-bitwise-match` deferred case. The universal preamble fixture (`preamble-v1.txt`) lives here too.
8
+ - **Provider-owned cases** — exercise a Provider's own `kinds` catalog. They live next to the Provider's manifest, under `<plugin-dir>/conformance/`. The reference impl ships one such suite at [`src/extensions/providers/claude/conformance/`](../../src/extensions/providers/claude/conformance/) covering Claude's five kinds (`skill` / `agent` / `command` / `hook` / `note`) via cases `basic-scan`, `rename-high`, `orphan-detection`.
9
+
10
+ The shape below is normative; the case count in either bucket expands before spec-v1.0.0 (see [`../versioning.md`](../versioning.md)). See [`coverage.md`](./coverage.md) for the spec-owned matrix and the Provider's own coverage file (e.g. `src/extensions/providers/claude/conformance/coverage.md`) for the matching Provider-owned matrix.
11
+
12
+ The reference CLI exposes both buckets via `sm conformance run`:
13
+
14
+ ```
15
+ sm conformance run --scope spec # spec-owned cases only
16
+ sm conformance run --scope provider:claude # the Claude Provider's cases
17
+ sm conformance run --scope all # both (default)
18
+ ```
19
+
20
+ External consumers (alt-impl authors, Provider authors validating their own work) can drive the suite without bespoke scripting — the verb provisions the same isolated tmp scope per case as the in-process reference runner does.
6
21
 
7
22
  ---
8
23
 
@@ -12,15 +27,18 @@ This directory is **stub-level** as of spec v0.1.0. Two cases ship (`basic-scan`
12
27
  spec/conformance/
13
28
  ├── README.md ← this file
14
29
  ├── fixtures/
15
- │ ├── minimal-claude/ ← controlled MD corpus (5 nodes, one per kind)
16
- │ │ ├── skills/hello.md
17
- │ │ ├── agents/reviewer.md
18
- │ │ ├── commands/status.md
19
- │ │ ├── hooks/pre-commit.md
20
- │ │ └── notes/architecture.md
21
30
  │ └── preamble-v1.txt ← verbatim preamble text for bitwise-match checks
22
31
  └── cases/
23
- └── basic-scan.json ← declarative case (see "Case format" below)
32
+ └── kernel-empty-boot.json ← declarative case (see "Case format" below)
33
+ ```
34
+
35
+ ```
36
+ src/extensions/providers/<id>/conformance/ ← Provider-owned, mirrors the layout
37
+ ├── coverage.md
38
+ ├── cases/
39
+ │ └── *.json
40
+ └── fixtures/
41
+ └── ...
24
42
  ```
25
43
 
26
44
  Fixtures are read-only inputs. Cases declare what to invoke and what to assert. A conformance runner is implementation-specific code that:
@@ -45,13 +63,13 @@ A case is a JSON document with this shape:
45
63
  "fixture": "string — folder under fixtures/ used as the scope root.",
46
64
 
47
65
  "setup": {
48
- "disableAllAdapters": false,
49
- "disableAllDetectors": false,
66
+ "disableAllProviders": false,
67
+ "disableAllExtractors": false,
50
68
  "disableAllRules": false
51
69
  },
52
70
 
53
71
  "invoke": {
54
- "verb": "scan | list | show | check | findings | graph | export | audit | job | record | ...",
72
+ "verb": "scan | list | show | check | findings | graph | export | job | record | ...",
55
73
  "sub": "submit | run | ...",
56
74
  "args": ["positional", "args"],
57
75
  "flags": ["--json", "--all", "..."]
@@ -97,10 +115,11 @@ Assertion types beyond this list MAY be proposed via spec-vX.Y.Z minor bumps. Im
97
115
 
98
116
  ## Current case inventory
99
117
 
118
+ ### Spec-owned (this directory)
119
+
100
120
  | Id | Verifies |
101
121
  |---|---|
102
- | `basic-scan` | Scanning `minimal-claude` detects one node per kind with no issues. |
103
- | `kernel-empty-boot` | With every adapter/detector/rule disabled, scanning an empty scope returns a valid empty graph. |
122
+ | `kernel-empty-boot` | With every Provider/Extractor/Rule disabled, scanning an empty scope returns a valid empty graph. |
104
123
 
105
124
  Cases explicitly referenced elsewhere in the spec (landing before v1.0):
106
125
 
@@ -108,6 +127,14 @@ Cases explicitly referenced elsewhere in the spec (landing before v1.0):
108
127
  |---|---|---|
109
128
  | `preamble-bitwise-match` | `prompt-preamble.md` | Rendered job files contain `preamble-v1.txt` byte-for-byte. Deferred to Step 10 (requires `sm job preview`). |
110
129
 
130
+ ### Provider-owned (per `<plugin-dir>/conformance/`)
131
+
132
+ | Provider | Id | Verifies |
133
+ |---|---|---|
134
+ | `claude` | `basic-scan` | Scanning the `minimal-claude` corpus detects exactly five nodes (one per kind) with no issues. Implicitly validates each per-kind schema. |
135
+ | `claude` | `rename-high` | High-confidence rename emits no issue; the new path is the sole node. |
136
+ | `claude` | `orphan-detection` | Deletion with no replacement triggers exactly one `orphan` issue (severity `info`). |
137
+
111
138
  ---
112
139
 
113
140
  ## Runner (reference pseudocode)
@@ -128,7 +155,9 @@ for (const caseFile of await readdir('spec/conformance/cases')) {
128
155
  }
129
156
  ```
130
157
 
131
- The reference implementation's runner will ship under `src/conformance/` during Step 0b; until then, the spec treats this suite as a schema (shape contract) rather than an executable test target.
158
+ A Provider-owned runner mirrors the loop with a different cases / fixtures root — `<plugin-dir>/conformance/cases/` and `<plugin-dir>/conformance/fixtures/`. The reference CLI ships both as `sm conformance run` (Phase 5 / A.13); the verb resolves the spec scope via `@skill-map/spec` and discovers Provider scopes by walking each built-in plugin's `conformance/` directory.
159
+
160
+ The reference implementation's runner ships under `src/conformance/index.ts`; the verb lives at `src/cli/commands/conformance.ts` and uses the runner one case at a time.
132
161
 
133
162
  ---
134
163
 
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
3
3
  "id": "kernel-empty-boot",
4
- "description": "With every adapter, detector, and rule disabled, scanning an empty scope MUST return a valid, zero-filled ScanResult. Enforces the kernel boot invariant from architecture.md.",
4
+ "description": "With every Provider, extractor, and rule disabled, scanning an empty scope MUST return a valid, zero-filled ScanResult. Enforces the kernel boot invariant from architecture.md.",
5
5
  "setup": {
6
- "disableAllAdapters": true,
7
- "disableAllDetectors": true,
6
+ "disableAllProviders": true,
7
+ "disableAllExtractors": true,
8
8
  "disableAllRules": true
9
9
  },
10
10
  "invoke": {