@skill-map/spec 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +777 -3
  2. package/README.md +11 -13
  3. package/architecture.md +118 -35
  4. package/cli-contract.md +38 -25
  5. package/conformance/README.md +43 -14
  6. package/conformance/cases/kernel-empty-boot.json +3 -3
  7. package/conformance/coverage.md +20 -24
  8. package/db-schema.md +61 -6
  9. package/index.json +35 -77
  10. package/interfaces/security-scanner.md +1 -1
  11. package/job-events.md +75 -1
  12. package/package.json +1 -1
  13. package/plugin-author-guide.md +409 -51
  14. package/schemas/conformance-case.schema.json +14 -5
  15. package/schemas/execution-record.schema.json +8 -8
  16. package/schemas/extensions/action.schema.json +2 -2
  17. package/schemas/extensions/base.schema.json +5 -5
  18. package/schemas/extensions/extractor.schema.json +48 -0
  19. package/schemas/extensions/formatter.schema.json +29 -0
  20. package/schemas/extensions/hook.schema.json +44 -0
  21. package/schemas/extensions/provider.schema.json +51 -0
  22. package/schemas/extensions/rule.schema.json +1 -1
  23. package/schemas/frontmatter/base.schema.json +2 -2
  24. package/schemas/link.schema.json +4 -4
  25. package/schemas/node.schema.json +4 -4
  26. package/schemas/plugins-registry.schema.json +19 -4
  27. package/schemas/project-config.schema.json +2 -2
  28. package/schemas/scan-result.schema.json +3 -3
  29. package/conformance/cases/basic-scan.json +0 -17
  30. package/conformance/cases/orphan-detection.json +0 -22
  31. package/conformance/cases/rename-high.json +0 -21
  32. package/conformance/fixtures/minimal-claude/agents/reviewer.md +0 -16
  33. package/conformance/fixtures/minimal-claude/commands/status.md +0 -17
  34. package/conformance/fixtures/minimal-claude/hooks/pre-commit.md +0 -13
  35. package/conformance/fixtures/minimal-claude/notes/architecture.md +0 -11
  36. package/conformance/fixtures/minimal-claude/skills/hello.md +0 -22
  37. package/conformance/fixtures/orphan-after/skills/keep.md +0 -13
  38. package/conformance/fixtures/orphan-before/skills/keep.md +0 -13
  39. package/conformance/fixtures/orphan-before/skills/lonely.md +0 -13
  40. package/conformance/fixtures/rename-high-after/skills/bar.md +0 -14
  41. package/conformance/fixtures/rename-high-before/skills/foo.md +0 -14
  42. package/schemas/extensions/adapter.schema.json +0 -40
  43. package/schemas/extensions/audit.schema.json +0 -47
  44. package/schemas/extensions/detector.schema.json +0 -41
  45. package/schemas/extensions/renderer.schema.json +0 -29
  46. package/schemas/frontmatter/agent.schema.json +0 -17
  47. package/schemas/frontmatter/command.schema.json +0 -39
  48. package/schemas/frontmatter/hook.schema.json +0 -29
  49. package/schemas/frontmatter/note.schema.json +0 -11
  50. package/schemas/frontmatter/skill.schema.json +0 -37
package/CHANGELOG.md CHANGED
@@ -1,5 +1,768 @@
1
1
  # Spec changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 88afe24: Cleanup pass post-v0.8.0 — finishing the renames and wiring the
