@skill-map/spec 0.26.0 → 0.28.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 (28) hide show
  1. package/CHANGELOG.md +334 -0
  2. package/architecture.md +29 -30
  3. package/cli-contract.md +55 -20
  4. package/conformance/README.md +4 -0
  5. package/conformance/cases/no-global-scope.json +13 -0
  6. package/conformance/cases/sidecar-end-to-end.json +4 -4
  7. package/conformance/coverage.md +7 -4
  8. package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/kind.json +3 -0
  9. package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/schema.json +6 -0
  10. package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json +3 -2
  11. package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/providers/bad-provider/index.js +17 -0
  12. package/db-schema.md +7 -7
  13. package/index.json +26 -21
  14. package/package.json +1 -1
  15. package/plugin-author-guide.md +94 -47
  16. package/schemas/extensions/action.schema.json +28 -44
  17. package/schemas/extensions/analyzer.schema.json +34 -32
  18. package/schemas/extensions/base.schema.json +26 -55
  19. package/schemas/extensions/extractor.schema.json +35 -22
  20. package/schemas/extensions/formatter.schema.json +4 -14
  21. package/schemas/extensions/hook.schema.json +2 -9
  22. package/schemas/extensions/provider-kind.schema.json +71 -0
  23. package/schemas/extensions/provider.schema.json +3 -90
  24. package/schemas/plugins-registry.schema.json +11 -27
  25. package/schemas/project-config.schema.json +0 -11
  26. package/schemas/scan-result.schema.json +1 -6
  27. package/schemas/user-settings.schema.json +39 -0
  28. package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/provider.js +0 -30
package/CHANGELOG.md CHANGED
@@ -1,5 +1,339 @@
1
1
  # Spec changelog
2
2
 
