@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.
- package/CHANGELOG.md +127 -0
- package/README.md +4 -4
- package/architecture.md +134 -128
- package/cli-contract.md +107 -104
- package/conformance/README.md +13 -13
- package/conformance/coverage.md +42 -39
- package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/provider.js +0 -1
- package/db-schema.md +45 -45
- package/index.json +41 -38
- package/interfaces/security-scanner.md +20 -20
- package/job-events.md +21 -21
- package/job-lifecycle.md +21 -21
- package/package.json +3 -2
- package/plugin-author-guide.md +135 -111
- package/plugin-kv-api.md +10 -10
- package/prompt-preamble.md +8 -8
- package/schemas/annotations.schema.json +3 -3
- package/schemas/api/rest-envelope.schema.json +15 -11
- package/schemas/conformance-result.schema.json +120 -0
- package/schemas/execution-record.schema.json +2 -2
- package/schemas/extensions/analyzer.schema.json +9 -0
- package/schemas/extensions/base.schema.json +4 -4
- package/schemas/extensions/extractor.schema.json +4 -4
- package/schemas/extensions/formatter.schema.json +1 -1
- package/schemas/extensions/hook.schema.json +3 -3
- package/schemas/extensions/provider.schema.json +6 -11
- package/schemas/frontmatter/base.schema.json +1 -1
- package/schemas/history-stats.schema.json +4 -4
- package/schemas/input-types.schema.json +3 -3
- package/schemas/issue.schema.json +1 -1
- package/schemas/job.schema.json +2 -2
- package/schemas/node.schema.json +5 -5
- package/schemas/plugins-doctor.schema.json +97 -0
- package/schemas/plugins-registry.schema.json +2 -2
- package/schemas/project-config.schema.json +10 -14
- package/schemas/refresh-report.schema.json +52 -0
- package/schemas/report-base-deterministic.schema.json +1 -1
- package/schemas/sidecar.schema.json +3 -3
- package/schemas/summaries/markdown.schema.json +1 -1
- package/schemas/summaries/skill.schema.json +1 -1
- package/schemas/view-slots.schema.json +7 -7
- package/versioning.md +7 -7
package/conformance/README.md
CHANGED
|
@@ -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
|
|
8
|
-
- **Provider-owned cases
|
|
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
|
|
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
|
|
61
|
-
"description": "string
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
166
|
-
- [`../versioning.md`](../versioning.md)
|
|
167
|
-
- [`../architecture.md`](../architecture.md)
|
|
168
|
-
- [`../prompt-preamble.md`](../prompt-preamble.md)
|
|
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
|
|
package/conformance/coverage.md
CHANGED
|
@@ -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
|
|
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`
|
|
13
|
-
| 3 | `issue.schema.json`
|
|
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
|
|
15
|
-
| 5 | `execution-record.schema.json`
|
|
16
|
-
| 6 | `project-config.schema.json`
|
|
17
|
-
| 7 | `plugins-registry.schema.json`
|
|
18
|
-
| 8 | `job.schema.json`
|
|
19
|
-
| 9 | `report-base.schema.json`
|
|
20
|
-
| 10 | `conformance-case.schema.json`
|
|
21
|
-
| 11 | `frontmatter/base.schema.json` | `orphan-markdown-fallback` | 🟢 covered | Universal frontmatter shape
|
|
22
|
-
| 12 | `summaries/skill.schema.json`
|
|
23
|
-
| 13 | `summaries/agent.schema.json`
|
|
24
|
-
| 14 | `summaries/command.schema.json`
|
|
25
|
-
| 15 | `summaries/hook.schema.json`
|
|
26
|
-
| 16 | `summaries/markdown.schema.json`
|
|
27
|
-
| 17 | `extensions/base.schema.json`
|
|
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
|
|
29
|
-
| 19 | `extensions/extractor.schema.json`
|
|
30
|
-
| 20 | `extensions/analyzer.schema.json`
|
|
31
|
-
| 21 | `extensions/action.schema.json`
|
|
32
|
-
| 22 | `extensions/formatter.schema.json`
|
|
33
|
-
| 23 | `history-stats.schema.json`
|
|
34
|
-
| 24 | `extensions/hook.schema.json`
|
|
35
|
-
| 25 | `api/rest-envelope.schema.json`
|
|
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`
|
|
39
|
-
| 29 | `report-base-deterministic.schema.json`
|
|
40
|
-
| 30 | `view-slots.schema.json`
|
|
41
|
-
| 31 | `input-types.schema.json`
|
|
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
|
|
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
|
|
56
|
-
| D | Duplicate detection
|
|
57
|
-
| E | `--force` bypass
|
|
58
|
-
| F | Nonce mismatch
|
|
59
|
-
| G | Reap
|
|
60
|
-
| H | `run.*` event envelope for Skill agent
|
|
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
|
|
63
|
-
| K | Plugin prefix injection
|
|
64
|
-
| L | Elapsed-time reporting
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
170
|
-
| `ran_at` | INTEGER | NOT NULL | Unix milliseconds
|
|
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
|
|
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
|
|
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
|
|
197
|
-
2. **Drop-on-disappear
|
|
198
|
-
3. **Upsert
|
|
199
|
-
4. **Stale flagging
|
|
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
|
|
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
|
|
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
|
|
233
|
-
2. **Catalog sweep
|
|
234
|
-
3. **Per-tuple sweep
|
|
235
|
-
4. **Upsert
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (?, ?, ?)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
418
|
-
2. `.skill-map/settings.json#/plugins/<id>/enabled
|
|
419
|
-
3. Installed default
|
|
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
|
|
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
|
|
490
|
-
2. **DDL validation (pre-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)
|
|
497
|
-
4. **Scoped connection (runtime)
|
|
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>]
|
|
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
|
|
538
|
-
- `sm orphans undo-rename <new.path
|
|
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
|
|
552
|
-
- No `state_job_contents` rows whose `content_hash` is referenced by zero `state_jobs` rows (GC stragglers
|
|
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)
|
|
562
|
-
- [`plugin-kv-api.md`](./plugin-kv-api.md)
|
|
563
|
-
- [`job-lifecycle.md`](./job-lifecycle.md)
|
|
564
|
-
- [`cli-contract.md`](./cli-contract.md)
|
|
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
|
|