8
+ conformance kill-switches.
9
+
10
+ **Pre-1.0 minor bump** per `spec/versioning.md` § Pre-1.0. The schema
11
+ field rename below is technically breaking, but ships as a minor while
12
+ the spec stays `0.Y.Z`.
13
+
14
+ ## Spec changes (`@skill-map/spec`)
15
+
16
+ ### Breaking — `conformance-case.schema.json`
17
+
18
+ - **Rename `setup.disableAllDetectors` → `setup.disableAllExtractors`.**
19
+ Finishes the kind rename Detector → Extractor introduced in 0.8.0
20
+ (Phase 2 of the plug-in model overhaul). The previous name was the
21
+ last residue and it never reached a release where anything consumed
22
+ it.
23
+ - **`setup.disableAll{Providers,Extractors,Rules}` are now consumed
24
+ end-to-end.** Until this release the three toggles were declared in
25
+ the schema and accepted by the runner, but the runner never threaded
26
+ them anywhere — the `kernel-empty-boot` case happened to pass
27
+ because its fixture is empty. The runner now injects
28
+ `SKILL_MAP_DISABLE_ALL_{PROVIDERS,EXTRACTORS,RULES}=1` into the
29
+ child process environment when the matching toggle is `true`, and
30
+ the CLI's scan composer drops every extension of the disabled kind
31
+ from the in-scan pipeline regardless of granularity gates and
32
+ `--no-built-ins`. Each toggle now has a docstring on the schema
33
+ property pointing at the env-var convention.
34
+ - `kernel-empty-boot` case updated for the rename.
35
+ - `conformance/README.md` example updated.
36
+
37
+ ### Non-breaking — copy fixes
38
+
39
+ - Comments and docstrings across `architecture.md` and friends already
40
+ refer to "Extractor" everywhere; only the schema field stayed on the
41
+ old name. No prose changes in this bump.
42
+
43
+ ## CLI changes (`@skill-map/cli`)
44
+
45
+ ### Breaking — `IDiscoveredPlugin.status` enum
46
+
47
+ - **Rename `'loaded'` → `'enabled'`.** The schema enum
48
+ (`plugins-registry.schema.json`) already used `enabled` since 0.8.0;
49
+ the runtime drifted to `loaded` and has now been pulled back so the
50
+ runtime status matches the spec contract. `'disabled'`, the
51
+ semantic pair, was already aligned. Every consumer (`sm plugins
52
+ list`, `sm plugins doctor`, `sm db prune` plugin filter, runtime
53
+ plugin composer) updated. No published consumers exist.
54
+
55
+ ### Non-breaking — sweep cleanup
56
+
57
+ - Old `Detector` / `detector` references (kind name, manifest field
58
+ names, JSDoc, comments, test fixture filenames, test variable
59
+ names) replaced with `Extractor` / `extractor` across the
60
+ production code and test suite. Excludes historical CHANGELOG
61
+ entries, explicit migration notes ("Renamed from Detector"), and
62
+ test data strings whose semantics are independent of the kind
63
+ name (e.g. `'@FooDetector'` in trigger normalization tests).
64
+ - A residual reference to "an audit reading `ScanResult.issues`" in
65
+ `validate-all`'s docstring rewritten without the removed kind name.
66
+
67
+ ## Tests
68
+
69
+ - `plugin-runtime-branches.test.ts` — five new unit tests covering
70
+ the env-var kill-switch in `composeScanExtensions` (per kind, all
71
+ three together, and stray-value resilience).
72
+ - `conformance-disable-flags.test.ts` — four new e2e tests pointing
73
+ the runner at a populated fixture with each toggle in turn (and a
74
+ baseline) so a regression in the env-var pipeline shows up
75
+ structurally rather than relying on the empty-fixture coincidence.
76
+
77
+ ## [Unreleased]
78
+
79
+ ### Minor (breaking, pre-1.0)
80
+
81
+ - **`conformance-case.schema.json` — rename `setup.disableAllDetectors`
82
+ → `setup.disableAllExtractors`.** Finishes the kind rename Detector →
83
+ Extractor introduced in 0.8.0 (Phase 2 of the plug-in model
84
+ overhaul). The previous name was a residue from an unfinished sweep
85
+ and never reached a release that consumed it.
86
+ - **`setup.disableAll{Providers,Extractors,Rules}` are now wired
87
+ end-to-end.** Until this release the toggles were declared in the
88
+ schema but the runner threaded them nowhere; the `kernel-empty-boot`
89
+ case happened to pass because its fixture is empty. The runner now
90
+ injects `SKILL_MAP_DISABLE_ALL_{PROVIDERS,EXTRACTORS,RULES}=1` into
91
+ the child process environment per toggle, and the CLI's scan
92
+ composer drops every extension of the disabled kind from the
93
+ in-scan pipeline (overriding granularity gates and `--no-built-ins`).
94
+ Migration: any case JSON authored against the unwired schema needs
95
+ to swap `disableAllDetectors` for `disableAllExtractors`; behaviour
96
+ changes only when the toggles were already `true` (those cases will
97
+ now actually disable the kind, where previously they relied on
98
+ fixture content for the same outcome).
99
+
100
+ ### Patch
101
+
102
+ - Updated `conformance/cases/kernel-empty-boot.json` for the field
103
+ rename above.
104
+ - Updated `conformance/README.md` example for the field rename above.
105
+ - Schema docstrings added to each `disableAll*` property documenting
106
+ the env-var convention the runner uses.
107
+
108
+ ## 0.8.0
109
+
110
+ ### Minor Changes
111
+
112
+ - 6dad772: v0.8.0 — Pre-1.0 stabilization pass.
113
+
114
+ This release combines two coherent pre-1.0 cleanup pieces that
115
+ both push the project closer to v1.0 stability: the cli-architect
116
+ audit review pass and the plugin model overhaul.
117
+
118
+ Pre-1.0 minor bumps per `versioning.md` § Pre-1.0; breaking
119
+ changes allowed within minor while in `0.Y.Z`. No real downstream
120
+ ecosystem exists yet, so the breaking surface costs nothing
121
+ today.
122
+
123
+ ## Part 1 — Pre-1.0 audit review pass
124
+
125
+ Pre-1.0 review pass — `cli-architect` audit findings.
126
+
127
+ Internal audit run by the `cli-architect` agent in REVIEW mode
128
+ produced a Critical / High / Medium / Low / Nit catalog. This
129
+ pass bundles the implementation of every actionable finding into
130
+ one unit so the review can be read end-to-end. **Pre-1.0 minor
131
+ bump**: a few breaking surface changes ride along (CLI sub-verb
132
+ split, exit-code enum exposed, plugin loader option). No
133
+ published downstream consumers exist yet.
134
+
135
+ ### Spec changes (`@skill-map/spec`)
136
+
137
+ - **`cli-contract.md`** — `sm scan compare-with <dump> [roots...]`
138
+ is now a sub-verb instead of a `--compare-with <path>` flag on
139
+ `sm scan`. Read-only delta report against a saved `ScanResult`
140
+ JSON dump. Read-only — does not modify the DB. Same exit codes
141
+ (`0` empty delta / `1` drift / `2` operational error). Old flag
142
+ form removed.
143
+ - **`cli-contract.md`** — exit-code `2` "Operational error" row
144
+ clarified to mention environment / runtime mismatches (wrong
145
+ Node version, missing native dependency) explicitly. The
146
+ "unhandled exception" catch-all already covered the case; this
147
+ just removes ambiguity for future implementers.
148
+ - **`cli-contract.md`** — new normative section **§Dry-run**
149
+ between §Exit codes and §Verb catalog defining the contract for
150
+ any verb exposing `-n` / `--dry-run`: no observable side effects
151
+ (DB / FS / config / network / spawns), no auto-provisioning of
152
+ scope directories, output mirrors the live mode with explicit
153
+ "would …" framing, exit codes mirror the live mode, dry-run
154
+ MUST short-circuit `--yes` / `--force` confirmation prompts.
155
+ Per-verb opt-in: the flag is not global, verbs that don't
156
+ declare it MUST reject it as an unknown option. Verb catalog
157
+ rows for `sm init`, `sm db reset` (default + `--state` +
158
+ `--hard`), and `sm db restore` amended to declare and describe
159
+ their `--dry-run` previews.
160
+
161
+ ### CLI changes (`@skill-map/cli`)
162
+
163
+ #### Critical — kernel & adapter hygiene
164
+
165
+ - **C1 — `runScanInternal` decomposed.** The 290-line monolith in
166
+ `kernel/orchestrator.ts` split into a thin composer + four pure
167
+ functions: `validateRoots`, `indexPriorSnapshot`,
168
+ `walkAndDetect`, `runRules`. Composer is now 89 lines reading
169
+ top-to-bottom through the pipeline phases. Zero behavioural
170
+ change.
171
+ - **C2 — `withSqlite(options, fn)` helper.** Single utility at
172
+ `cli/util/with-sqlite.ts` standardises the open / use / close
173
+ idiom every read-side command was open-coding. Eliminates four
174
+ classes of boilerplate bugs (forgotten close, `autoBackup`
175
+ drift, double-close, missing `try/finally`). Migrated 20 call
176
+ sites across `check`, `export`, `graph`, `history`, `init`,
177
+ `jobs`, `list`, `orphans`, `plugins`, `scan`, `show`, `watch`,
178
+ plus `cli/util/plugin-runtime.ts`. Companion `tryWithSqlite`
179
+ short-circuits when the DB file does not exist, replacing the
180
+ `if (existsSync) { withSqlite(...) }` chain. In `scan.ts` the
181
+ read-prior + persist double-open consolidated into a single
182
+ `withSqlite` callback that brackets read prior → run scan →
183
+ guard → persist when `willPersist`. Saves one migration
184
+ discovery pass + one WAL setup per normal scan (~50–100ms).
185
+
186
+ #### High — UX & contract integrity
187
+
188
+ - **H3 — `--dry-run` semantics unified across `init` / `db reset`
189
+ / `db restore`.** The new spec §Dry-run codifies the "no
190
+ writes, reads OK" contract; three verbs that did not previously
191
+ expose a preview now do: - `sm init --dry-run` — previews the would-create lines for
192
+ `.skill-map/`, `settings.json`, `settings.local.json`,
193
+ `.skill-mapignore`, the `.gitignore` entries that would be
194
+ appended (deduped against the existing file), the DB
195
+ provisioning, and the first-scan trigger. Honours `--force`
196
+ for the would-overwrite preview. Re-init over an existing
197
+ scope without `--force` still exits 2 (same gate as live). - `sm db reset --dry-run` (default + `--state`) — opens the DB
198
+ read-only, computes the row count per `scan_*` (and `state_*`
199
+ when `--state`) table, and prints them. No `DELETE`
200
+ statements issued. Bypasses the `--state` confirmation prompt
201
+ entirely. - `sm db reset --hard --dry-run` — reports the DB file path and
202
+ size that would be unlinked; missing-file case prints a clear
203
+ no-op line instead of an error. - `sm db restore <src> --dry-run` — validates the source exists
204
+ (still exits 5 if missing), reports the source size and
205
+ whether the target would be created or overwritten, plus the
206
+ WAL / SHM sidecars that would be dropped. Bypasses the
207
+ confirmation prompt.
208
+ Implementation: new helper `previewGitignoreEntries(scopeRoot,
209
+ entries)` in `init.ts` mirrors `ensureGitignoreEntries` parsing
210
+ so the preview tracks the live outcome exactly. Texts moved
211
+ into `cli/i18n/init.texts.ts` and `cli/i18n/db.texts.ts` per
212
+ the N4 pattern. **9 new tests** under `init-cli.test.ts` (5
213
+ cases) and `db-cli.test.ts` (9 cases) cover the previews + the
214
+ spec invariants ("DB file checksum unchanged after dry-run",
215
+ "scope directory absent after dry-run", "source-not-found
216
+ still exits 5", "confirmation prompt skipped under dry-run").
217
+ - **H1 — Centralised exit codes.** New `cli/util/exit-codes.ts`
218
+ exporting `ExitCode` (`Ok` / `Issues` / `Error` / `Duplicate` /
219
+ `NonceMismatch` / `NotFound`) and the type alias `TExitCode`.
220
+ Every `Command#execute()` migrated from numeric literals (123
221
+ sites across 17 files) to the enum. Single source of truth
222
+ aligned with `spec/cli-contract.md` §Exit codes. **Bug fix
223
+ surfaced en passant:** `sm job prune` returned `2` for "DB
224
+ missing" while every other read-side verb returned `5` via
225
+ `assertDbExists`; corrected to use the shared helper and return
226
+ `NotFound`. Companion test updated to expect `5`.
227
+ - **H2 — Plugin loader timeout.** `IPluginLoaderOptions.loadTimeoutMs`
228
+ (default `5000`, exported as `DEFAULT_PLUGIN_IMPORT_TIMEOUT_MS`).
229
+ Each dynamic `import()` now races against a timer; on timeout
230
+ the plugin is reported as `load-error` with a message naming
231
+ the elapsed budget and pointing at top-level side effects as
232
+ the likely cause (network call, infinite loop, large blocking
233
+ work). Without this a plugin with a hanging top-level `await`
234
+ blocks every host CLI command indefinitely.
235
+ - **H4 — `--strict` self-validates `--json` output.** When
236
+ `sm scan --strict --json` is invoked, the produced `ScanResult`
237
+ is validated against `scan-result.schema.json` before stdout.
238
+ Catches the case where a custom detector emits a Link that
239
+ passes the shallow `validateLink` guard but fails the full
240
+ schema, which would silently land in stdout and break a
241
+ downstream `sm scan compare-with -`.
242
+ - **H5 — External-link discrimination uses URL-shape regex.**
243
+ `isExternalUrlLink` was string-matching `http://` / `https://`
244
+ only; any other URL scheme (`mailto:`, `data:`, `file:///`,
245
+ `ftp://`) was silently classified as internal and polluted the
246
+ graph as a fake internal link with `byPath` lookups that always
247
+ missed. Replaced with the RFC 3986 scheme regex
248
+ (`/^[a-z][a-z0-9+\-.]+:/i`), guarding against Windows-style
249
+ absolute paths via the ≥ 2-char scheme constraint.
250
+ - **H6 — Prior snapshot validated under `--strict`.** Both
251
+ `sm scan` and `sm watch`, when run with `--strict`, validate
252
+ the DB-resident `ScanResult` against the spec schema before
253
+ handing it to the orchestrator. A DB corrupted manually or
254
+ mid-rollback used to slip nodes with malformed `bodyHash` /
255
+ `frontmatterHash` into the rename heuristic, where the
256
+ dereference would silently produce spurious matches.
257
+
258
+ #### Medium — surface & extensibility
259
+
260
+ - **M1 — `sm scan compare-with` sub-verb.** New
261
+ `ScanCompareCommand` in `cli/commands/scan-compare.ts`; the
262
+ `--compare-with` flag is removed from `ScanCommand`. The
263
+ sub-verb form structurally rejects flag combos that used to
264
+ require runtime guards (`--changed`, `--no-built-ins`,
265
+ `--allow-empty`, `--watch`): Clipanion rejects them at parse
266
+ time as unknown options.
267
+ - **M2 — `kernel/index.ts` enumerated exports.** Replaced the two
268
+ `export type *` wildcards (from `./types.js` and
269
+ `./ports/index.js`) with explicit named exports. Same set of
270
+ public types — the DTS size and tests confirm parity. Going
271
+ forward, any new domain type or port change requires an
272
+ explicit edit to the barrel, preventing silent surface drift.
273
+ - **M3 — Build hack documented (workaround retained).** Tried to
274
+ replace the post-build `restoreNodeSqliteImports` pass with
275
+ `external: ['node:sqlite']` in `tsup.config.ts`. Esbuild marks
276
+ the specifier as external but still strips the `node:` prefix;
277
+ same outcome with `[/^node:/]` regex and `packages: 'external'`
278
+ (which also externalises real npm deps). Reverted to the
279
+ post-build `replaceAll` pass, with a docstring documenting
280
+ every workaround attempted so the next agent does not repeat
281
+ the spike.
282
+ - **M4 — `tryWithSqlite` helper.** See C2.
283
+ - **M5 — `CamelCasePlugin` trap documented.** Added a
284
+ trap-warning block to `SqliteStorageAdapter`'s docstring:
285
+ `sql.raw` / `sql\`...\``template literals do NOT pass through
286
+ the`CamelCasePlugin`; raw SQL fragments must use snake_case to
287
+ match the migrations.
288
+ - **M6 — Per-extension error reporting.** When the orchestrator
289
+ drops a link emitted with an undeclared kind or an issue with
290
+ an invalid severity, it now emits a `type: 'extension.error'`
291
+ `ProgressEvent` instead of silently swallowing. The CLI
292
+ subscribes via the new `createCliProgressEmitter(stderr)`
293
+ helper and renders those events as `extension.error: <message>`
294
+ on stderr. Plugin authors finally see WHY their link / issue
295
+ disappears from the result. Wired in `scan` (normal +
296
+ compare-with), `watch`, and `init`.
297
+ - **M7 — Type naming convention documented (no rename).** Top-of-
298
+ file docstring in `kernel/types.ts` and a new section in
299
+ `AGENTS.md` describe the four-bucket convention the codebase
300
+ has always implicitly followed: domain types (no prefix,
301
+ mirrors spec schemas), hexagonal ports (`Port` suffix), runtime
302
+ extension contracts (`I` prefix), internal shapes (`I`
303
+ prefix). Mass rename was rejected after a cost-benefit pass —
304
+ naming changes are cheap to write but expensive to review;
305
+ existing names are mostly coherent. The agent base
306
+ (`_plugins/minions/shared/architect.md`) gained a "Naming
307
+ conventions check" sub-section in REVIEW mode so future audits
308
+ reach the same conclusion.
309
+
310
+ #### Low / nit — cleanup
311
+
312
+ - **L1 — `omitModule` JSON replacer precision.** Identifies the
313
+ ESM namespace by `[Symbol.toStringTag] === 'Module'` instead of
314
+ matching every `module` key blindly. A plugin manifest that
315
+ legitimately ships an unrelated `module` field (e.g. a string
316
+ property in `metadata`) is no longer silently dropped from
317
+ `sm plugins list --json` output.
318
+ - **L2 — Stub verbs flagged in `--help`.** Every
319
+ `not-yet-implemented` verb in `cli/commands/stubs.ts` carries a
320
+ `(planned)` suffix on its `description`, surfaced in
321
+ `sm --help`. The `notImplemented` helper now writes
322
+ `<verb>: not yet implemented (planned).` on stderr instead of
323
+ promising a specific Step number — roadmap step numbers shift
324
+ mid-flight, stale promises in `--help` are worse than no
325
+ promise.
326
+ - **L3 — Dead `eslint-disable` removed** from
327
+ `cli/util/plugin-runtime.ts`.
328
+ - **N1 — `Link.source` vs `Link.sources` doc clarified.** Both
329
+ fields now carry inline doc-comments calling out the singular /
330
+ plural naming trap. Spec-frozen, but the ambiguity is the
331
+ easiest way to misread the type for new contributors.
332
+ - **N2 — `sm check` Usage examples expanded.** The `-g/--global`
333
+ and `--db <path>` flags were declared but missing from the
334
+ `Usage.examples` block — asymmetry with `sm scan` and the rest
335
+ of the read-side verbs that ship the same flags. Two examples
336
+ added: `sm check --global` and `sm check --db
337
+ /path/to/skill-map.db`.
338
+ - **N4 — Error / hint strings extracted to `*.texts.ts` modules
339
+ with `{{name}}` template interpolation.** Pre-1.0 is the
340
+ natural moment to seed the pattern before the string set grows.
341
+ The workspace `ui/` already has a sibling layout at
342
+ `ui/src/i18n/` (functions returning template literals); CLI
343
+ takes a deliberately different shape — flat string templates
344
+ with `{{name}}` placeholders, interpolated by a tiny
345
+ `tx(template, vars)` helper. Rationale: the template form is
346
+ **drop-in compatible with Transloco / Mustache / Handlebars**
347
+ (the syntax they all share) so the day this project migrates to
348
+ a real i18n library, the strings move as-is. Functions would
349
+ have to be re-shaped first.
350
+
351
+ Helper at `kernel/util/tx.ts`. Contract:
352
+
353
+ - Every `{{name}}` token MUST have a matching key in the vars
354
+ object — missing key throws (silent fallback hides
355
+ forgotten args in production).
356
+ - `null` / `undefined` values throw — caller coerces
357
+ upstream.
358
+ - Whitespace inside the braces tolerated (`{{ name }}`) so
359
+ long templates wrap cleanly across `+`-joined lines.
360
+ - Plural / conditional logic does NOT live in the template;
361
+ the caller picks `*_singular` vs `*_plural` keys.
362
+
363
+ Files created:
364
+
365
+ - `kernel/util/tx.ts` — the helper itself, with 13 tests in
366
+ `test/tx.test.ts` (single / multi token, whitespace,
367
+ missing / null / undefined keys, identifier shapes, error
368
+ truncation).
369
+ - `kernel/i18n/orchestrator.texts.ts` — frontmatter
370
+ malformed/invalid templates, `extension.error` payloads,
371
+ root validation errors.
372
+ - `kernel/i18n/plugin-loader.texts.ts` — every `load-error` /
373
+ `invalid-manifest` / `incompatible-spec` reason, plus the
374
+ import timeout message.
375
+ - `cli/i18n/scan.texts.ts` — `sm scan` flag-clash / scan
376
+ failure / guard / summary templates, plus the `sm scan
377
+
378
+ compare-with`dump-load errors.
379
+ -`cli/i18n/watch.texts.ts`—`sm watch`lifecycle templates.
380
+ -`cli/i18n/init.texts.ts`—`sm init`templates including
381
+ the`--dry-run`previews and the singular/plural pair for
382
+ gitignore updates.
383
+ -`cli/i18n/db.texts.ts`—`sm db reset`/`sm db restore` templates including their`--dry-run`previews.
384
+ -`cli/i18n/cli-progress-emitter.texts.ts`— the
385
+ `extension.error: ...` stderr line.
386
+
387
+ String content moved verbatim — every existing test that
388
+ matches on stderr / stdout content keeps passing. Trivial
389
+ single-token strings (`'No issues.\n'`) and rare per-handler
390
+ bespoke phrases stay inline; the pattern is now established
391
+ for whoever wants to migrate them in a follow-up.
392
+
393
+ Note on `ui/` divergence: today the two workspaces use
394
+ different shapes for their text tables (functions in `ui/`,
395
+ templates in `cli/`). Aligning them is a follow-up — the day a
396
+ real i18n library lands, both converge on its native shape.
397
+ The CLI shape is closer to the eventual destination.
398
+
399
+ - **N6 — `TIssueSeverity` aliased to `Severity`.** SQLite schema
400
+ type now reads `type TIssueSeverity = Severity` instead of
401
+ duplicating the union literal. Keeps DB and runtime in
402
+ lock-step if the union ever evolves.
403
+
404
+ ### Migrations consolidation (kernel DB)
405
+
406
+ - **`src/migrations/001_initial.sql` + `002_scan_meta.sql`**
407
+ consolidated into a single `001_initial.sql`. Pre-1.0 with no
408
+ released DBs to forward-migrate, the two-file split was a
409
+ historical accident from an incremental shipment. After
410
+ consolidation: same 12 tables, same constraints, same indexes;
411
+ `PRAGMA user_version` of a freshly-initialised DB is now `1`
412
+ instead of `2`. Migration runner is unchanged (it tolerates any
413
+ count of `NNN_*.sql` files).
414
+
415
+ ### Test coverage (Part 1)
416
+
417
+ - New tests for H2 (plugin loader timeout — 2 cases),
418
+ M6 (orchestrator `extension.error` emission — 3 cases),
419
+ CLI progress emitter wiring (4 cases). The compare-with suite
420
+ (`scan-compare.test.ts`, 9 cases) was migrated to
421
+ `ScanCompareCommand` and the three flag-clash tests dropped
422
+ (the flags are now structurally absent on the sub-verb). Test
423
+ totals: 479 (start of pass) → 488 (after H2/M6 tests) → 485
424
+ (after the three flag-clash deletions).
425
+
426
+ ### Deferred / out of scope
427
+
428
+ The findings below were reviewed but did not warrant code
429
+ changes; each has its own resolution noted alongside.
430
+
431
+ - **L4 — `runScan` / `runScanWithRenames` unification.** Already
432
+ resolved by C1 (both are thin wrappers around
433
+ `runScanInternal`).
434
+ - **L5 — Node-version-guard exit code.** Reviewed against the
435
+ updated exit-code table; existing `2` is correct under
436
+ "operational error / unhandled exception". Spec table got the
437
+ environment-mismatch clarification (above).
438
+ - **L6 — `loadSchemaValidators()` cache.** Already cached at
439
+ module level since Step 5.12.
440
+ - **L7 — `pkg with { type: 'json' }` portability.** Stable in
441
+ Node ≥ 22; `engines.node": ">=24.0"` covers it. No fallback
442
+ needed.
443
+ - **N3 — `compare-with` "dump not found" exit code.** The error
444
+ paths in `ScanCompareCommand` already use the `ExitCode.Error`
445
+ enum (= 2) for dump load failures, matching the spec clause for
446
+ operational errors.
447
+ - **N5 — Exit-code list completeness.** Verified the comment in
448
+ `cli/entry.ts` against `spec/cli-contract.md` §Exit codes —
449
+ identical, no edit needed.
450
+
451
+ ## Part 2 — Plugin model overhaul (5-phase implementation)
452
+
453
+ ### Summary
454
+
455
+ The plugin model received a comprehensive overhaul before
456
+ stabilizing at v1.0. Plugin kinds total after this bump: **6**
457
+ (Provider, Extractor, Rule, Action, Formatter, Hook). All
458
+ breakings are pre-1.0 minor per `versioning.md` § Pre-1.0.
459
+
460
+ ### Phase 1 (commit 7354c26) — Foundation
461
+
462
+ Five sub-phases, additive or pre-1.0 minor breakings:
463
+
464
+ - **A.4** — three-tier frontmatter validation model documented in
465
+ `plugin-author-guide.md` (default permissive + `unknown-field`
466
+ rule + `scan.strict` promote-to-error). Behavior unchanged.
467
+ - **A.5** — plugin id global uniqueness: `directory ==
468
+ manifest.id` rule, new status `id-collision` (sixth),
469
+ validation in boot/scan/doctor. Cross-root collisions block
470
+ both involved plugins; user resolves by renaming.
471
+ - **A.6** — extension ids qualified `<plugin-id>/<ext-id>` in
472
+ registry. Built-ins classified into `claude/*` (4 Claude-
473
+ specific) and `core/*` (7 kernel built-ins) bundles. New
474
+ `Registry.get/find` APIs; `defaultRefreshAction` schema
475
+ requires the qualified pattern; `extension.error` events emit
476
+ qualified ids.
477
+ - **A.10** — optional `applicableKinds` filter on Detector
478
+ manifest; fail-fast skip for non-matching kinds (zero CPU/LLM
479
+ cost); doctor warning for kinds not declared by any installed
480
+ Provider. Empty array invalid; absence preserves apply-to-all
481
+ default.
482
+ - **Granularity** — Built-ins now respect `config_plugins`
483
+ enable/disable via granularity-aware filtering. New
484
+ `IBuiltInBundle` shape with `granularity: 'bundle' |
485
+ 'extension'`; `claude` ships as bundle (all-or-nothing), `core`
486
+ as extension (each toggleable). User plugins default to bundle;
487
+ opt in via `granularity` in `plugin.json`. Both plugin ids and
488
+ qualified extension ids accepted as keys in `config_plugins`
489
+ and `settings.json#/plugins` (no schema change needed).
490
+
491
+ 550/550 tests pass (+33 vs baseline 517).
492
+
493
+ ### Phase 2 (commit ae3eaa6) — Renames
494
+
495
+ Four sub-phases, all breaking but allowed in minor pre-1.0:
496
+
497
+ - **2a (Renderer → Formatter)** — Kind, types, files renamed.
498
+ Method `render(ctx)` → `format(ctx)`; manifest field `format`
499
+ → `formatId` (TS clash resolution). Same contract: graph →
500
+ string, deterministic-only.
501
+ - **2b (Adapter → Provider)** — New required field
502
+ `explorationDir` on the manifest (e.g. `~/.claude` for the
503
+ Claude Provider). DB schema migrated in-place (column
504
+ `nodes.adapter` → `nodes.provider`, etc.). The
505
+ hexagonal-architecture `RunnerPort.adapter` /
506
+ `StoragePort.adapter` is unchanged.
507
+ - **2c (Audit removed)** — Audit kind removed. The single
508
+ built-in `validate-all` migrated to a Rule (qualified id
509
+ `core/validate-all`, `evaluate(ctx) → Issue[]`). CLI verbs
510
+ `sm audit *` removed; users invoke via `sm check --rules
511
+ core/validate-all`.
512
+ - **2d (Detector → Extractor)** — Method signature changes from
513
+ `detect(ctx) → Link[]` to `extract(ctx) → void` — output flows
514
+ through three ctx callbacks: `emitLink`, `enrichNode`, `store`.
515
+ Built-ins migrated maintain functional parity using `emitLink`.
516
+ Persistence of `enrichNode` deferred to Phase 4 (A.8 stale
517
+ layer); orchestrator buffers in memory today.
518
+
519
+ 554/554 cli + 32/32 testkit pass.
520
+
521
+ ### Phase 3 (commit 34f993e) — Schema relocation
522
+
523
+ **A.2** — Per-kind frontmatter schemas relocate from spec to the
524
+ Provider that declares them. Spec keeps only `frontmatter/base`
525
+ (universal).
526
+
527
+ - 5 schemas moved (`git mv`):
528
+ `spec/schemas/frontmatter/{skill,agent,command,hook,note}.schema.json`
529
+ → built-in Claude Provider's `schemas/` directory. New `$id`:
530
+ `https://skill-map.dev/providers/claude/v1/frontmatter/<kind>`.
531
+ Cross-package `$ref` resolves via the spec base's `$id`
532
+ (`https://skill-map.dev/spec/v0/frontmatter/base.schema.json`);
533
+ AJV resolves by `$id` when both schemas register on the same
534
+ instance.
535
+ - Provider manifest gains a required `kinds` map subsuming three
536
+ former fields: `emits` (now derives from
537
+ `Object.keys(kinds)`), the flat `defaultRefreshAction` map (now
538
+ per-entry inside `kinds[<kind>].defaultRefreshAction`), and the
539
+ new `schema` (path to the per-kind schema relative to the
540
+ provider directory).
541
+ - Built-in Claude Provider migrated: 5 kind entries (skill,
542
+ agent, command, hook, note), each with `schema`, `schemaJson`
543
+ (runtime field, AJV-compiled at load), and qualified
544
+ `defaultRefreshAction` (`claude/summarize-<kind>`).
545
+ - Kernel orchestrator parse phase asks the Provider for the
546
+ schema via `IProviderFrontmatterValidator` (composed by scan
547
+ via `buildProviderFrontmatterValidator`) instead of reading
548
+ from spec/. Flow: validate base → look up provider → validate
549
+ per-kind schema from Provider.
550
+ - `schema-validators.ts` catalog loses the 5 per-kind frontmatter
551
+ entries; only `frontmatter-base` remains kernel-known.
552
+ `plugin-loader`'s `stripFunctionsAndPluginId` now also strips
553
+ `schemaJson` (runtime-only) from each `kinds` entry before
554
+ AJV-validating the manifest.
555
+ - Coverage matrix: 28 → 23 schemas (the 5 per-kind frontmatter
556
+ schemas are now Provider-owned and ship with their own
557
+ conformance suite in Phase 5 / A.13).
558
+
559
+ 556/556 cli + 32/32 testkit pass.
560
+
561
+ ### Phase 4 (commit e62695f) — Probabilistic infra
562
+
563
+ Five sub-phases, all breaking but allowed in minor pre-1.0:
564
+
565
+ - **4a (A.9)** — fine-grained Extractor cache via new
566
+ `scan_extractor_runs` table. Resolves gap where newly
567
+ registered Extractors silently skipped cached nodes; cache hit
568
+ logic now per-(node, extractor). Uninstalled Extractors cleaned
569
+ (rows + orphan links). Migration in-place.
570
+ - **4b (A.12)** — opt-in `outputSchema` for plugin custom
571
+ storage. Manifest gains `storage.schema` (Mode A) and
572
+ `storage.schemas` (Mode B) for AJV validation of
573
+ `ctx.store.write/.set` calls. Throws on shape violation;
574
+ default absent = permissive.
575
+ - **4c (A.8)** — enrichment layer + stale tracking. New
576
+ `node_enrichments` table persists per-(node, extractor)
577
+ partials separately from author's frontmatter (immutable).
578
+ Probabilistic enrichments track `body_hash_at_enrichment`; scan
579
+ flags `stale=1` on body change (NOT deleted, preserves LLM
580
+ cost). Helper `mergeNodeWithEnrichments` filters stale +
581
+ last-write-wins. New verbs `sm refresh <node>` and
582
+ `sm refresh --stale` (stubs awaiting Step 10).
583
+ - **4d (A.11)** — sixth plugin kind `hook`. Declarative
584
+ subscriber to a curated set of 8 lifecycle events (`scan.*`,
585
+ extractor/rule/action.completed,
586
+ job.spawning/completed/failed). Other events deliberately not
587
+ hookable. Manifest declares `triggers[]` (load-time validated)
588
+ and optional `filter`. Three new kernel events added to
589
+ catalog. Dual-mode (det dispatched in-process; prob deferred to
590
+ Step 10).
591
+ - **4e (A.7)** — `sm check --include-prob` opt-in flag (stub).
592
+ Default `sm check` unchanged: det only, CI-safe. With flag:
593
+ detects prob rules, emits stderr advisory; full dispatch awaits
594
+ Step 10. Combines with `--rules`, `-n`, `--no-plugins`.
595
+
596
+ 591/591 cli + 32/32 testkit pass.
597
+
598
+ ### Phase 5 (commit 03b5a65) — Conformance + cleanup
599
+
600
+ **A.13** — Conformance fixture relocation:
601
+
602
+ - 3 cases moved (`git mv`): `basic-scan`, `orphan-detection`,
603
+ `rename-high` →
604
+ `src/extensions/providers/claude/conformance/cases/`. 11
605
+ fixture files (`minimal-claude/`, `orphan-{before,after}/`,
606
+ `rename-high-{before,after}/`) moved alongside.
607
+ - New `coverage.md` per-Provider listing the 5 frontmatter
608
+ schemas (skill, agent, command, hook, note) and their cases.
609
+ - New verb `sm conformance run [--scope spec|provider:<id>|all]`.
610
+ Discovery by convention at `<plugin-dir>/conformance/`. The
611
+ existing runner gains optional `fixturesRoot` (default
612
+ `<specRoot>/conformance/fixtures` for compat); tooling using
613
+ the public API of `@skill-map/cli/conformance` keeps working.
614
+ `--json` deferred — reporter shape not yet frozen.
615
+ - Spec keeps only the kernel-agnostic case (`kernel-empty-boot`)
616
+ and the universal preamble fixture. Coverage matrix downgrades
617
+ conservatively (rows that depended on `basic-scan` are now
618
+ partial or missing, with cross-link to the Provider's matrix).
619
+
620
+ ROADMAP cleanup:
621
+
622
+ - The three "Status: target state for v0.8.0 — spec catch-up
623
+ pending" banners on §Plugin system / §Frontmatter standard /
624
+ §Enrichment are removed; prose shifts from future to present
625
+ ("kinds from v0.7.0 are renamed" → "were renamed in spec
626
+ 0.8.0"; Model B enrichment now describes the shipped
627
+ `node_enrichments` table with `body_hash_at_enrichment` rather
628
+ than "table or column set decided in PR").
629
+ - Decision-log entry for the working session rewritten to
630
+ reflect "shipped" rather than "pending".
631
+ - Last-updated header gains an "implementation" paragraph
632
+ listing the four prior phase commits.
633
+
634
+ 593/593 cli + 32/32 testkit pass (+2 vs Phase 4 baseline).
635
+ spec:check green (40 files hashed — down from 53 because the
636
+ Claude-specific cases and fixtures left the spec's hash set).
637
+
638
+ ### Breaking changes for plugin authors (Part 2)
639
+
640
+ Manifest renames:
641
+
642
+ - `kind: 'adapter'` → `kind: 'provider'`
643
+ - `kind: 'detector'` → `kind: 'extractor'`
644
+ - `kind: 'renderer'` → `kind: 'formatter'`
645
+ - `kind: 'audit'` removed (migrate to `kind: 'rule'`).
646
+
647
+ Method signatures:
648
+
649
+ - Detector `detect(ctx) → Link[]` → Extractor `extract(ctx) →
650
+ void` (output via `ctx.emitLink` / `ctx.enrichNode` /
651
+ `ctx.store`).
652
+ - Renderer `render(ctx) → string` → Formatter `format(ctx) →
653
+ string`.
654
+
655
+ Manifest fields:
656
+
657
+ - Provider gains required `explorationDir`.
658
+ - Provider's flat `defaultRefreshAction` map replaced by per-kind
659
+ entries inside `kinds[<kind>].defaultRefreshAction` (must
660
+ follow qualified pattern `<plugin-id>/<ext-id>`).
661
+ - Provider's `emits` derives from `Object.keys(kinds)` (the
662
+ manifest field is gone).
663
+ - Provider's per-kind schemas declared via `kinds[<kind>].schema`
664
+ (path relative to provider dir).
665
+ - Renderer's `format` field renamed to `formatId` on the
666
+ Formatter manifest (TS clash resolution).
667
+ - New plugin kind `hook` with `triggers[]` + optional `filter`.
668
+ - Optional `outputSchema` (`storage.schema` / `storage.schemas`)
669
+ for Mode A / Mode B plugin custom storage.
670
+ - Optional `applicableKinds` filter on Extractor manifest.
671
+
672
+ Extension ids:
673
+
674
+ - All extension ids must be qualified
675
+ `<plugin-id>/<extension-id>` (built-ins classified into
676
+ `claude/*` and `core/*`).
677
+
678
+ DB schema:
679
+
680
+ - Two new tables added in-place to `001_initial.sql` (pre-1.0
681
+ consolidation, no production DBs to migrate):
682
+ `scan_extractor_runs` and `node_enrichments`.
683
+ - Column rename `nodes.adapter` → `nodes.provider` (and parallel
684
+ in `result.adapters` → `result.providers`).
685
+
686
+ ## Test stats
687
+
688
+ 593/593 cli + 32/32 testkit pass (post-Phase 5).
689
+ Two new DB tables (`scan_extractor_runs`, `node_enrichments`)
690
+ added in-place to `001_initial.sql` (pre-1.0 consolidation, no
691
+ production DBs to migrate). The 5 per-kind frontmatter schemas
692
+ relocated from spec/ to the Claude Provider package.
693
+
694
+ ## [Unreleased]
695
+
696
+ ### Minor Changes
697
+
698
+ - Conformance fixture relocation (Phase 5 / A.13). The conformance suite splits along ownership lines: spec-owned cases (kernel-agnostic, today only `kernel-empty-boot` plus the deferred `preamble-bitwise-match`) keep living under `spec/conformance/`; Provider-owned cases that exercise a Provider's own kind catalog move next to that Provider's manifest, under `<plugin-dir>/conformance/`. The reference impl's Claude Provider now hosts `basic-scan`, `rename-high`, and `orphan-detection` together with their `minimal-claude` / `orphan-{before,after}` / `rename-high-{before,after}` fixtures at `src/extensions/providers/claude/conformance/`. The split mirrors the spec 0.8.0 Phase 3 schema relocation: cases that depend on Claude-specific kinds (`skill`) belong with the Provider that declares the kind, not in the spec. New CLI verb `sm conformance run [--scope spec|provider:<id>|all]` (default `all`) drives both buckets in one invocation; `--scope spec` and `--scope provider:claude` narrow to a single suite for targeted runs and CI matrices. The reference runner gains an optional `fixturesRoot` parameter so cases can resolve their fixtures against the Provider's directory instead of the spec's. `spec/conformance/README.md` updated for the dual-ownership layout (spec-owned + Provider-owned tables, `sm conformance run` documented, runner pseudocode amended). `spec/conformance/coverage.md` retargeted: rows that used to credit `basic-scan` (now Provider-owned) downgrade to `kernel-empty-boot`-only or `🔴 missing` and point to the Provider's coverage file (`src/extensions/providers/claude/conformance/coverage.md`); the rename-heuristic non-schema row notes the Provider ownership. `spec/cli-contract.md` adds a §Conformance subsection under §Verb catalog and adds `sm conformance run` to the elapsed-time §Scope. `spec/architecture.md` opening sentence credits both buckets. Pre-1.0 minor per `versioning.md` § Pre-1.0; breaking only for tooling that hard-codes the previous case paths under `spec/conformance/cases/{basic-scan,rename-high,orphan-detection}.json` — no real ecosystem affected today (the reference impl's runner already migrates).
699
+
700
+ - `sm check` gains `--include-prob` opt-in flag for probabilistic Rule dispatch (Phase 4 / A.7). Default unchanged: deterministic only, CI-safe — same status quo behaviour. With the flag, the verb loads the plugin runtime, finds Rules with `mode === 'probabilistic'` (filtered by `--rules` if set), and emits a stderr advisory naming the skipped rule ids. Full dispatch lands when the job subsystem ships at Step 10; until then the flag is a stub — prob rules never produce issues, never alter the exit code. New companion flag `--async` is reserved for the future encoding (returns job ids without waiting once jobs land); today it is a no-op the advisory mentions. Companion filters `-n <node.path>` and `--rules <ids>` (comma-separated qualified or short ids) added to `sm check` for granular reads — they restrict the persisted-issue list AND filter which prob rules surface in the advisory. Does NOT extend to `sm scan` or `sm list`. Documented in `cli-contract.md` §Browse and `plugin-author-guide.md` §Rules. Pre-1.0 minor per `versioning.md` § Pre-1.0; additive — no consumer breakage.
701
+
702
+ - Sixth plugin kind `hook` added (Phase 4 / A.11). Reacts declaratively to a curated set of 8 lifecycle events — `scan.started`, `scan.completed`, `extractor.completed`, `rule.completed`, `action.completed`, `job.spawning`, `job.completed`, `job.failed`. Other events (per-node `scan.progress`, `model.delta`, `run.*`, `job.claimed`, `job.callback.received`) are deliberately NOT hookable: too verbose, internal to the runner, or covered elsewhere. Manifest declares `triggers[]` (validated against the hookable set; an unknown trigger yields `invalid-manifest` at load time with a directed reason naming the offending trigger and the full hookable list) and an optional `filter` object (top-level field equality match against the event payload; cross-field validation is best-effort in v0.x). Dual-mode: `deterministic` (default) runs `on(ctx)` in-process during the dispatch of the matching event, synchronously between emission and the next pipeline step; `probabilistic` is enqueued as a job (deferred to the job subsystem at Step 10 — probabilistic hooks load but skip dispatch with a stderr advisory until then). Hooks REACT to events; they cannot mutate the pipeline, block emission, or alter outputs. Errors are caught by the dispatcher (logged through `extension.error` with `kind: 'hook-error'`) and never block the main flow. Three new event types added to the catalog so the aggregated Extractor / Rule / Action triggers have a normative shape: `extractor.completed` (one per Extractor, after the full walk), `rule.completed` (one per Rule, after issue validation), `action.completed` (one per Action invocation, after report recording — lands alongside the job subsystem at Step 10). New schema `schemas/extensions/hook.schema.json` (`$id` `https://skill-map.dev/spec/v0/extensions/hook.schema.json`); `schemas/extensions/base.schema.json#/properties/kind/enum` extended with `hook`. Documented in `architecture.md` §Extension kinds (table extended from 5 to 6 rows), §Mode capability matrix (Hook dual-mode), §Hook · curated trigger set (new dedicated section); `plugin-author-guide.md` retitled "## The six extension kinds" with a new Hooks subsection (worked example: Slack notifier on `scan.completed`); `job-events.md` cross-links Hook from each of the 8 hookable triggers, adds the three new aggregated event entries, and updates the experimental tag scope. Coverage matrix grows from 23 to 24 rows. Pre-1.0 minor per `versioning.md` § Pre-1.0; additive — no consumer breakage. Existing extension kinds (`provider`, `extractor`, `rule`, `action`, `formatter`) are untouched.
703
+
704
+ - Enrichment layer formalized (Phase 4 / A.8). New kernel table `node_enrichments(node_path, extractor_id, body_hash_at_enrichment, value_json, stale, enriched_at, is_probabilistic)` stores `ctx.enrichNode(partial)` outputs separately from the author's frontmatter (which remains IMMUTABLE from any Extractor — both deterministic and probabilistic). Per-Extractor attribution is preserved (one row per `(node, extractor)` pair). Probabilistic enrichments track `body_hash_at_enrichment`; when the scan loop sees a body change, those rows are flagged `stale = 1` (NOT deleted, so the LLM cost is preserved). Deterministic enrichments regenerate via the A.9 fine-grained cache and pisar via PRIMARY KEY conflict on the next re-extract — they are never stale-flagged. Read-side helper `mergeNodeWithEnrichments(node, enrichments)` produces a "merged view" by filtering stale rows, sorting by `enriched_at` ASC, and spread-merging onto the author frontmatter (last-write-wins per field). Stale visibility is opt-in (`includeStale: true`). Rules / `sm check` / `sm export` consume `node.frontmatter` directly (deterministic CI-safe baseline); enrichment consumption is opt-in by the caller. New verbs `sm refresh <node>` (granular) and `sm refresh --stale` (batch) re-run Extractors and upsert fresh enrichment rows — STUBBED until the job subsystem ships at Step 10: deterministic Extractors persist for real, probabilistic Extractors emit a stderr advisory and skip without touching their stale rows. Migration `001_initial.sql` updated in place per the pre-1.0 consolidation precedent (no released DBs to forward-migrate). Documented in `db-schema.md` §`node_enrichments`, `architecture.md` §Extractor · enrichment layer, `cli-contract.md` §Scan, and `plugin-author-guide.md` §Extractors. Pre-1.0 minor per `versioning.md` § Pre-1.0; additive — no consumer breakage.
705
+
706
+ - Plugin manifest gains optional `storage.schemas` map (Mode B / dedicated) and `storage.schema` (Mode A / KV) for opt-in JSON Schema validation of custom storage writes. AJV-validates `ctx.store.write(table, row)` and `ctx.store.set(key, value)` before persisting; throws on shape violation. Default absent = permissive (status quo). `emitLink` and `enrichNode` keep their universal kernel validation regardless. A schema file missing on disk or failing AJV compile at load time surfaces as `load-error` with a directed reason naming the plugin, the table (Mode B), and the schema path. Pre-1.0 minor per `versioning.md` § Pre-1.0; additive — no consumer breakage. Documented in `plugin-author-guide.md` §Storage and referenced from `architecture.md` §Extractor · output callbacks.
707
+
708
+ - New kernel table `scan_extractor_runs(node_path, extractor_id, body_hash_at_run, ran_at)` — fine-grained Extractor cache breadcrumbs (Phase 4 / A.9). Replaces the previous "trust the node-level body+frontmatter hash" model that silently bypassed any Extractor newly registered between scans. Cache decision per `(node, extractor)` pair: a new Extractor registered between scans yields a partial cache hit (only the newcomer runs over the cached node); an uninstalled Extractor's rows disappear via replace-all, and links whose sources are exclusively that Extractor disappear with them. Documented in `db-schema.md` §`scan_extractor_runs`. Migration `001_initial.sql` updated in place per the pre-1.0 consolidation precedent (no released DBs to forward-migrate). Pre-1.0 minor per `versioning.md` § Pre-1.0; additive — no consumer breakage.
709
+
710
+ - Per-kind frontmatter schemas relocate from spec to the Provider that declares them. Spec keeps only `frontmatter/base.schema.json` (universal — fields common to every node across every Provider). The Claude Provider gains a `kinds` map declaring its catalog (`skill` / `agent` / `command` / `hook` / `note`) with per-kind `schema` + `defaultRefreshAction`. The pre-0.8 flat fields `emits: string[]` and `defaultRefreshAction: { <kind>: actionId }` collapse into the new map: `emits` is removed (derived from `Object.keys(kinds)`); each `defaultRefreshAction[<kind>]` value moves into `kinds[<kind>].defaultRefreshAction`. The kernel parse phase asks the Provider for the schema instead of reading from `spec/schemas/frontmatter/<kind>.schema.json`. Schema files moved: `spec/schemas/frontmatter/{skill,agent,command,hook,note}.schema.json` → `src/extensions/providers/claude/schemas/{skill,agent,command,hook,note}.schema.json`; their `$id` updates from `https://skill-map.dev/spec/v0/frontmatter/<kind>.schema.json` to `https://skill-map.dev/providers/claude/v1/frontmatter/<kind>.schema.json`; their `$ref: 'base.schema.json'` updates to `$ref: 'https://skill-map.dev/spec/v0/frontmatter/base.schema.json'` (absolute `$ref`-by-`$id` so AJV resolves cross-package against the spec base registered into the same instance). `spec/schemas/extensions/provider.schema.json` updated: `kinds` is required, `emits` and the old shape of `defaultRefreshAction` removed. `spec/conformance/coverage.md` matrix shrinks from 28 to 23 rows (the five per-kind frontmatter rows belong to the Provider's own conformance suite, planned in Phase 5). `spec/index.json` no longer lists the per-kind schemas. `architecture.md` §Provider section retitled `Provider · kinds catalog and explorationDir`; `plugin-author-guide.md` Provider example updated; `README.md` directory tree updated to reflect spec/frontmatter/ now holds only `base.schema.json`. Pre-1.0 minor per `versioning.md` § Pre-1.0; breaking for any plugin or test referencing `spec/schemas/frontmatter/<kind>.schema.json` paths or `$id`s, the old `provider.emits` field, or the flat `provider.defaultRefreshAction` map — no real ecosystem affected today.
711
+
712
+ - Plugin kind `renderer` renamed to `formatter`. Method renamed `render(ctx) → format(ctx)`. Manifest field `format` (the identifier consumed by `--format`) renamed to `formatId` to avoid clashing with the new method name. Same contract otherwise: graph → string, deterministic-only. Aligns with industry tooling (ESLint formatter, Mocha reporter, Pandoc writer). `schemas/extensions/renderer.schema.json` renamed to `formatter.schema.json`; the `kind` const flips from `"renderer"` to `"formatter"`; `base.schema.json#/properties/kind/enum` updated. `architecture.md`, `cli-contract.md`, `plugin-author-guide.md`, `README.md` updated to match (Extension kinds table, Execution modes table, testkit helper names, worked CSV example). `conformance/coverage.md` row 28 retargeted at the new schema filename. Pre-1.0 minor per `versioning.md` § Pre-1.0; breaking for any plugin or test referencing `kind: "renderer"`, `IRenderer`, `r.format`, or `render(ctx)` — no real ecosystem affected today.
713
+
714
+ - Plugin kind `'detector'` renamed to `'extractor'`. Method signature
715
+ changes from `detect(ctx) → Link[]` to `extract(ctx) → void` — output
716
+ flows through three new ctx callbacks: `emitLink(link)` (kernel `links`
717
+ table), `enrichNode(partial)` (kernel enrichment layer, persisted into
718
+ `node_enrichments` per A.8), and the existing `ctx.store` (plugin's
719
+ own table). The Extractor absorbs what would have been a separate
720
+ `Enricher` kind via `enrichNode`. Built-ins migrated:
721
+ `claude/frontmatter`, `claude/slash`, `claude/at-directive`,
722
+ `core/external-url-counter` — all use `emitLink` to maintain
723
+ functional parity with their Detector ancestors. Schema files
724
+ renamed: `schemas/extensions/detector.schema.json` →
725
+ `schemas/extensions/extractor.schema.json`. Persisted DB rows are
726
+ unaffected (link `sources` carry extractor ids verbatim — the field
727
+ was always free-form). Pre-1.0 minor per `versioning.md` § Pre-1.0;
728
+ breaking for any plugin or test referencing `'detector'` as the
729
+ kind, `IDetector`, or the old `Link[]` return signature — no real
730
+ ecosystem affected today.
731
+
732
+ - Plugin kind `'audit'` removed. The single built-in `'validate-all'`
733
+ migrated to a Rule (qualified id `'core/validate-all'`, method
734
+ `evaluate(ctx) → Issue[]`). The kind had dual personality (composer +
735
+ standalone reporter); the standalone reporter case is naturally a Rule,
736
+ and the composer case is deferred to post-1.0 if a real use case
737
+ appears. CLI verbs `'sm audit run'` and `'sm audit show'` removed;
738
+ users invoke the rule via `sm check --rules core/validate-all`.
739
+ `state_executions.kind` enum narrowed to `['action']` (audit was the
740
+ only other value); the column is preserved as a forward-compatibility
741
+ lever. Schema files removed: `schemas/extensions/audit.schema.json`.
742
+ Coverage matrix shrinks from 29 to 28 rows. Pre-1.0 minor per
743
+ `versioning.md` § Pre-1.0; breaking for any plugin or test referencing
744
+ the audit kind, `IAudit`, `TAuditReport`, or `sm audit` verbs — no
745
+ real ecosystem affected today.
746
+
747
+ - Plugin kind `'adapter'` renamed to `'provider'`. Manifest gains required
748
+ field `'explorationDir'` (filesystem directory where the Provider's
749
+ content lives, e.g. `'~/.claude'` for the Claude Provider). Built-in
750
+ `claudeAdapter` renamed to `claudeProvider`. The hexagonal-architecture
751
+ `'adapter'` (`RunnerPort.adapter`, `StoragePort.adapter`,
752
+ `FilesystemPort.adapter`, `PluginLoaderPort.adapter`) is unchanged —
753
+ distinct concept, distinct namespace.
754
+ Persisted schema fields renamed: `node.adapter` → `node.provider`,
755
+ `scan-result.adapters` → `scan-result.providers` (pre-1.0 minor — no
756
+ production DBs to migrate; `001_initial.sql` was edited in place per
757
+ the consolidation precedent already established for pre-1.0).
758
+ Project config field renamed: `project-config.adapters` →
759
+ `project-config.providers`. Schema files renamed:
760
+ `schemas/extensions/adapter.schema.json` →
761
+ `schemas/extensions/provider.schema.json`. Pre-1.0 minor per
762
+ `versioning.md` § Pre-1.0; breaking for any plugin or test referencing
763
+ `'adapter'` as the kind, `IAdapter`, or any persisted/config schema
764
+ field renamed above — no real ecosystem affected today.
765
+
3
766
  ## 0.7.1
4
767
 
5
768
  ### Patch Changes
@@ -279,9 +1042,9 @@ kind, normalizedTrigger)` and prints one row per group with the
279
1042
  (`Links out (12, 9 unique)`). When N > 1 detector emits the same
280
1043
  logical link, the row also gets a `(×N)` suffix.
281
1044
 
282
- `--json` output is byte-identical to before — raw rows, no merge.
283
- Storage is byte-identical to before. The grouping is purely a
284
- read-time presentation choice for human eyes.
1045
+ `--json` output is byte-identical to before — raw rows, no merge.
1046
+ Storage is byte-identical to before. The grouping is purely a
1047
+ read-time presentation choice for human eyes.
285
1048
 
286
1049
  **Spec changes (patch)**:
287
1050
 
@@ -963,9 +1726,20 @@ Tag convention: `spec-vX.Y.Z` (distinct from CLI tags `cli-vX.Y.Z`).
963
1726
 
964
1727
  Initial public spec bootstrap (Step 0a phases 1–3).
965
1728
 
1729
+ ### Added
1730
+
1731
+ - `cli-contract.md` — new normative section **§Dry-run** between §Exit codes and §Verb catalog. Codifies the contract every verb that exposes `-n` / `--dry-run` MUST honour: no observable side effects (DB / FS / config / network / spawns), no auto-provisioning of scope directories, output mirrors live mode with explicit "would …" framing, exit codes mirror live mode, dry-run MUST short-circuit `--yes` / `--force` confirmation prompts. Per-verb opt-in: the flag is not global and verbs that don't declare it MUST reject it as an unknown option (exit `2`). Verb catalog rows for `sm init`, `sm db reset` (default + `--state` + `--hard`), and `sm db restore` amended to declare and describe their `--dry-run` previews. Pre-1.0 minor (additive normative).
1732
+ - `plugin-author-guide.md` — consolidated section on the three-tier frontmatter validation model (default permissive `additionalProperties: true` + always-active `unknown-field` rule emitting `warn` + `scan.strict` / `--strict` promoting warnings to `error`). Includes a worked example through all three tiers and an explicit note on why no "schema-extender" plugin kind exists (the path for custom validation is a deterministic Rule, not a new plugin kind). Editorial only. No normative change — the model already exists implicitly via `base.schema.json`'s permissive `additionalProperties` and `project-config.schema.json#/properties/scan/properties/strict`. Patch.
1733
+ - `PluginManifest` gains optional `granularity` field (enum `bundle` / `extension`, default `bundle`). Built-in `claude` bundle is `granularity: bundle` (toggle the whole bundle); built-in `core` bundle is `granularity: extension` (each built-in toggle-able individually under `core/<ext-id>`). `sm plugins enable / disable` validates the supplied id against the bundle's declared granularity (bundle granularity rejects qualified ids; extension granularity rejects bare bundle ids) and persists in `config_plugins` with the appropriate key. `--all` operates only on bundle-granularity plugin ids; the "disable every kernel built-in" intent is served by `--no-built-ins`. `plugin-author-guide.md` adds a §Granularity — bundle vs extension section with the per-verb behaviour table and the built-in mapping; `architecture.md` §`PluginLoaderPort` documents the runtime split (loader's pre-import resolveEnabled is coarse / bundle-level; the CLI's runtime composer drops per-extension disabled extensions before they reach the orchestrator). Closes the spec-vs-impl drift between the spec promise that "no extension is privileged, removable" and the prior implementation where built-ins were always-on. Pre-1.0 minor per `versioning.md` § Pre-1.0; additive on the manifest schema, breaking only for users who relied on the `claude` adapter loading without an explicit `config_plugins` row (none today, since the row had no effect on built-ins before).
1734
+ - Detector manifest gains optional `applicableKinds` filter (array, `minItems: 1`, kebab-case strings, `uniqueItems: true`). When declared, the kernel skips invocation for nodes whose `kind` is not in the list — fail-fast, before the detect context is built, so a probabilistic detector wastes zero LLM cost (and a deterministic detector zero CPU) on inapplicable nodes. Absent = applies to every kind (the default); no wildcard syntax. Empty array `[]` is rejected at load time. Unknown kinds (no installed Adapter declares them via `defaultRefreshAction`) load OK with a `sm plugins doctor` warning — the Provider may arrive later — and the doctor exit code is NOT promoted by the warning. `plugin-author-guide.md` adds a §Detector `applicableKinds` — narrow the pipeline section under Granularity with the per-shape behaviour table and a worked example; `architecture.md` adds a §Detector · `applicableKinds` filter subsection above trigger normalization; `schemas/extensions/detector.schema.json` declares the new property. Pre-1.0 minor per `versioning.md` § Pre-1.0; additive, non-breaking for existing detectors (they all behave as if `applicableKinds: undefined`).
1735
+
966
1736
  ### Changed
967
1737
 
968
1738
  - `cli-contract.md`: `--all` is no longer a global flag. It is valid only on verbs that explicitly document fan-out semantics: `sm job submit`, `sm job run`, `sm job cancel`, and `sm plugins enable/disable`.
1739
+ - `cli-contract.md`: `sm scan compare-with <dump> [roots...]` is now a sub-verb instead of a `--compare-with <path>` flag on `sm scan`. Read-only delta report against a saved `ScanResult` JSON dump. Same exit codes (`0` empty delta / `1` drift / `2` operational error). Old flag form removed. Pre-1.0 breaking change shipped as minor per `versioning.md` § Pre-1.0.
1740
+ - Plugin discovery — directory name MUST equal manifest id (else `invalid-manifest`); cross-root id collisions yield new `id-collision` status (sixth status, both collided plugins blocked, no precedence). `plugin-author-guide.md` Diagnostics table grows from five to six rows; `architecture.md` §`PluginLoaderPort` documents the two enforcement points; `schemas/plugins-registry.schema.json#/$defs/DiscoveredPlugin/status` adds `id-collision` to the enum. Pre-1.0 minor per `versioning.md` § Pre-1.0; breaking for any plugin whose directory name does not match its manifest id, but no real ecosystem affected today.
1741
+ - Plugin extensions are now identified by qualified ids `<plugin-id>/<extension-id>`. Built-in extensions adopt the `core/` namespace; the Claude adapter and its kind-aware detectors (frontmatter, slash, at-directive) live under `claude/`. The loader injects `pluginId` from `plugin.json#/id` into every extension at load time; an explicit `pluginId` field on an extension that disagrees with the manifest id is `invalid-manifest`. `architecture.md` §`PluginLoaderPort` documents the qualifier composition; `plugin-author-guide.md` adds a §Qualified extension ids section with the built-in mapping table; `schemas/extensions/base.schema.json` clarifies that extension `id` stays unqualified (single kebab-case segment, no `/`); `schemas/extensions/adapter.schema.json#/properties/defaultRefreshAction` now requires qualified action ids (pattern `^<plugin-id>/<action-id>$`). Pre-1.0 minor per `versioning.md` § Pre-1.0; breaking for any plugin or test that referenced an extension by short id.
1742
+ - `cli-contract.md`: exit-code `2` "Operational error" row clarified to mention runtime / environment mismatches (wrong Node version, missing native dependency) explicitly. The "unhandled exception" catch-all already covered the case; this just removes ambiguity for future implementers.
969
1743
  - `job-events.md`: the common `runId` envelope now explicitly documents the optional mode segment (`r-<mode>-YYYYMMDD-HHMMSS-XXXX`) used by external Skill claims, scan runs, and standalone issue recomputations.
970
1744
  - `versioning.md` and related prose: replace ambiguous milestone terminology with explicit versioned release language.
971
1745