@skill-map/spec 0.7.0 → 0.8.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 (49) hide show
  1. package/CHANGELOG.md +788 -26
  2. package/README.md +11 -13
  3. package/architecture.md +118 -35
  4. package/cli-contract.md +38 -25
  5. package/conformance/README.md +42 -13
  6. package/conformance/cases/kernel-empty-boot.json +2 -2
  7. package/conformance/coverage.md +19 -23
  8. package/db-schema.md +61 -6
  9. package/index.json +34 -76
  10. package/job-events.md +75 -1
  11. package/package.json +1 -1
  12. package/plugin-author-guide.md +409 -51
  13. package/schemas/conformance-case.schema.json +3 -3
  14. package/schemas/execution-record.schema.json +8 -8
  15. package/schemas/extensions/action.schema.json +2 -2
  16. package/schemas/extensions/base.schema.json +5 -5
  17. package/schemas/extensions/extractor.schema.json +48 -0
  18. package/schemas/extensions/formatter.schema.json +29 -0
  19. package/schemas/extensions/hook.schema.json +44 -0
  20. package/schemas/extensions/provider.schema.json +51 -0
  21. package/schemas/extensions/rule.schema.json +1 -1
  22. package/schemas/frontmatter/base.schema.json +2 -2
  23. package/schemas/link.schema.json +4 -4
  24. package/schemas/node.schema.json +4 -4
  25. package/schemas/plugins-registry.schema.json +19 -4
  26. package/schemas/project-config.schema.json +2 -2
  27. package/schemas/scan-result.schema.json +3 -3
  28. package/conformance/cases/basic-scan.json +0 -17
  29. package/conformance/cases/orphan-detection.json +0 -22
  30. package/conformance/cases/rename-high.json +0 -21
  31. package/conformance/fixtures/minimal-claude/agents/reviewer.md +0 -16
  32. package/conformance/fixtures/minimal-claude/commands/status.md +0 -17
  33. package/conformance/fixtures/minimal-claude/hooks/pre-commit.md +0 -13
  34. package/conformance/fixtures/minimal-claude/notes/architecture.md +0 -11
  35. package/conformance/fixtures/minimal-claude/skills/hello.md +0 -22
  36. package/conformance/fixtures/orphan-after/skills/keep.md +0 -13
  37. package/conformance/fixtures/orphan-before/skills/keep.md +0 -13
  38. package/conformance/fixtures/orphan-before/skills/lonely.md +0 -13
  39. package/conformance/fixtures/rename-high-after/skills/bar.md +0 -14
  40. package/conformance/fixtures/rename-high-before/skills/foo.md +0 -14
  41. package/schemas/extensions/adapter.schema.json +0 -40
  42. package/schemas/extensions/audit.schema.json +0 -47
  43. package/schemas/extensions/detector.schema.json +0 -41
  44. package/schemas/extensions/renderer.schema.json +0 -29
  45. package/schemas/frontmatter/agent.schema.json +0 -17
  46. package/schemas/frontmatter/command.schema.json +0 -39
  47. package/schemas/frontmatter/hook.schema.json +0 -29
  48. package/schemas/frontmatter/note.schema.json +0 -11
  49. package/schemas/frontmatter/skill.schema.json +0 -37
package/CHANGELOG.md CHANGED
@@ -1,5 +1,756 @@
1
1
  # Spec changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6dad772: v0.8.0 — Pre-1.0 stabilization pass.
