@skill-map/spec 0.21.0 → 0.23.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 (42) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +4 -4
  3. package/architecture.md +134 -128
  4. package/cli-contract.md +107 -104
  5. package/conformance/README.md +13 -13
  6. package/conformance/coverage.md +42 -39
  7. package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/provider.js +0 -1
  8. package/db-schema.md +45 -45
  9. package/index.json +41 -38
  10. package/interfaces/security-scanner.md +20 -20
  11. package/job-events.md +21 -21
  12. package/job-lifecycle.md +21 -21
  13. package/package.json +3 -2
  14. package/plugin-author-guide.md +135 -111
  15. package/plugin-kv-api.md +10 -10
  16. package/prompt-preamble.md +8 -8
  17. package/schemas/annotations.schema.json +3 -3
  18. package/schemas/api/rest-envelope.schema.json +15 -11
  19. package/schemas/conformance-result.schema.json +120 -0
  20. package/schemas/execution-record.schema.json +2 -2
  21. package/schemas/extensions/analyzer.schema.json +9 -0
  22. package/schemas/extensions/base.schema.json +4 -4
  23. package/schemas/extensions/extractor.schema.json +4 -4
  24. package/schemas/extensions/formatter.schema.json +1 -1
  25. package/schemas/extensions/hook.schema.json +3 -3
  26. package/schemas/extensions/provider.schema.json +6 -11
  27. package/schemas/frontmatter/base.schema.json +1 -1
  28. package/schemas/history-stats.schema.json +4 -4
  29. package/schemas/input-types.schema.json +3 -3
  30. package/schemas/issue.schema.json +1 -1
  31. package/schemas/job.schema.json +2 -2
  32. package/schemas/node.schema.json +5 -5
  33. package/schemas/plugins-doctor.schema.json +97 -0
  34. package/schemas/plugins-registry.schema.json +2 -2
  35. package/schemas/project-config.schema.json +10 -14
  36. package/schemas/refresh-report.schema.json +52 -0
  37. package/schemas/report-base-deterministic.schema.json +1 -1
  38. package/schemas/sidecar.schema.json +3 -3
  39. package/schemas/summaries/markdown.schema.json +1 -1
  40. package/schemas/summaries/skill.schema.json +1 -1
  41. package/schemas/view-slots.schema.json +7 -7
  42. package/versioning.md +7 -7
@@ -4,8 +4,8 @@ Language-neutral test suite the specification demands. A conforming implementati
4
4
 
5
5
  The suite splits across two ownership boundaries:
6
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`.
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
9
 
10
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
11
 
@@ -17,7 +17,7 @@ sm conformance run --scope provider:claude # the Claude Provider's cases
17
17
  sm conformance run --scope all # both (default)
18
18
  ```
19
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.
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.
21
21
 
22
22
  ---
23
23
 
@@ -57,10 +57,10 @@ A case is a JSON document with this shape:
57
57
 
58
58
  ```jsonc
59
59
  {
60
- "id": "string kebab-case, globally unique among cases.",
61
- "description": "string one-to-three sentences, what the case verifies.",
60
+ "id": "string, kebab-case, globally unique among cases.",
61
+ "description": "string, one-to-three sentences, what the case verifies.",
62
62
 
63
- "fixture": "string folder under fixtures/ used as the scope root.",
63
+ "fixture": "string, folder under fixtures/ used as the scope root.",
64
64
 
65
65
  "setup": {
66
66
  "disableAllProviders": false,
@@ -97,7 +97,7 @@ A case is a JSON document with this shape:
97
97
  | `invoke.flags` | no | Flags. Order-significant iff the CLI defines it (the reference impl accepts them in any order). |
98
98
  | `assertions` | yes | Array, ≥ 1 item. Ordering matters for reporting only. |
99
99
 
100
- ### Assertion types (stub-level expansion before v1.0)
100
+ ### Assertion types (stub-level, expansion before v1.0)
101
101
 
102
102
  | `type` | Fields | Meaning |
103
103
  |---|---|---|
@@ -108,7 +108,7 @@ A case is a JSON document with this shape:
108
108
  | `file-matches-schema` | `path: string`, `schema: string` | File at `path` (glob permitted; resolves to exactly one) MUST be valid JSON and MUST validate against `schemas/<schema>`. |
109
109
  | `stderr-matches` | `pattern: string` | stderr MUST match the regex (ECMAScript). |
110
110
 
111
- Assertion types beyond this list MAY be proposed via spec-vX.Y.Z minor bumps. Implementations MUST reject unknown assertion types loudly silently skipping a check is a conformance violation in itself.
111
+ Assertion types beyond this list MAY be proposed via spec-vX.Y.Z minor bumps. Implementations MUST reject unknown assertion types loudly, silently skipping a check is a conformance violation in itself.
112
112
 
113
113
  ---
114
114
 
@@ -154,7 +154,7 @@ for (const caseFile of await readdir('spec/conformance/cases')) {
154
154
  }
155
155
  ```
156
156
 
157
- 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`; the verb resolves the spec scope via `@skill-map/spec` and discovers Provider scopes by walking each built-in plugin's `conformance/` directory.
157
+ 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`; the verb resolves the spec scope via `@skill-map/spec` and discovers Provider scopes by walking each built-in plugin's `conformance/` directory.
158
158
 
159
159
  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.
160
160
 
@@ -162,10 +162,10 @@ The reference implementation's runner ships under `src/conformance/index.ts`; th
162
162
 
163
163
  ## See also
164
164
 
165
- - [`coverage.md`](./coverage.md) schema-to-case coverage matrix and release gates.
166
- - [`../versioning.md`](../versioning.md) what constitutes a major/minor/patch change to the suite.
167
- - [`../architecture.md`](../architecture.md) kernel empty-boot invariant exercised by `kernel-empty-boot`.
168
- - [`../prompt-preamble.md`](../prompt-preamble.md) verbatim text checked by `preamble-bitwise-match` (deferred).
165
+ - [`coverage.md`](./coverage.md), schema-to-case coverage matrix and release gates.
166
+ - [`../versioning.md`](../versioning.md), what constitutes a major/minor/patch change to the suite.
167
+ - [`../architecture.md`](../architecture.md), kernel empty-boot invariant exercised by `kernel-empty-boot`.
168
+ - [`../prompt-preamble.md`](../prompt-preamble.md), verbatim text checked by `preamble-bitwise-match` (deferred).
169
169
 
170
170
  ---
171
171
 
@@ -1,6 +1,6 @@
1
1
  # Conformance coverage
2
2
 
3
- Authoritative map of JSON Schemas in [`../schemas/`](../schemas/) to the conformance cases that exercise them. Every schema MUST have at least one case before spec v1.0.0 ships missing case → missing release ([`../../context/spec.md`](../../context/spec.md) §Analyzers for AI agents editing spec/).
3
+ Authoritative map of JSON Schemas in [`../schemas/`](../schemas/) to the conformance cases that exercise them. Every schema MUST have at least one case before spec v1.0.0 ships, missing case → missing release ([`../../context/spec.md`](../../context/spec.md) §Analyzers for AI agents editing spec/).
4
4
 
5
5
  This file is hand-maintained. A CI check before spec release compares the schema inventory against this table and fails if any schema lacks a case.
6
6
 
@@ -9,38 +9,41 @@ This file is hand-maintained. A CI check before spec release compares the schema
9
9
  | # | Schema | Case(s) | Status | Notes |
10
10
  |---|---|---|---|---|
11
11
  | 1 | `node.schema.json` | `kernel-empty-boot` (indirect) | 🟡 partial | Empty-boot validates the zero-filled ScanResult shape end-to-end. Direct cases that exercise populated `Node` rows are Provider-specific and live in the Provider's own conformance suite (see `provider:claude` for `basic-scan`). |
