@skill-map/spec 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +680 -1
  2. package/README.md +6 -6
  3. package/architecture.md +244 -41
  4. package/cli-contract.md +48 -20
  5. package/conformance/README.md +2 -2
  6. package/conformance/cases/kernel-empty-boot.json +2 -2
  7. package/conformance/cases/orphan-markdown-fallback.json +22 -0
  8. package/conformance/cases/plugin-missing-ui-rejected.json +2 -1
  9. package/conformance/cases/sidecar-end-to-end.json +3 -4
  10. package/conformance/coverage.md +8 -6
  11. package/conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md +6 -0
  12. package/conformance/fixtures/orphan-markdown/ARCHITECTURE.md +10 -0
  13. package/conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm +2 -2
  14. package/conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.sm +1 -1
  15. package/conformance/fixtures/sidecar-example/agent-example.md +1 -1
  16. package/conformance/fixtures/sidecar-example/agent-example.sm +1 -1
  17. package/db-schema.md +68 -23
  18. package/index.json +47 -42
  19. package/interfaces/security-scanner.md +2 -2
  20. package/job-events.md +12 -12
  21. package/job-lifecycle.md +1 -1
  22. package/package.json +1 -1
  23. package/plugin-author-guide.md +374 -69
  24. package/plugin-kv-api.md +5 -5
  25. package/prompt-preamble.md +1 -1
  26. package/schemas/annotations.schema.json +5 -9
  27. package/schemas/api/rest-envelope.schema.json +55 -11
  28. package/schemas/conformance-case.schema.json +2 -2
  29. package/schemas/extensions/analyzer.schema.json +43 -0
  30. package/schemas/extensions/base.schema.json +14 -4
  31. package/schemas/extensions/extractor.schema.json +3 -10
  32. package/schemas/extensions/hook.schema.json +6 -4
  33. package/schemas/extensions/provider.schema.json +1 -1
  34. package/schemas/frontmatter/base.schema.json +6 -1
  35. package/schemas/input-types.schema.json +260 -0
  36. package/schemas/issue.schema.json +6 -6
  37. package/schemas/link.schema.json +2 -2
  38. package/schemas/node.schema.json +1 -19
  39. package/schemas/plugins-registry.schema.json +14 -2
  40. package/schemas/project-config.schema.json +25 -0
  41. package/schemas/sidecar.schema.json +6 -6
  42. package/schemas/summaries/agent.schema.json +1 -1
  43. package/schemas/summaries/command.schema.json +1 -1
  44. package/schemas/summaries/hook.schema.json +1 -1
  45. package/schemas/summaries/markdown.schema.json +1 -1
  46. package/schemas/view-slots.schema.json +335 -0
  47. 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>` | Write to user config (scope-aware: `-g` writes to global). |
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,16 @@ 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
+
189
199
  ---
190
200
 
191
201
  ### Scan
@@ -196,13 +206,23 @@ Keys are dot-paths (`jobs.minimumTtlSeconds`, `scan.tokenize`). Unknown keys →
196
206
  | `sm scan -n <node.path>` | Partial scan: one node. |
197
207
  | `sm scan --changed` | Incremental: only files changed since last scan (mtime heuristic). |
198
208
  | `sm scan --watch` | Long-running: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`. |
209
+ | `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
210
  | `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