8
+
9
+ This release combines two coherent pre-1.0 cleanup pieces that
10
+ both push the project closer to v1.0 stability: the cli-architect
11
+ audit review pass and the plugin model overhaul.
12
+
13
+ Pre-1.0 minor bumps per `versioning.md` § Pre-1.0; breaking
14
+ changes allowed within minor while in `0.Y.Z`. No real downstream
15
+ ecosystem exists yet, so the breaking surface costs nothing
16
+ today.
17
+
18
+ ## Part 1 — Pre-1.0 audit review pass
19
+
20
+ Pre-1.0 review pass — `cli-architect` audit findings.
21
+
22
+ Internal audit run by the `cli-architect` agent in REVIEW mode
23
+ produced a Critical / High / Medium / Low / Nit catalog. This
24
+ pass bundles the implementation of every actionable finding into
25
+ one unit so the review can be read end-to-end. **Pre-1.0 minor
26
+ bump**: a few breaking surface changes ride along (CLI sub-verb
27
+ split, exit-code enum exposed, plugin loader option). No
28
+ published downstream consumers exist yet.
29
+
30
+ ### Spec changes (`@skill-map/spec`)
31
+
32
+ - **`cli-contract.md`** — `sm scan compare-with <dump> [roots...]`
33
+ is now a sub-verb instead of a `--compare-with <path>` flag on
34
+ `sm scan`. Read-only delta report against a saved `ScanResult`
35
+ JSON dump. Read-only — does not modify the DB. Same exit codes
36
+ (`0` empty delta / `1` drift / `2` operational error). Old flag
37
+ form removed.
38
+ - **`cli-contract.md`** — exit-code `2` "Operational error" row
39
+ clarified to mention environment / runtime mismatches (wrong
40
+ Node version, missing native dependency) explicitly. The
41
+ "unhandled exception" catch-all already covered the case; this
42
+ just removes ambiguity for future implementers.
43
+ - **`cli-contract.md`** — new normative section **§Dry-run**
44
+ between §Exit codes and §Verb catalog defining the contract for
45
+ any verb exposing `-n` / `--dry-run`: no observable side effects
46
+ (DB / FS / config / network / spawns), no auto-provisioning of
47
+ scope directories, output mirrors the live mode with explicit
48
+ "would …" framing, exit codes mirror the live mode, dry-run
49
+ MUST short-circuit `--yes` / `--force` confirmation prompts.
50
+ Per-verb opt-in: the flag is not global, verbs that don't
51
+ declare it MUST reject it as an unknown option. Verb catalog
52
+ rows for `sm init`, `sm db reset` (default + `--state` +
53
+ `--hard`), and `sm db restore` amended to declare and describe
54
+ their `--dry-run` previews.
55
+
56
+ ### CLI changes (`@skill-map/cli`)
57
+
58
+ #### Critical — kernel & adapter hygiene
59
+
60
+ - **C1 — `runScanInternal` decomposed.** The 290-line monolith in
61
+ `kernel/orchestrator.ts` split into a thin composer + four pure
62
+ functions: `validateRoots`, `indexPriorSnapshot`,
63
+ `walkAndDetect`, `runRules`. Composer is now 89 lines reading
64
+ top-to-bottom through the pipeline phases. Zero behavioural
65
+ change.
66
+ - **C2 — `withSqlite(options, fn)` helper.** Single utility at
67
+ `cli/util/with-sqlite.ts` standardises the open / use / close
68
+ idiom every read-side command was open-coding. Eliminates four
69
+ classes of boilerplate bugs (forgotten close, `autoBackup`
70
+ drift, double-close, missing `try/finally`). Migrated 20 call
71
+ sites across `check`, `export`, `graph`, `history`, `init`,
72
+ `jobs`, `list`, `orphans`, `plugins`, `scan`, `show`, `watch`,
73
+ plus `cli/util/plugin-runtime.ts`. Companion `tryWithSqlite`
74
+ short-circuits when the DB file does not exist, replacing the
75
+ `if (existsSync) { withSqlite(...) }` chain. In `scan.ts` the
76
+ read-prior + persist double-open consolidated into a single
77
+ `withSqlite` callback that brackets read prior → run scan →
78
+ guard → persist when `willPersist`. Saves one migration
79
+ discovery pass + one WAL setup per normal scan (~50–100ms).
80
+
81
+ #### High — UX & contract integrity
82
+
83
+ - **H3 — `--dry-run` semantics unified across `init` / `db reset`
84
+ / `db restore`.** The new spec §Dry-run codifies the "no
85
+ writes, reads OK" contract; three verbs that did not previously
86
+ expose a preview now do:
87
+ - `sm init --dry-run` — previews the would-create lines for
88
+ `.skill-map/`, `settings.json`, `settings.local.json`,
89
+ `.skill-mapignore`, the `.gitignore` entries that would be
90
+ appended (deduped against the existing file), the DB
91
+ provisioning, and the first-scan trigger. Honours `--force`
92
+ for the would-overwrite preview. Re-init over an existing
93
+ scope without `--force` still exits 2 (same gate as live).
94
+ - `sm db reset --dry-run` (default + `--state`) — opens the DB
95
+ read-only, computes the row count per `scan_*` (and `state_*`
96
+ when `--state`) table, and prints them. No `DELETE`
97
+ statements issued. Bypasses the `--state` confirmation prompt
98
+ entirely.
99
+ - `sm db reset --hard --dry-run` — reports the DB file path and
100
+ size that would be unlinked; missing-file case prints a clear
101
+ no-op line instead of an error.
102
+ - `sm db restore <src> --dry-run` — validates the source exists
103
+ (still exits 5 if missing), reports the source size and
104
+ whether the target would be created or overwritten, plus the
105
+ WAL / SHM sidecars that would be dropped. Bypasses the
106
+ confirmation prompt.
107
+ Implementation: new helper `previewGitignoreEntries(scopeRoot,
108
+ entries)` in `init.ts` mirrors `ensureGitignoreEntries` parsing
109
+ so the preview tracks the live outcome exactly. Texts moved
110
+ into `cli/i18n/init.texts.ts` and `cli/i18n/db.texts.ts` per
111
+ the N4 pattern. **9 new tests** under `init-cli.test.ts` (5
112
+ cases) and `db-cli.test.ts` (9 cases) cover the previews + the
113
+ spec invariants ("DB file checksum unchanged after dry-run",
114
+ "scope directory absent after dry-run", "source-not-found
115
+ still exits 5", "confirmation prompt skipped under dry-run").
116
+ - **H1 — Centralised exit codes.** New `cli/util/exit-codes.ts`
117
+ exporting `ExitCode` (`Ok` / `Issues` / `Error` / `Duplicate` /
118
+ `NonceMismatch` / `NotFound`) and the type alias `TExitCode`.
119
+ Every `Command#execute()` migrated from numeric literals (123
120
+ sites across 17 files) to the enum. Single source of truth
121
+ aligned with `spec/cli-contract.md` §Exit codes. **Bug fix
122
+ surfaced en passant:** `sm job prune` returned `2` for "DB
123
+ missing" while every other read-side verb returned `5` via
124
+ `assertDbExists`; corrected to use the shared helper and return
125
+ `NotFound`. Companion test updated to expect `5`.
126
+ - **H2 — Plugin loader timeout.** `IPluginLoaderOptions.loadTimeoutMs`
127
+ (default `5000`, exported as `DEFAULT_PLUGIN_IMPORT_TIMEOUT_MS`).
128
+ Each dynamic `import()` now races against a timer; on timeout
129
+ the plugin is reported as `load-error` with a message naming
130
+ the elapsed budget and pointing at top-level side effects as
131
+ the likely cause (network call, infinite loop, large blocking
132
+ work). Without this a plugin with a hanging top-level `await`
133
+ blocks every host CLI command indefinitely.
134
+ - **H4 — `--strict` self-validates `--json` output.** When
135
+ `sm scan --strict --json` is invoked, the produced `ScanResult`
136
+ is validated against `scan-result.schema.json` before stdout.
137
+ Catches the case where a custom detector emits a Link that
138
+ passes the shallow `validateLink` guard but fails the full
139
+ schema, which would silently land in stdout and break a
140
+ downstream `sm scan compare-with -`.
141
+ - **H5 — External-link discrimination uses URL-shape regex.**
142
+ `isExternalUrlLink` was string-matching `http://` / `https://`
143
+ only; any other URL scheme (`mailto:`, `data:`, `file:///`,
144
+ `ftp://`) was silently classified as internal and polluted the
145
+ graph as a fake internal link with `byPath` lookups that always
146
+ missed. Replaced with the RFC 3986 scheme regex
147
+ (`/^[a-z][a-z0-9+\-.]+:/i`), guarding against Windows-style
148
+ absolute paths via the ≥ 2-char scheme constraint.
149
+ - **H6 — Prior snapshot validated under `--strict`.** Both
150
+ `sm scan` and `sm watch`, when run with `--strict`, validate
151
+ the DB-resident `ScanResult` against the spec schema before
152
+ handing it to the orchestrator. A DB corrupted manually or
153
+ mid-rollback used to slip nodes with malformed `bodyHash` /
154
+ `frontmatterHash` into the rename heuristic, where the
155
+ dereference would silently produce spurious matches.
156
+
157
+ #### Medium — surface & extensibility
158
+
159
+ - **M1 — `sm scan compare-with` sub-verb.** New
160
+ `ScanCompareCommand` in `cli/commands/scan-compare.ts`; the
161
+ `--compare-with` flag is removed from `ScanCommand`. The
162
+ sub-verb form structurally rejects flag combos that used to
163
+ require runtime guards (`--changed`, `--no-built-ins`,
164
+ `--allow-empty`, `--watch`): Clipanion rejects them at parse
165
+ time as unknown options.
166
+ - **M2 — `kernel/index.ts` enumerated exports.** Replaced the two
167
+ `export type *` wildcards (from `./types.js` and
168
+ `./ports/index.js`) with explicit named exports. Same set of
169
+ public types — the DTS size and tests confirm parity. Going
170
+ forward, any new domain type or port change requires an
171
+ explicit edit to the barrel, preventing silent surface drift.
172
+ - **M3 — Build hack documented (workaround retained).** Tried to
173
+ replace the post-build `restoreNodeSqliteImports` pass with
174
+ `external: ['node:sqlite']` in `tsup.config.ts`. Esbuild marks
175
+ the specifier as external but still strips the `node:` prefix;
176
+ same outcome with `[/^node:/]` regex and `packages: 'external'`
177
+ (which also externalises real npm deps). Reverted to the
178
+ post-build `replaceAll` pass, with a docstring documenting
179
+ every workaround attempted so the next agent does not repeat
180
+ the spike.
181
+ - **M4 — `tryWithSqlite` helper.** See C2.
182
+ - **M5 — `CamelCasePlugin` trap documented.** Added a
183
+ trap-warning block to `SqliteStorageAdapter`'s docstring:
184
+ `sql.raw` / `sql\`...\``template literals do NOT pass through
185
+ the`CamelCasePlugin`; raw SQL fragments must use snake_case to
186
+ match the migrations.
187
+ - **M6 — Per-extension error reporting.** When the orchestrator
188
+ drops a link emitted with an undeclared kind or an issue with
189
+ an invalid severity, it now emits a `type: 'extension.error'`
190
+ `ProgressEvent` instead of silently swallowing. The CLI
191
+ subscribes via the new `createCliProgressEmitter(stderr)`
192
+ helper and renders those events as `extension.error: <message>`
193
+ on stderr. Plugin authors finally see WHY their link / issue
194
+ disappears from the result. Wired in `scan` (normal +
195
+ compare-with), `watch`, and `init`.
196
+ - **M7 — Type naming convention documented (no rename).** Top-of-
197
+ file docstring in `kernel/types.ts` and a new section in
198
+ `AGENTS.md` describe the four-bucket convention the codebase
199
+ has always implicitly followed: domain types (no prefix,
200
+ mirrors spec schemas), hexagonal ports (`Port` suffix), runtime
201
+ extension contracts (`I` prefix), internal shapes (`I`
202
+ prefix). Mass rename was rejected after a cost-benefit pass —
203
+ naming changes are cheap to write but expensive to review;
204
+ existing names are mostly coherent. The agent base
205
+ (`_plugins/minions/shared/architect.md`) gained a "Naming
206
+ conventions check" sub-section in REVIEW mode so future audits
207
+ reach the same conclusion.
208
+
209
+ #### Low / nit — cleanup
210
+
211
+ - **L1 — `omitModule` JSON replacer precision.** Identifies the
212
+ ESM namespace by `[Symbol.toStringTag] === 'Module'` instead of
213
+ matching every `module` key blindly. A plugin manifest that
214
+ legitimately ships an unrelated `module` field (e.g. a string
215
+ property in `metadata`) is no longer silently dropped from
216
+ `sm plugins list --json` output.
217
+ - **L2 — Stub verbs flagged in `--help`.** Every
218
+ `not-yet-implemented` verb in `cli/commands/stubs.ts` carries a
219
+ `(planned)` suffix on its `description`, surfaced in
220
+ `sm --help`. The `notImplemented` helper now writes
221
+ `<verb>: not yet implemented (planned).` on stderr instead of
222
+ promising a specific Step number — roadmap step numbers shift
223
+ mid-flight, stale promises in `--help` are worse than no
224
+ promise.
225
+ - **L3 — Dead `eslint-disable` removed** from
226
+ `cli/util/plugin-runtime.ts`.
227
+ - **N1 — `Link.source` vs `Link.sources` doc clarified.** Both
228
+ fields now carry inline doc-comments calling out the singular /
229
+ plural naming trap. Spec-frozen, but the ambiguity is the
230
+ easiest way to misread the type for new contributors.
231
+ - **N2 — `sm check` Usage examples expanded.** The `-g/--global`
232
+ and `--db <path>` flags were declared but missing from the
233
+ `Usage.examples` block — asymmetry with `sm scan` and the rest
234
+ of the read-side verbs that ship the same flags. Two examples
235
+ added: `sm check --global` and `sm check --db
236
+ /path/to/skill-map.db`.
237
+ - **N4 — Error / hint strings extracted to `*.texts.ts` modules
238
+ with `{{name}}` template interpolation.** Pre-1.0 is the
239
+ natural moment to seed the pattern before the string set grows.
240
+ The workspace `ui/` already has a sibling layout at
241
+ `ui/src/i18n/` (functions returning template literals); CLI
242
+ takes a deliberately different shape — flat string templates
243
+ with `{{name}}` placeholders, interpolated by a tiny
244
+ `tx(template, vars)` helper. Rationale: the template form is
245
+ **drop-in compatible with Transloco / Mustache / Handlebars**
246
+ (the syntax they all share) so the day this project migrates to
247
+ a real i18n library, the strings move as-is. Functions would
248
+ have to be re-shaped first.
249
+
250
+ Helper at `kernel/util/tx.ts`. Contract:
251
+
252
+ - Every `{{name}}` token MUST have a matching key in the vars
253
+ object — missing key throws (silent fallback hides
254
+ forgotten args in production).
255
+ - `null` / `undefined` values throw — caller coerces
256
+ upstream.
257
+ - Whitespace inside the braces tolerated (`{{ name }}`) so
258
+ long templates wrap cleanly across `+`-joined lines.
259
+ - Plural / conditional logic does NOT live in the template;
260
+ the caller picks `*_singular` vs `*_plural` keys.
261
+
262
+ Files created:
263
+
264
+ - `kernel/util/tx.ts` — the helper itself, with 13 tests in
265
+ `test/tx.test.ts` (single / multi token, whitespace,
266
+ missing / null / undefined keys, identifier shapes, error
267
+ truncation).
268
+ - `kernel/i18n/orchestrator.texts.ts` — frontmatter
269
+ malformed/invalid templates, `extension.error` payloads,
270
+ root validation errors.
271
+ - `kernel/i18n/plugin-loader.texts.ts` — every `load-error` /
272
+ `invalid-manifest` / `incompatible-spec` reason, plus the
273
+ import timeout message.
274
+ - `cli/i18n/scan.texts.ts` — `sm scan` flag-clash / scan
275
+ failure / guard / summary templates, plus the `sm scan
276
+ compare-with` dump-load errors.
277
+ - `cli/i18n/watch.texts.ts` — `sm watch` lifecycle templates.
278
+ - `cli/i18n/init.texts.ts` — `sm init` templates including
279
+ the `--dry-run` previews and the singular/plural pair for
280
+ gitignore updates.
281
+ - `cli/i18n/db.texts.ts` — `sm db reset` / `sm db restore`
282
+ templates including their `--dry-run` previews.
283
+ - `cli/i18n/cli-progress-emitter.texts.ts` — the
284
+ `extension.error: ...` stderr line.
285
+
286
+ String content moved verbatim — every existing test that
287
+ matches on stderr / stdout content keeps passing. Trivial
288
+ single-token strings (`'No issues.\n'`) and rare per-handler
289
+ bespoke phrases stay inline; the pattern is now established
290
+ for whoever wants to migrate them in a follow-up.
291
+
292
+ Note on `ui/` divergence: today the two workspaces use
293
+ different shapes for their text tables (functions in `ui/`,
294
+ templates in `cli/`). Aligning them is a follow-up — the day a
295
+ real i18n library lands, both converge on its native shape.
296
+ The CLI shape is closer to the eventual destination.
297
+
298
+ - **N6 — `TIssueSeverity` aliased to `Severity`.** SQLite schema
299
+ type now reads `type TIssueSeverity = Severity` instead of
300
+ duplicating the union literal. Keeps DB and runtime in
301
+ lock-step if the union ever evolves.
302
+
303
+ ### Migrations consolidation (kernel DB)
304
+
305
+ - **`src/migrations/001_initial.sql` + `002_scan_meta.sql`**
306
+ consolidated into a single `001_initial.sql`. Pre-1.0 with no
307
+ released DBs to forward-migrate, the two-file split was a
308
+ historical accident from an incremental shipment. After
309
+ consolidation: same 12 tables, same constraints, same indexes;
310
+ `PRAGMA user_version` of a freshly-initialised DB is now `1`
311
+ instead of `2`. Migration runner is unchanged (it tolerates any
312
+ count of `NNN_*.sql` files).
313
+
314
+ ### Test coverage (Part 1)
315
+
316
+ - New tests for H2 (plugin loader timeout — 2 cases),
317
+ M6 (orchestrator `extension.error` emission — 3 cases),
318
+ CLI progress emitter wiring (4 cases). The compare-with suite
319
+ (`scan-compare.test.ts`, 9 cases) was migrated to
320
+ `ScanCompareCommand` and the three flag-clash tests dropped
321
+ (the flags are now structurally absent on the sub-verb). Test
322
+ totals: 479 (start of pass) → 488 (after H2/M6 tests) → 485
323
+ (after the three flag-clash deletions).
324
+
325
+ ### Deferred / out of scope
326
+
327
+ The findings below were reviewed but did not warrant code
328
+ changes; each has its own resolution noted alongside.
329
+
330
+ - **L4 — `runScan` / `runScanWithRenames` unification.** Already
331
+ resolved by C1 (both are thin wrappers around
332
+ `runScanInternal`).
333
+ - **L5 — Node-version-guard exit code.** Reviewed against the
334
+ updated exit-code table; existing `2` is correct under
335
+ "operational error / unhandled exception". Spec table got the
336
+ environment-mismatch clarification (above).
337
+ - **L6 — `loadSchemaValidators()` cache.** Already cached at
338
+ module level since Step 5.12.
339
+ - **L7 — `pkg with { type: 'json' }` portability.** Stable in
340
+ Node ≥ 22; `engines.node": ">=24.0"` covers it. No fallback
341
+ needed.
342
+ - **N3 — `compare-with` "dump not found" exit code.** The error
343
+ paths in `ScanCompareCommand` already use the `ExitCode.Error`
344
+ enum (= 2) for dump load failures, matching the spec clause for
345
+ operational errors.
346
+ - **N5 — Exit-code list completeness.** Verified the comment in
347
+ `cli/entry.ts` against `spec/cli-contract.md` §Exit codes —
348
+ identical, no edit needed.
349
+
350
+ ## Part 2 — Plugin model overhaul (5-phase implementation)
351
+
352
+ ### Summary
353
+
354
+ The plugin model received a comprehensive overhaul before
355
+ stabilizing at v1.0. Plugin kinds total after this bump: **6**
356
+ (Provider, Extractor, Rule, Action, Formatter, Hook). All
357
+ breakings are pre-1.0 minor per `versioning.md` § Pre-1.0.
358
+
359
+ ### Phase 1 (commit 7354c26) — Foundation
360
+
361
+ Five sub-phases, additive or pre-1.0 minor breakings:
362
+
363
+ - **A.4** — three-tier frontmatter validation model documented in
364
+ `plugin-author-guide.md` (default permissive + `unknown-field`
365
+ rule + `scan.strict` promote-to-error). Behavior unchanged.
366
+ - **A.5** — plugin id global uniqueness: `directory ==
367
+ manifest.id` rule, new status `id-collision` (sixth),
368
+ validation in boot/scan/doctor. Cross-root collisions block
369
+ both involved plugins; user resolves by renaming.
370
+ - **A.6** — extension ids qualified `<plugin-id>/<ext-id>` in
371
+ registry. Built-ins classified into `claude/*` (4 Claude-
372
+ specific) and `core/*` (7 kernel built-ins) bundles. New
373
+ `Registry.get/find` APIs; `defaultRefreshAction` schema
374
+ requires the qualified pattern; `extension.error` events emit
375
+ qualified ids.
376
+ - **A.10** — optional `applicableKinds` filter on Detector
377
+ manifest; fail-fast skip for non-matching kinds (zero CPU/LLM
378
+ cost); doctor warning for kinds not declared by any installed
379
+ Provider. Empty array invalid; absence preserves apply-to-all
380
+ default.
381
+ - **Granularity** — Built-ins now respect `config_plugins`
382
+ enable/disable via granularity-aware filtering. New
383
+ `IBuiltInBundle` shape with `granularity: 'bundle' |
384
+ 'extension'`; `claude` ships as bundle (all-or-nothing), `core`
385
+ as extension (each toggleable). User plugins default to bundle;
386
+ opt in via `granularity` in `plugin.json`. Both plugin ids and
387
+ qualified extension ids accepted as keys in `config_plugins`
388
+ and `settings.json#/plugins` (no schema change needed).
389
+
390
+ 550/550 tests pass (+33 vs baseline 517).
391
+
392
+ ### Phase 2 (commit ae3eaa6) — Renames
393
+
394
+ Four sub-phases, all breaking but allowed in minor pre-1.0:
395
+
396
+ - **2a (Renderer → Formatter)** — Kind, types, files renamed.
397
+ Method `render(ctx)` → `format(ctx)`; manifest field `format`
398
+ → `formatId` (TS clash resolution). Same contract: graph →
399
+ string, deterministic-only.
400
+ - **2b (Adapter → Provider)** — New required field
401
+ `explorationDir` on the manifest (e.g. `~/.claude` for the
402
+ Claude Provider). DB schema migrated in-place (column
403
+ `nodes.adapter` → `nodes.provider`, etc.). The
404
+ hexagonal-architecture `RunnerPort.adapter` /
405
+ `StoragePort.adapter` is unchanged.
406
+ - **2c (Audit removed)** — Audit kind removed. The single
407
+ built-in `validate-all` migrated to a Rule (qualified id
408
+ `core/validate-all`, `evaluate(ctx) → Issue[]`). CLI verbs
409
+ `sm audit *` removed; users invoke via `sm check --rules
410
+ core/validate-all`.
411
+ - **2d (Detector → Extractor)** — Method signature changes from
412
+ `detect(ctx) → Link[]` to `extract(ctx) → void` — output flows
413
+ through three ctx callbacks: `emitLink`, `enrichNode`, `store`.
414
+ Built-ins migrated maintain functional parity using `emitLink`.
415
+ Persistence of `enrichNode` deferred to Phase 4 (A.8 stale
416
+ layer); orchestrator buffers in memory today.
417
+
418
+ 554/554 cli + 32/32 testkit pass.
419
+
420
+ ### Phase 3 (commit 34f993e) — Schema relocation
421
+
422
+ **A.2** — Per-kind frontmatter schemas relocate from spec to the
423
+ Provider that declares them. Spec keeps only `frontmatter/base`
424
+ (universal).
425
+
426
+ - 5 schemas moved (`git mv`):
427
+ `spec/schemas/frontmatter/{skill,agent,command,hook,note}.schema.json`
428
+ → built-in Claude Provider's `schemas/` directory. New `$id`:
429
+ `https://skill-map.dev/providers/claude/v1/frontmatter/<kind>`.
430
+ Cross-package `$ref` resolves via the spec base's `$id`
431
+ (`https://skill-map.dev/spec/v0/frontmatter/base.schema.json`);
432
+ AJV resolves by `$id` when both schemas register on the same
433
+ instance.
434
+ - Provider manifest gains a required `kinds` map subsuming three
435
+ former fields: `emits` (now derives from
436
+ `Object.keys(kinds)`), the flat `defaultRefreshAction` map (now
437
+ per-entry inside `kinds[<kind>].defaultRefreshAction`), and the
438
+ new `schema` (path to the per-kind schema relative to the
439
+ provider directory).
440
+ - Built-in Claude Provider migrated: 5 kind entries (skill,
441
+ agent, command, hook, note), each with `schema`, `schemaJson`
442
+ (runtime field, AJV-compiled at load), and qualified
443
+ `defaultRefreshAction` (`claude/summarize-<kind>`).
444
+ - Kernel orchestrator parse phase asks the Provider for the
445
+ schema via `IProviderFrontmatterValidator` (composed by scan
446
+ via `buildProviderFrontmatterValidator`) instead of reading
447
+ from spec/. Flow: validate base → look up provider → validate
448
+ per-kind schema from Provider.
449
+ - `schema-validators.ts` catalog loses the 5 per-kind frontmatter
450
+ entries; only `frontmatter-base` remains kernel-known.
451
+ `plugin-loader`'s `stripFunctionsAndPluginId` now also strips
452
+ `schemaJson` (runtime-only) from each `kinds` entry before
453
+ AJV-validating the manifest.
454
+ - Coverage matrix: 28 → 23 schemas (the 5 per-kind frontmatter
455
+ schemas are now Provider-owned and ship with their own
456
+ conformance suite in Phase 5 / A.13).
457
+
458
+ 556/556 cli + 32/32 testkit pass.
459
+
460
+ ### Phase 4 (commit e62695f) — Probabilistic infra
461
+
462
+ Five sub-phases, all breaking but allowed in minor pre-1.0:
463
+
464
+ - **4a (A.9)** — fine-grained Extractor cache via new
465
+ `scan_extractor_runs` table. Resolves gap where newly
466
+ registered Extractors silently skipped cached nodes; cache hit
467
+ logic now per-(node, extractor). Uninstalled Extractors cleaned
468
+ (rows + orphan links). Migration in-place.
469
+ - **4b (A.12)** — opt-in `outputSchema` for plugin custom
470
+ storage. Manifest gains `storage.schema` (Mode A) and
471
+ `storage.schemas` (Mode B) for AJV validation of
472
+ `ctx.store.write/.set` calls. Throws on shape violation;
473
+ default absent = permissive.
474
+ - **4c (A.8)** — enrichment layer + stale tracking. New
475
+ `node_enrichments` table persists per-(node, extractor)
476
+ partials separately from author's frontmatter (immutable).
477
+ Probabilistic enrichments track `body_hash_at_enrichment`; scan
478
+ flags `stale=1` on body change (NOT deleted, preserves LLM
479
+ cost). Helper `mergeNodeWithEnrichments` filters stale +
480
+ last-write-wins. New verbs `sm refresh <node>` and
481
+ `sm refresh --stale` (stubs awaiting Step 10).
482
+ - **4d (A.11)** — sixth plugin kind `hook`. Declarative
483
+ subscriber to a curated set of 8 lifecycle events (`scan.*`,
484
+ extractor/rule/action.completed,
485
+ job.spawning/completed/failed). Other events deliberately not
486
+ hookable. Manifest declares `triggers[]` (load-time validated)
487
+ and optional `filter`. Three new kernel events added to
488
+ catalog. Dual-mode (det dispatched in-process; prob deferred to
489
+ Step 10).
490
+ - **4e (A.7)** — `sm check --include-prob` opt-in flag (stub).
491
+ Default `sm check` unchanged: det only, CI-safe. With flag:
492
+ detects prob rules, emits stderr advisory; full dispatch awaits
493
+ Step 10. Combines with `--rules`, `-n`, `--no-plugins`.
494
+
495
+ 591/591 cli + 32/32 testkit pass.
496
+
497
+ ### Phase 5 (commit 03b5a65) — Conformance + cleanup
498
+
499
+ **A.13** — Conformance fixture relocation:
500
+
501
+ - 3 cases moved (`git mv`): `basic-scan`, `orphan-detection`,
502
+ `rename-high` →
503
+ `src/extensions/providers/claude/conformance/cases/`. 11
504
+ fixture files (`minimal-claude/`, `orphan-{before,after}/`,
505
+ `rename-high-{before,after}/`) moved alongside.
506
+ - New `coverage.md` per-Provider listing the 5 frontmatter
507
+ schemas (skill, agent, command, hook, note) and their cases.
508
+ - New verb `sm conformance run [--scope spec|provider:<id>|all]`.
509
+ Discovery by convention at `<plugin-dir>/conformance/`. The
510
+ existing runner gains optional `fixturesRoot` (default
511
+ `<specRoot>/conformance/fixtures` for compat); tooling using
512
+ the public API of `@skill-map/cli/conformance` keeps working.
513
+ `--json` deferred — reporter shape not yet frozen.
514
+ - Spec keeps only the kernel-agnostic case (`kernel-empty-boot`)
515
+ and the universal preamble fixture. Coverage matrix downgrades
516
+ conservatively (rows that depended on `basic-scan` are now
517
+ partial or missing, with cross-link to the Provider's matrix).
518
+
519
+ ROADMAP cleanup:
520
+
521
+ - The three "Status: target state for v0.8.0 — spec catch-up
522
+ pending" banners on §Plugin system / §Frontmatter standard /
523
+ §Enrichment are removed; prose shifts from future to present
524
+ ("kinds from v0.7.0 are renamed" → "were renamed in spec
525
+ 0.8.0"; Model B enrichment now describes the shipped
526
+ `node_enrichments` table with `body_hash_at_enrichment` rather
527
+ than "table or column set decided in PR").
528
+ - Decision-log entry for the working session rewritten to
529
+ reflect "shipped" rather than "pending".
530
+ - Last-updated header gains an "implementation" paragraph
531
+ listing the four prior phase commits.
532
+
533
+ 593/593 cli + 32/32 testkit pass (+2 vs Phase 4 baseline).
534
+ spec:check green (40 files hashed — down from 53 because the
535
+ Claude-specific cases and fixtures left the spec's hash set).
536
+
537
+ ### Breaking changes for plugin authors (Part 2)
538
+
539
+ Manifest renames:
540
+
541
+ - `kind: 'adapter'` → `kind: 'provider'`
542
+ - `kind: 'detector'` → `kind: 'extractor'`
543
+ - `kind: 'renderer'` → `kind: 'formatter'`
544
+ - `kind: 'audit'` removed (migrate to `kind: 'rule'`).
545
+
546
+ Method signatures:
547
+
548
+ - Detector `detect(ctx) → Link[]` → Extractor `extract(ctx) →
549
+ void` (output via `ctx.emitLink` / `ctx.enrichNode` /
550
+ `ctx.store`).
551
+ - Renderer `render(ctx) → string` → Formatter `format(ctx) →
552
+ string`.
553
+
554
+ Manifest fields:
555
+
556
+ - Provider gains required `explorationDir`.
557
+ - Provider's flat `defaultRefreshAction` map replaced by per-kind
558
+ entries inside `kinds[<kind>].defaultRefreshAction` (must
559
+ follow qualified pattern `<plugin-id>/<ext-id>`).
560
+ - Provider's `emits` derives from `Object.keys(kinds)` (the
561
+ manifest field is gone).
562
+ - Provider's per-kind schemas declared via `kinds[<kind>].schema`
563
+ (path relative to provider dir).
564
+ - Renderer's `format` field renamed to `formatId` on the
565
+ Formatter manifest (TS clash resolution).
566
+ - New plugin kind `hook` with `triggers[]` + optional `filter`.
567
+ - Optional `outputSchema` (`storage.schema` / `storage.schemas`)
568
+ for Mode A / Mode B plugin custom storage.
569
+ - Optional `applicableKinds` filter on Extractor manifest.
570
+
571
+ Extension ids:
572
+
573
+ - All extension ids must be qualified
574
+ `<plugin-id>/<extension-id>` (built-ins classified into
575
+ `claude/*` and `core/*`).
576
+
577
+ DB schema:
578
+
579
+ - Two new tables added in-place to `001_initial.sql` (pre-1.0
580
+ consolidation, no production DBs to migrate):
581
+ `scan_extractor_runs` and `node_enrichments`.
582
+ - Column rename `nodes.adapter` → `nodes.provider` (and parallel
583
+ in `result.adapters` → `result.providers`).
584
+
585
+ ## Test stats
586
+
587
+ 593/593 cli + 32/32 testkit pass (post-Phase 5).
588
+ Two new DB tables (`scan_extractor_runs`, `node_enrichments`)
589
+ added in-place to `001_initial.sql` (pre-1.0 consolidation, no
590
+ production DBs to migrate). The 5 per-kind frontmatter schemas
591
+ relocated from spec/ to the Claude Provider package.
592
+
593
+ ## [Unreleased]
594
+
595
+ ### Minor Changes
596
+
597
+ - 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).
598
+
599
+ - `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.
600
+
601
+ - 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.
602
+
603
+ - 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.
604
+
605
+ - 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.
606
+
607
+ - 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.
608
+
609
+ - 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.
610
+
611
+ - 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.
612
+
613
+ - Plugin kind `'detector'` renamed to `'extractor'`. Method signature
614
+ changes from `detect(ctx) → Link[]` to `extract(ctx) → void` — output
615
+ flows through three new ctx callbacks: `emitLink(link)` (kernel `links`
616
+ table), `enrichNode(partial)` (kernel enrichment layer, persisted into
617
+ `node_enrichments` per A.8), and the existing `ctx.store` (plugin's
618
+ own table). The Extractor absorbs what would have been a separate
619
+ `Enricher` kind via `enrichNode`. Built-ins migrated:
620
+ `claude/frontmatter`, `claude/slash`, `claude/at-directive`,
621
+ `core/external-url-counter` — all use `emitLink` to maintain
622
+ functional parity with their Detector ancestors. Schema files
623
+ renamed: `schemas/extensions/detector.schema.json` →
624
+ `schemas/extensions/extractor.schema.json`. Persisted DB rows are
625
+ unaffected (link `sources` carry extractor ids verbatim — the field
626
+ was always free-form). Pre-1.0 minor per `versioning.md` § Pre-1.0;
627
+ breaking for any plugin or test referencing `'detector'` as the
628
+ kind, `IDetector`, or the old `Link[]` return signature — no real
629
+ ecosystem affected today.
630
+
631
+ - Plugin kind `'audit'` removed. The single built-in `'validate-all'`
632
+ migrated to a Rule (qualified id `'core/validate-all'`, method
633
+ `evaluate(ctx) → Issue[]`). The kind had dual personality (composer +
634
+ standalone reporter); the standalone reporter case is naturally a Rule,
635
+ and the composer case is deferred to post-1.0 if a real use case
636
+ appears. CLI verbs `'sm audit run'` and `'sm audit show'` removed;
637
+ users invoke the rule via `sm check --rules core/validate-all`.
638
+ `state_executions.kind` enum narrowed to `['action']` (audit was the
639
+ only other value); the column is preserved as a forward-compatibility
640
+ lever. Schema files removed: `schemas/extensions/audit.schema.json`.
641
+ Coverage matrix shrinks from 29 to 28 rows. Pre-1.0 minor per
642
+ `versioning.md` § Pre-1.0; breaking for any plugin or test referencing
643
+ the audit kind, `IAudit`, `TAuditReport`, or `sm audit` verbs — no
644
+ real ecosystem affected today.
645
+
646
+ - Plugin kind `'adapter'` renamed to `'provider'`. Manifest gains required
647
+ field `'explorationDir'` (filesystem directory where the Provider's
648
+ content lives, e.g. `'~/.claude'` for the Claude Provider). Built-in
649
+ `claudeAdapter` renamed to `claudeProvider`. The hexagonal-architecture
650
+ `'adapter'` (`RunnerPort.adapter`, `StoragePort.adapter`,
651
+ `FilesystemPort.adapter`, `PluginLoaderPort.adapter`) is unchanged —
652
+ distinct concept, distinct namespace.
653
+ Persisted schema fields renamed: `node.adapter` → `node.provider`,
654
+ `scan-result.adapters` → `scan-result.providers` (pre-1.0 minor — no
655
+ production DBs to migrate; `001_initial.sql` was edited in place per
656
+ the consolidation precedent already established for pre-1.0).
657
+ Project config field renamed: `project-config.adapters` →
658
+ `project-config.providers`. Schema files renamed:
659
+ `schemas/extensions/adapter.schema.json` →
660
+ `schemas/extensions/provider.schema.json`. Pre-1.0 minor per
661
+ `versioning.md` § Pre-1.0; breaking for any plugin or test referencing
662
+ `'adapter'` as the kind, `IAdapter`, or any persisted/config schema
663
+ field renamed above — no real ecosystem affected today.
664
+
665
+ ## 0.7.1
666
+
667
+ ### Patch Changes
668
+
669
+ - 0463a0f: Step 9.4 — plugin author guide + reference plugin + diagnostics polish.
670
+ **Step 9 fully closed** with this changeset.
671
+
672
+ ### Spec — plugin author guide (additive prose)
673
+
674
+ New document at `spec/plugin-author-guide.md` covering:
675
+
676
+ - Discovery roots (`<project>/.skill-map/plugins/`,
677
+ `~/.skill-map/plugins/`, `--plugin-dir <path>`).
678
+ - Manifest fields with the normative schema reference.
679
+ - `specCompat` strategy — narrow ranges pre-`v1.0.0`, `^1.0.0`
680
+ recommendation post-`v1.0.0`.
681
+ - The six extension kinds with one minimal worked example each
682
+ (detector, rule, renderer in full; adapter / audit / action flagged
683
+ for later expansion alongside Step 10).
684
+ - Storage choice (KV vs Dedicated) cross-linking `plugin-kv-api.md`
685
+ and the Step 9.2 triple-protection rule.
686
+ - Execution modes (deterministic / probabilistic) cross-linking
687
+ `architecture.md`.
688
+ - Testkit usage with `runDetectorOnFixture`, `runRuleOnGraph`,
689
+ `runRendererOnGraph`, `makeFakeRunner`.
690
+ - The five plugin statuses (`loaded` / `disabled` / `incompatible-spec`
691
+ / `invalid-manifest` / `load-error`) and how to read them.
692
+ - Stability section (document is stable; widening additions are minor
693
+ bumps; breaking edits are major).
694
+
695
+ `spec/package.json#files` updated to ship the new doc; `spec/index.json`
696
+ regenerated (57 → 58 hashed files). `coverage.md` unchanged because the
697
+ guide is prose, not a schema.
698
+
699
+ ### Reference plugin — `examples/hello-world/`
700
+
701
+ Smallest viable plugin in the principal repo (Arquitecto's pick: in
702
+ the main repo, not separate). One detector (`hello-world-greet`)
703
+ emitting `references` links per `@greet:<name>` token in node bodies.
704
+ Includes:
705
+
706
+ - `plugin.json` declaring one extension and pinning `specCompat: ^1.0.0`.
707
+ - `extensions/greet-detector.mjs` — runtime instance with both
708
+ manifest fields and the `detect` method.
709
+ - `README.md` — what it does, file layout, three-step "try it
710
+ locally" recipe, what's intentionally missing (storage,
711
+ multi-extension, probabilistic mode), pointers for production-grade
712
+ patterns.
713
+ - `test/greet-detector.test.mjs` — four-assertion test using
714
+ `@skill-map/testkit`, runnable via `node --test` with no build step.
715
+
716
+ Verified end-to-end: the example plugin loads cleanly under
717
+ `sm plugins list`, scans contribute its links to the persisted graph,
718
+ and the testkit-based test passes. The example is **not** registered
719
+ as a workspace — it's intentionally standalone so users can copy it.
720
+
721
+ ### CLI — diagnostics polish on `PluginLoader.reason`
722
+
723
+ Each failure-mode reason string now carries an actionable hint:
724
+
725
+ - `invalid-manifest` (JSON parse): names the manifest path, suggests
726
+ validating the JSON.
727
+ - `invalid-manifest` (AJV): names the manifest path AND points at
728
+ `spec/schemas/plugins-registry.schema.json#/$defs/PluginManifest`.
729
+ - `invalid-manifest` (specCompat not a valid range): suggests a range
730
+ shape (`"^1.0.0"`).
731
+ - `incompatible-spec`: suggests two remediations (update the plugin's
732
+ `specCompat`, or pin sm to a compatible spec version).
733
+ - `load-error` (extension file not found): includes the absolute
734
+ resolved path, pointer to `plugin.json#/extensions`.
735
+ - `load-error` (default export missing kind): lists the valid kinds.
736
+ - `load-error` (unknown kind): lists the valid kinds.
737
+ - `load-error` (extension manifest schema fails): names the
738
+ per-kind schema (`spec/schemas/extensions/<kind>.schema.json`).
739
+
740
+ 6 new tests under `test/plugin-loader.test.ts` (`Step 9.4 diagnostics
741
+ polish` describe block) assert each hint shape is present without
742
+ pinning the full text. Test count 437 → **443 cli + 30 testkit = 473**.
743
+
744
+ ### Step 9 closed
745
+
746
+ The four sub-steps — 9.1 (plugin runtime wiring), 9.2 (plugin
747
+ migrations + triple protection), 9.3 (`@skill-map/testkit` workspace),
748
+ 9.4 (author guide + reference plugin + diagnostics polish) — together
749
+ turn `skill-map` plugins from "discovered but inert" into a
750
+ first-class authoring surface with documentation, tests, and a
751
+ working reference. Next step: **Step 10 — job subsystem + first
752
+ probabilistic extension** (wave 2 begins).
753
+
3
754
  ## 0.7.0
