@skill-map/spec 0.19.0 → 0.21.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 +607 -6
- package/README.md +6 -6
- package/architecture.md +129 -57
- package/cli-contract.md +71 -25
- package/conformance/README.md +2 -2
- package/conformance/cases/kernel-empty-boot.json +2 -2
- package/conformance/cases/sidecar-end-to-end.json +3 -3
- package/conformance/coverage.md +5 -5
- package/conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm +1 -1
- package/conformance/fixtures/sidecar-example/agent-example.md +1 -1
- package/db-schema.md +22 -18
- package/index.json +36 -36
- package/interfaces/security-scanner.md +2 -2
- package/job-events.md +12 -12
- package/job-lifecycle.md +1 -1
- package/package.json +1 -1
- package/plugin-author-guide.md +131 -82
- package/plugin-kv-api.md +6 -6
- package/prompt-preamble.md +1 -1
- package/schemas/annotations.schema.json +4 -4
- package/schemas/api/rest-envelope.schema.json +4 -4
- package/schemas/conformance-case.schema.json +2 -2
- package/schemas/extensions/analyzer.schema.json +43 -0
- package/schemas/extensions/base.schema.json +5 -5
- package/schemas/extensions/extractor.schema.json +1 -1
- package/schemas/extensions/hook.schema.json +6 -4
- package/schemas/issue.schema.json +6 -6
- package/schemas/link.schema.json +2 -2
- package/schemas/plugins-registry.schema.json +1 -1
- package/schemas/project-config.schema.json +19 -1
- package/schemas/sidecar.schema.json +2 -2
- package/schemas/summaries/agent.schema.json +1 -1
- package/schemas/summaries/command.schema.json +1 -1
- package/schemas/summaries/hook.schema.json +1 -1
- package/schemas/{view-contracts.schema.json → view-slots.schema.json} +93 -55
- package/schemas/extensions/rule.schema.json +0 -43
package/cli-contract.md
CHANGED
|
@@ -178,7 +178,7 @@ Consumers: docs generator, shell completion, Web UI form generation, IDE extensi
|
|
|
178
178
|
|---|---|
|
|
179
179
|
| `sm config list` | Effective config after layered merge. |
|
|
180
180
|
| `sm config get <key>` | Single value. |
|
|
181
|
-
| `sm config set <key> <value
|
|
181
|
+
| `sm config set <key> <value> [--yes]` | Write to user config (scope-aware: `-g` writes to global). Privacy-sensitive keys require `--yes` to confirm — see §Privacy-sensitive config below. |
|
|
182
182
|
| `sm config reset <key>` | Remove user override; revert to default or higher-scope value. |
|
|
183
183
|
| `sm config show <key> --source` | Reveals origin: `default` / `project` / `global` / `env` / `flag`. |
|
|
184
184
|
|
|
@@ -186,6 +186,24 @@ Config precedence (lowest → highest): library defaults → user config → env
|
|
|
186
186
|
|
|
187
187
|
Keys are dot-paths (`jobs.minimumTtlSeconds`, `scan.tokenize`). Unknown keys → exit 5.
|
|
188
188
|
|
|
189
|
+
#### Privacy-sensitive config
|
|
190
|
+
|
|
191
|
+
Keys whose value opens disk access OUTSIDE the project root (today: `scan.includeHome`, `scan.extraRoots`, `scan.referencePaths`) are gated behind `--yes` so the user never expands the scan surface by accident. The analyzer:
|
|
192
|
+
|
|
193
|
+
- `sm config set <privacy-key> <value>` (without `--yes`) — when the new value would expand the surface (toggling `includeHome` from `false`→`true`, or adding paths to `extraRoots` / `referencePaths` that resolve outside the project root) — exits with code `2` and prints the full list of paths the change would expose to stderr, suggesting `--yes` to confirm.
|
|
194
|
+
- `sm config set <privacy-key> <value> --yes` — proceeds with the write and prints the same list as a confirmation receipt.
|
|
195
|
+
- Writes that NARROW the surface (toggling `includeHome` from `true`→`false`, removing paths) do not require `--yes`.
|
|
196
|
+
|
|
197
|
+
The Settings UI's Project section enforces the same analyzer via a confirm dialog that enumerates the paths.
|
|
198
|
+
|
|
199
|
+
#### Project-local-only config
|
|
200
|
+
|
|
201
|
+
The three privacy-sensitive keys above PLUS `allowEditSmFiles` are members of `PROJECT_LOCAL_ONLY_KEYS` (see [`architecture.md` §Config layering · Per-key locality](./architecture.md#per-key-locality)). The values are per-user-per-project and MUST NOT travel via the committed repo:
|
|
202
|
+
|
|
203
|
+
- `sm config set` writes them to `<cwd>/.skill-map/settings.local.json` (gitignored) by default. The user MAY also set them at user scope with `-g` (writes to `~/.skill-map/settings.json`).
|
|
204
|
+
- Attempting to write any of them with `--scope project` (or equivalent forcing flag) is REJECTED with exit `2` and a directed message pointing to `settings.local.json` or `-g`.
|
|
205
|
+
- The loader strips them (with a warning) when found in the committed `project` layer. An older install that wrote one of these keys to `settings.json` keeps validating against the schema, but the value is ignored at read time and `sm config show --source` surfaces the warning.
|
|
206
|
+
|
|
189
207
|
---
|
|
190
208
|
|
|
191
209
|
### Scan
|
|
@@ -196,6 +214,7 @@ Keys are dot-paths (`jobs.minimumTtlSeconds`, `scan.tokenize`). Unknown keys →
|
|
|
196
214
|
| `sm scan -n <node.path>` | Partial scan: one node. |
|
|
197
215
|
| `sm scan --changed` | Incremental: only files changed since last scan (mtime heuristic). |
|
|
198
216
|
| `sm scan --watch` | Long-running: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`. |
|
|
217
|
+
| `sm scan -g` / `sm scan --global` | Global scan. Roots default to every active Provider's `explorationDir` resolved against `~` (typically `~/.claude`, `~/.gemini`, `~/.agents`); the cwd is NOT included. Config + DB resolve from the global scope (`~/.skill-map/...`). Mutually exclusive with explicit positional roots — passing both is rejected with exit `2`. |
|
|
199
218
|
| `sm scan compare-with <dump> [roots...]` | Delta report: run a fresh scan in memory and compare against the saved `ScanResult` dump at `<dump>`. Read-only — does not modify the DB. Exit `0` on empty delta, `1` on any drift, `2` on operational error (missing or malformed dump, schema violation). |
|
|
200
219
|
| `sm watch [roots...]` | Long-running watcher. Same semantics as `sm scan --watch`, exposed as a top-level verb because the watcher is a loop, not a one-shot scan. |
|
|
201
220
|
| `sm refresh <node.path>` | Re-run Extractors against a single node and upsert their outputs into the universal enrichment layer (`node_enrichments`, see [`db-schema.md`](./db-schema.md#node_enrichments)). Extractors are deterministic-only — they run synchronously and persist. Exit `0` on success, `2` on failure, `5` if the node is not in the persisted scan. |
|
|
@@ -203,6 +222,15 @@ Keys are dot-paths (`jobs.minimumTtlSeconds`, `scan.tokenize`). Unknown keys →
|
|
|
203
222
|
|
|
204
223
|
`--json` output conforms to `schemas/scan-result.schema.json`. `sm watch` (and `sm scan --watch`) emit one ScanResult per batch — under `--json` this is an `ndjson` stream of ScanResult documents.
|
|
205
224
|
|
|
225
|
+
**Effective roots** (one-shot `sm scan`):
|
|
226
|
+
|
|
227
|
+
- `sm scan [roots...]`: when positional roots are given, they ARE the effective roots (verbatim). When omitted: the effective roots are `[cwd]` plus the appended sets below.
|
|
228
|
+
- `scan.includeHome === true` appends every active Provider's `explorationDir` resolved against `~` to the effective roots.
|
|
229
|
+
- `scan.extraRoots[]` is appended verbatim (entries starting with `~` resolve against the user home; relative entries resolve against the project root).
|
|
230
|
+
- `sm scan -g`: effective roots are the active Providers' `explorationDir` only; `scan.includeHome` and `scan.extraRoots` are ignored. The cwd is NOT included. Config + DB resolve from the global scope.
|
|
231
|
+
|
|
232
|
+
**Reference paths** (`scan.referencePaths[]`): walked in parallel by the scan to collect existing absolute paths into a side set. Files there are NOT parsed and NOT indexed as nodes; the kernel passes the set to analyzers via `IAnalyzerContext.referenceablePaths` so `core/broken-ref` can resolve a link against the filesystem when the in-graph lookup misses.
|
|
233
|
+
|
|
206
234
|
The watcher subscribes to the same roots that `sm scan` walks and respects `.skillmapignore` plus `config.ignore` exactly as the one-shot scan does. Filesystem events are grouped using `scan.watch.debounceMs` (default 300ms) before the watcher re-runs the incremental scan and persists. `SIGINT` / `SIGTERM` close the watcher cleanly. Exit code on clean shutdown is 0.
|
|
207
235
|
|
|
208
236
|
Exit: 0 on clean (or clean watcher shutdown), 1 if error-severity issues exist (one-shot scan only — the watcher does not flip exit code based on per-batch issues), 2 on operational error.
|
|
@@ -215,7 +243,7 @@ Exit: 0 on clean (or clean watcher shutdown), 1 if error-severity issues exist (
|
|
|
215
243
|
|---|---|
|
|
216
244
|
| `sm list [--kind <k>] [--issue] [--sort-by ...] [--limit N]` | Tabular listing. `--json` emits an array conforming to `node.schema.json`. |
|
|
217
245
|
| `sm show <node.path>` | Node detail: weight (bytes/tokens triple-split), frontmatter, links in/out, issues, findings, summary. `--json` emits a detail object with the raw link rows. Pretty output groups identical-shape links (same endpoint, kind, normalized trigger) onto one line and lists the union of extractor ids in a `sources:` field; the section header reports both the raw row count and the unique-after-grouping count, e.g. `Links out (12, 9 unique)`. Storage keeps one row per extractor (`scan_links` is unchanged) — the grouping is purely a read-time presentation choice. |
|
|
218
|
-
| `sm check [-n <node.path>] [--
|
|
246
|
+
| `sm check [-n <node.path>] [--analyzers <ids>] [--include-prob] [--async]` | Print all current issues. Equivalent to `sm scan --json \| jq '.issues'` but faster (reads from DB). `-n` restricts to issues whose `nodeIds` include the path; `--analyzers <ids>` accepts a comma-separated list of qualified or short analyzer ids and restricts the issue read accordingly. Default behaviour is deterministic-only (CI-safe, status quo). `--include-prob` is the opt-in flag for probabilistic Analyzer dispatch (spec § A.7): the verb loads the plugin runtime, finds Analyzers with `mode === 'probabilistic'` (filtered by `--analyzers` if set), and emits a stderr advisory naming the analyzer ids. Full prob dispatch requires the job subsystem (Step 10); until then `--include-prob` is a stub — prob analyzers never produce issues, never alter the exit code, and `--async` (reserved companion: returns job ids without waiting once jobs land) is a no-op the advisory simply mentions. The flag does NOT extend to `sm scan` or `sm list`. |
|
|
219
247
|
| `sm findings [--kind ...] [--since ...] [--threshold <n>]` | Probabilistic findings (injection, stale summaries, low confidence). `--json` emits an array of finding objects. |
|
|
220
248
|
| `sm graph [--format ascii\|mermaid\|dot]` | Render the full graph via the named formatter. |
|
|
221
249
|
| `sm export <query> --format json\|md\|mermaid` | Filtered export. Query syntax is implementation-defined pre-1.0. |
|
|
@@ -240,11 +268,11 @@ The built-in deterministic `core/bump` Action is the canonical write channel for
|
|
|
240
268
|
|
|
241
269
|
| Command | Purpose |
|
|
242
270
|
|---|---|
|
|
243
|
-
| `sm bump <node.path> [--force]` | Single-node bump. Wraps `core/bump`. Refuses on a fresh node (`{ ok: false, reason: 'fresh' }`, exit `2`) unless `--force` is passed; with `--force` on a fresh node the verb is a silent no-op (exit `0`, no stdout). On a stale node (or first-time creation) increments `annotations.version`, refreshes `for.{bodyHash, frontmatterHash}`, and stamps the audit block (`audit.lastBumpedAt` + `audit.lastBumpedBy: 'cli'`; on first creation also `audit.createdAt` + `audit.createdBy: 'cli'`). Exit `5` if the node is not in the persisted scan. `--json` emits the report shape declared by `bump-report.schema.json`. |
|
|
244
|
-
| `sm bump --pending [--staged] [--force]` | Batch bump. Walks every node whose sidecar overlay reports drift in `node.path` ASC order and bumps each. `--staged` runs `git add <sidecar-path>` after each successful bump so the new content lands in the same commit; `git add` failure degrades to a stderr warning, the batch keeps running. Empty stale set → exit `0` with a "nothing to do" advisory. `--json` envelope: `{ bumped, refused, skipped, errors[], elapsedMs }`. Exit `0` on a clean run; `1` when at least one per-node error landed in `errors[]`. **Git error matrix for `--staged`**: not inside a git repo (no `.git/` parent of `cwd`) → exit `5`; `git` binary not on PATH (spawn ENOENT) → exit `2`. Both checks run BEFORE any sidecar write so a misconfigured environment never produces partial state. |
|
|
245
|
-
| `sm sidecar refresh <node.path
|
|
246
|
-
| `sm sidecar prune [--dry-run] [--yes]` | Delete orphan `.sm` files (sidecars whose accompanying `<basename>.md` does not exist on disk). Destructive — without `--dry-run` prompts for interactive confirmation listing every file to be deleted (per the §Dry-run
|
|
247
|
-
| `sm sidecar annotate <node.path> [--force]` | Pure scaffolding. Writes a minimal `.sm` next to the `.md` with the `identity:` block populated and an empty `annotations: {}` block, ready for editing. Refuses if the file exists; `--force` overwrites. The optional legacy-frontmatter migration helper (`--from-frontmatter`) is deferred — no released consumer demands it. |
|
|
271
|
+
| `sm bump <node.path> [--force] [--yes]` | Single-node bump. Wraps `core/bump`. Refuses on a fresh node (`{ ok: false, reason: 'fresh' }`, exit `2`) unless `--force` is passed; with `--force` on a fresh node the verb is a silent no-op (exit `0`, no stdout). On a stale node (or first-time creation) increments `annotations.version`, refreshes `for.{bodyHash, frontmatterHash}`, and stamps the audit block (`audit.lastBumpedAt` + `audit.lastBumpedBy: 'cli'`; on first creation also `audit.createdAt` + `audit.createdBy: 'cli'`). Exit `5` if the node is not in the persisted scan. `--json` emits the report shape declared by `bump-report.schema.json`. `--yes` confirms consent for `.sm` writes in this project — see §`.sm` write consent below. |
|
|
272
|
+
| `sm bump --pending [--staged] [--force] [--yes]` | Batch bump. Walks every node whose sidecar overlay reports drift in `node.path` ASC order and bumps each. `--staged` runs `git add <sidecar-path>` after each successful bump so the new content lands in the same commit; `git add` failure degrades to a stderr warning, the batch keeps running. Empty stale set → exit `0` with a "nothing to do" advisory. `--json` envelope: `{ bumped, refused, skipped, errors[], elapsedMs }`. Exit `0` on a clean run; `1` when at least one per-node error landed in `errors[]`. **Git error matrix for `--staged`**: not inside a git repo (no `.git/` parent of `cwd`) → exit `5`; `git` binary not on PATH (spawn ENOENT) → exit `2`. Both checks run BEFORE any sidecar write so a misconfigured environment never produces partial state. `--yes` confirms consent for `.sm` writes — see §`.sm` write consent below. |
|
|
273
|
+
| `sm sidecar refresh <node.path> [--yes]` | Hash-only update on the sidecar. Refreshes `for.{bodyHash, frontmatterHash}` to match the live node WITHOUT bumping `annotations.version` and WITHOUT touching the audit block. Useful when the user knows a body change is editorial-only and doesn't want to spend a version increment. Distinct from the top-level `sm refresh` (which targets the enrichment layer at Step A.8) — different storage, different concept; the sub-namespace prefix prevents the collision. Exit `5` if the node has no sidecar or is not in the persisted scan. No-op on a fresh node (informational stderr, exit `0`). `--yes` confirms consent for `.sm` writes — see §`.sm` write consent below. |
|
|
274
|
+
| `sm sidecar prune [--dry-run] [--yes]` | Delete orphan `.sm` files (sidecars whose accompanying `<basename>.md` does not exist on disk). Destructive — without `--dry-run` prompts for interactive confirmation listing every file to be deleted (per the §Dry-run analyzer for destructive verbs). `--yes` (alias `--force`) bypasses the destructive-confirmation prompt for non-interactive callers (CI, the pre-commit hook, scripts) — NOT to be confused with the `.sm` write-consent gate, which does not apply to prune (delete is not a write). With `--dry-run` reports what would be deleted without touching disk and never prompts. Different domain from `sm orphans` — that verb operates on the node graph (rename heuristic); this one operates on the filesystem layer. `--json` envelope: `{ deleted, wouldDelete, errors, items[], elapsedMs }`. Exit `1` when delete failures landed in `errors`. |
|
|
275
|
+
| `sm sidecar annotate <node.path> [--force] [--yes]` | Pure scaffolding. Writes a minimal `.sm` next to the `.md` with the `identity:` block populated and an empty `annotations: {}` block, ready for editing. Refuses if the file exists; `--force` overwrites. The optional legacy-frontmatter migration helper (`--from-frontmatter`) is deferred — no released consumer demands it. `--yes` confirms consent for `.sm` writes — see §`.sm` write consent below. |
|
|
248
276
|
| `sm hooks install pre-commit-bump [--dry-run]` | Install (or chain into) a git pre-commit hook that runs `sm bump --pending --staged` so any staged drift in `.sm` sidecars auto-bumps before the commit lands. Idempotent: re-running detects the skill-map marker and no-ops. When the repo already has a custom `pre-commit`, the verb appends the skill-map block to the existing file rather than replacing it. `--dry-run` prints the planned content with `--- target: <path> ---` markers and writes nothing. Exit `5` if no `.git/` parent is found at or above `cwd`; exit `2` on write failures or unknown hook flavours. |
|
|
249
277
|
|
|
250
278
|
**`.sm` round-trip contract.** The `bump` verb, `sm sidecar refresh`, and `sm sidecar annotate` write through `FilesystemSidecarStore`, which re-serialises the merged result via `js-yaml` `dump` with `sortKeys: true`. **`.sm` files are managed artifacts; comments and key order are not preserved on round-trip.** Author commentary belongs in the markdown body or in a separate documentation file, not inside `.sm`. The integrity guarantee is that the merged YAML always validates against `sidecar.schema.json` + `annotations.schema.json` and that the file is written atomically (`.tmp + rename`).
|
|
@@ -300,11 +328,12 @@ The Hono BFF exposes the single-node bump flow over REST so the UI can drive the
|
|
|
300
328
|
| Field | Value |
|
|
301
329
|
|---|---|
|
|
302
330
|
| Method + path | `POST /api/sidecar/bump` |
|
|
303
|
-
| Request body | `{ "nodePath": <string, required>, "force"?: <boolean, default false> }` (JSON). `nodePath` is the canonical scope-root-relative `Node.path`. |
|
|
331
|
+
| Request body | `{ "nodePath": <string, required>, "force"?: <boolean, default false>, "confirm"?: <boolean, default false> }` (JSON). `nodePath` is the canonical scope-root-relative `Node.path`. `confirm` is the per-request `.sm` write-consent bypass — see §`.sm` write consent below. |
|
|
304
332
|
| 200 envelope | `{ "schemaVersion": "1", "kind": "sidecar.bumped", "value": { "nodePath": <string>, "version": <int|null>, "status": "fresh" }, "elapsedMs": <int> }`. The `kind` value is part of the canonical `rest-envelope.schema.json#/properties/kind/enum` and validates under the action-result `oneOf` variant (`value` + `elapsedMs` siblings, no `filters` / `counts` / `kindRegistry`). |
|
|
305
333
|
| 409 envelope | `{ "ok": false, "error": { "code": "sidecar-fresh", "message": <string>, "details": null } }`. Returned when the target node is fresh and `force !== true`. The `'sidecar-fresh'` code is added to `app.ts`'s `TErrorCode` union. |
|
|
334
|
+
| 412 envelope | `{ "ok": false, "error": { "code": "confirm-required", "message": <string>, "details": { "key": "allowEditSmFiles" } } }`. Returned when `allowEditSmFiles` is `false` and `confirm !== true`. The UI catches this and opens a ConfirmDialog; on accept it retries the POST with `{ ..., "confirm": true }` — the kernel then persists `allowEditSmFiles: true` to `<cwd>/.skill-map/settings.local.json` and performs the bump. See §`.sm` write consent below. |
|
|
306
335
|
| 404 envelope | Standard `'not-found'` envelope. Returned when the DB is missing OR `nodePath` is not in the persisted scan. |
|
|
307
|
-
| 400 envelope | Standard `'bad-query'` envelope. Body must be a JSON object with `nodePath` (non-empty string); `force` (when present) must be
|
|
336
|
+
| 400 envelope | Standard `'bad-query'` envelope. Body must be a JSON object with `nodePath` (non-empty string); `force` / `confirm` (when present) must be booleans. |
|
|
308
337
|
| 200 force-on-fresh | Per the Action spec, `force: true` on a fresh node is a silent no-op — the response carries the existing `version` (read off the sidecar overlay) and `status: 'fresh'`. **No WS broadcast** is emitted in this case (decision: no-op = no event; nothing changed on disk, sending `sidecar.bumped` would tell every connected UI to refresh state that hasn't moved). |
|
|
309
338
|
|
|
310
339
|
**WS event — `sidecar.bumped`** (Step 9.6.5; canonical envelope shape locked in 9.6.7 / R9). After every successful bump that materialises a write, the BFF broadcasts a `sidecar.bumped` event over `/ws` so all connected clients refresh in lockstep. The event uses the canonical `IWsEventEnvelope` wire shape (matches every other kernel→broadcaster bridge — `scan.*`, `watcher.*`, etc.):
|
|
@@ -321,7 +350,7 @@ The Hono BFF exposes the single-node bump flow over REST so the UI can drive the
|
|
|
321
350
|
}
|
|
322
351
|
```
|
|
323
352
|
|
|
324
|
-
Emission
|
|
353
|
+
Emission analyzers:
|
|
325
354
|
|
|
326
355
|
- Emitted on a successful 200 bump (stale → fresh, or first-time-create → fresh).
|
|
327
356
|
- **NOT** emitted on a force-on-fresh no-op 200 (nothing changed on disk).
|
|
@@ -343,6 +372,19 @@ Read-only catalog of plugin-contributed annotation keys. The endpoint is a pure
|
|
|
343
372
|
| Empty case | When the kernel was booted with no plugin contributions (or `--no-plugins`): `{ "items": [], "counts": { "total": 0 } }`. |
|
|
344
373
|
| Refresh policy | Same as the rest of the BFF's plugin surface — discovery happens once at `sm serve` boot. An operator that installs a new plugin restarts the server (matches the watcher's "loaded ONCE at boot" contract). |
|
|
345
374
|
|
|
375
|
+
##### `.sm` write consent
|
|
376
|
+
|
|
377
|
+
Every verb in this section that writes `.sm` (the `bump` table rows, `sm sidecar refresh`, `sm sidecar annotate`, and the BFF's `POST /api/sidecar/bump`) consults the `allowEditSmFiles` setting (see [`architecture.md` §Config layering · Per-key locality](./architecture.md#per-key-locality) and §Annotation system · Write consent). Behaviour:
|
|
378
|
+
|
|
379
|
+
- **`allowEditSmFiles === true`** — the verb proceeds silently. No prompt, no flag mutation, identical to the pre-consent behaviour.
|
|
380
|
+
- **`allowEditSmFiles === false` and the operator passes `--yes` (CLI) or `{ "confirm": true }` (BFF body)** — the kernel persists `allowEditSmFiles: true` to `<cwd>/.skill-map/settings.local.json` (gitignored) and proceeds with the write. The flag flip is durable; the next invocation will not re-ask.
|
|
381
|
+
- **`allowEditSmFiles === false` and the operator did NOT confirm**:
|
|
382
|
+
- **CLI on a TTY** — the verb prints a one-paragraph explanation of what `.sm` files are and where they will land, then runs an interactive `confirm()` prompt. Accept proceeds (same effect as `--yes`); decline aborts the verb without persisting the rejection (exit `2`, the verb's reported `errors[]` carries one entry with code `confirm-required`). The next invocation re-asks; declining is never "remembered".
|
|
383
|
+
- **CLI without a TTY** (CI, piped stdin, agent harness) — the verb exits `2` immediately with a stderr message: `consent required: pass --yes to allow .sm sidecars in this project (writes to .skill-map/settings.local.json — gitignored)`.
|
|
384
|
+
- **BFF** — the route returns 412 `confirm-required` (envelope shown in the bump-endpoint table above). The UI catches the code and opens a `ConfirmationService.confirm({ ... })` dialog; on accept it retries the original request with `{ "confirm": true }`; on reject the action is silently abandoned (no toast spam — the user opted out).
|
|
385
|
+
|
|
386
|
+
`sm sidecar prune --yes` is unaffected: `--yes` on `prune` bypasses the destructive-delete confirmation prompt (the verb does not write `.sm`; it deletes orphans). The two flags share a spelling but address orthogonal concerns.
|
|
387
|
+
|
|
346
388
|
---
|
|
347
389
|
|
|
348
390
|
### Jobs
|
|
@@ -411,7 +453,7 @@ Authentication: the nonce is the sole credential. An implementation MUST reject
|
|
|
411
453
|
| `sm plugins list` | Auto-discovered plugins with status. `--json` emits an array of `DiscoveredPlugin`. |
|
|
412
454
|
| `sm plugins show <id>` | Full manifest + compat detail. |
|
|
413
455
|
| `sm plugins enable <id> \| --all` | Toggle on. Persists in `config_plugins`. `--all` applies to every discovered plugin. |
|
|
414
|
-
| `sm plugins disable <id> \| --all` | Toggle off; does not delete the plugin directory. `--all` applies to every discovered plugin. |
|
|
456
|
+
| `sm plugins disable <id> \| --all` | Toggle off; does not delete the plugin directory. Eagerly purges the plugin's rows from `scan_contributions` so its UI chips disappear before the next scan (plugin-managed state in `state_plugin_kvs` / dedicated tables is preserved — see `plugin-kv-api.md`). `--all` applies to every discovered plugin. |
|
|
415
457
|
| `sm plugins doctor` | Revalidate all plugins against current spec version; update `status` fields. |
|
|
416
458
|
|
|
417
459
|
---
|
|
@@ -432,7 +474,7 @@ See `db-schema.md` for the table catalog.
|
|
|
432
474
|
| `sm db dump [--tables ...]` | SQL dump. |
|
|
433
475
|
| `sm db migrate [--dry-run \| --status \| --to <n> \| --kernel-only \| --plugin <id> \| --no-backup]` | Migration controls. |
|
|
434
476
|
|
|
435
|
-
Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interactive confirmation unless `--yes` (non-interactive mode for scripts) or `--force` (alias, kept for backward compatibility) is passed. `sm db reset` without a modifier is non-destructive and never prompts. **`--dry-run` short-circuits the confirmation prompt entirely** (per §Dry-run
|
|
477
|
+
Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interactive confirmation unless `--yes` (non-interactive mode for scripts) or `--force` (alias, kept for backward compatibility) is passed. `sm db reset` without a modifier is non-destructive and never prompts. **`--dry-run` short-circuits the confirmation prompt entirely** (per §Dry-run analyzer: dry-run MUST NOT depend on `--yes` / `--force`).
|
|
436
478
|
|
|
437
479
|
---
|
|
438
480
|
|
|
@@ -467,21 +509,24 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
|
|
|
467
509
|
| `GET /api/nodes?kind=&hasIssues=&path=&limit=&offset=` | implemented | `RestEnvelope` (`kind: 'nodes'`) — paginated, filtered list. Filters share the `kind=` / `has=issues` / `path=<glob>` grammar with `sm export`. `hasIssues=false` is a server-side post-filter (not representable in the kernel grammar). Pagination defaults `offset=0`, `limit=100`; max `limit=1000`. |
|
|
468
510
|
| `GET /api/nodes/:pathB64[?include=body]` | implemented | Single-node detail envelope: `{ schemaVersion, kind: 'node', item: Node, links: { incoming: Link[], outgoing: Link[] }, issues: Issue[] }`. `:pathB64` is base64url (RFC 4648 §5, no padding) of `node.path`. Missing node or malformed `pathB64` → 404 `not-found`. **`?include=body`** (Step 14.5.a) — opt-in flag that adds `item.body: string \| null` to the response. The body is read from disk on demand at request time (the kernel persists `bodyHash` only). `null` indicates the source file was missing / unreadable when the request landed (the watcher will re-emit `scan.completed` when it catches up). Without the flag, `item.body` is `undefined` and the handler does not touch the filesystem. |
|
|
469
511
|
| `GET /api/links?kind=&from=&to=` | implemented | `RestEnvelope` (`kind: 'links'`) — list of links. Filters: `kind` (CSV whitelist of `link.kind`), `from` (exact match on `link.source`), `to` (exact match on `link.target`). No pagination at v14.2. |
|
|
470
|
-
| `GET /api/issues?severity=&
|
|
512
|
+
| `GET /api/issues?severity=&analyzerId=&node=` | implemented | `RestEnvelope` (`kind: 'issues'`) — list of issues. Filters: `severity` (CSV from `error\|warn\|info`), `analyzerId` (CSV; qualified or short suffix per `sm check --analyzers`), `node` (filter to issues whose `nodeIds` includes the path). No pagination at v14.2. |
|
|
471
513
|
| `GET /api/graph?format=ascii\|json\|md` | implemented | formatter-rendered graph. `Content-Type` per format: `text/plain` (ascii), `application/json` (json), `text/markdown` (md / mermaid). Default `format=ascii`. Unknown format → 400 `bad-query`. |
|
|
472
514
|
| `GET /api/config` | implemented | `RestEnvelope` (`kind: 'config'`) — merged effective config (defaults → user → user-local → project → project-local → override). |
|
|
473
|
-
| `GET /api/plugins` | implemented | `RestEnvelope` (`kind: 'plugins'`) — list of installed plugins (built-in + drop-in) with status. Item shape: `{ id, version, kinds, status, reason, source: 'built-in'\|'project'\|'global', granularity: 'bundle'\|'extension', description?: string, extensions?: Array<{ id, kind, version, enabled, description?: string }> }`. The `granularity` field reflects the manifest declaration (built-ins: hardcoded per `built-in-plugins/built-ins.ts`; drop-ins: from `plugin.json#/granularity`, default `'bundle'`). The `description` field on the bundle item carries the manifest-declared description (built-ins: hardcoded on `IBuiltInBundle`; drop-ins: `plugin.json#/description`); each `extensions[]` entry carries its extension manifest's `description` per `IExtensionBase` (`extensions/base.schema.json#/properties/description`). The SPA's Settings list renders the descriptions as muted secondary text and includes them in its substring-search index alongside the ids. The `extensions` array is present **only** when `granularity === 'extension'` AND the plugin loaded successfully; each entry's `enabled` reflects the per-extension override resolution (DB > settings.json > installed default). For `granularity: 'bundle'` plugins the array is omitted (the bundle is the only toggle-able key). |
|
|
474
|
-
| `PATCH /api/plugins/:id` | implemented | Toggle one plugin's user override. `:id` MUST be a top-level bundle id; qualified-id form (`bundle/extension`) is the sibling route below. Body `{ enabled: boolean }` (JSON). Persists to `config_plugins` via `IConfigPluginsPort.set` — same path the CLI's `sm plugins enable\|disable` uses. Response is the canonical `{ id, version, kinds, status, reason, source }` row for the affected plugin (post-write `status` reflects the new override resolution). **Granularity** — rejected with 400 `bad-query` when the target bundle declares `granularity: 'extension'` (only the qualified-id form is toggle-able for those). **
|
|
475
|
-
| `PATCH /api/plugins/:bundleId/extensions/:extensionId` | implemented | Qualified-id form for `granularity: 'extension'` bundles (today: `core` + any user plugin that opts in). Body `{ enabled: boolean }`. Both segments are URL-path-segment-encoded (no slash inside `:bundleId` or `:extensionId`). Rejected with 400 `bad-query` when the target bundle declares `granularity: 'bundle'` (use the sibling route above). Same persistence +
|
|
515
|
+
| `GET /api/plugins` | implemented | `RestEnvelope` (`kind: 'plugins'`) — list of installed plugins (built-in + drop-in) with status. Item shape: `{ id, version, kinds, status, reason, source: 'built-in'\|'project'\|'global', granularity: 'bundle'\|'extension', description?: string, locked?: boolean, startsAsDisabled?: boolean, extensions?: Array<{ id, kind, version, enabled, description?: string, locked?: boolean }> }`. The `granularity` field reflects the manifest declaration (built-ins: hardcoded per `built-in-plugins/built-ins.ts`; drop-ins: from `plugin.json#/granularity`, default `'bundle'`). The `description` field on the bundle item carries the manifest-declared description (built-ins: hardcoded on `IBuiltInBundle`; drop-ins: `plugin.json#/description`); each `extensions[]` entry carries its extension manifest's `description` per `IExtensionBase` (`extensions/base.schema.json#/properties/description`). The SPA's Settings list renders the descriptions as muted secondary text and includes them in its substring-search index alongside the ids. The `extensions` array is present **only** when `granularity === 'extension'` AND the plugin loaded successfully; each entry's `enabled` reflects the per-extension override resolution (DB > settings.json > installed default). For `granularity: 'bundle'` plugins the array is omitted (the bundle is the only toggle-able key). The optional `locked: true` flag is stamped when the bundle id (or qualified extension id) appears in the host's lock-list (`src/server/locked-plugins.ts`); locked items render the toggle disabled in the SPA and any `PATCH` against them returns `403 locked`. The flag is omitted when false. The optional `startsAsDisabled: true` flag is stamped on drop-in plugins (never built-ins) whose discovery-time `status` was `'disabled'` — that is, the user had them disabled in `config_plugins` / `settings.json` at `sm serve` boot, so their handlers were never bucketed into the runtime. The SPA renders a per-row hint when this flag is set AND the user toggles the row back on, since re-enabling them requires `sm serve` restart (the rest of the toggle pipeline applies live). The flag is omitted when false. |
|
|
516
|
+
| `PATCH /api/plugins/:id` | implemented | Toggle one plugin's user override. `:id` MUST be a top-level bundle id; qualified-id form (`bundle/extension`) is the sibling route below. Body `{ enabled: boolean }` (JSON). Persists to `config_plugins` via `IConfigPluginsPort.set` — same path the CLI's `sm plugins enable\|disable` uses. Response is the canonical `{ id, version, kinds, status, reason, source }` row for the affected plugin (post-write `status` reflects the new override resolution). **Granularity** — rejected with 400 `bad-query` when the target bundle declares `granularity: 'extension'` (only the qualified-id form is toggle-able for those). **Lock** — rejected with 403 `locked` when the bundle id is in the host lock-list (`src/server/locked-plugins.ts`). **Apply window** — the override applies on the next scan (manual via `POST /api/scan` or `sm scan`, automatic via watcher batch); both the BFF and the watcher build a fresh resolver from `config_plugins` before composing extensions, so the toggle is honoured without restarting `sm serve`. The endpoint purges `scan_contributions` rows for the plugin immediately on disable so the UI stops rendering its chips before the next scan. **Exception** — drop-in plugins whose discovery-time `status` was `'disabled'` (carried as `startsAsDisabled: true` on the read shape) are NOT in the runtime extension buckets; re-enabling them via PATCH persists the override but requires `sm serve` restart for the handlers to be loaded. The SPA surfaces this per-row when the user re-enables such a plugin. The endpoint does NOT broadcast a WS event today. |
|
|
517
|
+
| `PATCH /api/plugins/:bundleId/extensions/:extensionId` | implemented | Qualified-id form for `granularity: 'extension'` bundles (today: `core` + any user plugin that opts in). Body `{ enabled: boolean }`. Both segments are URL-path-segment-encoded (no slash inside `:bundleId` or `:extensionId`). Rejected with 400 `bad-query` when the target bundle declares `granularity: 'bundle'` (use the sibling route above). **Lock** — rejected with 403 `locked` when either the bundle id or the qualified `bundleId/extensionId` appears in the host lock-list. Same persistence + apply-window semantics as the bundle form (including the `startsAsDisabled` exception). |
|
|
518
|
+
| `PATCH /api/plugins` | implemented | Bulk toggle. Body `{ "changes": Array<{ "id": string, "enabled": boolean }> }` where each `id` is either a bare bundle id or a qualified `<bundle>/<extension>` id (the dispatcher branches on the slash exactly like the single-id routes above). Empty `changes` array is accepted as a no-op (returns the current `GET /api/plugins` envelope). The route validates the **entire batch** before writing — any invalid entry (`unknown-plugin` / `granularity-mismatch` / `locked`) rejects the whole request with the offending id in `error.details.id`, and the DB is not touched. Valid batches are applied in **one SQLite transaction**: `IConfigPluginsPort.set` per entry, then one grouped `scan_contributions` purge per disabled plugin (enables skip the purge). Response is the same `RestEnvelope` (`kind: 'plugins'`) shape as `GET /api/plugins`, reflecting the post-write state — the SPA replaces its modal state from this envelope. Apply-window and `startsAsDisabled` exception semantics match the per-id routes. The single-id `PATCH /api/plugins/:id` and qualified-id sibling stay available for CLI / external automation; the bulk variant exists so the SPA can stage edits in a buffered modal and ship the final delta atomically. |
|
|
476
519
|
| `ALL /api/*` (other) | reserved | structured 404 envelope (see below); future endpoints land in subsequent sub-steps. |
|
|
477
520
|
| `GET /ws` | implemented (v14.4.a) | accepts WebSocket upgrade and registers the client with the BFF broadcaster. Server-push only — the server fans `scan.*` (and forthcoming `issue.*`) events to every connected client. See **WebSocket protocol** below. |
|
|
478
521
|
| `GET *` | implemented | static asset from the resolved UI bundle, falling back to `index.html` for SPA deep links. |
|
|
479
522
|
|
|
480
523
|
List endpoints conform to [`schemas/api/rest-envelope.schema.json`](schemas/api/rest-envelope.schema.json). The `/api/scan` and `/api/health` responses carry their underlying `ScanResult` / `IHealthResponse` shapes directly (no envelope wrap). The `/api/graph` response carries the formatter's native textual output.
|
|
481
524
|
|
|
482
|
-
**`kindRegistry` envelope field.** Every payload-bearing variant of the REST envelope (`nodes` / `links` / `issues` / `plugins` lists, the `node` single, the `config` value envelope) embeds a required `kindRegistry: { [kindName]: { providerId, label, color, colorDark?, emoji?, icon? } }` field. Sentinel envelopes (`health`, `scan`, `graph`) are exempt — they carry no payload at the wire level. The BFF assembles the registry once at boot from
|
|
525
|
+
**`kindRegistry` envelope field.** Every payload-bearing variant of the REST envelope (`nodes` / `links` / `issues` / `plugins` lists, the `node` single, the `config` value envelope) embeds a required `kindRegistry: { [kindName]: { providerId, label, color, colorDark?, emoji?, icon? } }` field. Sentinel envelopes (`health`, `scan`, `graph`) are exempt — they carry no payload at the wire level. The BFF assembles the registry once at boot from EVERY built-in Provider's `kinds[*].ui` block (regardless of the boot-time enabled verdict — their module code is statically imported by `built-in-bundles.ts` and always in memory) PLUS every drop-in user Provider that loaded successfully at boot (see [`architecture.md` §Provider · `ui` presentation](architecture.md#provider--ui-presentation)). The registry is then attached to every applicable response. Built-ins are listed unconditionally because a user re-enabling one mid-session expects its kinds to render on the next scan; the runtime enabled/disabled axis is enforced at SCAN-TIME by `composeScanExtensions` reading the fresh resolver, not by hiding kinds from the registry. Drop-ins that loaded as `disabled` carry `startsAsDisabled: true` on `GET /api/plugins` and need `sm serve` restart to register — their module code was never imported. The UI consumes `kindRegistry` directly to render kind palettes, list rows, and inspector headers — built-in and user-plugin kinds render identically. A kind appearing in a response payload (e.g. `node.kind`) without a matching `kindRegistry` entry is a contract violation; the kernel rejects Providers without a `ui` block at load time so the registry is always complete for whatever kinds appear in the response.
|
|
526
|
+
|
|
527
|
+
**`contributionsRegistry` envelope field.** Same payload-bearing envelopes also embed `contributionsRegistry: { "<pluginId>/<extensionId>/<contributionId>": { pluginId, extensionId, contributionId, slot, label?, tooltip?, icon?, emptyText?, emitWhenEmpty } }`. Same boot-time assembly discipline as `kindRegistry`: ALL built-in declarations are listed regardless of enabled state (so re-enabling a built-in mid-session renders correctly on the next scan), plus drop-in user plugins that loaded at boot. The `slot` value comes from the closed catalog in `spec/schemas/view-slots.schema.json`. A view contribution emitted by an extension whose qualified id is missing from the registry is dropped by the UI's slot host (mirrors the kindRegistry contract — `startsAsDisabled` drop-ins illustrate the absence path).
|
|
483
528
|
|
|
484
|
-
**Error envelope** (mirrors `§Machine-readable output
|
|
529
|
+
**Error envelope** (mirrors `§Machine-readable output analyzers`):
|
|
485
530
|
|
|
486
531
|
```json
|
|
487
532
|
{
|
|
@@ -501,9 +546,10 @@ Error code sources at v14.2:
|
|
|
501
546
|
- `not-found` (404) — unknown `/api/*` path; missing node on `/api/nodes/:pathB64`; malformed `pathB64` (treated as "no such node" so the client UX is uniform).
|
|
502
547
|
- `bad-query` (400) — `ExportQueryError` from `parseExportQuery`; pagination beyond `limit ≤ 1000`; non-integer / negative `limit` / `offset`; unknown formatter on `/api/graph`; `?fresh=1` when the server started with `--no-built-ins` or `--no-plugins`.
|
|
503
548
|
- `internal` (500) — uncaught error during a request (e.g. config-load failure, DB corruption surfacing through `loadScanResult`).
|
|
504
|
-
- `db-missing` (500) — emitted by mutation endpoints (`PATCH /api/plugins/:id`, `PATCH /api/plugins/:bundleId/extensions/:extensionId`) when the project DB is absent. Read-side routes uniformly degrade to the empty shape (`/api/scan`) or zero items (list endpoints) so they do not emit this code; mutation endpoints cannot persist without a DB so they fail fast instead of silently dropping the write.
|
|
505
|
-
- `not-found` (404) on `PATCH /api/plugins/:id` — unknown plugin id (no built-in bundle, no discovered drop-in matches). The qualified-id form returns the same code when either segment misses.
|
|
506
|
-
- `bad-query` (400) on `PATCH /api/plugins/:id` — granularity mismatch (bundle-level call against a `granularity: 'extension'` bundle, or qualified-id call against a `granularity: 'bundle'` bundle), malformed body (missing `enabled`, wrong type), unknown extension id under a known bundle.
|
|
549
|
+
- `db-missing` (500) — emitted by mutation endpoints (`PATCH /api/plugins/:id`, `PATCH /api/plugins/:bundleId/extensions/:extensionId`, `PATCH /api/plugins`) when the project DB is absent. Read-side routes uniformly degrade to the empty shape (`/api/scan`) or zero items (list endpoints) so they do not emit this code; mutation endpoints cannot persist without a DB so they fail fast instead of silently dropping the write.
|
|
550
|
+
- `not-found` (404) on `PATCH /api/plugins/:id` — unknown plugin id (no built-in bundle, no discovered drop-in matches). The qualified-id form returns the same code when either segment misses. The bulk `PATCH /api/plugins` returns the same code with `error.details.id` set to the first offending id; the batch is rejected before any DB write.
|
|
551
|
+
- `bad-query` (400) on `PATCH /api/plugins/:id` — granularity mismatch (bundle-level call against a `granularity: 'extension'` bundle, or qualified-id call against a `granularity: 'bundle'` bundle), malformed body (missing `enabled`, wrong type), unknown extension id under a known bundle. The bulk `PATCH /api/plugins` returns the same code for the same conditions (per-entry granularity mismatch, missing/typeless `enabled`, malformed `changes` array), with `error.details.id` set to the first offending entry's id.
|
|
552
|
+
- `locked` (403) on `PATCH /api/plugins/:id` and the qualified-id sibling — the target bundle id or qualified extension id is in the host lock-list (`src/server/locked-plugins.ts`). The list is hardcoded, host-only, and not user-editable; `GET /api/plugins` mirrors the same analyzer by stamping `locked: true` on the affected items. The bulk `PATCH /api/plugins` returns the same code with `error.details.id` set to the first locked entry; the batch is rejected before any DB write.
|
|
507
553
|
- `bad-query` (400) on `POST /api/scan` — the server was started with `--no-built-ins` or `--no-plugins` (partial pipeline would persist a misleading DB).
|
|
508
554
|
- `scan-busy` (409) on `POST /api/scan` — another scan (a watcher batch or another POST) is already in flight. Retry once the in-flight scan resolves; the WS `scan.completed` envelope is the unambiguous "now safe" signal.
|
|
509
555
|
|
|
@@ -532,9 +578,9 @@ The `/ws` endpoint is the live-events channel for the SPA. Clients connect once
|
|
|
532
578
|
- `scan.started` (per `job-events.md` §Scan events line 325).
|
|
533
579
|
- `scan.progress` (per `job-events.md` line 345 — emitted by the kernel orchestrator at every node; throttling deferred to a follow-up).
|
|
534
580
|
- `scan.completed` (per `job-events.md` line 363).
|
|
535
|
-
- `extractor.completed` (per `job-events.md` line 384) and `
|
|
581
|
+
- `extractor.completed` (per `job-events.md` line 384) and `analyzer.completed` (per `job-events.md` line 404) ride along as side effects of the same emitter bridge.
|
|
536
582
|
- `extension.error` (kernel-internal — emitted when an extension violates its declared contract; the BFF forwards verbatim).
|
|
537
|
-
- `watcher.started` and `watcher.error` — BFF-internal advisories. Non-normative; consumers MUST ignore unknown event types per the forward-compatibility
|
|
583
|
+
- `watcher.started` and `watcher.error` — BFF-internal advisories. Non-normative; consumers MUST ignore unknown event types per the forward-compatibility analyzer.
|
|
538
584
|
- **Deferred to a follow-up**: `issue.added` / `issue.resolved` (per `job-events.md` §Issue events line 446) and `scan.failed`. The 14.4.a surface fans out only the events the kernel emitter already produces; the diff-based issue events and a dedicated batch-failure event require additional plumbing inside the BFF watcher loop.
|
|
539
585
|
- **Connection lifecycle**:
|
|
540
586
|
1. Client opens `ws://<host>:<port>/ws`. The server completes the WS handshake and registers the underlying socket with the broadcaster.
|
|
@@ -567,7 +613,7 @@ Per-Provider conformance suites live next to the Provider's manifest under `<plu
|
|
|
567
613
|
|
|
568
614
|
---
|
|
569
615
|
|
|
570
|
-
## Machine-readable output
|
|
616
|
+
## Machine-readable output analyzers
|
|
571
617
|
|
|
572
618
|
When `--json` is set:
|
|
573
619
|
|
|
@@ -633,7 +679,7 @@ The `done in …` stderr line, its format grammar, and the `elapsedMs` field con
|
|
|
633
679
|
|
|
634
680
|
## See also
|
|
635
681
|
|
|
636
|
-
- [`architecture.md`](./architecture.md) — CLI as a driving adapter; kernel-first design; dependency
|
|
682
|
+
- [`architecture.md`](./architecture.md) — CLI as a driving adapter; kernel-first design; dependency analyzers.
|
|
637
683
|
- [`job-lifecycle.md`](./job-lifecycle.md) — state machine behind `sm job` verbs.
|
|
638
684
|
- [`job-events.md`](./job-events.md) — event stream emitted via `--json` and `--stream-output`.
|
|
639
685
|
- [`db-schema.md`](./db-schema.md) — tables behind `sm db` verbs.
|
package/conformance/README.md
CHANGED
|
@@ -65,7 +65,7 @@ A case is a JSON document with this shape:
|
|
|
65
65
|
"setup": {
|
|
66
66
|
"disableAllProviders": false,
|
|
67
67
|
"disableAllExtractors": false,
|
|
68
|
-
"
|
|
68
|
+
"disableAllAnalyzers": false
|
|
69
69
|
},
|
|
70
70
|
|
|
71
71
|
"invoke": {
|
|
@@ -118,7 +118,7 @@ Assertion types beyond this list MAY be proposed via spec-vX.Y.Z minor bumps. Im
|
|
|
118
118
|
|
|
119
119
|
| Id | Verifies |
|
|
120
120
|
|---|---|
|
|
121
|
-
| `kernel-empty-boot` | With every Provider/Extractor/
|
|
121
|
+
| `kernel-empty-boot` | With every Provider/Extractor/Analyzer disabled, scanning an empty scope returns a valid empty graph. |
|
|
122
122
|
|
|
123
123
|
Cases explicitly referenced elsewhere in the spec (landing before v1.0):
|
|
124
124
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
|
|
3
3
|
"id": "kernel-empty-boot",
|
|
4
|
-
"description": "With every Provider, extractor, and
|
|
4
|
+
"description": "With every Provider, extractor, and analyzer disabled, scanning an empty scope MUST return a valid, zero-filled ScanResult. Enforces the kernel boot invariant from architecture.md.",
|
|
5
5
|
"setup": {
|
|
6
6
|
"disableAllProviders": true,
|
|
7
7
|
"disableAllExtractors": true,
|
|
8
|
-
"
|
|
8
|
+
"disableAllAnalyzers": true
|
|
9
9
|
},
|
|
10
10
|
"invoke": {
|
|
11
11
|
"verb": "scan",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
|
|
3
3
|
"id": "sidecar-end-to-end",
|
|
4
|
-
"description": "Step 9.6.6 — co-located `.sm` sidecar end-to-end. Scanning a fixture that carries a stale sidecar (wrong `identity.{bodyHash,frontmatterHash}`) plus an orphan sidecar (no sibling `.md`) MUST surface `sidecar.status` on the node and emit both `annotation-stale` (per stale node) and `annotation-orphan` (per orphan `.sm`) issues from the built-in core
|
|
4
|
+
"description": "Step 9.6.6 — co-located `.sm` sidecar end-to-end. Scanning a fixture that carries a stale sidecar (wrong `identity.{bodyHash,frontmatterHash}`) plus an orphan sidecar (no sibling `.md`) MUST surface `sidecar.status` on the node and emit both `annotation-stale` (per stale node) and `annotation-orphan` (per orphan `.sm`) issues from the built-in core analyzers.",
|
|
5
5
|
"fixture": "sidecar-end-to-end",
|
|
6
6
|
"invoke": {
|
|
7
7
|
"verb": "scan",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
{ "type": "json-path", "path": "$.nodes[0].sidecar.status", "matches": "^stale-(body|frontmatter|both)$" },
|
|
17
17
|
{ "type": "json-path", "path": "$.nodes[0].sidecar.annotations.version", "equals": 7 },
|
|
18
18
|
{ "type": "json-path", "path": "$.stats.issuesCount", "equals": 2 },
|
|
19
|
-
{ "type": "json-path", "path": "$.issues[0].
|
|
19
|
+
{ "type": "json-path", "path": "$.issues[0].analyzerId", "equals": "annotation-stale" },
|
|
20
20
|
{ "type": "json-path", "path": "$.issues[0].severity", "equals": "warn" },
|
|
21
|
-
{ "type": "json-path", "path": "$.issues[1].
|
|
21
|
+
{ "type": "json-path", "path": "$.issues[1].analyzerId", "equals": "annotation-orphan" },
|
|
22
22
|
{ "type": "json-path", "path": "$.issues[1].severity", "equals": "warn" },
|
|
23
23
|
{ "type": "json-path", "path": "$.issues[1].data.expectedMdPath", "equals": ".claude/agents/orphan.md" }
|
|
24
24
|
]
|
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 — missing case → missing release ([`../../context/spec.md`](../../context/spec.md) §
|
|
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
|
|
|
@@ -27,17 +27,17 @@ This file is hand-maintained. A CI check before spec release compares the schema
|
|
|
27
27
|
| 17 | `extensions/base.schema.json` | — | 🔴 missing | Meta-case: every manifest under `src/extensions/` validates against the appropriate kind schema (which extends base via `allOf`). |
|
|
28
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
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/
|
|
30
|
+
| 20 | `extensions/analyzer.schema.json` | — | 🔴 missing | Case: `trigger-collision`, `broken-ref`, `superseded` manifests validate. |
|
|
31
31
|
| 21 | `extensions/action.schema.json` | — | 🔴 missing | Case: a `deterministic` action manifest validates; a `probabilistic` action WITHOUT `promptTemplateRef` fails. |
|
|
32
32
|
| 22 | `extensions/formatter.schema.json` | — | 🔴 missing | Case: `ascii` formatter manifest validates. |
|
|
33
33
|
| 23 | `history-stats.schema.json` | — | 🔴 missing | Blocked by Step 5 (history). Case: seed `state_executions` with a deterministic fixture, run `sm history stats --json --since <T0> --until <T1> --period month --top 5`, assert the document validates and that `totals.executionsCount == sum(perAction.executionsCount)` and `errorRates.global == totals.failedCount / totals.executionsCount`. Percentiles (`p95`/`p99`) intentionally omitted in v1 — add later as a minor bump without breaking consumers. |
|
|
34
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
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
|
-
| 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
|
|
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
|
|
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
|
+
| 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
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
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-
|
|
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
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
42
|
|
|
43
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Orphan sidecar — no sibling `agents/orphan.md` exists. The kernel walker
|
|
2
2
|
# discovers this via `discoverOrphanSidecars` and the built-in
|
|
3
|
-
# `core/annotation-orphan`
|
|
3
|
+
# `core/annotation-orphan` analyzer emits one `warn` issue per orphan.
|
|
4
4
|
|
|
5
5
|
identity:
|
|
6
6
|
path: agents/orphan.md
|
|
@@ -10,7 +10,7 @@ tools:
|
|
|
10
10
|
|
|
11
11
|
# Code reviewer
|
|
12
12
|
|
|
13
|
-
Walks the diff, flags type holes, suggests idiomatic refactors. Pairs with the local lint suite — never duplicates
|
|
13
|
+
Walks the diff, flags type holes, suggests idiomatic refactors. Pairs with the local lint suite — never duplicates analyzers a linter already enforces.
|
|
14
14
|
|
|
15
15
|
## When to invoke
|
|
16
16
|
|
package/db-schema.md
CHANGED
|
@@ -41,7 +41,7 @@ Every kernel table belongs to exactly one zone, identified by a mandatory name p
|
|
|
41
41
|
|
|
42
42
|
## Naming conventions (normative)
|
|
43
43
|
|
|
44
|
-
These
|
|
44
|
+
These analyzers apply to every kernel table and to every plugin-authored table under its prefix.
|
|
45
45
|
|
|
46
46
|
- **Tables**: `snake_case`, plural. Zone prefix REQUIRED. Example: `scan_nodes`, `state_jobs`.
|
|
47
47
|
- **Columns**: `snake_case`. Primary key column is always `id`.
|
|
@@ -57,7 +57,7 @@ These rules apply to every kernel table and to every plugin-authored table under
|
|
|
57
57
|
- **Constraints**: `fk_`, `uq_`, `ck_` prefixes.
|
|
58
58
|
- **SQL keywords**: UPPERCASE. Identifiers lowercase.
|
|
59
59
|
|
|
60
|
-
The kernel MUST reject any plugin migration that violates these
|
|
60
|
+
The kernel MUST reject any plugin migration that violates these analyzers at validation time (see `plugin-kv-api.md`).
|
|
61
61
|
|
|
62
62
|
Domain types exposed to driving adapters use `camelCase`. The SQLite reference impl uses Kysely's `CamelCasePlugin` to bridge `snake_case ↔ camelCase` at the port boundary.
|
|
63
63
|
|
|
@@ -117,12 +117,12 @@ Indexes: `ix_scan_links_source_path`, `ix_scan_links_target_path`, `ix_scan_link
|
|
|
117
117
|
|
|
118
118
|
### `scan_issues`
|
|
119
119
|
|
|
120
|
-
One row per
|
|
120
|
+
One row per analyzer-emitted issue, matching [`schemas/issue.schema.json`](./schemas/issue.schema.json).
|
|
121
121
|
|
|
122
122
|
| Column | Type | Constraint | Notes |
|
|
123
123
|
|---|---|---|---|
|
|
124
124
|
| `id` | INTEGER | PRIMARY KEY AUTOINCREMENT | |
|
|
125
|
-
| `
|
|
125
|
+
| `analyzer_id` | TEXT | NOT NULL | |
|
|
126
126
|
| `severity` | TEXT | NOT NULL, CHECK in (`error`, `warn`, `info`) | |
|
|
127
127
|
| `node_ids_json` | TEXT | NOT NULL | JSON array. |
|
|
128
128
|
| `link_indices_json` | TEXT | NULL | JSON array of `scan_links.id`. |
|
|
@@ -131,7 +131,7 @@ One row per rule-emitted issue, matching [`schemas/issue.schema.json`](./schemas
|
|
|
131
131
|
| `fix_json` | TEXT | NULL | |
|
|
132
132
|
| `data_json` | TEXT | NULL | |
|
|
133
133
|
|
|
134
|
-
Indexes: `
|
|
134
|
+
Indexes: `ix_scan_issues_analyzer_id`, `ix_scan_issues_severity`.
|
|
135
135
|
|
|
136
136
|
### `scan_meta`
|
|
137
137
|
|
|
@@ -166,6 +166,7 @@ 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). |
|
|
169
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. |
|
|
170
171
|
|
|
171
172
|
Primary key: `(node_path, extractor_id)`. Indexes: `ix_scan_extractor_runs_node`, `ix_scan_extractor_runs_extractor`.
|
|
@@ -197,7 +198,7 @@ Primary key: `(node_path, extractor_id)`. Indexes: `ix_node_enrichments_node`, `
|
|
|
197
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.
|
|
198
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.
|
|
199
200
|
|
|
200
|
-
**Read-side `node.merged` view.**
|
|
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:
|
|
201
202
|
|
|
202
203
|
1. Filters `enrichments` to rows targeting this node AND not flagged stale.
|
|
203
204
|
2. Sorts by `enriched_at` ASC.
|
|
@@ -212,7 +213,7 @@ Stale row visibility is opt-in via `mergeNodeWithEnrichments(node, enrichments,
|
|
|
212
213
|
|
|
213
214
|
### `scan_contributions`
|
|
214
215
|
|
|
215
|
-
Phase 3 / View contribution system. Per-node typed payloads emitted by extractors via `ctx.emitContribution(id, payload)` (and
|
|
216
|
+
Phase 3 / View contribution system. Per-node typed payloads emitted by extractors via `ctx.emitContribution(id, payload)` (and analyzers via `ctx.emitScopeContribution(id, payload)` for scope-level slots). One row per `(plugin_id, extension_id, node_path, contribution_id)` tuple.
|
|
216
217
|
|
|
217
218
|
| Column | Type | Constraint |
|
|
218
219
|
|---|---|---|
|
|
@@ -220,24 +221,27 @@ Phase 3 / View contribution system. Per-node typed payloads emitted by extractor
|
|
|
220
221
|
| `extension_id` | TEXT | NOT NULL | Extension id within the plugin. |
|
|
221
222
|
| `node_path` | TEXT | NOT NULL | FK semantically to `scan_nodes.path`; orphan-swept on persist when the parent node disappears. |
|
|
222
223
|
| `contribution_id` | TEXT | NOT NULL | Manifest Record key under `extension.viewContributions[<contributionId>]`. |
|
|
223
|
-
| `
|
|
224
|
-
| `payload_json` | TEXT | NOT NULL | JSON-serialised payload, already validated against the
|
|
224
|
+
| `slot` | TEXT | NOT NULL | Closed-enum-by-spec slot name; mirror of `view-slots.schema.json#/$defs/SlotName`. Kept open at the SQL layer (no CHECK) so catalog evolution does not need a DDL migration; `sm plugins upgrade` handles renames at the manifest layer. |
|
|
225
|
+
| `payload_json` | TEXT | NOT NULL | JSON-serialised payload, already validated against the slot's payload schema (`view-slots.schema.json#/$defs/payloads/<slot>`) at emit time. Off-shape payloads emit `extension.error` and drop silently. |
|
|
225
226
|
| `emitted_at` | INTEGER | NOT NULL | Unix milliseconds. |
|
|
226
227
|
|
|
227
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`).
|
|
228
229
|
|
|
229
|
-
**Persistence — orphan + catalog 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
|
|
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
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.
|
|
232
|
-
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 plugins
|
|
233
|
-
3. **
|
|
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`.
|
|
234
236
|
|
|
235
|
-
Cached nodes' rows survive untouched — they're neither orphaned (still in the live set) nor uninstalled (still in the catalog) nor in the buffer (no re-emit). The next time the body changes, the orchestrator re-runs the extractor,
|
|
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.
|
|
236
238
|
|
|
237
|
-
**Backwards-compat fallbacks.** `IPersistOptions.livePaths`, `IPersistOptions.registeredContributionKeys` are
|
|
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).
|
|
238
240
|
|
|
239
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.
|
|
240
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".
|
|
244
|
+
|
|
241
245
|
### `scan_node_tags`
|
|
242
246
|
|
|
243
247
|
Tags · dual-source. One row per `(node_path, tag, source)` triple, projected at persist time from BOTH `frontmatter.tags` (with `source='author'`) and `sidecar.annotations.tags` (with `source='user'`). Drives `sm list --tag <name>` and the UI's tag-faceted search; the `(tag)` index keeps "find all nodes with tag X" `O(log n)`.
|
|
@@ -521,12 +525,12 @@ Implementations MUST apply a rename heuristic at scan time **before** committing
|
|
|
521
525
|
- Emit no issue. Log at `info` level.
|
|
522
526
|
3. Remaining pairs where `newPath.frontmatterHash == deletedPath.frontmatterHash` (body differs, frontmatter is a perfect match) → classify as **medium-confidence rename**. The kernel MUST:
|
|
523
527
|
- Apply the same FK migration.
|
|
524
|
-
- Emit an issue with `
|
|
525
|
-
4. Any `deletedPath` left without a match after steps 2–3 becomes an **orphan**: the kernel emits an issue with `
|
|
528
|
+
- Emit an issue with `analyzerId: auto-rename-medium` (severity `warn`) pointing to both paths. The issue's `data` MUST include `{ from: <old.path>, to: <new.path>, confidence: "medium" }` so `sm orphans undo-rename <new.path>` can read the prior path without user input.
|
|
529
|
+
4. Any `deletedPath` left without a match after steps 2–3 becomes an **orphan**: the kernel emits an issue with `analyzerId: orphan` (severity `info`) and keeps the `state_*` rows referencing the dead path untouched until the user runs `sm orphans reconcile <dead.path> --to <new.path>` or accepts the orphan.
|
|
526
530
|
|
|
527
531
|
Matching is 1-to-1: once a `newPath` is claimed as the rename target of some `deletedPath`, no other deletion can match it in the same scan. Ambiguity (two deletions share a body hash with the same new path) → fall back to the orphan path for all candidates, with issue `auto-rename-ambiguous` listing every conflict. `auto-rename-ambiguous` issues MUST populate `data` with `{ to: <new.path>, candidates: [<old.path.a>, <old.path.b>, ...] }`; in this case `sm orphans undo-rename` requires the user to pass `--from <old.path>` to disambiguate.
|
|
528
532
|
|
|
529
|
-
Note on casing: `bodyHash` / `frontmatterHash` / `
|
|
533
|
+
Note on casing: `bodyHash` / `frontmatterHash` / `analyzerId` / `data` are the domain-object field names (per `node.schema.json` and `issue.schema.json`). The SQLite reference impl stores the same values in `body_hash` / `frontmatter_hash` / `analyzer_id` / `data_json` columns; the storage adapter bridges the two (see §Naming conventions above). The heuristic is specified against the domain types, not the columns.
|
|
530
534
|
|
|
531
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:
|
|
532
536
|
|
|
@@ -554,7 +558,7 @@ Failures are reported with suggested remediation (e.g., "run `sm db migrate`", "
|
|
|
554
558
|
|
|
555
559
|
## See also
|
|
556
560
|
|
|
557
|
-
- [`architecture.md`](./architecture.md) — `StoragePort` interface definition and dependency
|
|
561
|
+
- [`architecture.md`](./architecture.md) — `StoragePort` interface definition and dependency analyzers.
|
|
558
562
|
- [`plugin-kv-api.md`](./plugin-kv-api.md) — `ctx.store` accessor for mode A / mode B persistence.
|
|
559
563
|
- [`job-lifecycle.md`](./job-lifecycle.md) — atomic claim and TTL/reap semantics that drive `state_jobs`.
|
|
560
564
|
- [`cli-contract.md`](./cli-contract.md) — `sm db` verb surface (reset, backup, restore, migrate).
|