3
+ ## 0.28.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e21216e: Simplify plugin manifest fields beyond the file-layout refactor. The
8
+ previous `structure-as-truth-plugins` changeset moved bundle / kind /
9
+ id discovery onto the filesystem; this one extends the same principle
10
+ into the manifest schemas themselves so the only fields that survive
11
+ are the ones the kernel cannot derive from disk.
12
+
13
+ **Plugin manifest (`plugin.json`):**
14
+
15
+ - Drop `id` (the directory name is the id; AJV rejects manifests that
16
+ declare it).
17
+ - `description` and `catalogCompat` are now required (were optional).
18
+ - `granularity` is now optional with a default of `'extension'` (was
19
+ required). Most plugins drop the field entirely.
20
+ - Drop `settings` at the plugin level; settings move to the extension
21
+ manifests that actually consume them.
22
+
23
+ **Extension base (every kind):**
24
+
25
+ - Drop `id`, `kind`, `stability`, `preconditions` (free-form). The
26
+ loader injects `id` / `kind` / `pluginId` from the folder layout;
27
+ the other two were display-only and free-form respectively, and
28
+ the kernel never consumed them.
29
+ - `description` is now required.
30
+ - Rename `annotationContributions` (map) to `annotation` (singular):
31
+ one extension contributes at most one annotation key, and the key
32
+ is the extension's folder name. Use multiple extensions to
33
+ contribute multiple keys.
34
+ - Rename `viewContributions` to `ui` on the manifest. The
35
+ runtime-aggregated catalog (`Kernel.getRegisteredViewContributions()`,
36
+ `IPluginRuntimeBundle.viewContributions`) keeps its name.
37
+ - Add `settings: Record<id, ISettingDeclaration>` (moved from the
38
+ plugin manifest).
39
+
40
+ **Provider:**
41
+
42
+ - Drop the inline `kinds` map. The kind catalog now lives under
43
+ `<plugin>/kinds/<kindName>/` with two files per kind:
44
+ `schema.json` (frontmatter schema) and `kind.json` carrying the
45
+ `{ ui }` block. The loader walks the directory and projects each
46
+ entry into the runtime `kinds` descriptor.
47
+ - New schema `extensions/provider-kind.schema.json` validates the
48
+ `kind.json` shape.
49
+ - Drop `defaultRefreshAction`. The UI's `🧠 prob` refresh button is
50
+ retired; a replacement UX is TBD.
51
+ - `roots` is enforcement-grade: a Provider with declared `roots`
52
+ only sees files matching at least one glob; a Provider without
53
+ `roots` acts as the fallback for files unmatched by any other
54
+ Provider. Supported patterns: `prefix/**` (deep), `prefix/*`
55
+ (shallow), exact path. Two Providers whose roots both match the
56
+ same file produce `provider-ambiguous` (already in spec) and the
57
+ file stays unclassified.
58
+
59
+ **Extractor:**
60
+
61
+ - Drop `emitsLinkKinds` (the global closed enum of link kinds is the
62
+ contract; off-enum emissions drop with `extension.error`).
63
+ - Drop `defaultConfidence` (declare confidence per-emit on
64
+ `ctx.emitLink({ ..., confidence })`).
65
+ - Drop `applicableKinds` (array). Use `precondition.kind` instead with
66
+ qualified ids like `'claude/agent'`. The same `precondition` shape
67
+ is shared with Analyzer and Action.
68
+
69
+ **Analyzer:**
70
+
71
+ - Drop `emitsAnalyzerIds` (the qualified extension id is the default
72
+ `analyzer_id`).
73
+ - Drop `defaultSeverity` (declare severity per-emit on
74
+ `ctx.emitIssue({ ..., severity })`).
75
+ - Drop `consumes`, `configurable`, `recommendedActions`. The
76
+ analyzer↔action relationship is now declared from the Action side
77
+ via `precondition.analyzerIds` (Modelo B): one Action says "I
78
+ resolve these analyzer findings", instead of one Analyzer saying
79
+ "these actions help".
80
+ - Add `precondition: { kind?, provider? }` (same shape as Extractor).
81
+
82
+ **Action:**
83
+
84
+ - Drop `reportSchemaRef` and `promptTemplateRef`. The kernel now
85
+ resolves these by convention from the action folder:
86
+ `<action-dir>/report.schema.json` (always required) and
87
+ `<action-dir>/prompt.md` (required when `mode='probabilistic'`,
88
+ forbidden when `mode='deterministic'`).
89
+ - Drop `expectedTools`, `fanOutPolicy`, `precondition.stability`,
90
+ `precondition.custom`.
91
+ - Add `precondition.analyzerIds` (Modelo B).
92
+ - Rename `expectedDurationSeconds` to `probExpectedDurationSeconds`
93
+ to mark it as probabilistic-only via the `prob*` prefix convention.
94
+ - `mode` is now optional with default `'deterministic'` (was
95
+ required).
96
+
97
+ **Formatter:**
98
+
99
+ - Drop `formatId` (comes from the folder name; the loader injects it
100
+ into the runtime instance).
101
+ - Drop `supportsFilter` (every formatter supports `--filter`).
102
+
103
+ **Hook:**
104
+
105
+ - Drop `mode`. Hooks are deterministic-only; LLM-dependent reactions
106
+ are modeled as a deterministic hook that enqueues a probabilistic
107
+ Action via `ctx.queue('<plugin>/<action>', payload)`.
108
+
109
+ **Loader changes (`src/kernel/adapters/plugin-loader/`):**
110
+
111
+ - The exported manifest is stripped of any `id` / `kind` / `pluginId`
112
+ / `kinds` / `formatId` keys before AJV validation; the loader
113
+ injects the canonical values from the folder layout. Legacy
114
+ manifests that still inline these fields load cleanly.
115
+ - New `discoverProviderKinds(...)` reads `<plugin>/kinds/<k>/{schema.json,
116
+ kind.json}` and merges the result into the runtime Provider
117
+ instance. Failure modes: missing or unparseable `schema.json`
118
+ → `load-error`; missing, unparseable, or AJV-invalid `kind.json`
119
+ → `invalid-manifest`.
120
+ - New `validateActionFileConventions(...)` enforces the
121
+ `report.schema.json` / `prompt.md` conventions.
122
+ - New `matchesAnyRoot(...)` powers Provider `roots` enforcement
123
+ inside `processRawNode`.
124
+
125
+ **Spec docs:**
126
+
127
+ - `architecture.md` §Extension kinds table, §Provider · `kinds`
128
+ catalog, §View contribution system updated.
129
+ - `plugin-author-guide.md` §Manifest section rewritten (id from
130
+ folder; description/catalogCompat required), §Extractor section
131
+ reworked around `precondition.kind`, drop guidance for
132
+ `emitsLinkKinds` / `defaultConfidence`.
133
+ - `view-slots.md` references `ui` map.
134
+
135
+ **Built-ins migration:**
136
+
137
+ - `core/bump/report.schema.json` and `core/mark-superseded/report.schema.json`
138
+ added (file conventions).
139
+ - `core/tools-count` extractor uses
140
+ `precondition: { kind: ['claude/agent'] }`.
141
+ - All built-in extensions drop `stability`, `preconditions`,
142
+ `emitsLinkKinds`, `defaultConfidence`, `emitsAnalyzerIds`,
143
+ `defaultSeverity`, `consumes`, `configurable`, `recommendedActions`,
144
+ `defaultRefreshAction`, `formatId` (formatter), `supportsFilter`,
145
+ `mode` (hook).
146
+ - `scripts/generate-built-ins.js` updated: `id` from bundle folder,
147
+ `granularity ?? 'extension'`, `toExtensionRow` drops the retired
148
+ display fields.
149
+
150
+ **Testkit:**
151
+
152
+ - `makeExtractorContext` populates `settings: {}` so test fixtures
153
+ satisfy the new required field on `IExtractorContext`.
154
+
155
+ ## User-facing
156
+
157
+ **Plugin manifests are smaller.** `plugin.json` drops `id`; every extension declares only `version` + `description` plus kind-specific fields. View contributions move to `ui:`. Provider kinds live under `kinds/<kindName>/`. Run `sm plugins doctor` after upgrading.
158
+
159
+ - 8b7abbf: Structure-as-truth refactor for plugin extensions. The filesystem
160
+ layout (rather than declarative manifest fields) is now the single
161
+ source of truth for bundle / kind / extension id.
162
+
163
+ **Schema changes:**
164
+
165
+ - `PluginManifest` drops the required `extensions: string[]` array;
166
+ the kernel now auto-discovers extensions by walking
167
+ `<plugin-dir>/<kind>s/<name>/index.{js,mjs,ts}` for each known
168
+ kind. `granularity` is now required (no implicit default).
169
+ - `ExtensionBase` drops the `entry` field (it was an override for
170
+ the now-gone `extensions[]` path; the loader computes the entry
171
+ path from the discovered file).
172
+ - `viewContributions` moves from `base.schema.json` to
173
+ `extractor.schema.json` and `analyzer.schema.json`. Runtime
174
+ `ctx.emitContribution` only exists for those two kinds; declaring
175
+ view contributions on other kinds used to be a silent no-op and
176
+ is now rejected at manifest load.
177
+
178
+ **Loader changes:**
179
+
180
+ - `<plugin-dir>/<kind>s/<name>/` is the unit of discovery; the
181
+ loader walks `providers`, `extractors`, `analyzers`, `actions`,
182
+ `formatters`, `hooks` in canonical order, looking for an
183
+ `index.{js,mjs,ts}` inside each name directory.
184
+ - A manifest whose `kind` disagrees with the folder it lives under
185
+ (e.g. an `extractor` placed under `analyzers/`) is rejected as
186
+ `invalid-manifest` with a directed reason.
187
+ - Containment is enforced by construction: the loader never reads
188
+ paths the manifest could redirect, so `..`-escape and
189
+ absolute-path lanes are closed without runtime checks.
190
+
191
+ **Built-ins reorganization:**
192
+
193
+ - Source tree renamed `src/built-in-plugins/` → `src/plugins/` and
194
+ reorganized to `src/plugins/<bundle>/<kind>s/<name>/index.ts`.
195
+ Each bundle (`core`, `claude`, `gemini`, `agent-skills`) gains a
196
+ `plugin.json` with its metadata.
197
+ - `src/plugins/built-ins.ts` is now generated by
198
+ `scripts/generate-built-ins.js` (runs as `prebuild`, checked for
199
+ drift by `built-ins:check` in CI). The generator walks the
200
+ filesystem, reads each bundle's `plugin.json`, and emits static
201
+ imports + the legacy API surface (`builtIns()`,
202
+ `listBuiltIns()`, `builtInBundles`).
203
+
204
+ **Scaffolder (`sm plugins create`):**
205
+
206
+ - Emits the new layout: `extractors/<plugin-id>-extractor/index.js`
207
+ plus a `plugin.json` without `extensions` and without
208
+ `pluginId` on the extension export (the loader injects it from
209
+ `plugin.json#/id`). The legacy `mode: 'deterministic'` field on
210
+ the extractor stub was a no-op holdover from when extractors had
211
+ a mode and has been removed.
212
+
213
+ **Per-extension co-located files convention:**
214
+
215
+ Files that share the extension's folder with `index.{js,mjs,ts}`
216
+ are author-owned siblings. Two blessed names so consumers know
217
+ where to look:
218
+
219
+ - `text.ts` for externalised user-facing strings (one per
220
+ extension, imported by `index.ts` as `./text.js`).
221
+ - `<extension-name>.test.{ts,mjs,js}` for the colocated test
222
+ suite (picked up by the workspace's `plugins/**/*.test.ts`
223
+ glob).
224
+
225
+ Both are optional; the loader ignores anything that is not
226
+ `index.{js,mjs,ts}`, so future schemas / fixtures / conformance
227
+ scopes can live next to the code without manifest plumbing. The
228
+ in-tree built-ins under `src/plugins/` were migrated to this
229
+ shape: each analyzer's user-facing strings now live at
230
+ `<bundle>/analyzers/<name>/text.ts` instead of a centralised
231
+ `i18n/` directory.
232
+
233
+ ## User-facing
234
+
235
+ **Plugin layout changed.** Extensions now live at `<kind>s/<name>/index.js` (e.g. `extractors/keyword-counter/index.js`); `plugin.json` no longer lists `extensions[]` and requires `granularity`. Run `sm plugins doctor` after migrating, or use `sm plugins create` for new plugins.
236
+
237
+ ## 0.27.0
238
+
239
+ ### Minor Changes
240
+
241
+ - f1efd1b: Remove the `-g/--global` flag and every implicit `$HOME` read from
242
+ skill-map. The CLI now operates exclusively on the project scope
243
+ (`<cwd>/.skill-map/`); there is no global / user scope, no
244
+ `SKILL_MAP_SCOPE` env var, no silent merge of user-level config or
245
+ plugins.
246
+
247
+ The user extends the scan beyond the project root via the existing
248
+ `scan.extraFolders` setting in project-local config (privacy-gated
249
+ through `sm config set --yes` or the Settings UI confirm dialog).
250
+ Plugins outside the project install per-project at
251
+ `<cwd>/.skill-map/plugins/` or load via the `--plugin-dir <path>`
252
+ escape hatch on the `sm plugins …` verb family.
253
+
254
+ **Narrow documented exception**: a single `~/.skill-map/settings.json`
255
+ file (validated by `user-settings.schema.json`) holds genuinely
256
+ per-machine preferences. Today it carries the update-check toggle +
257
+ its throttle bookkeeping; future per-machine settings (locale, theme)
258
+ extend it under their own sub-keys. There is no `.local` partner.
259
+ The file is NOT part of the project config layer system; it is read
260
+ directly by the module that owns each feature. `src/cli/util/user-settings-store.ts`
261
+ is the only module that calls `os.homedir()` for this file. The two
262
+ remaining `os.homedir()` callsites (`core/config/helper.ts`,
263
+ `core/runtime/reference-paths-walker.ts`) handle user-typed `~/foo`
264
+ expansion inside `scan.extraFolders` / `scan.referencePaths`, the
265
+ read is user-authored per invocation, not skill-map's own default.
266
+
267
+ Removed surface (`@skill-map/cli`):
268
+
269
+ - `-g/--global` flag inherited by every `SmCommand` verb (`bump`,
270
+ `check`, `config`, `export`, `graph`, `history`, `init`, `jobs`,
271
+ `list`, `orphans`, `refresh`, `scan`, `serve`, `show`, `sidecar`,
272
+ `watch`, every `plugins` subcommand). Calling any verb with
273
+ `-g/--global` now exits 2 with Clipanion's "unknown option" error.
274
+ - `SKILL_MAP_SCOPE=global` env var translation.
275
+ - `sm serve --scope project|global` flag.
276
+ - `sm config --source global` literal in `--source` outputs (the
277
+ source set is now `default | project | project-local | env | flag`).
278
+ - `IRuntimeContext.homedir` field.
279
+ - `IDbLocationOptions.global` field; `resolveDbPath` reduces to
280
+ `db ?? defaultProjectDbPath(ctx)`.
281
+ - `defaultUserPluginsDir` helper.
282
+ - `loadConfig` `scope: 'project' | 'global'` parameter and the
283
+ `user` / `user-local` file-pair iteration; the layer list is now
284
+ `defaults` → `project` → `project-local` → `override`.
285
+ - `USER_ONLY_KEYS` constant and the per-key locality enforcement
286
+ pinned to it. `updateCheck.enabled` is no longer part of the
287
+ config layer system; its toggle lives alongside the throttle
288
+ cache.
289
+ - `GET /api/health` response field `scope: 'project'|'global'`.
290
+ - `GET /api/plugins` item field `source: 'built-in'|'project'|'global'`
291
+ reduces to `'built-in'|'project'`.
292
+ - `scan_meta.scope` SQLite column and the matching `IScanResult.scope`
293
+ kernel field.
294
+
295
+ Removed surface (`@skill-map/spec`):
296
+
297
+ - `spec/cli-contract.md` § Global flags row for `-g/--global` and
298
+ the `SKILL_MAP_SCOPE` row in the env-var table.
299
+ - `spec/cli-contract.md` § serve flag table `--scope project|global`
300
+ row.
301
+ - `spec/architecture.md` § Config layering layers `user` and
302
+ `user-local`; `USER_ONLY_KEYS` set.
303
+ - `spec/db-schema.md` two-scope diagram; `scan_meta.scope` column;
304
+ `scope: 'global'` from `--source` enum text.
305
+ - `spec/schemas/scan-result.schema.json` `scope` property (was in
306
+ `required`).
307
+ - `spec/schemas/project-config.schema.json` `updateCheck`
308
+ description rewritten as the documented exception.
309
+ - `spec/schemas/plugins-registry.schema.json` status description's
310
+ `project / global / --plugin-dir` reference.
311
+
312
+ Added surface:
313
+
314
+ - `spec/cli-contract.md` § "Scope is always project-local"
315
+ normative paragraph at the top of the file, stating the
316
+ no-`$HOME`-reads principle and the update-check exception.
317
+ - `AGENTS.md` § Analyzers gains the matching operating rule for
318
+ agents working in the repo, "Skill-map MUST NEVER read `$HOME`
319
+ by default…".
320
+ - Regression test at `src/test/global-flag-removed.test.ts`
321
+ asserting Clipanion's "unknown option" error on `sm scan -g`.
322
+
323
+ Migration (no compat shim): pre-1.0, greenfield. Users who relied
324
+ on `~/.skill-map/skill-map.db`, `~/.skill-map/settings*.json`, or
325
+ `~/.skill-map/plugins/` move the files into their project
326
+ (`<cwd>/.skill-map/`) or pass `--plugin-dir <path>` per invocation.
327
+ Older DBs are not migrated, a fresh `sm init` regenerates without
328
+ the `scope` column.
329
+
330
+ ## User-facing
331
+
332
+ `-g/--global` is gone. `sm` reads only the current project
333
+ (`<cwd>/.skill-map/`). To scan outside the project, add paths via
334
+ `scan.extraFolders` in Settings. User-scope plugins move to
335
+ `<cwd>/.skill-map/plugins/` or load with `--plugin-dir <path>`.
336
+
3
337
  ## 0.26.0