211
  | `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
- | `sm refresh <node.path>` | Re-run Extractors against a single node and upsert their outputs into the universal enrichment layer (`node_enrichments`, see [`db-schema.md`](./db-schema.md#node_enrichments)). Stub state until the job subsystem ships at Step 10: deterministic Extractors run for real and persist; probabilistic Extractors emit a stderr advisory and skip without touching their stale rows. Exit `0` on success (with possible stub advisory), `2` on failure, `5` if the node is not in the persisted scan. |
202
- | `sm refresh --stale` | Batch form of `sm refresh <node>` — refreshes every node carrying at least one stale probabilistic enrichment row. Same stub caveat: deterministic Extractors persist; probabilistic Extractors skip with a stderr advisory. Exit `0` (including when the stale set is empty prints a "nothing to do" advisory). |
212
+ | `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. |
213
+ | `sm refresh --stale` | Batch form of `sm refresh <node>` — refreshes every node carrying at least one stale enrichment row. With Extractors deterministic-only, the stale set is empty in this revision (Extractor writes never set `stale = 1`) so `--stale` always exits `0` with a "nothing to do" advisory. The verb is preserved for the future Action-prob enrichment revision (see [`architecture.md` §Extractor · enrichment layer](./architecture.md#extractor--enrichment-layer)) where queued LLM jobs will populate stale rows. |
203
214
 
204
215
  `--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
216
 
217
+ **Effective roots** (one-shot `sm scan`):
218
+
219
+ - `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.
220
+ - `scan.includeHome === true` appends every active Provider's `explorationDir` resolved against `~` to the effective roots.
221
+ - `scan.extraRoots[]` is appended verbatim (entries starting with `~` resolve against the user home; relative entries resolve against the project root).
222
+ - `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.
223
+
224
+ **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.
225
+
206
226
  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
227
 
208
228
  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 +235,7 @@ Exit: 0 on clean (or clean watcher shutdown), 1 if error-severity issues exist (
215
235
  |---|---|
216
236
  | `sm list [--kind <k>] [--issue] [--sort-by ...] [--limit N]` | Tabular listing. `--json` emits an array conforming to `node.schema.json`. |
217
237
  | `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>] [--rules <ids>] [--include-prob] [--async]` | Print all current issues. Equivalent to `sm scan --json \| jq '.issues'` but faster (reads from DB). `-n` restricts to issues whose `nodeIds` include the path; `--rules <ids>` accepts a comma-separated list of qualified or short rule ids and restricts the issue read accordingly. Default behaviour is deterministic-only (CI-safe, status quo). `--include-prob` is the opt-in flag for probabilistic Rule dispatch (spec § A.7): the verb loads the plugin runtime, finds Rules with `mode === 'probabilistic'` (filtered by `--rules` if set), and emits a stderr advisory naming the rule ids. Full prob dispatch requires the job subsystem (Step 10); until then `--include-prob` is a stub — prob rules never produce issues, never alter the exit code, and `--async` (reserved companion: returns job ids without waiting once jobs land) is a no-op the advisory simply mentions. The flag does NOT extend to `sm scan` or `sm list`. |
238
+ | `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
239
  | `sm findings [--kind ...] [--since ...] [--threshold <n>]` | Probabilistic findings (injection, stale summaries, low confidence). `--json` emits an array of finding objects. |
220
240
  | `sm graph [--format ascii\|mermaid\|dot]` | Render the full graph via the named formatter. |
221
241
  | `sm export <query> --format json\|md\|mermaid` | Filtered export. Query syntax is implementation-defined pre-1.0. |
@@ -243,8 +263,8 @@ The built-in deterministic `core/bump` Action is the canonical write channel for
243
263
  | `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
264
  | `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
265
  | `sm sidecar refresh <node.path>` | 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`). |
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 rule for destructive verbs). `--yes` (alias `--force`) bypasses the prompt for non-interactive callers (CI, the pre-commit hook, scripts). 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`. |
247
- | `sm sidecar annotate <node.path> [--force]` | Pure scaffolding. Writes a minimal `.sm` next to the `.md` with the identity (`for:`) 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. |
266
+ | `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 prompt for non-interactive callers (CI, the pre-commit hook, scripts). 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`. |
267
+ | `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. |
248
268
  | `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
269
 
250
270
  **`.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`).
@@ -252,7 +272,7 @@ The built-in deterministic `core/bump` Action is the canonical write channel for
252
272
  Concretely, a hand-edited sidecar like this:
253
273
 
254
274
  ```yaml
255
- for:
275
+ identity:
256
276
  path: agents/reviewer.md
257
277
  bodyHash: 3dd7d0...
258
278
  frontmatterHash: 271d1e...
@@ -283,7 +303,7 @@ audit:
283
303
  createdBy: cli
284
304
  lastBumpedAt: '2026-05-07T10:00:00.000Z'
285
305
  lastBumpedBy: cli
286
- for:
306
+ identity:
287
307
  bodyHash: 3dd7d0...
288
308
  frontmatterHash: 271d1e...
289
309
  path: agents/reviewer.md
@@ -321,7 +341,7 @@ The Hono BFF exposes the single-node bump flow over REST so the UI can drive the
321
341
  }
322
342
  ```
323
343
 
324
- Emission rules:
344
+ Emission analyzers:
325
345
 
326
346
  - Emitted on a successful 200 bump (stale → fresh, or first-time-create → fresh).
327
347
  - **NOT** emitted on a force-on-fresh no-op 200 (nothing changed on disk).
@@ -432,7 +452,7 @@ See `db-schema.md` for the table catalog.
432
452
  | `sm db dump [--tables ...]` | SQL dump. |