12
- | 2 | `link.schema.json` | | 🔴 missing | Needs fixture with at least one `invokes` + `references` + `mentions` link, both `high`/`medium`/`low` confidence. |
13
- | 3 | `issue.schema.json` | | 🔴 missing | Needs fixture triggering `trigger-collision` + `broken-ref` + `superseded`. |
14
- | 4 | `scan-result.schema.json` | `kernel-empty-boot`, `orphan-markdown-fallback` | 🟢 covered | Zero-filled case via empty-boot. `orphan-markdown-fallback` (spec 0.18.0) asserts a populated `ScanResult` over a multi-Provider corpus where one node lands via the universal `core/markdown` fallback and another via vendor-specific claude classification locks the orchestrator's path-dedup contract. Populated rename / orphan cases live under `provider:claude` (`basic-scan` / `rename-high` / `orphan-detection`). |
15
- | 5 | `execution-record.schema.json` | | 🔴 missing | Blocked by Step 5 (history). Needs a case that runs a `deterministic` action and inspects `state_executions` via `sm history --json`. |
16
- | 6 | `project-config.schema.json` | | 🔴 missing | Case: init a scope, write a partial `.skill-map/settings.json` (optionally with a `.skill-map/settings.local.json` overlay), assert effective config after the layered merge. |
17
- | 7 | `plugins-registry.schema.json` | | 🔴 missing | Two sub-cases required: (a) `PluginManifest` validation via `sm plugins show --json`; (b) aggregate `PluginsRegistry` via `sm plugins list --json`. |
18
- | 8 | `job.schema.json` | | 🔴 missing | Blocked by Step 10 (job system). Needs a case that submits a local action (no LLM), inspects `sm job show --json`. |
19
- | 9 | `report-base.schema.json` | | 🔴 missing | Indirect coverage once any summarizer case lands. Direct contract case: validate a handcrafted minimal report ({confidence, safety}) against the base schema. |
20
- | 10 | `conformance-case.schema.json` | | 🔴 missing | Self-referential: every `*.json` under `cases/` MUST validate against this schema. Add a meta-case that enumerates + validates all cases. |
21
- | 11 | `frontmatter/base.schema.json` | `orphan-markdown-fallback` | 🟢 covered | Universal frontmatter shape `name` + `description` only, `additionalProperties: true`. Per-kind schemas live with the Provider that emits them: vendor kinds (`skill` / `agent` / `command`) under `src/built-in-plugins/providers/{claude,gemini,agent-skills}/schemas/`; the format-named generic `markdown` kind under `src/built-in-plugins/providers/core-markdown/schemas/` (spec 0.18.0 markdown is provider-agnostic). All extend this base via `$ref`-by-`$id`. `orphan-markdown-fallback` exercises base-only frontmatter end-to-end via the `ARCHITECTURE.md` fixture file (no kind-specific extras). |
22
- | 12 | `summaries/skill.schema.json` | | 🔴 missing | Blocked by Step 10 (`skill-summarizer`). Case: submit summarizer, validate report. |
23
- | 13 | `summaries/agent.schema.json` | | 🔴 missing | Blocked by Step 11. |
24
- | 14 | `summaries/command.schema.json` | | 🔴 missing | Blocked by Step 11. |
25
- | 15 | `summaries/hook.schema.json` | | 🔴 missing | Blocked by Step 11. |
26
- | 16 | `summaries/markdown.schema.json` | | 🔴 missing | Blocked by Step 11. |
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 cases for missing `kinds` / `explorationDir` 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. |
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. |
32
- | 22 | `extensions/formatter.schema.json` | | 🔴 missing | Case: `ascii` formatter manifest validates. |
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. |
35
- | 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. |
12
+ | 2 | `link.schema.json` |, | 🔴 missing | Needs fixture with at least one `invokes` + `references` + `mentions` link, both `high`/`medium`/`low` confidence. |
13
+ | 3 | `issue.schema.json` |, | 🔴 missing | Needs fixture triggering `trigger-collision` + `broken-ref` + `superseded`. |
14
+ | 4 | `scan-result.schema.json` | `kernel-empty-boot`, `orphan-markdown-fallback` | 🟢 covered | Zero-filled case via empty-boot. `orphan-markdown-fallback` (spec 0.18.0) asserts a populated `ScanResult` over a multi-Provider corpus where one node lands via the universal `core/markdown` fallback and another via vendor-specific claude classification, locks the orchestrator's path-dedup contract. Populated rename / orphan cases live under `provider:claude` (`basic-scan` / `rename-high` / `orphan-detection`). |
15
+ | 5 | `execution-record.schema.json` |, | 🔴 missing | Blocked by Step 5 (history). Needs a case that runs a `deterministic` action and inspects `state_executions` via `sm history --json`. |
16
+ | 6 | `project-config.schema.json` |, | 🔴 missing | Case: init a scope, write a partial `.skill-map/settings.json` (optionally with a `.skill-map/settings.local.json` overlay), assert effective config after the layered merge. |
17
+ | 7 | `plugins-registry.schema.json` |, | 🔴 missing | Two sub-cases required: (a) `PluginManifest` validation via `sm plugins show --json`; (b) aggregate `PluginsRegistry` via `sm plugins list --json`. |
18
+ | 8 | `job.schema.json` |, | 🔴 missing | Blocked by Step 10 (job system). Needs a case that submits a local action (no LLM), inspects `sm job show --json`. |
19
+ | 9 | `report-base.schema.json` |, | 🔴 missing | Indirect coverage once any summarizer case lands. Direct contract case: validate a handcrafted minimal report ({confidence, safety}) against the base schema. |
20
+ | 10 | `conformance-case.schema.json` |, | 🔴 missing | Self-referential: every `*.json` under `cases/` MUST validate against this schema. Add a meta-case that enumerates + validates all cases. |
21
+ | 11 | `frontmatter/base.schema.json` | `orphan-markdown-fallback` | 🟢 covered | Universal frontmatter shape, `name` + `description` only, `additionalProperties: true`. Per-kind schemas live with the Provider that emits them: vendor kinds (`skill` / `agent` / `command`) under `src/built-in-plugins/providers/{claude,gemini,agent-skills}/schemas/`; the format-named generic `markdown` kind under `src/built-in-plugins/providers/core-markdown/schemas/` (spec 0.18.0, markdown is provider-agnostic). All extend this base via `$ref`-by-`$id`. `orphan-markdown-fallback` exercises base-only frontmatter end-to-end via the `ARCHITECTURE.md` fixture file (no kind-specific extras). |
22
+ | 12 | `summaries/skill.schema.json` |, | 🔴 missing | Blocked by Step 10 (`skill-summarizer`). Case: submit summarizer, validate report. |
23
+ | 13 | `summaries/agent.schema.json` |, | 🔴 missing | Blocked by Step 11. |
24
+ | 14 | `summaries/command.schema.json` |, | 🔴 missing | Blocked by Step 11. |
25
+ | 15 | `summaries/hook.schema.json` |, | 🔴 missing | Blocked by Step 11. |
26
+ | 16 | `summaries/markdown.schema.json` |, | 🔴 missing | Blocked by Step 11. |
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. |
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. |
32
+ | 22 | `extensions/formatter.schema.json` |, | 🔴 missing | Case: `ascii` formatter manifest validates. |
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. |
35
+ | 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
36
  | 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
37
  | 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. |