4
755
 
5
756
  ### Minor Changes
@@ -165,34 +916,34 @@ job prune` + retention enforcement.
165
916
 
166
917
  Two pieces:
167
918
 
168
- 1. **New built-in rule `link-conflict`** (`src/extensions/rules/link-conflict/`).
169
- Surfaces detector disagreement. Groups links by `(source, target)` and
170
- emits one `warn` Issue per pair where the set of distinct `kind` values
171
- has size ≥ 2. Agreement (single kind across multiple detectors) is
172
- silent — by design, to avoid massive noise on real graphs.
173
- Issue payload (`data`) carries `{ source, target, variants }` where
174
- each `variant` is `{ kind, sources: detectorId[], confidence }`. Variant
175
- sources are deduped + sorted; confidence is the highest across rows
176
- of the same kind (`high` > `medium` > `low`).
177
-
178
- This is the kernel piece of Decision #90 read-time "consumers that
179
- need uniqueness aggregate at read time" — the rule is one such
180
- consumer, on the alarming side. Storage stays untouched (one row
181
- per detector, no merge, no dedup). Severity is `warn`, not `error`:
182
- the rule cannot pick which kind is correct, so per `cli-contract.md`
183
- §Exit codes the verb stays exit 0.
184
-
185
- 2. **`sm show` pretty link aggregation** (`src/cli/commands/show.ts`).
186
- The human renderer now groups `linksOut` / `linksIn` by `(endpoint,
919
+ 1. **New built-in rule `link-conflict`** (`src/extensions/rules/link-conflict/`).
920
+ Surfaces detector disagreement. Groups links by `(source, target)` and
921
+ emits one `warn` Issue per pair where the set of distinct `kind` values
922
+ has size ≥ 2. Agreement (single kind across multiple detectors) is
923
+ silent — by design, to avoid massive noise on real graphs.
924
+ Issue payload (`data`) carries `{ source, target, variants }` where
925
+ each `variant` is `{ kind, sources: detectorId[], confidence }`. Variant
926
+ sources are deduped + sorted; confidence is the highest across rows
927
+ of the same kind (`high` > `medium` > `low`).
928
+
929
+ This is the kernel piece of Decision #90 read-time "consumers that
930
+ need uniqueness aggregate at read time" — the rule is one such
931
+ consumer, on the alarming side. Storage stays untouched (one row
932
+ per detector, no merge, no dedup). Severity is `warn`, not `error`:
933
+ the rule cannot pick which kind is correct, so per `cli-contract.md`
934
+ §Exit codes the verb stays exit 0.
935
+
936
+ 2. **`sm show` pretty link aggregation** (`src/cli/commands/show.ts`).
937
+ The human renderer now groups `linksOut` / `linksIn` by `(endpoint,
187
938
  kind, normalizedTrigger)` and prints one row per group with the
188
- union of detector ids in a `sources:` field. The section header
189
- reports both the raw row count and the unique-after-grouping count
190
- (`Links out (12, 9 unique)`). When N > 1 detector emits the same
191
- logical link, the row also gets a `(×N)` suffix.
939
+ union of detector ids in a `sources:` field. The section header
940
+ reports both the raw row count and the unique-after-grouping count
941
+ (`Links out (12, 9 unique)`). When N > 1 detector emits the same
942
+ logical link, the row also gets a `(×N)` suffix.
192
943
 
193
- `--json` output is byte-identical to before — raw rows, no merge.
194
- Storage is byte-identical to before. The grouping is purely a
195
- read-time presentation choice for human eyes.
944
+ `--json` output is byte-identical to before — raw rows, no merge.
945
+ Storage is byte-identical to before. The grouping is purely a
946
+ read-time presentation choice for human eyes.
196
947
 
197
948
  **Spec changes (patch)**:
198
949
 
@@ -874,9 +1625,20 @@ Tag convention: `spec-vX.Y.Z` (distinct from CLI tags `cli-vX.Y.Z`).
874
1625
 
875
1626
  Initial public spec bootstrap (Step 0a phases 1–3).
876
1627
 
1628
+ ### Added
1629
+
1630
+ - `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).
1631
+ - `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.
1632
+ - `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).
1633
+ - 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`).
1634
+
877
1635
  ### Changed
878
1636
 
879
1637
  - `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`.
1638
+ - `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.
1639
+ - 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.
1640
+ - 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.
1641
+ - `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.
880
1642
  - `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.
881
1643
  - `versioning.md` and related prose: replace ambiguous milestone terminology with explicit versioned release language.
882
1644