433
453
  | `sm db migrate [--dry-run \| --status \| --to <n> \| --kernel-only \| --plugin <id> \| --no-backup]` | Migration controls. |
434
454
 
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 rule: dry-run MUST NOT depend on `--yes` / `--force`).
455
+ 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
456
 
437
457
  ---
438
458
 
@@ -460,16 +480,19 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
460
480
 
461
481
  | Path | Status | Shape |
462
482
  |---|---|---|
463
- | `GET /api/health` | implemented | `{ ok: true, schemaVersion, specVersion, implVersion, scope: 'project'\|'global', db: 'present'\|'missing' }` |
483
+ | `GET /api/health` | implemented | `{ ok: true, schemaVersion, specVersion, implVersion, scope: 'project'\|'global', db: 'present'\|'missing', cwd: string, dbPath: string }`. `cwd` is the absolute project root the BFF resolves against (`runtimeContext.cwd`); `dbPath` is the absolute project DB path (`IServerOptions.dbPath`). Both are surfaced so the SPA's About panel can show "you are looking at <project>" + the DB location without a second endpoint. |
464
484
  | `GET /api/scan` | implemented | latest persisted `ScanResult` (1:1 with `scan-result.schema.json`; byte-equal to `sm scan --json` modulo whitespace). DB absent → empty `ScanResult` shape (zero `nodes` / `links` / `issues`). |
465
485
  | `GET /api/scan?fresh=1` | implemented | runs an in-memory scan and returns the produced `ScanResult` without persistence. Rejects with `bad-query` (400) when the server was started with `--no-built-ins` or `--no-plugins` (would yield empty / partial results). |
486
+ | `POST /api/scan` | implemented | Run a fresh scan **and persist it** through the same `runScanWithRenames` + `persistScanResult` pipeline the watcher uses. Body is empty (`{}` or no body). Response: the persisted `ScanResult` inline (same shape as `GET /api/scan`). Side effects: broadcasts `scan.started` then `scan.completed` over `/ws` so other connected clients can refresh — the per-batch sequence is identical to a watcher-driven batch. **Concurrency**: only one scan may run at a time across the whole BFF process. A POST that arrives while a watcher batch is in flight (or while another POST is in flight) is rejected with `409 scan-busy` so the caller can decide whether to retry. **Pipeline gate**: rejected with `400 bad-query` when the server was started with `--no-built-ins` or `--no-plugins` (a partial pipeline would persist a misleading DB the next watcher boot would have to reconcile). **DB gate**: rejected with `500 db-missing` when the project DB file is absent — the read-side `/api/scan` degrades to the empty shape, but a write path cannot, so it fails fast. |
466
487
  | `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`. |
467
488
  | `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. |
468
489
  | `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. |
469
- | `GET /api/issues?severity=&ruleId=&node=` | implemented | `RestEnvelope` (`kind: 'issues'`) — list of issues. Filters: `severity` (CSV from `error\|warn\|info`), `ruleId` (CSV; qualified or short suffix per `sm check --rules`), `node` (filter to issues whose `nodeIds` includes the path). No pagination at v14.2. |
490
+ | `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. |
470
491
  | `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`. |
471
492
  | `GET /api/config` | implemented | `RestEnvelope` (`kind: 'config'`) — merged effective config (defaults → user → user-local → project → project-local → override). |
472
- | `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' }`. |
493
+ | `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, 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. |
494
+ | `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`). **Restart required** — the loaded plugin runtime is boot-cached; the new value applies on the next `sm scan` or `sm serve` restart. The endpoint does NOT broadcast a WS event today. |
495
+ | `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 + restart-required semantics as the bundle form. |
473
496
  | `ALL /api/*` (other) | reserved | structured 404 envelope (see below); future endpoints land in subsequent sub-steps. |
474
497
  | `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. |
475
498
  | `GET *` | implemented | static asset from the resolved UI bundle, falling back to `index.html` for SPA deep links. |
@@ -478,7 +501,7 @@ List endpoints conform to [`schemas/api/rest-envelope.schema.json`](schemas/api/
478
501
 
479
502
  **`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 enabled Provider's `kinds[*].ui` block (see [`architecture.md` §Provider · `ui` presentation](architecture.md#provider--ui-presentation)) and attaches the same map to every applicable response. 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.
480
503
 
481
- **Error envelope** (mirrors `§Machine-readable output rules`):
504
+ **Error envelope** (mirrors `§Machine-readable output analyzers`):
482
505
 