4
338
 
5
339
  ### Minor Changes
package/architecture.md CHANGED
@@ -70,7 +70,7 @@ Operations: `discover(scopes)`, `load(pluginPath)`, `validateManifest(json)`.
70
70
  The loader enforces two id-uniqueness analyzers during discovery (see [`plugin-author-guide.md` §Plugin id uniqueness](./plugin-author-guide.md#plugin-id-uniqueness) for the author-facing summary):
71
71
 
72
72
  1. **Directory name == manifest id.** A plugin lives at `<root>/<id>/plugin.json`. A mismatch surfaces as status `invalid-manifest`. This analyzer eliminates same-root collisions by construction.
73
- 2. **Cross-root id collision blocks both sides.** Two plugins reachable from different roots (project + global, or any `--plugin-dir` combination) that declare the same `id` BOTH receive status `id-collision`. No precedence analyzer applies, coherent with §Boot invariant ("no extension is privileged"). The user resolves by renaming one of them.
73
+ 2. **Cross-root id collision blocks both sides.** Two plugins reachable from different roots (e.g. the project default `<cwd>/.skill-map/plugins/` and any `--plugin-dir` combination) that declare the same `id` BOTH receive status `id-collision`. No precedence analyzer applies, coherent with §Boot invariant ("no extension is privileged"). The user resolves by renaming one of them.
74
74
 
75
75
  In addition, the loader **qualifies every extension** with its owning plugin id before registering it. The registry stores extensions under the qualified id `<plugin-id>/<extension-id>` (e.g. `core/slash`, `core/broken-ref`, `hello-world/greet`). Authors continue to declare the short `id` in each extension manifest; the loader composes the qualified form from `manifest.id` at load time. Built-in extensions bundled with the reference impl declare their `pluginId` directly in `built-ins.ts`, `core/` for kernel-internal primitives (every analyzer, the formatter, the cross-vendor extractors `annotations` / `slash` / `at-directive` / `markdown-link` / `external-url-counter` / `stability`) and vendor-specific bundles such as `claude/` (the Claude provider) for Provider integrations whose territory is platform-bound. If a plugin author injects a `pluginId` field on an extension that disagrees with `plugin.json`'s `id`, the loader emits `invalid-manifest` with a directed reason.
76
76
 
@@ -173,12 +173,12 @@ Six kinds, all first-class, all loaded through the same registry. Each kind has
173
173
 
174
174
  | Kind | Role | Input | Output |
175
175
  |---|---|---|---|
176
- | **Provider** | Recognizes a platform. Declares the catalog of node `kind`s it emits via the `kinds` map; each map entry pairs the kind's frontmatter schema (path relative to the Provider's package directory) with its `defaultRefreshAction` (a qualified action id that drives the probabilistic-refresh surface). The Provider's walker hardcodes the paths it scans within the project (e.g. `.claude/`, `.gemini/`); it does NOT extend the scan into the user's HOME. Deterministic-only. | Filesystem walk results, candidate path. | `{ kind, provider } \| null`. |
177
- | **Extractor** | Extracts signals from a node body. Deterministic-only: runs synchronously inside `sm scan`. Output flows through three context callbacks (no return value): `ctx.emitLink(link)` for the kernel's `links` table, `ctx.enrichNode(partial)` for the kernel's enrichment layer (separate from the author's frontmatter), `ctx.store` for the plugin's own KV / dedicated tables. | Parsed node (frontmatter + body) + callbacks. | `void` (output via callbacks). |
178
- | **Analyzer** | Evaluates the graph. Dual-mode: `deterministic` runs in `sm check`, `probabilistic` runs in jobs. | Full graph (nodes + links). | `Issue[]`. |
179
- | **Action** | Operates on one or more nodes. Dual-mode: `deterministic` (in-process code) or `probabilistic` (rendered prompt the runner executes). | Node(s), optional args. | Deterministic: report JSON. Probabilistic: rendered prompt that a runner executes. |
180
- | **Formatter** | Serializes the graph. Deterministic-only. | Graph + optional filter. | String (ASCII / Mermaid / DOT / JSON / user-defined). |
181
- | **Hook** | Reacts declaratively to one of ten curated lifecycle events, eight pipeline-driven (`scan.started`, `scan.completed`, `extractor.completed`, `analyzer.completed`, `action.completed`, `job.spawning`, `job.completed`, `job.failed`) plus two CLI-process-driven (`boot` before verb routing, `shutdown` after the verb's exit code resolves). Dual-mode: `deterministic` runs in-process during the dispatch, `probabilistic` is enqueued as a job. Hooks REACT to events; they cannot block, mutate, or steer the pipeline. | A curated event payload (run-scoped, scan-scoped, job-scoped, or process-scoped) plus an optional declarative `filter` map. | `void` (reactions are side effects). |
176
+ | **Provider** | Recognizes a platform. The kind catalog lives on disk under `<plugin>/kinds/<kindName>/{schema.json, kind.json}` (structure-as-truth); the loader projects it onto the runtime descriptor. The Provider's walker hardcodes the paths it scans within the project (e.g. `.claude/`, `.gemini/`); it does NOT extend the scan into the user's HOME. `Provider.roots` is enforcement-grade: a Provider with declared roots only sees matching files; a Provider without `roots` acts as the fallback. Deterministic-only. | Filesystem walk results, candidate path. | `{ kind, provider } \| null`. |
177
+ | **Extractor** | Extracts signals from a node body. Deterministic-only: runs synchronously inside `sm scan`. Output flows through context callbacks (no return value): `ctx.emitLink(link)` for the kernel's `links` table (validated against the global closed enum of link kinds; per-extractor allowlist was retired with the structure-as-truth refactor), `ctx.enrichNode(partial)` for the kernel's enrichment layer (separate from the author's frontmatter), `ctx.emitContribution(id, payload)` for view contributions, `ctx.store` for the plugin's own KV / dedicated tables. | Parsed node (frontmatter + body) + callbacks. | `void` (output via callbacks). |
178
+ | **Analyzer** | Evaluates the graph. Dual-mode: `deterministic` runs in `sm check`, `probabilistic` runs in jobs. The analyzer↔action relationship is declared from the Action side via `precondition.analyzerIds` (Modelo B). | Full graph (nodes + links). | `Issue[]`. |
179
+ | **Action** | Operates on one or more nodes. Dual-mode: `deterministic` (in-process code) or `probabilistic` (rendered prompt the runner executes). Files-by-convention: every Action carries `<action-dir>/report.schema.json`; probabilistic Actions additionally carry `<action-dir>/prompt.md`. The retired `reportSchemaRef` / `promptTemplateRef` / `expectedTools` / `fanOutPolicy` manifest fields were replaced by these conventions and the simplified `precondition` block. | Node(s), optional args. | Deterministic: report JSON. Probabilistic: rendered prompt that a runner executes. |
180
+ | **Formatter** | Serializes the graph. Deterministic-only. The `formatId` consumed by `sm graph --format <name>` comes from the formatter's folder name. | Graph + optional filter. | String (ASCII / Mermaid / DOT / JSON / user-defined). |
181
+ | **Hook** | Reacts declaratively to one of ten curated lifecycle events, eight pipeline-driven (`scan.started`, `scan.completed`, `extractor.completed`, `analyzer.completed`, `action.completed`, `job.spawning`, `job.completed`, `job.failed`) plus two CLI-process-driven (`boot` before verb routing, `shutdown` after the verb's exit code resolves). **Deterministic-only** since the structure-as-truth refactor: LLM-dependent reactions are modeled as a deterministic Hook that enqueues a probabilistic Action via `ctx.queue('<plugin>/<action>', payload)`. Hooks REACT to events; they cannot block, mutate, or steer the pipeline. | A curated event payload (run-scoped, scan-scoped, job-scoped, or process-scoped) plus an optional declarative `filter` map. | `void` (reactions are side effects). |
182
182
 
183
183
  ### IO discipline, extensions never write to the filesystem
184
184
 
@@ -194,13 +194,14 @@ This invariant is what makes the consent gate at the kernel boundary sufficient:
194
194
 
195
195
  ### Provider · `kinds` catalog
196
196
 
197
- Every `Provider` MUST declare a non-empty map `kinds: { <kind>: { schema, defaultRefreshAction, ui } }` covering every `kind` it classifies into. Each entry carries three required fields:
197
+ Every `Provider` declares its kind catalog via the filesystem (structure-as-truth): each kind lives under `<plugin>/kinds/<kindName>/` and ships exactly two files:
198
198
 
199
- - **`schema`**, path (relative to the Provider package) to the kind's frontmatter JSON Schema. The schema MUST extend [`frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) via `allOf` + `$ref` to base's `$id`. The kernel registers it with AJV at boot and validates every node's frontmatter against the entry matching its classified kind.
200
- - **`defaultRefreshAction`**, qualified action id (`<plugin-id>/<action-id>`) the UI's probabilistic-refresh surface (`🧠 prob`) dispatches for nodes of this kind. The action MUST exist in the registry; a dangling reference disables the Provider with status `invalid-manifest`. Plugins MAY override per-node via `metadata.refreshAction`; the Provider default is normative.
201
- - **`ui`**, presentation block: `{ label, color, colorDark?, emoji?, icon? }`. See §Provider · `ui` presentation below.
199
+ - **`schema.json`**, the kind's frontmatter JSON Schema. MUST extend [`frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) via `allOf` + `$ref` to base's `$id`. The kernel reads it once at boot, registers it with AJV, and validates every node's frontmatter against the entry matching its classified kind.
200
+ - **`kind.json`**, the per-kind metadata, today just `{ ui: { label, color, colorDark?, emoji?, icon? } }`. See §Provider · `ui` presentation below. Validated against [`schemas/extensions/provider-kind.schema.json`](./schemas/extensions/provider-kind.schema.json) at load time.
202
201
 
203
- The catalog is the single source of truth for "which kinds does this Provider emit", the `IProvider` runtime contract derives the kind set from `Object.keys(kinds)`.
202
+ The loader's discovery (`discoverProviderKinds`) projects every `kinds/<kindName>/` directory into the runtime descriptor `instance.kinds[<kindName>] = { schema, schemaJson, ui }`. The `IProvider` runtime contract derives the kind set from `Object.keys(kinds)`; authors do not write the map by hand any more.
203
+
204
+ The retired manifest field `defaultRefreshAction` (the qualified action id the UI's `🧠 prob` button dispatched) was removed alongside the button. A replacement UX is TBD; until then, the kernel does not surface a Provider-declared "default refresh" path.
204
205
 
205
206
  ### Provider · `ui` presentation
206
207
 
@@ -362,7 +363,7 @@ Hooks introduce no new persisted state and do NOT participate in the determinist
362
363
 
363
364
  ### Locality
364
365
 
365
- - **Drop-in**: extensions live inside plugins, discovered at boot from `.skill-map/plugins/<id>/` and `~/.skill-map/plugins/<id>/`.
366
+ - **Drop-in**: extensions live inside plugins, discovered at boot from `<cwd>/.skill-map/plugins/<id>/` only. The `--plugin-dir <path>` escape hatch on the `sm plugins …` verb family loads a custom directory per invocation when the user explicitly opts in.
366
367
  - **Built-in**: the reference impl bundles a default extension set (one Provider, four extractors, five analyzers, one formatter, one hook). The fifth analyzer, `core/validate-all`, replays every scanned node and link through the authoritative spec schemas via AJV, the kernel-side guard against persisting non-conforming graph rows. The first built-in Hook is `core/update-check`, which subscribes to `shutdown` and runs the once-per-day "update available" probe + banner that lived on the CLI entry path before the Hook kind had concrete consumers. These are loaded from `src/extensions/` and are indistinguishable from plugin-supplied extensions from the kernel's point of view.
367
368
 
368
369
  ---
@@ -440,23 +441,21 @@ This is what makes "CLI-first" a coherent analyzer: every CLI verb is a kernel f
440
441
  | # | Layer | Source | Audience |
441
442
  |---|---|---|---|
442
443
  | 1 | `defaults` | Bundled `defaults.json` (ships in the CLI binary). | Every install. |
443
- | 2 | `user` | `~/.skill-map/settings.json` | Per-machine, per-user; lives in `$HOME` (never in the repo). |
444
- | 3 | `user-local` | `~/.skill-map/settings.local.json` | Same audience as `user`; intended for values the user might want to keep out of dotfile sync. |
445
- | 4 | `project` | `<cwd>/.skill-map/settings.json` | **Committed to the repo**, values are shared with every collaborator and CI. |
446
- | 5 | `project-local` | `<cwd>/.skill-map/settings.local.json` | **Gitignored**, values are per-checkout, never travel via the repo. |
447
- | 6 | `override` | Caller-supplied (env vars, CLI flags). | Process-scoped, ephemeral. |
444
+ | 2 | `project` | `<cwd>/.skill-map/settings.json` | **Committed to the repo**, values are shared with every collaborator and CI. |
445
+ | 3 | `project-local` | `<cwd>/.skill-map/settings.local.json` | **Gitignored**, values are per-checkout, never travel via the repo. |
446
+ | 4 | `override` | Caller-supplied (env vars, CLI flags). | Process-scoped, ephemeral. |
448
447
 
449
448
  The merge is per dot-path: a value declared at a higher layer replaces the value at lower layers; objects recurse, arrays replace. The loader records which layer last wrote each key in a `sources` map so `sm config show --source` can attribute every effective value.
450
449
 
451
- Layers 1, 2, 3, 5, 6 carry **per-user / per-machine state**. Only layer 4 (`project`) travels via the shared repo, so values landing in `project` are part of the contract every collaborator inherits.
450
+ Only layer 2 (`project`) travels via the shared repo, so values landing in `project` are part of the contract every collaborator inherits. Layers 1, 3, 4 carry **per-machine / per-checkout state** that never leaves the project.
452
451
 
453
- ### Per-key locality
452
+ Skill-map deliberately has **no user-scope config layer**: there is no merge of `$HOME` state on top of the project. The CLI honours the principle "never read `$HOME` by default" (see `cli-contract.md` §Scope is always project-local). The narrow exception, `~/.skill-map/settings.json`, holds genuinely per-machine preferences (the update-check toggle + its throttle bookkeeping today; future locale / theme) but is **NOT** part of the config layer system: it is read directly by the module that owns the feature, never merged into the project layers above. See `cli-contract.md` §User-settings file for the contract.
454
453
 
455
- Two locality classes constrain which layers a given key MAY live in. Both are enforced in code (reference impl: `core/config/helper.ts`), not in the JSON Schema, the schema stays additive so older settings files keep validating even when a key is reclassified.
454
+ ### Per-key locality
456
455
 
457
- - **`USER_ONLY_KEYS`**, keys describing per-user preferences that have no project meaning. Read forces `scope: 'global'` (project layers ignored); write rejects `target: 'project'` with a directed error. Today: `updateCheck.enabled`.
456
+ One locality class constrains which layers a given key MAY live in. It is enforced in code (reference impl: `core/config/helper.ts`), not in the JSON Schema, the schema stays additive so older settings files keep validating even when a key is reclassified.
458
457
 
459
- - **`PROJECT_LOCAL_ONLY_KEYS`**, keys describing per-user-per-project preferences. Valid in layers 1, 2, 3, 5, 6. **Stripped (with a warning) from layer 4 (`project`)** because the value is inherently per-user and must not be shared via the committed repo. Writes target `project-local` (`<cwd>/.skill-map/settings.local.json`); `sm config set` rejects `--scope project` for these keys.
458
+ - **`PROJECT_LOCAL_ONLY_KEYS`**, keys describing per-user-per-project preferences. Valid in layers 1, 3, 4. **Stripped (with a warning) from layer 2 (`project`)** because the value is inherently per-user and must not be shared via the committed repo. Writes target `project-local` (`<cwd>/.skill-map/settings.local.json`); `sm config set` rejects writes to `project` for these keys with a directed error.
460
459
 
461
460
  Members:
462
461
  - `allowEditSmFiles`, per-project consent to create / modify `.sm` sidecars.
@@ -465,7 +464,7 @@ Two locality classes constrain which layers a given key MAY live in. Both are en
465
464
 
466
465
  All three describe disk access the local operator opted into; sharing them via the repo would silently expand every collaborator's scan surface to paths that only make sense on the original author's machine.
467
466
 
468
- Adding a new entry to either set is a behaviour change for older installs that wrote the key into a committed file, the value gets stripped (PROJECT_LOCAL_ONLY) or ignored (USER_ONLY) at read time. The changeset that adds the entry MUST document the migration.
467
+ Adding a new entry is a behaviour change for older installs that wrote the key into a committed file, the value gets stripped at read time. The changeset that adds the entry MUST document the migration.
469
468
 
470
469
  ---
471
470
 
@@ -590,7 +589,7 @@ Two schemas describe the wire shape:
590
589
 
591
590
  ### Identity
592
591
 
593
- Each view contribution is identified by the qualified id `<pluginId>/<extensionId>/<contributionId>`. The plugin author declares contributions in the extension manifest under `viewContributions: Record<string, IViewContribution>`; the loader composes the qualified id from the plugin id, the extension id, and the Record key.
592
+ Each view contribution is identified by the qualified id `<pluginId>/<extensionId>/<contributionId>`. The plugin author declares contributions in the extension manifest under `ui: Record<string, IViewContribution>` (renamed from `viewContributions` with the structure-as-truth refactor); the loader composes the qualified id from the plugin id, the extension id, and the Record key. The runtime catalog aggregated by `Kernel.getRegisteredViewContributions()` keeps the original `viewContributions` name, only the manifest-side field changed.
594
593
 
595
594
  ### Manifest
596
595
 
@@ -598,7 +597,7 @@ Each entry picks a `slot` name from the closed catalog and supplies presentation
598
597
 
599
598
  ```jsonc
600
599
  {
601
- "viewContributions": {
600
+ "ui": {
602
601
  "breakdown": {
603
602
  "slot": "inspector.body.panel.breakdown",
604
603
  "label": "Keyword hits",
@@ -618,13 +617,13 @@ The plugin author picks ONE slot per contribution; that single decision determin
618
617
 
619
618
  ### Settings
620
619
 
621
- Plugin user-configurable settings live at the manifest root in `settings: Record<string, ISettingDeclaration>` (see [`schemas/plugins-registry.schema.json`](./schemas/plugins-registry.schema.json)). Each setting picks an input-type from the closed catalog at [`schemas/input-types.schema.json`](./schemas/input-types.schema.json) (`string-list`, `single-string`, `boolean-flag`, `integer`, `enum-pick`, `enum-multipick`, `path-glob`, `regex`, `secret`, `key-value-list`). The kernel exposes resolved settings to extractors via `ctx.settings.<settingId>`; the UI generates a form per declaration; the CLI's `sm plugins config <id>` exposes the same surface.
620
+ Plugin user-configurable settings live **on each extension's manifest** (structure-as-truth) in `settings: Record<string, ISettingDeclaration>` (see [`schemas/extensions/base.schema.json`](./schemas/extensions/base.schema.json) and [`schemas/input-types.schema.json`](./schemas/input-types.schema.json)). Each setting picks an input-type from the closed catalog (`string-list`, `single-string`, `boolean-flag`, `integer`, `enum-pick`, `enum-multipick`, `path-glob`, `regex`, `secret`, `key-value-list`). The kernel exposes resolved settings via `ctx.settings.<settingId>` to the extension's runtime methods (`extract`, `evaluate`, `invoke`, etc.); the UI generates a form per declaration; the CLI's `sm plugins config <plugin>/<extension>` exposes the same surface. Plugin-level settings are no longer supported; the field was moved from `plugin.json` to each extension that consumes it.
622
621
 
623
- Settings are read once at extractor invocation; changing a setting requires `sm scan` to re-emit affected contributions. The UI surfaces a "settings changed, rescan needed" indicator when the manifest detects mismatch; live re-emission is explicitly out of scope (rescan-required is a stability decision per `ROADMAP.md` §UI contribution system D4).
622
+ Settings are read once at extension invocation; changing a setting requires `sm scan` to re-emit affected contributions. The UI surfaces a "settings changed, rescan needed" indicator when the manifest detects mismatch; live re-emission is explicitly out of scope (rescan-required is a stability decision per `ROADMAP.md` §UI contribution system D4).
624
623
 
625
624
  ### Runtime catalog
626
625
 
627
- The kernel exposes a runtime catalog (`Kernel.getRegisteredViewContributions()`) listing every plugin-contributed view contribution with its `pluginId`, `extensionId`, `contributionId`, `slot`, and the manifest-declared `label` / `tooltip` / `icon` / `emptyText` / `emitWhenEmpty`. The catalog is built once at boot from every loaded extension's `viewContributions` map, AJV-validated, and frozen, same lifecycle as `getRegisteredAnnotationKeys()`.
626
+ The kernel exposes a runtime catalog (`Kernel.getRegisteredViewContributions()`) listing every plugin-contributed view contribution with its `pluginId`, `extensionId`, `contributionId`, `slot`, and the manifest-declared `label` / `tooltip` / `icon` / `emptyText` / `emitWhenEmpty`. The catalog is built once at boot from every loaded extension's `ui` map (renamed from `viewContributions` with the structure-as-truth refactor), AJV-validated, and frozen, same lifecycle as `getRegisteredAnnotationKeys()`.
628
627
 
629
628
  Analyzers see the catalog through `IAnalyzerContext.viewContributions` so cross-cutting checks (`core/unknown-slot`, `core/contribution-orphan`) can reason about emissions.
630
629
 
@@ -714,7 +713,7 @@ Same honest-note posture as [`plugin-kv-api.md`](./plugin-kv-api.md): isolated a
714
713
 
715
714
  Two built-ins ship with the system to cover catalog evolution and rename edge cases:
716
715
 
717
- - **`core/unknown-slot`**, walks every loaded plugin's `viewContributions[*].slot`; emits an `Issue` of severity `warn` for any slot not in the current kernel catalog. Parallel to `core/unknown-field` for annotations. Note: AJV at manifest load already rejects unknown slots as `invalid-manifest`; this analyzer covers the soft-warning path when a plugin remains loaded across a catalog version bump.
716
+ - **`core/unknown-slot`**, walks every loaded plugin's `ui[*].slot`; emits an `Issue` of severity `warn` for any slot not in the current kernel catalog. Parallel to `core/unknown-field` for annotations. Note: AJV at manifest load already rejects unknown slots as `invalid-manifest`; this analyzer covers the soft-warning path when a plugin remains loaded across a catalog version bump.
718
717
  - **`core/contribution-orphan`**, joins `scan_contributions` against the live `scan_nodes` set; emits an `Issue` of severity `warn` for emissions whose `node_path` no longer exists (post-rename heuristic miss).
719
718
 
720
719
  ### Catalog versioning