38
- | 28 | `bump-report.schema.json` | | 🔴 missing | Report shape produced by the built-in deterministic `bump` Action (Step 9.6.3, Decision #125). Extends `report-base-deterministic.schema.json` (row 29) the deterministic counterpart to `report-base.schema.json` (which is LLM-specific via `confidence` + `safety`). Three concrete shapes: success-with-write, silent-no-op (under `force`), and refusal (`fresh`). Direct conformance case lands together with the `sm bump` CLI verb in Step 9.6.4 it'll exercise all three branches via `sm bump --json` against a primed fixture. Implementation tests at `src/test/bump-action.test.ts` cover the runtime behaviour today. |
39
- | 29 | `report-base-deterministic.schema.json` | (indirect via row 28) | 🟡 partial | Deterministic counterpart to `report-base.schema.json`; every deterministic Action's report extends this base. Direct contract case still pending landed when first conformance case directly validates a deterministic report against this schema. |
40
- | 30 | `view-slots.schema.json` | | 🔴 missing | Closed catalog of 15 view slots + the `IViewContribution` manifest declaration shape + per-slot payload schemas. Cases required (3): (a) `plugin-view-contributions-valid` a plugin manifest declaring contributions of every slot validates; (b) `plugin-view-contributions-invalid-slot` a manifest referencing a slot not in the catalog rejects with `invalid-manifest`; (c) `plugin-view-contributions-payload-mismatch` an extractor emitting an off-shape payload triggers `extension.error` and drops silently. Implementation lands with the kernel surface in Phase 2 of the UI contributions plan; conformance fixtures land alongside. |
41
- | 31 | `input-types.schema.json` | | 🔴 missing | Closed catalog of 10 input-types for plugin settings + the `ISettingDeclaration` discriminated-union manifest shape. Cases required (2): (a) `plugin-settings-valid` a plugin manifest declaring at least one setting of each input-type validates; (b) `plugin-settings-invalid-type` a manifest referencing a `type` not in the catalog rejects with `invalid-manifest`. Lands together with the spec/CLI surface for `sm plugins config <id>`. |
38
+ | 28 | `bump-report.schema.json` |, | 🔴 missing | Report shape produced by the built-in deterministic `bump` Action (Step 9.6.3, Decision #125). Extends `report-base-deterministic.schema.json` (row 29), the deterministic counterpart to `report-base.schema.json` (which is LLM-specific via `confidence` + `safety`). Three concrete shapes: success-with-write, silent-no-op (under `force`), and refusal (`fresh`). Direct conformance case lands together with the `sm bump` CLI verb in Step 9.6.4, it'll exercise all three branches via `sm bump --json` against a primed fixture. Implementation tests at `src/test/bump-action.test.ts` cover the runtime behaviour today. |
39
+ | 29 | `report-base-deterministic.schema.json` |, (indirect via row 28) | 🟡 partial | Deterministic counterpart to `report-base.schema.json`; every deterministic Action's report extends this base. Direct contract case still pending, landed when first conformance case directly validates a deterministic report against this schema. |
40
+ | 30 | `view-slots.schema.json` |, | 🔴 missing | Closed catalog of 15 view slots + the `IViewContribution` manifest declaration shape + per-slot payload schemas. Cases required (3): (a) `plugin-view-contributions-valid`, a plugin manifest declaring contributions of every slot validates; (b) `plugin-view-contributions-invalid-slot`, a manifest referencing a slot not in the catalog rejects with `invalid-manifest`; (c) `plugin-view-contributions-payload-mismatch`, an extractor emitting an off-shape payload triggers `extension.error` and drops silently. Implementation lands with the kernel surface in Phase 2 of the UI contributions plan; conformance fixtures land alongside. |
41
+ | 31 | `input-types.schema.json` |, | 🔴 missing | Closed catalog of 10 input-types for plugin settings + the `ISettingDeclaration` discriminated-union manifest shape. Cases required (2): (a) `plugin-settings-valid`, a plugin manifest declaring at least one setting of each input-type validates; (b) `plugin-settings-invalid-type`, a manifest referencing a `type` not in the catalog rejects with `invalid-manifest`. Lands together with the spec/CLI surface for `sm plugins config <id>`. |
42
+ | 32 | `refresh-report.schema.json` |, | 🔴 missing | Machine-readable output of `sm refresh <node.path> --json` and `sm refresh --stale --json`. Reports the count of enrichment rows persisted across targeted nodes (universal enrichment layer per `architecture.md` §A.8). Direct conformance case pending: seed a fixture with one Provider-classified node, run `sm refresh <node> --json`, assert the envelope validates and `refreshed >= 0`. Implementation tests at `src/test/node-enrichments.test.ts` cover the runtime behaviour today. |
43
+ | 33 | `plugins-doctor.schema.json` |, | 🔴 missing | Machine-readable output of `sm plugins doctor --json`. Aggregates per-status counts plus structured issue / warning lists. Direct conformance case pending: prime a scope with one healthy + one invalid-manifest drop-in plugin, run `sm plugins doctor --json`, assert the envelope validates and the invalid plugin appears under `issues[]`. Implementation tests at `src/test/plugins-cli.test.ts` cover the runtime behaviour. |
44
+ | 34 | `conformance-result.schema.json` |, | 🔴 missing | Machine-readable output of `sm conformance run --json`. Self-referential by design (a conformance case would invoke the verb against itself); a direct case is deferred until the runner gains a meta-loopback mode. Implementation tests at `src/test/conformance-cli.test.ts` cover the envelope shape today. |
42
45
 
43
- > **Note on Provider-owned schemas.** Per-kind frontmatter schemas (`skill`, `agent`, `command`, `note` for the built-in Claude Provider; other Providers MAY declare different kinds) live with the Provider that emits them for the built-in Claude Provider, under `src/extensions/providers/claude/schemas/`. Those schemas are NOT counted in the spec's coverage matrix above; they belong to the Provider's own conformance suite at `src/extensions/providers/claude/conformance/coverage.md`. The same split applies to the cases that exercise Provider-specific kinds (`basic-scan`, `rename-high`, `orphan-detection`) they live in the Provider's `cases/` directory.
46
+ > **Note on Provider-owned schemas.** Per-kind frontmatter schemas (`skill`, `agent`, `command`, `note` for the built-in Claude Provider; other Providers MAY declare different kinds) live with the Provider that emits them, for the built-in Claude Provider, under `src/extensions/providers/claude/schemas/`. Those schemas are NOT counted in the spec's coverage matrix above; they belong to the Provider's own conformance suite at `src/extensions/providers/claude/conformance/coverage.md`. The same split applies to the cases that exercise Provider-specific kinds (`basic-scan`, `rename-high`, `orphan-detection`), they live in the Provider's `cases/` directory.
44
47
 
45
48
  Status legend: 🟢 covered (at least one case asserts the schema end-to-end) · 🟡 partial (covered only indirectly or via a sub-shape) · 🔴 missing.
46
49
 
@@ -52,16 +55,16 @@ These have their own conformance cases even though they are not JSON Schemas.
52
55
  |---|---|---|---|---|
53
56
  | A | Preamble verbatim text | `preamble-bitwise-match` | 🟠 deferred | Deferred to Step 10 (needs `sm job preview` to print the rendered content from `state_job_contents`). Fixture: `fixtures/preamble-v1.txt` (already present, byte-identical to `prompt-preamble.md` source). |
54
57
  | B | Kernel empty-boot invariant | `kernel-empty-boot` | 🟢 covered | All extensions disabled → empty ScanResult. |
55
- | C | Atomic-claim race safety | | 🔴 missing | Blocked by Step 10. Two concurrent `sm job claim` invocations against a single queued row exactly one MUST succeed. |
56
- | D | Duplicate detection | | 🔴 missing | Blocked by Step 10. Two `sm job submit` with same `(action, version, node, contentHash)` second exits 3. |
57
- | E | `--force` bypass | | 🔴 missing | Blocked by Step 10. |
58
- | F | Nonce mismatch | | 🔴 missing | Blocked by Step 10. `sm record` with wrong nonce → exit 4. |
59
- | G | Reap | | 🔴 missing | Blocked by Step 10. Set TTL to 1s; claim; wait; next `sm job run` reaps with reason `abandoned`. |
60
- | H | `run.*` event envelope for Skill agent | | 🔴 missing | Blocked by Step 10. Skill-agent flow emits synthetic `r-ext-*` run envelope around one job. |
58
+ | C | Atomic-claim race safety |, | 🔴 missing | Blocked by Step 10. Two concurrent `sm job claim` invocations against a single queued row, exactly one MUST succeed. |
59
+ | D | Duplicate detection |, | 🔴 missing | Blocked by Step 10. Two `sm job submit` with same `(action, version, node, contentHash)`, second exits 3. |
60
+ | E | `--force` bypass |, | 🔴 missing | Blocked by Step 10. |
61
+ | F | Nonce mismatch |, | 🔴 missing | Blocked by Step 10. `sm record` with wrong nonce → exit 4. |
62
+ | G | Reap |, | 🔴 missing | Blocked by Step 10. Set TTL to 1s; claim; wait; next `sm job run` reaps with reason `abandoned`. |
63
+ | H | `run.*` event envelope for Skill agent |, | 🔴 missing | Blocked by Step 10. Skill-agent flow emits synthetic `r-ext-*` run envelope around one job. |
61
64
  | I | Rename heuristic | `rename-high`, `orphan-detection` (Provider-owned) | 🟢 covered | High-confidence rename emits no issue and the new path is the sole node. Orphan branch emits exactly one `orphan` issue (severity `info`) when a deleted node has no replacement. Cases live with the Claude Provider (they reach a Provider's `kinds` catalog by construction); see [`src/extensions/providers/claude/conformance/`](../../src/extensions/providers/claude/conformance/). Medium / ambiguous branches are exercised by `src/test/rename-heuristic.test.ts` until the conformance schema grows richer assertions. |
62
- | J | Plugin DDL rejection | | 🔴 missing | Blocked by Step 9. Plugin migration referencing `state_jobs` → disabled with `invalid-manifest`. |
63
- | K | Plugin prefix injection | | 🔴 missing | Blocked by Step 9. Plugin declares `CREATE TABLE foo` → kernel applies as `plugin_<id>_foo`. |
64
- | L | Elapsed-time reporting | | 🔴 missing | Blocked by Step 4 (first real verb work). Run any in-scope verb; stderr last line MUST match `/^done in (\d+ms\|\d+\.\d+s\|\d+m \d+s)$/`. In-scope verb with `--json` returning an object MUST carry `elapsedMs`. Exempt verb (`sm version`) MUST NOT emit the line. |
65
+ | J | Plugin DDL rejection |, | 🔴 missing | Blocked by Step 9. Plugin migration referencing `state_jobs` → disabled with `invalid-manifest`. |
66
+ | K | Plugin prefix injection |, | 🔴 missing | Blocked by Step 9. Plugin declares `CREATE TABLE foo` → kernel applies as `plugin_<id>_foo`. |
67
+ | L | Elapsed-time reporting |, | 🔴 missing | Blocked by Step 4 (first real verb work). Run any in-scope verb; stderr last line MUST match `/^done in (\d+ms\|\d+\.\d+s\|\d+m \d+s)$/`. In-scope verb with `--json` returning an object MUST carry `elapsedMs`. Exempt verb (`sm version`) MUST NOT emit the line. |
65
68
 
66
69
  ## Release gates
67
70
 
@@ -11,7 +11,6 @@ export default {
11
11
  version: '0.1.0',
12
12
  description: 'provider whose markdown kind is missing the ui block',
13
13
  stability: 'experimental',
14
- explorationDir: '~/.bad',
15
14
  kinds: {
16
15
  markdown: {
17
16
  schema: './schemas/markdown.schema.json',
package/db-schema.md CHANGED
@@ -19,7 +19,7 @@ Two scopes. Each has its own database file and its own migration ledger.
19
19
  | `project` (default) | `./.skill-map/skill-map.db` | The current repository. |
20
20
  | `global` (`-g`) | `~/.skill-map/skill-map.db` | User-level skill directories (e.g. `~/.claude/`). |
21
21
 
22
- The project DB is gitignored by default. Teams MAY opt in to sharing it by setting `history.share: true` in `.skill-map/settings.json` the file is then committed and the execution log becomes a team artifact. Both zones use the same schema.
22
+ The project DB is gitignored by default. Teams MAY opt in to sharing it by setting `history.share: true` in `.skill-map/settings.json`, the file is then committed and the execution log becomes a team artifact. Both zones use the same schema.
23
23
 
24
24
  The `--db <path>` CLI flag overrides location for both scopes as an escape hatch.
25
25
 
@@ -35,7 +35,7 @@ Every kernel table belongs to exactly one zone, identified by a mandatory name p
35
35
  | State | `state_` | Persistent operational data: jobs, executions, summaries, enrichment, plugin KV. | No | Yes | `state_jobs` |
36
36
  | Config | `config_` | User-owned configuration: plugin enable/disable, preferences, migration ledger. | No | Yes | `config_plugins` |
37
37
 
38
- `sm db reset` drops `scan_*` only (non-destructive equivalent to forcing the next scan from a clean slate). `sm db reset --state` also drops `state_*` (destructive to operational history). `sm db reset --hard` deletes the DB file entirely. `sm db backup` preserves `state_*` + `config_*`; `scan_*` is always regenerated on demand and is never included in backups.
38
+ `sm db reset` drops `scan_*` only (non-destructive, equivalent to forcing the next scan from a clean slate). `sm db reset --state` also drops `state_*` (destructive to operational history). `sm db reset --hard` deletes the DB file entirely. `sm db backup` preserves `state_*` + `config_*`; `scan_*` is always regenerated on demand and is never included in backups.
39
39
 
40
40
  ---
41
41
 
@@ -72,7 +72,7 @@ One row per detected node, matching [`schemas/node.schema.json`](./schemas/node.
72
72
  | Column | Type | Constraint | Notes |
73
73
  |---|---|---|---|
74
74
  | `path` | TEXT | PRIMARY KEY | Relative path from scope root. Canonical node identifier. |
75
- | `kind` | TEXT | NOT NULL | Open-by-design (`node.schema.json#/properties/kind`): the value is whatever the classifying Provider declares. Built-in catalogs: `claude` ships `skill` / `agent` / `command`; `gemini` ships `agent` / `skill`; `agent-skills` ships `skill`; `core/markdown` ships the format-named generic fallback `markdown` (universal picks up any `.md` no vendor Provider claims, see `architecture.md` §Provider · dispatch order). External Providers MAY emit their own. |
75
+ | `kind` | TEXT | NOT NULL | Open-by-design (`node.schema.json#/properties/kind`): the value is whatever the classifying Provider declares. Built-in catalogs: `claude` ships `skill` / `agent` / `command`; `gemini` ships `agent` / `skill`; `agent-skills` ships `skill`; `core/markdown` ships the format-named generic fallback `markdown` (universal, picks up any `.md` no vendor Provider claims, see `architecture.md` §Provider · dispatch order). External Providers MAY emit their own. |
76
76
  | `provider` | TEXT | NOT NULL | Provider extension id. |
77
77
  | `title` | TEXT | NULL | |
78
78
  | `description` | TEXT | NULL | |
@@ -137,7 +137,7 @@ Indexes: `ix_scan_issues_analyzer_id`, `ix_scan_issues_severity`.
137
137
 
138
138
  Single-row table holding the metadata of the last persisted scan. Lets `loadScanResult` return the real `scope` / `roots` / `scannedAt` / `scannedBy` / `providers` / `stats.filesWalked|filesSkipped|durationMs` instead of synthesising them. Replaced atomically with the rest of the `scan_*` zone on every `sm scan`.
139
139
 
140
- `nodesCount` / `linksCount` / `issuesCount` are not stored here they derive from `COUNT(*)` of the sibling tables.
140
+ `nodesCount` / `linksCount` / `issuesCount` are not stored here, they derive from `COUNT(*)` of the sibling tables.
141
141
 
142
142
  | Column | Type | Constraint |
143
143
  |---|---|---|
@@ -166,8 +166,8 @@ The orchestrator consults this table on `sm scan --changed`: a node-level cache
166
166
  | `node_path` | TEXT | NOT NULL | FK semantically to `scan_nodes.path`; MAY be unenforced (the row is deleted in the same tx as the parent node when the file disappears). |
167
167
  | `extractor_id` | TEXT | NOT NULL | Qualified id `<plugin_id>/<id>` per spec § A.6. |
168
168
  | `body_hash_at_run` | TEXT | NOT NULL | The `node.body_hash` the Extractor processed; sha256, hex. |
169
- | `sidecar_annotations_hash_at_run` | TEXT | NOT NULL | sha256 of the canonical-form `node.sidecar.annotations` block the Extractor saw on its run. Always populated an absent sidecar or one without annotations canonicalises to `{}` so the hash stays stable across "no sidecar" → "empty annotations" transitions. Participates in the cache hit condition for every Extractor: a `.sm`-only edit invalidates the cached run, no opt-in flag required. The author-facing alternative was considered and rejected because forgetting the flag yielded silent stale-data bugs; universal invalidation costs one re-run on sidecar edits (negligible sidecars change rarely, Extractors are pure-CPU). |
170
- | `ran_at` | INTEGER | NOT NULL | Unix milliseconds wall-clock when the Extractor finished or was last carried forward via cache reuse. Used for diagnostics + future GC of stale rows. |
169
+ | `sidecar_annotations_hash_at_run` | TEXT | NOT NULL | sha256 of the canonical-form `node.sidecar.annotations` block the Extractor saw on its run. Always populated, an absent sidecar or one without annotations canonicalises to `{}` so the hash stays stable across "no sidecar" → "empty annotations" transitions. Participates in the cache hit condition for every Extractor: a `.sm`-only edit invalidates the cached run, no opt-in flag required. The author-facing alternative was considered and rejected because forgetting the flag yielded silent stale-data bugs; universal invalidation costs one re-run on sidecar edits (negligible, sidecars change rarely, Extractors are pure-CPU). |
170
+ | `ran_at` | INTEGER | NOT NULL | Unix milliseconds, wall-clock when the Extractor finished or was last carried forward via cache reuse. Used for diagnostics + future GC of stale rows. |
171
171
 
172
172
  Primary key: `(node_path, extractor_id)`. Indexes: `ix_scan_extractor_runs_node`, `ix_scan_extractor_runs_extractor`.
173
173
 
@@ -184,19 +184,19 @@ One row per `(node_path, extractor_id)` pair an Extractor enriched. Extractors a
184
184
  | `node_path` | TEXT | NOT NULL | FK semantically to `scan_nodes.path`; replaced when a rename heuristic fires (mirrors the `state_*` FK migration). |
185
185
  | `extractor_id` | TEXT | NOT NULL | Qualified id `<plugin_id>/<id>` per spec § A.6. |
186
186
  | `body_hash_at_enrichment` | TEXT | NOT NULL | The `node.body_hash` the Extractor saw when it produced this enrichment. Always equal to the live body hash for Extractor writes; reserved for future Action-issued probabilistic enrichments where stale tracking is meaningful. |
187
- | `value_json` | TEXT | NOT NULL | JSON-serialised `Partial<Node>` the cumulative merge of every `enrichNode(...)` call the Extractor made for this node within its `extract()` invocation. |
187
+ | `value_json` | TEXT | NOT NULL | JSON-serialised `Partial<Node>`, the cumulative merge of every `enrichNode(...)` call the Extractor made for this node within its `extract()` invocation. |
188
188
  | `stale` | INTEGER | NOT NULL DEFAULT 0, CHECK in (0, 1) | Reserved. Always `0` in this revision (Extractors are deterministic; re-running is free). The flag and its index are kept for the future Action-prob enrichment revision where queued LLM jobs must preserve paid output across body changes. |
189
- | `enriched_at` | INTEGER | NOT NULL | Unix milliseconds when the Extractor produced this enrichment. Drives the read-time merge order (`ASC` → last-write-wins per field) inside `mergeNodeWithEnrichments`. |
189
+ | `enriched_at` | INTEGER | NOT NULL | Unix milliseconds, when the Extractor produced this enrichment. Drives the read-time merge order (`ASC` → last-write-wins per field) inside `mergeNodeWithEnrichments`. |
190
190
  | `is_probabilistic` | INTEGER | NOT NULL DEFAULT 0, CHECK in (0, 1) | Reserved. Always `0` for Extractor writes (Extractors are deterministic-only). Reserved for the future Action-prob enrichment revision where the writer's mode is denormalised onto the row so the stale-flag query stays single-table. |
191
191
 
192
192
  Primary key: `(node_path, extractor_id)`. Indexes: `ix_node_enrichments_node`, `ix_node_enrichments_stale`. The `_stale` index is dormant in this revision (every row has `stale = 0`); it is preserved so the future Action-prob revision can ship without a schema migration.
193
193
 
194
194
  **Persistence flow** (per `sm scan`):
195
195
 
196
- 1. **Rename migration** for every `RenameOp` from the rename heuristic, update `node_enrichments.node_path` from `op.from` to `op.to` so the audit trail tracks the file like `state_*` rows do.
197
- 2. **Drop-on-disappear** delete every row whose `node_path` is no longer in the live node set.
198
- 3. **Upsert** for every `(node_path, extractor_id)` pair the orchestrator emitted in this scan, upsert with `stale = 0`, `is_probabilistic = 0`, and the current `body_hash`. The PRIMARY KEY conflict refreshes `body_hash_at_enrichment` / `value_json` / `enriched_at` on every re-run.
199
- 4. **Stale flagging** no-op in this revision (Extractors are deterministic-only; the sweep finds nothing to flag). The step is preserved in the persistence flow so the future Action-prob revision slots in without reshaping the contract.
196
+ 1. **Rename migration**, for every `RenameOp` from the rename heuristic, update `node_enrichments.node_path` from `op.from` to `op.to` so the audit trail tracks the file like `state_*` rows do.
197
+ 2. **Drop-on-disappear**, delete every row whose `node_path` is no longer in the live node set.
198
+ 3. **Upsert**, for every `(node_path, extractor_id)` pair the orchestrator emitted in this scan, upsert with `stale = 0`, `is_probabilistic = 0`, and the current `body_hash`. The PRIMARY KEY conflict refreshes `body_hash_at_enrichment` / `value_json` / `enriched_at` on every re-run.
199
+ 4. **Stale flagging**, no-op in this revision (Extractors are deterministic-only; the sweep finds nothing to flag). The step is preserved in the persistence flow so the future Action-prob revision slots in without reshaping the contract.
200
200
 
201
201
  **Read-side `node.merged` view.** Analyzers / `sm check` / `sm export` consume `node.frontmatter` directly (deterministic CI-safe baseline). UI / future opt-in consumers call `mergeNodeWithEnrichments(node, enrichments)` which:
202
202
 
@@ -208,7 +208,7 @@ Stale row visibility is opt-in via `mergeNodeWithEnrichments(node, enrichments,
208
208
 
209
209
  **Refresh verbs** (see [`cli-contract.md` §Scan](./cli-contract.md#scan)):
210
210
 
211
- - `sm refresh <node.path>` re-runs Extractors against a single node and upserts their enrichment rows. Extractors are deterministic-only they always run for real and persist.
211
+ - `sm refresh <node.path>` re-runs Extractors against a single node and upserts their enrichment rows. Extractors are deterministic-only, they always run for real and persist.
212
212
  - `sm refresh --stale` batches the granular form across every node carrying at least one stale row; in this revision the stale set is always empty so the verb prints a "nothing to do" advisory and exits `0`.
213
213
 
214
214
  ### `scan_contributions`
@@ -227,20 +227,20 @@ Phase 3 / View contribution system. Per-node typed payloads emitted by extractor
227
227
 
228
228
  Primary key: `(plugin_id, extension_id, node_path, contribution_id)`. Indexes: `ix_scan_contributions_node_path` (inspector lazy-fetch + orphan sweep), `ix_scan_contributions_plugin_id` (catalog sweep + `purgeByPlugin`).
229
229
 
230
- **Persistence orphan + catalog + per-tuple sweep + upsert (NOT pure replace-all).** The watcher's cached pass leaves the contributions buffer empty for cached nodes the orchestrator skips `extract()` when the per-(node, extractor) cache hits, so no `emitContribution` fires. A naive wipe-all would silently drop the prior valid rows on every watcher boot. The persist runs four passes inside the same tx as the rest of the scan zone:
230
+ **Persistence, orphan + catalog + per-tuple sweep + upsert (NOT pure replace-all).** The watcher's cached pass leaves the contributions buffer empty for cached nodes, the orchestrator skips `extract()` when the per-(node, extractor) cache hits, so no `emitContribution` fires. A naive wipe-all would silently drop the prior valid rows on every watcher boot. The persist runs four passes inside the same tx as the rest of the scan zone:
231
231
 
232
- 1. **Orphan sweep** drops every row whose `node_path` is NOT in the current live node set (`livePaths` derived from `result.nodes`). Disappeared nodes lose their contributions automatically.
233
- 2. **Catalog sweep** drops every row whose qualified id `(pluginId, extensionId, contributionId)` is NOT in the registered runtime catalog (`registeredContributionKeys` collected via `collectRegisteredContributionKeys(composed)`). Uninstalled-on-disk plugins and removed contributions lose their rows on the next scan. Disabled bundles are normally purged eagerly by `sm plugins disable` (see `purgeByPlugin` below), so the catalog sweep here is the fallback for the rare "config flipped between scans without going through the CLI" case.
234
- 3. **Per-tuple sweep** for every `(pluginId, extensionId, node_path)` tuple in `freshlyRunTuples` (extension actually ran against that node this scan: extractor cache miss, OR analyzer), drop any row carrying that triple whose `contribution_id` is NOT refreshed by the buffer. Catches the "extractor used to emit, now does not" case without touching cached-extractor rows. Tuple format: `<pluginId>/<extensionId>/<nodePath>`.
235
- 4. **Upsert** `INSERT ... ON CONFLICT DO UPDATE SET payload_json = excluded.payload_json, slot = excluded.slot` for every row in the buffer. PK conflict refreshes `payload_json` + `slot` + `emitted_at`.
232
+ 1. **Orphan sweep**, drops every row whose `node_path` is NOT in the current live node set (`livePaths` derived from `result.nodes`). Disappeared nodes lose their contributions automatically.
233
+ 2. **Catalog sweep**, drops every row whose qualified id `(pluginId, extensionId, contributionId)` is NOT in the registered runtime catalog (`registeredContributionKeys` collected via `collectRegisteredContributionKeys(composed)`). Uninstalled-on-disk plugins and removed contributions lose their rows on the next scan. Disabled bundles are normally purged eagerly by `sm plugins disable` (see `purgeByPlugin` below), so the catalog sweep here is the fallback for the rare "config flipped between scans without going through the CLI" case.
234
+ 3. **Per-tuple sweep**, for every `(pluginId, extensionId, node_path)` tuple in `freshlyRunTuples` (extension actually ran against that node this scan: extractor cache miss, OR analyzer), drop any row carrying that triple whose `contribution_id` is NOT refreshed by the buffer. Catches the "extractor used to emit, now does not" case without touching cached-extractor rows. Tuple format: `<pluginId>/<extensionId>/<nodePath>`.
235
+ 4. **Upsert**, `INSERT ... ON CONFLICT DO UPDATE SET payload_json = excluded.payload_json, slot = excluded.slot` for every row in the buffer. PK conflict refreshes `payload_json` + `slot` + `emitted_at`.
236
236
 
237
- Cached nodes' rows survive untouched they're neither orphaned (still in the live set) nor uninstalled (still in the catalog) nor in `freshlyRunTuples` (extractor short-circuited via the per-(node, extractor) cache) nor in the buffer (no re-emit). The next time the body changes, the orchestrator re-runs the extractor, the tuple lands in the freshly-run set, and either the upsert refreshes the row or the per-tuple sweep drops it.
237
+ Cached nodes' rows survive untouched, they're neither orphaned (still in the live set) nor uninstalled (still in the catalog) nor in `freshlyRunTuples` (extractor short-circuited via the per-(node, extractor) cache) nor in the buffer (no re-emit). The next time the body changes, the orchestrator re-runs the extractor, the tuple lands in the freshly-run set, and either the upsert refreshes the row or the per-tuple sweep drops it.
238
238
 
239
- **Backwards-compat fallbacks.** `IPersistOptions.livePaths`, `IPersistOptions.registeredContributionKeys`, `IPersistOptions.freshlyRunTuples` are all optional. Absent / empty `livePaths` falls back to wipe-all (legacy behaviour). Absent / empty `registeredContributionKeys` skips the catalog sweep (rows for disabled plugins linger until next purge). Absent / empty `freshlyRunTuples` skips the per-tuple sweep (rows that should have been dropped because an extractor stopped emitting linger until the node body, the extractor registration, or the node existence changes again older callers preserve the pre-fix behaviour).
239
+ **Backwards-compat fallbacks.** `IPersistOptions.livePaths`, `IPersistOptions.registeredContributionKeys`, `IPersistOptions.freshlyRunTuples` are all optional. Absent / empty `livePaths` falls back to wipe-all (legacy behaviour). Absent / empty `registeredContributionKeys` skips the catalog sweep (rows for disabled plugins linger until next purge). Absent / empty `freshlyRunTuples` skips the per-tuple sweep (rows that should have been dropped because an extractor stopped emitting linger until the node body, the extractor registration, or the node existence changes again, older callers preserve the pre-fix behaviour).
240
240
 
241
- NOT analogous to `state_plugin_kvs` (which is plugin-managed). Belongs to the `scan_*` family sweep semantics replace pure replace-all but the data is still scan-derived.
241
+ NOT analogous to `state_plugin_kvs` (which is plugin-managed). Belongs to the `scan_*` family, sweep semantics replace pure replace-all but the data is still scan-derived.
242
242
 
243
- **Eager purge on disable.** `sm plugins disable <id>` calls `StoragePort.contributions.purgeByPlugin(pluginId, extensionId?)` immediately after persisting `config_plugins[<id>].enabled = false`. `extensionId` is omitted for bundle-granularity ids (e.g. `claude`) and supplied for qualified ids (e.g. `core/slash`), mirroring how the catalog sweep groups rows. The eager purge avoids the "I disabled the plugin but its chips are still rendered in the UI until I re-scan" gap. Re-enabling (`sm plugins enable <id>`) does NOT restore the rows the next scan re-emits them, same as a cold start. Contributions are scan-derived, so this is cheap; for plugin-managed state (`state_plugin_kvs`, dedicated tables) the opposite policy holds see `plugin-kv-api.md` § "disable does not drop data".
243
+ **Eager purge on disable.** `sm plugins disable <id>` calls `StoragePort.contributions.purgeByPlugin(pluginId, extensionId?)` immediately after persisting `config_plugins[<id>].enabled = false`. `extensionId` is omitted for bundle-granularity ids (e.g. `claude`) and supplied for qualified ids (e.g. `core/slash`), mirroring how the catalog sweep groups rows. The eager purge avoids the "I disabled the plugin but its chips are still rendered in the UI until I re-scan" gap. Re-enabling (`sm plugins enable <id>`) does NOT restore the rows, the next scan re-emits them, same as a cold start. Contributions are scan-derived, so this is cheap; for plugin-managed state (`state_plugin_kvs`, dedicated tables) the opposite policy holds, see `plugin-kv-api.md` § "disable does not drop data".
244
244
 
245
245
  ### `scan_node_tags`
246
246
 
@@ -254,9 +254,9 @@ Tags · dual-source. One row per `(node_path, tag, source)` triple, projected at
254
254
 
255
255
  Primary key: `(node_path, tag, source)`. Indexes: `ix_scan_node_tags_tag` (search by tag), `ix_scan_node_tags_node_path` (per-node lookup, e.g. inspector projection).
256
256
 
257
- **Persistence replace-all per scan.** Every persisted scan rebuilds the table for the live node set: rows whose `node_path` is NOT in `livePaths` are dropped (orphan sweep, same as the contributions table); rows for nodes in the live set are wiped and re-inserted from the projected source state. Cached nodes' tag rows are projected from the cached `node.frontmatter.tags` / `node.sidecar.annotations.tags` (both already in memory), so the rebuild is cheap regardless of cache hit / miss. Storage is small a 50-node project with avg 3 tags/node is ~150 rows ≈ 7.5 KB.
257
+ **Persistence, replace-all per scan.** Every persisted scan rebuilds the table for the live node set: rows whose `node_path` is NOT in `livePaths` are dropped (orphan sweep, same as the contributions table); rows for nodes in the live set are wiped and re-inserted from the projected source state. Cached nodes' tag rows are projected from the cached `node.frontmatter.tags` / `node.sidecar.annotations.tags` (both already in memory), so the rebuild is cheap regardless of cache hit / miss. Storage is small, a 50-node project with avg 3 tags/node is ~150 rows ≈ 7.5 KB.
258
258
 
259
- The wire shape on `/api/nodes` joins this table to project `node.tags = { byAuthor: string[], byUser: string[] }`. The kernel `Node` interface (TypeScript) does NOT carry `tags` consumers walking the canonical sources read `node.frontmatter.tags` and `node.sidecar.annotations.tags` directly (consistent with the post-decision-#2 posture).
259
+ The wire shape on `/api/nodes` joins this table to project `node.tags = { byAuthor: string[], byUser: string[] }`. The kernel `Node` interface (TypeScript) does NOT carry `tags`, consumers walking the canonical sources read `node.frontmatter.tags` and `node.sidecar.annotations.tags` directly (consistent with the post-decision-#2 posture).
260
260
 
261
261
  ---
262
262
 
@@ -301,7 +301,7 @@ Content-addressed store for the rendered MD content of every queued or completed
301
301
 
302
302
  No indexes (PK already covers lookup by hash; the table is keyed-by-hash exclusively).
303
303
 
304
- **Insertion semantics**: `INSERT OR IGNORE INTO state_job_contents(content_hash, content, created_at) VALUES (?, ?, ?)` an existing row for the same hash is a no-op (the prior insert already paid the storage cost).
304
+ **Insertion semantics**: `INSERT OR IGNORE INTO state_job_contents(content_hash, content, created_at) VALUES (?, ?, ?)`, an existing row for the same hash is a no-op (the prior insert already paid the storage cost).
305
305
 
306
306
  **GC contract**: `sm job prune` MUST delete every row whose `content_hash` is no longer referenced by any `state_jobs` row, in the same transaction that prunes the job rows. Implementations MUST NOT delete `state_job_contents` rows on `sm job cancel` (a cancelled job's content is recoverable via `sm job submit --force` of the same content_hash and dedup is desirable).
307
307
 
@@ -384,7 +384,7 @@ Primary key: `(plugin_id, node_id, key)` with `node_id` using a sentinel empty s
384
384
 
385
385
  ### `state_node_favorites`
386
386
 
387
- Per-node "favorite" flag set by the local user from the UI. The set is small (typical projects pin a handful of skills/agents/commands), so the table degenerates to one row per favorited node absence of a row means "not favorited". Exists in zone `state_` because it is user-authored preference, not regenerable scan output: it must survive `sm scan` truncation and `sm db reset` (which drops only `scan_*`).
387
+ Per-node "favorite" flag set by the local user from the UI. The set is small (typical projects pin a handful of skills/agents/commands), so the table degenerates to one row per favorited node, absence of a row means "not favorited". Exists in zone `state_` because it is user-authored preference, not regenerable scan output: it must survive `sm scan` truncation and `sm db reset` (which drops only `scan_*`).
388
388
 
389
389
  | Column | Type | Constraint |
390
390
  |---|---|---|
@@ -393,9 +393,9 @@ Per-node "favorite" flag set by the local user from the UI. The set is small (ty
393
393
 
394
394
  No indexes (PK already covers lookup by path; the table is keyed-by-path exclusively).
395
395
 
396
- `node_path` is FK-semantic to `scan_nodes.path`. Per `§ Rename detection` below, the rename heuristic MUST migrate rows in this table when a path is renamed (same protocol as `state_jobs` / `state_summaries` / `state_enrichments` / `state_plugin_kvs`). A simple PK update suffices there is no composite key, so collisions cannot occur (the destination path either has a row already, in which case the migrating row is dropped to preserve the live one, or it does not).
396
+ `node_path` is FK-semantic to `scan_nodes.path`. Per `§ Rename detection` below, the rename heuristic MUST migrate rows in this table when a path is renamed (same protocol as `state_jobs` / `state_summaries` / `state_enrichments` / `state_plugin_kvs`). A simple PK update suffices, there is no composite key, so collisions cannot occur (the destination path either has a row already, in which case the migrating row is dropped to preserve the live one, or it does not).
397
397
 
398
- The BFF's `/api/nodes` route loads the full set of favorited paths once per request (`SELECT node_path FROM state_node_favorites`) and decorates each emitted `Node` with a derived `isFavorite` boolean by Set membership no SQL JOIN against `scan_nodes` is required, and the table participates in zero of the per-scan persistence transactions.
398
+ The BFF's `/api/nodes` route loads the full set of favorited paths once per request (`SELECT node_path FROM state_node_favorites`) and decorates each emitted `Node` with a derived `isFavorite` boolean by Set membership, no SQL JOIN against `scan_nodes` is required, and the table participates in zero of the per-scan persistence transactions.
399
399
 
400
400
  ---
401
401
 
@@ -414,11 +414,11 @@ Persists user-toggled enable/disable overrides. Discovery is still filesystem-ba
414
414
 
415
415
  **Effective enable/disable resolution.** A plugin is enabled iff the highest-precedence layer that mentions it says so. Order from highest to lowest:
416
416
 
417
- 1. `config_plugins.enabled` for the row whose `plugin_id` matches written by `sm plugins enable/disable`. Local-machine user override; never committed (the DB is gitignored unless `history.share: true`).
418
- 2. `.skill-map/settings.json#/plugins/<id>/enabled` committed team-shared baseline.
419
- 3. Installed default every discovered plugin is enabled until told otherwise.
417
+ 1. `config_plugins.enabled` for the row whose `plugin_id` matches, written by `sm plugins enable/disable`. Local-machine user override; never committed (the DB is gitignored unless `history.share: true`).
418
+ 2. `.skill-map/settings.json#/plugins/<id>/enabled`, committed team-shared baseline.
419
+ 3. Installed default, every discovered plugin is enabled until told otherwise.
420
420
 
421
- The DB intentionally takes precedence over `settings.json` so a developer can locally disable a misbehaving plugin without committing the toggle to the team's config. Conversely, a team baseline that explicitly enables a plugin is overridable per-machine no agreement is required to experiment.
421
+ The DB intentionally takes precedence over `settings.json` so a developer can locally disable a misbehaving plugin without committing the toggle to the team's config. Conversely, a team baseline that explicitly enables a plugin is overridable per-machine, no agreement is required to experiment.
422
422
 
423
423
  ### `config_preferences`
424
424
 
@@ -486,15 +486,15 @@ Collisions after normalization are a load-time error; both plugins are disabled
486
486
 
487
487
  The kernel MUST enforce all three layers **in this exact order** for every plugin migration:
488
488
 
489
- 1. **Parse** the kernel parses each plugin migration SQL file into an AST. Parse errors disable the plugin with status `load-error`.
490
- 2. **DDL validation (pre-rewrite)** the AST is validated against the original table names authored by the plugin. Kernel MUST reject, before any rewrite:
489
+ 1. **Parse**, the kernel parses each plugin migration SQL file into an AST. Parse errors disable the plugin with status `load-error`.
490
+ 2. **DDL validation (pre-rewrite)**, the AST is validated against the original table names authored by the plugin. Kernel MUST reject, before any rewrite:
491
491
  - References (FK / trigger / view) to any kernel table (prefix `scan_`, `state_`, `config_`) or to another plugin's table (prefix `plugin_<other-id>_`).
492
492
  - `DROP` / `ALTER` / `TRUNCATE` against anything outside the plugin's own logical table names.
493
493
  - `ATTACH DATABASE` statements.
494
494
  - Global `PRAGMA` statements (anything not scoped to a plugin-owned table).
495
495
  Rejection here is intentional: validation runs **before** prefix injection so kernel tables are named as the plugin wrote them, making the reject test straightforward.
496
- 3. **Prefix injection (rewrite)** the kernel rewrites the AST so every table name the plugin authored becomes `plugin_<normalizedId>_<originalName>` if it doesn't already carry the prefix. Index and constraint names get the same treatment. A plugin CANNOT create un-prefixed tables.
497
- 4. **Scoped connection (runtime)** at runtime, the plugin receives a `Database` wrapper (not a raw handle). The wrapper rejects any query that touches tables whose name doesn't start with this plugin's prefix. This is the last-line defense: even if a migration-time layer were bypassed, runtime queries still cannot reach out-of-namespace data.
496
+ 3. **Prefix injection (rewrite)**, the kernel rewrites the AST so every table name the plugin authored becomes `plugin_<normalizedId>_<originalName>` if it doesn't already carry the prefix. Index and constraint names get the same treatment. A plugin CANNOT create un-prefixed tables.
497
+ 4. **Scoped connection (runtime)**, at runtime, the plugin receives a `Database` wrapper (not a raw handle). The wrapper rejects any query that touches tables whose name doesn't start with this plugin's prefix. This is the last-line defense: even if a migration-time layer were bypassed, runtime queries still cannot reach out-of-namespace data.
498
498
 
499
499
  Step 4 is separate from 1–3 because it applies at query time, not migration time. Together the four steps form the "triple protection" referenced across the spec (the name predates the explicit parse step).
500
500
 
@@ -504,7 +504,7 @@ Honest note: plugins are user-placed code. Protection guards against accidents (
504
504
 
505
505
  ## Backups
506
506
 
507
- - `sm db backup [--out <path>]` WAL checkpoint (SQLite; engine-equivalent for others) + file copy.
507
+ - `sm db backup [--out <path>]`, WAL checkpoint (SQLite; engine-equivalent for others) + file copy.
508
508
  - Default backup location: `.skill-map/backups/<timestamp>.db`.
509
509
  - Auto-backup before migrations: `.skill-map/backups/skill-map-pre-migrate-v<N>.db`.
510
510
  - `sm db restore <path>` swaps the current DB with the supplied file. Interactive confirmation required unless `--force`.
@@ -534,8 +534,8 @@ Note on casing: `bodyHash` / `frontmatterHash` / `analyzerId` / `data` are the d
534
534
 
535
535
  The heuristic runs inside the scan transaction, so either all renames land or none do. `sm scan` is the only surface that triggers automatic rename detection. Two manual verbs exist for cases the heuristic missed or got wrong:
536
536
 
537
- - `sm orphans reconcile <orphan.path> --to <new.path>` forward direction. Attaches FKs of an orphan to a live node. Use when the heuristic could not match (semantic rename, body rewrite).
538
- - `sm orphans undo-rename <new.path>` reverse direction. Reads `issue.data.from` from the active `auto-rename-medium` (or `--from`-disambiguated `auto-rename-ambiguous`) issue on `<new.path>`, migrates `state_*` FKs back, and resolves the issue. The prior path becomes an `orphan`. Use when the heuristic matched two unrelated files that happened to share a frontmatter hash.
537
+ - `sm orphans reconcile <orphan.path> --to <new.path>`, forward direction. Attaches FKs of an orphan to a live node. Use when the heuristic could not match (semantic rename, body rewrite).
538
+ - `sm orphans undo-rename <new.path>`, reverse direction. Reads `issue.data.from` from the active `auto-rename-medium` (or `--from`-disambiguated `auto-rename-ambiguous`) issue on `<new.path>`, migrates `state_*` FKs back, and resolves the issue. The prior path becomes an `orphan`. Use when the heuristic matched two unrelated files that happened to share a frontmatter hash.
539
539
 
540
540
  Both verbs operate on FK ownership only; neither edits files on disk.
541
541
 
@@ -548,8 +548,8 @@ Both verbs operate on FK ownership only; neither edits files on disk.
548
548
  - DB file exists and is readable.
549
549
  - `PRAGMA quick_check` (or equivalent) returns OK.
550
550
  - Applied migration version matches code-bundled migrations.
551
- - No `state_jobs` rows whose `content_hash` is missing from `state_job_contents` (corrupt state the content row was deleted out from under a live job).
552
- - No `state_job_contents` rows whose `content_hash` is referenced by zero `state_jobs` rows (GC stragglers `sm job prune` should have collected these).
551
+ - No `state_jobs` rows whose `content_hash` is missing from `state_job_contents` (corrupt state, the content row was deleted out from under a live job).
552
+ - No `state_job_contents` rows whose `content_hash` is referenced by zero `state_jobs` rows (GC stragglers, `sm job prune` should have collected these).
553
553
  - No plugin in `load-error` or `incompatible-spec` status.
554
554
 
555
555
  Failures are reported with suggested remediation (e.g., "run `sm db migrate`", "run `sm job prune`").
@@ -558,10 +558,10 @@ Failures are reported with suggested remediation (e.g., "run `sm db migrate`", "
558
558
 
559
559
  ## See also
560
560
 
561
- - [`architecture.md`](./architecture.md) `StoragePort` interface definition and dependency analyzers.
562
- - [`plugin-kv-api.md`](./plugin-kv-api.md) `ctx.store` accessor for mode A / mode B persistence.
563
- - [`job-lifecycle.md`](./job-lifecycle.md) atomic claim and TTL/reap semantics that drive `state_jobs`.
564
- - [`cli-contract.md`](./cli-contract.md) `sm db` verb surface (reset, backup, restore, migrate).
561
+ - [`architecture.md`](./architecture.md), `StoragePort` interface definition and dependency analyzers.
562
+ - [`plugin-kv-api.md`](./plugin-kv-api.md), `ctx.store` accessor for mode A / mode B persistence.
563
+ - [`job-lifecycle.md`](./job-lifecycle.md), atomic claim and TTL/reap semantics that drive `state_jobs`.
564
+ - [`cli-contract.md`](./cli-contract.md), `sm db` verb surface (reset, backup, restore, migrate).
565
565
 
566
566
  ---
567
567