483
506
  ```json
484
507
  {
@@ -491,14 +514,19 @@ List endpoints conform to [`schemas/api/rest-envelope.schema.json`](schemas/api/
491
514
  }
492
515
  ```
493
516
 
494
- HTTP status mapping: `400` → `bad-query`, `404` → `not-found`, `500` → `internal` / `db-missing`.
517
+ HTTP status mapping: `400` → `bad-query`, `404` → `not-found`, `409` → `sidecar-fresh` (`POST /api/sidecar/bump`) or `scan-busy` (`POST /api/scan`), `500` → `internal` / `db-missing`.
495
518
 
496
519
  Error code sources at v14.2:
497
520
 
498
521
  - `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).
499
522
  - `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`.
500
523
  - `internal` (500) — uncaught error during a request (e.g. config-load failure, DB corruption surfacing through `loadScanResult`).
501
- - `db-missing` (500) — reserved for endpoints that cannot degrade to an empty result. The v14.2 routes uniformly degrade (`/api/scan` returns the empty shape; list endpoints return zero items) so this code is not currently emitted by any handler it is documented for future endpoints (post-v0.6.0 mutations) where degradation is not safe.
524
+ - `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.
525
+ - `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.
526
+ - `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.
527
+ - `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.
528
+ - `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).
529
+ - `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.
502
530
 
503
531
  **Flag surface**:
504
532
 
@@ -525,9 +553,9 @@ The `/ws` endpoint is the live-events channel for the SPA. Clients connect once
525
553
  - `scan.started` (per `job-events.md` §Scan events line 325).
526
554
  - `scan.progress` (per `job-events.md` line 345 — emitted by the kernel orchestrator at every node; throttling deferred to a follow-up).
527
555
  - `scan.completed` (per `job-events.md` line 363).
528
- - `extractor.completed` (per `job-events.md` line 384) and `rule.completed` (per `job-events.md` line 404) ride along as side effects of the same emitter bridge.
556
+ - `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.
529
557
  - `extension.error` (kernel-internal — emitted when an extension violates its declared contract; the BFF forwards verbatim).
530
- - `watcher.started` and `watcher.error` — BFF-internal advisories. Non-normative; consumers MUST ignore unknown event types per the forward-compatibility rule.
558
+ - `watcher.started` and `watcher.error` — BFF-internal advisories. Non-normative; consumers MUST ignore unknown event types per the forward-compatibility analyzer.
531
559
  - **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.
532
560
  - **Connection lifecycle**:
533
561
  1. Client opens `ws://<host>:<port>/ws`. The server completes the WS handshake and registers the underlying socket with the broadcaster.
@@ -560,7 +588,7 @@ Per-Provider conformance suites live next to the Provider's manifest under `<plu
560
588
 
561
589
  ---
562
590
 
563
- ## Machine-readable output rules
591
+ ## Machine-readable output analyzers
564
592
 
565
593
  When `--json` is set:
566
594
 
@@ -626,7 +654,7 @@ The `done in …` stderr line, its format grammar, and the `elapsedMs` field con
626
654
 
627
655
  ## See also
628
656
 
629
- - [`architecture.md`](./architecture.md) — CLI as a driving adapter; kernel-first design; dependency rules.
657
+ - [`architecture.md`](./architecture.md) — CLI as a driving adapter; kernel-first design; dependency analyzers.
630
658
  - [`job-lifecycle.md`](./job-lifecycle.md) — state machine behind `sm job` verbs.
631
659
  - [`job-events.md`](./job-events.md) — event stream emitted via `--json` and `--stream-output`.
632
660
  - [`db-schema.md`](./db-schema.md) — tables behind `sm db` verbs.
@@ -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
- "disableAllRules": false
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/Rule disabled, scanning an empty scope returns a valid empty graph. |
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 rule disabled, scanning an empty scope MUST return a valid, zero-filled ScanResult. Enforces the kernel boot invariant from architecture.md.",
4
+ "description": "With every Provider, extractor, and 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
- "disableAllRules": true
8
+ "disableAllAnalyzers": true
9
9
  },
10
10
  "invoke": {
11
11
  "verb": "scan",
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
3
+ "id": "orphan-markdown-fallback",
4
+ "description": "spec 0.18.0 universal markdown fallback. A `.md` file no vendor-specific Provider classifies (e.g. `ARCHITECTURE.md` at the project root) MUST be picked up by the built-in `core/markdown` Provider, classified as kind `markdown`, and attributed to the `markdown` provider id. The orchestrator's path-dedup ensures vendor Providers retain priority on files inside their territory (`.claude/agents/reviewer.md` here stays with `claude` as `agent`). Locks the contract that markdown is provider-agnostic and the kernel emits no privileged kinds.",
5
+ "fixture": "orphan-markdown",
6
+ "invoke": {
7
+ "verb": "scan",
8
+ "flags": ["--json"]
9
+ },
10
+ "assertions": [
11
+ { "type": "exit-code", "value": 0 },
12
+ { "type": "json-path", "path": "$.schemaVersion", "equals": 1 },
13
+ { "type": "json-path", "path": "$.stats.nodesCount", "equals": 2 },
14
+ { "type": "json-path", "path": "$.stats.issuesCount", "equals": 0 },
15
+ { "type": "json-path", "path": "$.nodes[0].path", "equals": ".claude/agents/reviewer.md" },
16
+ { "type": "json-path", "path": "$.nodes[0].kind", "equals": "agent" },
17
+ { "type": "json-path", "path": "$.nodes[0].provider", "equals": "claude" },
18
+ { "type": "json-path", "path": "$.nodes[1].path", "equals": "ARCHITECTURE.md" },
19
+ { "type": "json-path", "path": "$.nodes[1].kind", "equals": "markdown" },
20
+ { "type": "json-path", "path": "$.nodes[1].provider", "equals": "markdown" }
21
+ ]
22
+ }
@@ -10,10 +10,11 @@
10
10
  "assertions": [
11
11
  { "type": "exit-code", "value": 0 },
12
12
  { "type": "stderr-matches", "pattern": "plugin bad-provider:.*invalid.*must have required property 'ui'" },
13
- { "type": "json-path", "path": "$.providers.length", "equals": 3 },
13
+ { "type": "json-path", "path": "$.providers.length", "equals": 4 },
14
14
  { "type": "json-path", "path": "$.providers[0]", "equals": "claude" },
15
15
  { "type": "json-path", "path": "$.providers[1]", "equals": "gemini" },
16
16
  { "type": "json-path", "path": "$.providers[2]", "equals": "agent-skills" },
17
+ { "type": "json-path", "path": "$.providers[3]", "equals": "markdown" },
17
18
  { "type": "json-path", "path": "$.nodes.length", "equals": 1 }
18
19
  ]
19
20
  }
@@ -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 `for.{bodyHash,frontmatterHash}`) plus an orphan sidecar (no sibling `.md`) MUST surface `sidecar_status` queryable on the node, denormalise `annotations.version` into the node row, and emit both `annotation-stale` (per stale node) and `annotation-orphan` (per orphan `.sm`) issues from the built-in core rules.",
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",
@@ -12,14 +12,13 @@
12
12
  { "type": "json-path", "path": "$.schemaVersion", "equals": 1 },
13
13
  { "type": "json-path", "path": "$.stats.nodesCount", "equals": 1 },
14
14
  { "type": "json-path", "path": "$.nodes[0].path", "equals": ".claude/agents/stale.md" },
15
- { "type": "json-path", "path": "$.nodes[0].version", "equals": 7 },
16
15
  { "type": "json-path", "path": "$.nodes[0].sidecar.present", "equals": true },
17
16
  { "type": "json-path", "path": "$.nodes[0].sidecar.status", "matches": "^stale-(body|frontmatter|both)$" },
18
17
  { "type": "json-path", "path": "$.nodes[0].sidecar.annotations.version", "equals": 7 },
19
18
  { "type": "json-path", "path": "$.stats.issuesCount", "equals": 2 },
20
- { "type": "json-path", "path": "$.issues[0].ruleId", "equals": "annotation-stale" },
19
+ { "type": "json-path", "path": "$.issues[0].analyzerId", "equals": "annotation-stale" },
21
20
  { "type": "json-path", "path": "$.issues[0].severity", "equals": "warn" },
22
- { "type": "json-path", "path": "$.issues[1].ruleId", "equals": "annotation-orphan" },
21
+ { "type": "json-path", "path": "$.issues[1].analyzerId", "equals": "annotation-orphan" },
23
22
  { "type": "json-path", "path": "$.issues[1].severity", "equals": "warn" },
24
23
  { "type": "json-path", "path": "$.issues[1].data.expectedMdPath", "equals": ".claude/agents/orphan.md" }
25
24
  ]
@@ -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) §Rules for AI agents editing spec/).
3
+ Authoritative map of JSON Schemas in [`../schemas/`](../schemas/) to the conformance cases that exercise them. Every schema MUST have at least one case before spec v1.0.0 ships — missing case → missing release ([`../../context/spec.md`](../../context/spec.md) §Analyzers for AI agents editing spec/).
4
4
 
5
5
  This file is hand-maintained. A CI check before spec release compares the schema inventory against this table and fails if any schema lacks a case.
6
6
 
@@ -11,14 +11,14 @@ This file is hand-maintained. A CI check before spec release compares the schema
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
12
  | 2 | `link.schema.json` | — | 🔴 missing | Needs fixture with at least one `invokes` + `references` + `mentions` link, both `high`/`medium`/`low` confidence. |
13
13
  | 3 | `issue.schema.json` | — | 🔴 missing | Needs fixture triggering `trigger-collision` + `broken-ref` + `superseded`. |
14
- | 4 | `scan-result.schema.json` | `kernel-empty-boot` | 🟡 partial | Zero-filled case asserted via empty-boot. Populated cases (rename / orphan branches) moved with the Claude Provider — see `provider:claude` cases `basic-scan` / `rename-high` / `orphan-detection`. |
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
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
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
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
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
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
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` | | 🔴 missing | Universal frontmatter shape — `name` + `description` only, `additionalProperties: true`. Per-kind schemas (`skill` / `agent` / `command` / `markdown`) live with the Provider that emits them (the Claude Provider ships them under `src/extensions/providers/claude/schemas/`) and extend this base via `$ref`-by-`$id`. Direct spec-level case still pending: fixture with min-required frontmatter only, no Provider needed (Provider-disabled mode + a single `notes/<file>.md` with `name: ...` + `description: ...`). |
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
22
  | 12 | `summaries/skill.schema.json` | — | 🔴 missing | Blocked by Step 10 (`skill-summarizer`). Case: submit summarizer, validate report. |
23
23
  | 13 | `summaries/agent.schema.json` | — | 🔴 missing | Blocked by Step 11. |
24
24
  | 14 | `summaries/command.schema.json` | — | 🔴 missing | Blocked by Step 11. |
@@ -27,16 +27,18 @@ 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/rule.schema.json` | — | 🔴 missing | Case: `trigger-collision`, `broken-ref`, `superseded` manifests validate. |
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 rules. 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 rule 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. |
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-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>`. |
40
42
 
41
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.
42
44
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: reviewer
3
+ description: Trivial agent so the claude Provider has something to claim.
4
+ ---
5
+
6
+ Body.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: architecture
3
+ description: Top-level markdown that no vendor Provider claims. Picked up by core/markdown's universal fallback classify.
4
+ ---
5
+
6
+ This file lives at the project root with no platform-specific path
7
+ prefix. The claude / gemini / agent-skills Providers all return null
8
+ on it; the built-in `core/markdown` Provider claims it as kind
9
+ `markdown`. Without the universal fallback it would be silently
10
+ dropped from the scan.
@@ -1,8 +1,8 @@
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` rule emits one `warn` issue per orphan.
3
+ # `core/annotation-orphan` analyzer emits one `warn` issue per orphan.
4
4
 
5
- for:
5
+ identity:
6
6
  path: agents/orphan.md
7
7
  bodyHash: '1111111111111111111111111111111111111111111111111111111111111111'
8
8
  frontmatterHash: '1111111111111111111111111111111111111111111111111111111111111111'
@@ -7,7 +7,7 @@
7
7
  # `annotations:` block denormalises through the SQLite scan — the value
8
8
  # survives a round-trip through `state_scan_nodes.annotations_json`.
9
9
 
10
- for:
10
+ identity:
11
11
  path: agents/stale.md
12
12
  bodyHash: '0000000000000000000000000000000000000000000000000000000000000000'
13
13
  frontmatterHash: '0000000000000000000000000000000000000000000000000000000000000000'
@@ -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 rules a linter already enforces.
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
 
@@ -6,7 +6,7 @@
6
6
  # of the frontmatter via `js-yaml` dump with sortKeys+noCompatMode). Regenerate
7
7
  # when agent-example.md changes (the fixture is meant to be drift-free).
8
8
 
9
- for:
9
+ identity:
10
10
  # Scope-root-relative — when this fixture is treated as its own
11
11
  # mini-scope (the typical reading), the .md sits in the same
12
12
  # directory as this .sm, so `for.path` is just the filename.