@skill-map/spec 0.39.0 → 0.41.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 (57) hide show
  1. package/CHANGELOG.md +55 -2307
  2. package/README.md +8 -11
  3. package/architecture.md +74 -51
  4. package/cli-contract.md +38 -9
  5. package/conformance/README.md +1 -1
  6. package/conformance/cases/extractor-emits-signal.json +1 -1
  7. package/conformance/cases/kernel-empty-boot.json +1 -1
  8. package/conformance/cases/no-global-scope.json +1 -1
  9. package/conformance/cases/orphan-markdown-fallback.json +1 -1
  10. package/conformance/cases/plugin-missing-ui-rejected.json +1 -1
  11. package/conformance/cases/sidecar-end-to-end.json +1 -1
  12. package/conformance/cases/signal-collision-detection.json +1 -1
  13. package/conformance/coverage.md +1 -1
  14. package/conformance/fixtures/sidecar-example/agent-example.sm +3 -3
  15. package/db-schema.md +21 -7
  16. package/index.json +56 -55
  17. package/package.json +3 -2
  18. package/plugin-author-guide.md +273 -776
  19. package/schemas/annotations.schema.json +2 -2
  20. package/schemas/api/rest-envelope.schema.json +1 -1
  21. package/schemas/bump-report.schema.json +1 -1
  22. package/schemas/conformance-case.schema.json +1 -1
  23. package/schemas/conformance-result.schema.json +1 -1
  24. package/schemas/execution-record.schema.json +1 -1
  25. package/schemas/extensions/action.schema.json +1 -1
  26. package/schemas/extensions/analyzer.schema.json +1 -1
  27. package/schemas/extensions/base.schema.json +1 -1
  28. package/schemas/extensions/extractor.schema.json +1 -1
  29. package/schemas/extensions/formatter.schema.json +1 -1
  30. package/schemas/extensions/hook.schema.json +1 -1
  31. package/schemas/extensions/provider-kind.schema.json +1 -1
  32. package/schemas/extensions/provider.schema.json +1 -1
  33. package/schemas/frontmatter/base.schema.json +2 -7
  34. package/schemas/history-stats.schema.json +1 -1
  35. package/schemas/input-types.schema.json +1 -1
  36. package/schemas/issue.schema.json +1 -1
  37. package/schemas/job.schema.json +1 -1
  38. package/schemas/link.schema.json +1 -1
  39. package/schemas/node.schema.json +1 -1
  40. package/schemas/plugins-doctor.schema.json +1 -1
  41. package/schemas/plugins-registry.schema.json +1 -1
  42. package/schemas/project-config.schema.json +1 -1
  43. package/schemas/refresh-report.schema.json +1 -1
  44. package/schemas/report-base-deterministic.schema.json +1 -1
  45. package/schemas/report-base.schema.json +1 -1
  46. package/schemas/scan-result.schema.json +1 -1
  47. package/schemas/sidecar.schema.json +1 -1
  48. package/schemas/signal.schema.json +1 -1
  49. package/schemas/summaries/agent.schema.json +1 -1
  50. package/schemas/summaries/command.schema.json +1 -1
  51. package/schemas/summaries/hook.schema.json +1 -1
  52. package/schemas/summaries/markdown.schema.json +1 -1
  53. package/schemas/summaries/skill.schema.json +1 -1
  54. package/schemas/user-settings.schema.json +32 -1
  55. package/schemas/view-slots.schema.json +1 -1
  56. package/telemetry.md +294 -0
  57. package/versioning.md +2 -2
package/README.md CHANGED
@@ -60,8 +60,9 @@ spec/ ← published as @skill-map/spec
60
60
  ├── [db-schema.md](./db-schema.md) ← table catalog (kernel-owned)
61
61
  ├── [plugin-kv-api.md](./plugin-kv-api.md) ← ctx.store contract for storage mode A
62
62
  ├── [job-lifecycle.md](./job-lifecycle.md) ← queued → running → completed | failed
63
+ ├── [telemetry.md](./telemetry.md) ← opt-in error reporting (default OFF)
63
64
 
64
- ├── schemas/ ← 29 JSON Schemas, draft 2020-12, camelCase keys
65
+ ├── schemas/ ← JSON Schemas, draft 2020-12, camelCase keys (authoritative list + sha256 in index.json)
65
66
  │ ├── node.schema.json ┐
66
67
  │ ├── link.schema.json │
67
68
  │ ├── issue.schema.json │
@@ -74,13 +75,9 @@ spec/ ← published as @skill-map/spec
74
75
  │ ├── conformance-case.schema.json │
75
76
  │ ├── history-stats.schema.json ┘
76
77
  │ │
77
- │ ├── extensions/ ← one per extension kind; validated at plugin load
78
- │ │ ├── base.schema.json ┐
79
- │ │ ├── provider.schema.json
80
- │ │ ├── extractor.schema.json │ 6 extension schemas
81
- │ │ ├── analyzer.schema.json │ (base + 5 kinds)
82
- │ │ ├── action.schema.json │
83
- │ │ └── formatter.schema.json ┘
78
+ │ ├── extensions/ ← base + one per kind (provider, extractor,
79
+ │ │ analyzer, action, formatter, hook) +
80
+ │ │ provider-kind.schema.json; validated at plugin load
84
81
  │ │
85
82
  │ ├── frontmatter/ ← user-authored; additionalProperties: true
86
83
  │ │ └── base.schema.json ← universal shape; per-kind schemas live with
@@ -137,10 +134,10 @@ npm i @skill-map/spec
137
134
  import specIndex from '@skill-map/spec';
138
135
  import nodeSchema from '@skill-map/spec/schemas/node.schema.json' with { type: 'json' };
139
136
 
140
- console.log(specIndex.specPackageVersion); // → "0.2.0" (npm package version; source of truth for `spec` in `sm version`)
137
+ console.log(specIndex.specPackageVersion); // npm package version; source of truth for `spec` in `sm version`
141
138
  console.log(specIndex.indexPayloadVersion); // → "0.0.1" (payload shape of `index.json` itself; bumps only when this manifest's structure changes)
142
139
  console.log(specIndex.integrity.algorithm); // → "sha256"
143
- console.log(nodeSchema.$id); // → "https://skill-map.dev/spec/v0/node.schema.json"
140
+ console.log(nodeSchema.$id); // → "https://skill-map.ai/spec/v0/node.schema.json"
144
141
  ```
145
142
 
146
143
  Every JSON Schema is exported individually via `@skill-map/spec/schemas/*.json`. Prose documents ship in the tarball for reference but are not `exports`-surfaced.
@@ -161,7 +158,7 @@ console.log(actual === index.integrity.files[file] ? 'ok' : 'drift');
161
158
 
162
159
  ### JSON Schema Store
163
160
 
164
- The schemas will be registered on JSON Schema Store once the canonical URLs under `skill-map.dev/spec/v0/` are stable (Step 14).
161
+ The schemas will be registered on JSON Schema Store once the canonical URLs under `skill-map.ai/spec/v0/` are stable (Step 14).
165
162
 
166
163
  ## License
167
164
 
package/architecture.md CHANGED
@@ -8,30 +8,57 @@ Any conforming implementation, reference or third-party, MUST respect these boun
8
8
 
9
9
  ## Layering
10
10
 
11
+ ```mermaid
12
+ flowchart TB
13
+ subgraph DRIVERS["Driving adapters (primary)"]
14
+ direction LR
15
+ CLI["CLI<br/><i>sm command</i>"]
16
+ SERVER["Server<br/><i>Hono BFF (src/server/)</i>"]
17
+ SKILL["Skill<br/><i>agent / IDE</i>"]
18
+ end
19
+
20
+ UI["UI · Angular SPA<br/><i>(ui/)</i>"]:::ui
21
+ UI -.->|"HTTP / WS"| SERVER
22
+
23
+ subgraph KERNEL["Kernel (domain-pure, hexagonal)"]
24
+ direction LR
25
+ REG["Registry"]
26
+ ORCH["Orchestrator"]
27
+ UC["Use cases<br/><i>scan · refresh · action · watch</i>"]
28
+ CONFIG["Config layering<br/><i>defaults → project → project-local → override</i>"]
29
+ end
30
+
31
+ CLI ==>|"ports"| KERNEL
32
+ SERVER ==>|"ports"| KERNEL
33
+ SKILL ==>|"ports"| KERNEL
34
+
35
+ subgraph DRIVEN["Driven adapters (secondary)"]
36
+ direction LR
37
+ STORAGE["Storage<br/><i>SQLite</i>"]
38
+ FS["FS<br/><i>walker · watcher (chokidar)</i>"]
39
+ subgraph PLUGINS["Plugins (closed catalog, 6 kinds)"]
40
+ direction TB
41
+ EXT["extractors"]
42
+ ANA["analyzers"]
43
+ ACT["actions"]
44
+ HOOK["hooks"]
45
+ FMT["formatters"]
46
+ PROV["providers"]
47
+ end
48
+ end
49
+
50
+ KERNEL ==> STORAGE
51
+ KERNEL ==> FS
52
+ KERNEL ==> PLUGINS
53
+
54
+ classDef ui fill:#bac8ff,stroke:#3b5bdb,stroke-width:1px,color:#000,stroke-dasharray: 5 3
55
+ class CLI,SERVER,SKILL driver
56
+ class REG,ORCH,UC,CONFIG kernel
57
+ class STORAGE,FS adapter
58
+ class EXT,ANA,ACT,HOOK,FMT,PROV plugin
11
59
  ```
12
- Driving adapters (primary)
13
-
14
- ┌─────────┐ ┌─────────┐ ┌──────┐
15
- │ CLI │ │ Server │ │Skill │
16
- └────┬────┘ └────┬────┘ └───┬──┘
17
- │ │ │
18
- └─────────────────┼────────────────┘
19
-
20
- ┌──────────────┐
21
- │ Kernel │ ← domain core
22
- │ │
23
- │ Registry │
24
- │ Orchestrator│
25
- │ Use cases │
26
- └──┬───┬───┬───┘
27
- │ │ │
28
- ┌─────────────┘ │ └──────────────┐
29
- ▼ ▼ ▼
30
- ┌────────┐ ┌─────────┐ ┌─────────┐
31
- │ Storage│ │ FS │ │ Plugins │
32
- └────────┘ └─────────┘ └─────────┘
33
- Driven adapters (secondary)
34
- ```
60
+
61
+ The UI is **not** a driving adapter; it is an HTTP/WS client of the Server. Exactly one Provider is active per project (see §Active Provider Lens), and config layering is always project-scoped (see §Config layering).
35
62
 
36
63
  - **Driving adapters** call into the kernel. The spec defines three: `CLI`, `Server`, `Skill`. A fourth driving adapter MAY be built by third parties (IDE extension, VSCode command palette, TUI) without spec changes.
37
64
  - **Driven adapters** implement ports the kernel declares. An implementation MUST ship adapters for every port, no port may be left unimplemented at runtime.
@@ -322,13 +349,18 @@ The lookup uses the ACTIVE PROVIDER LENS deliberately, mirroring the extractor g
322
349
 
323
350
  Each Provider MAY declare an optional `reservedNames: Record<kind, string[]>` map listing, for each `node.kind` the runtime owns, the set of invocation names the runtime itself consumes. Anthropic's Claude CLI reserves `/help`, `/clear`, `/init`, `/agents`, `/model`, `/cost`, `/compact`, `/login`, `/logout`, … under `command`, and `general-purpose`, `output-style-setup`, `statusline-setup` under `agent`; a user-authored `.claude/commands/help.md` is silently shadowed at runtime (the built-in runs, the file is ignored).
324
351
 
325
- The kernel intersects each Provider's `reservedNames[kind]` catalog with the scanned graph at orchestrator time: for every node, the post-walk pipeline derives its normalised identifiers via the §Provider · kind identifiers contract and asks "does any identifier fall in `reservedNames[node.kind]` for this node's Provider?". Matches land in a per-scan `Set<nodePath>` consumed by two surfaces:
352
+ The kernel intersects each Provider's `reservedNames[kind]` catalog with the scanned graph at orchestrator time. For every node the post-walk pipeline derives its normalised identifiers via the §Provider · kind identifiers contract, then tests them against a reserved set resolved under **two scopes**:
353
+
354
+ 1. **Self scope.** `reservedNames[node.kind]` of the node's OWN Provider (`node.provider`). The self-contained case: Claude classifies `.claude/commands/help.md` as `claude`/`command` and reserves `help` under `command`, so the file is flagged.
355
+ 2. **Lens scope.** When a Provider is the active lens (`activeProvider === provider.id`) it ALSO lends its catalog to nodes that a *universal* (ungated) Provider classified, matched by `node.kind`. This is required by runtimes that adopt the open `.agents/skills/` standard instead of a vendor directory: their user invocables are owned by the neutral `agent-skills` Provider (`kind: skill`), not by the vendor Provider, so self scope alone would never reach them. Google's Antigravity is exactly this shape, it is metadata-only (classifies nothing) and reserves its `agy` built-in slash commands under `skill`; when `activeProvider === 'antigravity'`, a user `.agents/skills/goal/SKILL.md` is flagged because `/goal` is a built-in. Lens scope is skipped when `node.provider === activeProvider` (it would duplicate self scope) and when no lens is resolved (`activeProvider === null`).
356
+
357
+ A node's identifiers are always derived from its OWN kind contract; only the reserved-set lookup widens under the lens. A node landing in either scope's set joins a per-scan `Set<nodePath>` consumed by two surfaces:
326
358
 
327
359
  1. **The `core/name-reserved` analyzer projects one `warn` issue per reserved-shadow node** (`severity: 'warn'`, message points at the offending file and suggests renaming). The analyzer is a pure projector, detection lives in the orchestrator so the same set drives the next surface.
328
360
 
329
361
  2. **The post-walk confidence-lift transform downgrades any link that resolves to a reserved target** (by path OR by name match) to `RESERVED_TARGET_CONFIDENCE = 0.1` instead of bumping it to `1.0`. The visual weight in the graph drops well below the `0.5` / `0.8` extractor emit floors so the operator sees at a glance that the edge resolves to a file the runtime ignores. When the trigger has multiple candidates (name index collision) and the strict-kind filter accepts more than one, the resolver picks the first allowed candidate, if it is non-reserved, the link bumps to `1.0` normally; only when EVERY accepted candidate is reserved does the downgrade apply.
330
362
 
331
- The lookup normalises both sides through the §Extractor · trigger normalization pipeline, so a literal `Init-Project` in the manifest still matches a user `name: init project` or filename `Init-Project.md`. The catalog is intentionally per-kind, not global: a name reserved for commands (`/help`) MAY legitimately appear as a skill (a "help" skill triggered through a non-command channel).
363
+ The lookup normalises both sides through the §Extractor · trigger normalization pipeline, so a literal `Init-Project` in the manifest still matches a user `name: init project` or filename `Init-Project.md`. The catalog is intentionally per-kind, not global: a name reserved for commands (`/help`) MAY legitimately appear as a skill (a "help" skill triggered through a non-command channel). Lens scope respects the same per-kind boundary: Antigravity declares its reserved names under `skill` (not `command`) precisely because the invocable they shadow is a skill file, so only `skill`-kind nodes are tested against it.
332
364
 
333
365
  **Update policy.** Built-in catalogs drift as vendor runtimes evolve. Each catalog change ships as a kernel patch with a changeset entry; the catalog is considered API surface that users rely on the analyzer to reflect. User-installed Providers MAY declare their own `reservedNames` with the same shape; the analyzer and the downgrade run uniformly across built-in and user-installed Providers.
334
366
 
@@ -338,7 +370,7 @@ Default `undefined` ≡ empty map ≡ no reserved names. Path matches against no
338
370
 
339
371
  The `Extractor` runtime contract is `extract(ctx) → void`. The extractor emits its work through three callbacks the kernel binds onto `ctx`:
340
372
 
341
- - `ctx.emitLink(link)`, append a `Link` to the kernel's `links` table. The kernel validates the link against the extractor's declared `emitsLinkKinds` before persistence; off-contract links are dropped and surface as `extension.error` events. URL-shaped targets (`http(s)://…`) are partitioned out into `node.externalRefsCount` and never persisted.
373
+ - `ctx.emitLink(link)`, append a `Link` to the kernel's `links` table. The kernel validates `link.kind` against the **global closed enum** of link kinds (`invokes`, `references`, `mentions`, `supersedes`) before persistence; off-enum links are dropped and surface as `extension.error` events (the per-extractor `emitsLinkKinds` allowlist was retired with the structure-as-truth refactor; confidence is declared per emit, default `'medium'`). URL-shaped targets (`http(s)://…`) are partitioned out into `node.externalRefsCount` and never persisted.
342
374
  - `ctx.enrichNode(partial)`, merge canonical, kernel-curated properties onto the current node's enrichment layer (persisted into [`node_enrichments`](./db-schema.md#node_enrichments)). **Strictly separate from the author-supplied frontmatter** (the latter remains immutable across scans). The enrichment layer is the right home for kernel-derived facts (computed titles, summaries, signals an Extractor inferred from the body) without polluting what the user wrote on disk. See §Enrichment layer below for the full lifecycle (per-extractor attribution, refresh verbs).
343
375
  - `ctx.store`, plugin-scoped persistence. Optional, present only when the plugin declares `storage.mode` in `plugin.json`. Shape depends on the mode (`KvStore` for mode A, scoped `Database` for mode B). See [`plugin-kv-api.md`](./plugin-kv-api.md). The plugin author MAY opt into shape validation for their own writes by declaring `storage.schema` (Mode A) or `storage.schemas` (Mode B) in the manifest, JSON Schemas the kernel AJV-compiles at load time and runs against every `ctx.store.set(key, value)` / `ctx.store.write(table, row)` call. Absent = permissive (status quo). `emitLink` and `enrichNode` keep their universal validation against `link.schema.json` / `node.schema.json` regardless of this opt-in. See [`plugin-author-guide.md` §`outputSchema`](./plugin-author-guide.md#outputschema--opt-in-correctness-for-custom-storage-writes).
344
376
 
@@ -380,9 +412,9 @@ Analyzers / `sm check` / `sm export` consume `node.frontmatter` directly (determ
380
412
 
381
413
  Refresh verbs (`sm refresh <node>` and `sm refresh --stale`) re-run the Extractor pipeline against a node or the stale set and upsert fresh enrichment rows, see [`cli-contract.md` §Scan](./cli-contract.md#scan). With Extractors deterministic-only, `--stale` is a no-op today (no rows are stale-flagged); it remains in the contract for the future Action-prob enrichment revision noted above.
382
414
 
383
- ### Extractor · `applicableKinds` filter
415
+ ### Extractor · `precondition` filter
384
416
 
385
- Extractors MAY declare an optional `applicableKinds: string[]` on their manifest. When declared, the kernel filters fail-fast: `extract()` is invoked **only** for nodes whose `kind` appears in the list. The skip happens BEFORE the extractor context is built so the extractor wastes zero CPU on inapplicable nodes. Absent (`undefined`) is the default and means "applies to every kind"; there is no wildcard syntax. An empty array (`[]`) is invalid (`minItems: 1` in the schema). Unknown kinds (no installed Provider declares them in its `kinds` catalog) are non-blocking: the extractor keeps `loaded` status and `sm plugins doctor` surfaces an informational warning so the author sees typos and missing-Provider cases, but the doctor's exit code is NOT promoted by this warning. See [`plugin-author-guide.md` §Extractor `applicableKinds`](./plugin-author-guide.md#extractor-applicablekinds--narrow-the-pipeline) for the full author-side contract.
417
+ Extractors MAY declare an optional `precondition` block (`{ kind?: string[]; provider?: string[] }`, the same shape Analyzers and Actions share). When declared, the kernel filters fail-fast: `extract()` is invoked **only** for nodes that satisfy every declared sub-filter (`kind` lists qualified `<plugin>/<kindName>` ids; `provider` lists plugin ids; both apply as AND). The skip happens BEFORE the extractor context is built so the extractor wastes zero CPU on inapplicable nodes. Absent (`undefined`) is the default and means "applies to every kind"; there is no wildcard syntax. Unknown qualified kinds (no installed Provider declares them) are non-blocking: the extractor keeps `loaded` status and `sm plugins doctor` surfaces an informational `precondition-kind-unknown` warning so the author sees typos and missing-Provider cases, but the doctor's exit code is NOT promoted by this warning. See [`plugin-author-guide.md` §`precondition`](./plugin-author-guide.md#extractor--analyzer--action-precondition-narrow-the-pipeline) for the full author-side contract.
386
418
 
387
419
  ### Extractor · fine-grained scan cache
388
420
 
@@ -433,16 +465,11 @@ Characters outside the separator set that are not letters or digits (e.g. `/`, `
433
465
  | `@FooExtractor` | `@fooextractor` |
434
466
  | `skill-map:explore` | `skill map:explore` |
435
467
 
436
- ### Analyzer · `recommendedActions` hint
468
+ ### Analyzer Action relationship (Modelo B)
437
469
 
438
- An Analyzer MAY declare `recommendedActions: string[]` in its manifest, listing the qualified ids (`<plugin-id>/<extension-id>`) of the per-node Actions that resolve its findings. The UI surfaces matching Actions in the node inspector under "Recommended for issues" whenever the analyzer emitted against the focused node, alongside the always-applicable list driven by the Action's own precondition (see [`schemas/extensions/action.schema.json`](./schemas/extensions/action.schema.json)).
470
+ The "which Action resolves this analyzer's findings?" relationship is declared from the **Action** side, not the Analyzer side (the `Analyzer.recommendedActions` map was retired with the structure-as-truth refactor). An Action's `precondition.analyzerIds: string[]` lists the qualified ids of the analyzers whose findings it is intended to resolve. The UI joins on this field: when an analyzer emitted against the focused node, the inspector surfaces every Action whose `precondition.analyzerIds` includes that analyzer, under "Recommended for issues", alongside the always-applicable list driven by the rest of the Action's `precondition`.
439
471
 
440
- The two surfaces are deliberately split:
441
-
442
- - **`Action.precondition`**, declared on the Action side. Answers "which nodes does this Action apply to?". Evaluated continuously against the node the inspector is focused on, regardless of any issue.
443
- - **`Analyzer.recommendedActions`**, declared on the Analyzer side. Answers "when this analyzer fires, which Actions are the natural fix?". Surfaces only on nodes the analyzer emitted against.
444
-
445
- Each `recommendedActions` entry MUST be the qualified id of a registered Action. The kernel logs an `extension.error` event with `kind: 'recommended-action-missing'` when a referenced action is not loaded; the analyzer stays registered and continues emitting issues, only the recommendation hint is dropped. Project-level cleanup verbs (orphan file prune, contribution relink) are CLI commands, not Actions, and are NOT linked through this field. Analyzers whose issues surface deliberate user declarations rather than fixable problems (e.g. `core/node-superseded`) omit the field.
472
+ The two surfaces stay distinct: the `kind` / `provider` sub-filters answer "which nodes does this Action apply to?" (evaluated continuously against the focused node); `analyzerIds` answers "when which analyzer fires is this Action the natural fix?" (surfaces only on nodes the named analyzer emitted against). Project-level cleanup verbs (orphan file prune, contribution relink) are CLI commands, not Actions, and are NOT linked through this field. Actions resolving deliberate user declarations rather than fixable problems omit `analyzerIds`.
446
473
 
447
474
  ### Hook · curated trigger set
448
475
 
@@ -457,7 +484,8 @@ Hooks subscribe declaratively to a curated set of kernel lifecycle events and re
457
484
  | `analyzer.completed` | Once per Analyzer, after every issue has been validated. | `analyzerId: string` (qualified). | Per-Analyzer alerting, downstream tooling. |
458
485
  | `action.completed` | Once per Action invocation, after the report has been recorded. | `actionId: string` (qualified), `node`, `jobResult`. | Per-Action notification, integration glue. |
459
486
  | `job.spawning` | Pre-spawn of a runner subprocess (job subsystem; Step 10). | `jobId`, `actionId`, spawn metadata. | Pre-flight checks, audit logging. |
460
- | `job.spawning`, `job.completed`, `job.failed` | The three job-lifecycle hookables; same payload shapes as the [`job-events.md`](./job-events.md) entries of the same name. | See [`job-events.md` §Event catalog](./job-events.md#event-catalog). | Most common Hook surface (notifications, retries, billing). |
487
+ | `job.completed` | Once per job that finishes successfully (job subsystem; Step 10). Same payload shape as the [`job-events.md`](./job-events.md) entry of the same name. | See [`job-events.md` §Event catalog](./job-events.md#event-catalog). | Most common Hook surface (notifications, retries, billing). |
488
+ | `job.failed` | Once per job that fails (job subsystem; Step 10). Same payload shape as the [`job-events.md`](./job-events.md) entry of the same name. | See [`job-events.md` §Event catalog](./job-events.md#event-catalog). | Alerting, retry triggers. |
461
489
  | `shutdown` | Once per CLI process invocation, AFTER the verb returns its exit code and BEFORE `process.exit`. The dispatcher awaits subscribed hooks so they finish before the process terminates, but every hook MUST be fast (the user already saw the verb's output and is waiting for the prompt back). The dispatcher catches every hook error so a buggy hook never alters the verb's exit code; it can only delay the exit. | `exitCode: number` (the verb's resolved exit code, `0..5`). | Cleanup, post-run telemetry, the `core/update-check` banner. |
462
490
 
463
491
  A hook MAY narrow further with an optional declarative `filter` map: keys are payload field paths (top-level only in v0.x); values are the literal expected match. The dispatcher walks `event.data` for each declared key and short-circuits the invocation when any value disagrees. Examples:
@@ -638,9 +666,9 @@ The flag lives in `project-local` (gitignored) so each collaborator consents ind
638
666
 
639
667
  ### Plugin contributions
640
668
 
641
- Plugins extend the annotation surface via the `annotationContributions` manifest field, a map of contributed key → `{ schema, ownership, location }`. Inline JSON Schema (no `$ref` to external files). Two location modes:
669
+ Plugins extend the annotation surface via the optional `annotation` block on an extension manifest (`{ schema, ownership?, location? }`, inline JSON Schema, no `$ref` to external files). It is a **single** declaration per extension and **the contributed key is the extension's id** (its folder name); an extension that needs several keys splits into several extensions, one per key. Two location modes:
642
670
 
643
- - `location: 'namespaced'` (default), writes go to the plugin's `<plugin-id>:` block at the sidecar root. Default `ownership: 'shared'`. Plugins write to their own namespace without coordination; AJV validates contributed keys against the plugin's declared schema.
671
+ - `location: 'namespaced'` (default), writes go to the plugin's `<plugin-id>:` block at the sidecar root. Default `ownership: 'shared'`. Plugins write to their own namespace without coordination; AJV validates the contributed value against the extension's declared schema.
644
672
  - `location: 'root'`, writes go to a top-level key of the sidecar (alongside `identity` / `annotations` / `settings` / `audit`). Requires `ownership: 'exclusive'` (claiming a root key is elevated trust). Two plugins claiming the same root key with `exclusive` is a **hard fatal** at orchestrator startup, the kernel refuses to boot rather than route writes ambiguously.
645
673
 
646
674
  The kernel exposes a runtime catalog (`Kernel.getRegisteredAnnotationKeys()`) listing every plugin-contributed key with its `pluginId`, `location`, `ownership`, and `schema`, consumed by the BFF (`GET /api/annotations/registered`) for UI autocomplete.
@@ -654,22 +682,17 @@ Two columns on `scan_nodes` source from the sidecar's `annotations:` block when
654
682
 
655
683
  A `scan_nodes.annotations_json` column carries the full parsed `annotations:` block; `sidecar_present` and `sidecar_status` carry the drift-detection state. The full sidecar overlay (parsed `annotations`, `status`, `present`) is exposed on `Node.sidecar` so REST and UI consumers see it as part of the canonical wire shape.
656
684
 
657
- ### Tags · dual-source
658
-
659
- Skill-map's tag system is **dual-source** by design:
685
+ ### Tags
660
686
 
661
- - **Author tags** live in `frontmatter.tags` (in the `.md`). Universal optional field declared on [`schemas/frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) so every Provider's per-kind schema accepts it without each having to redeclare. These represent intrinsic categories the file's author wrote into the frontmatter (vendor-supplied or your own writing).
662
- - **User tags** live in `sidecar.annotations.tags` (in the `.sm`). Curated annotation field declared on [`schemas/annotations.schema.json`](./schemas/annotations.schema.json). These represent the post-hoc tags whoever curates the project assigned to the node from their sidecar.
687
+ Tags are a **skill-map concept**, not a vendor field: no agent format (Claude, Cursor, Obsidian, the Agent Skills open standard, …) carries `tags` in its frontmatter, so skill-map keeps them where it owns the surface, the `.sm` sidecar.
663
688
 
664
- The two surfaces are **not aliases**. They capture different intent layers and both are first-class:
689
+ - **Tags** live in `sidecar.annotations.tags` (in the `.sm`). Curated annotation field declared on [`schemas/annotations.schema.json`](./schemas/annotations.schema.json). These are the tags whoever curates the project assigned to the node from their sidecar.
665
690
 
666
- - Search and listings (`sm list --tag <name>`, UI faceted search) match the **union**: a hit on either source returns the node.
667
- - The optional `--tag-source author|user` flag filters one source.
668
- - The UI distinguishes them visually so the attribution stays explicit (different chip style; author chips render first, user chips after).
691
+ Search and listings (`sm list --tag <name>`, UI faceted search) match this field: a hit returns the node. The UI renders them as chips on the node card and in the inspector.
669
692
 
670
- Persistence layer projects rows into a normalized [`scan_node_tags`](./db-schema.md#scan_node_tags) table at write time, one row per `(node_path, tag, source)` triple, so SQL queries can index on `(tag)` for `O(log n)` lookup. Replace-all per scan keeps the table in sync with the live frontmatter + sidecar state; deleting a tag from either source removes its row on the next scan.
693
+ Persistence layer projects rows into a normalized [`scan_node_tags`](./db-schema.md#scan_node_tags) table at write time, one row per `(node_path, tag)` pair, so SQL queries can index on `(tag)` for `O(log n)` lookup. Replace-all per scan keeps the table in sync with the live sidecar state; deleting a tag from a sidecar removes its row on the next scan.
671
694
 
672
- The wire shape (`/api/nodes` and `/api/nodes/:pathB64`) projects `node.tags = { byAuthor: string[], byUser: string[] }` so consumers see the split with attribution. The kernel `Node` interface (TypeScript) does NOT carry `tags`, consumers that walk the canonical sources read `node.frontmatter.tags` and `node.sidecar.annotations.tags` directly (consistent with the post-decision-#2 posture of "no Node-level denormalisations").
695
+ The wire shape (`/api/nodes` and `/api/nodes/:pathB64`) projects `node.tags = string[]`. The kernel `Node` interface (TypeScript) does NOT carry `tags`, consumers that walk the canonical source read `node.sidecar.annotations.tags` directly (consistent with the post-decision-#2 posture of "no Node-level denormalisations").
673
696
 
674
697
  ### Stability
675
698
 
@@ -760,7 +783,7 @@ ctx.emitContribution(contributionId, payload);
760
783
  ctx.emitContribution(nodePath, contributionId, payload);
761
784
  ```
762
785
 
763
- Parallel to `ctx.emitLink(link)`. The kernel buffers the emission, validates the payload against the slot's payload schema in `$defs/payloads/<slot>` (AJV-compiled at boot), and persists the row to `scan_contributions` during `persistScanResult`. Off-shape payloads emit an `extension.error` event and drop silently, same posture as `emitLink` rejecting off-`emitsLinkKinds` links. Both Extractor and Analyzer emissions land in the same `scan_contributions` rows; the row's `extension_id` records which kind of extension produced it.
786
+ Parallel to `ctx.emitLink(link)`. The kernel buffers the emission, validates the payload against the slot's payload schema in `$defs/payloads/<slot>` (AJV-compiled at boot), and persists the row to `scan_contributions` during `persistScanResult`. Off-shape payloads emit an `extension.error` event and drop silently, same posture as `emitLink` rejecting off-enum link kinds. Both Extractor and Analyzer emissions land in the same `scan_contributions` rows; the row's `extension_id` records which kind of extension produced it.
764
787
 
765
788
  The Extractor-emit signature binds `nodePath` implicitly (the extractor runs per-node, with `ctx.node.path` available as the only sensible target). The Analyzer-emit signature requires the analyzer to declare the target node explicitly because Analyzers see the full graph at once and may emit for any subset of nodes, the canonical use case is a analyzer that derives per-node values from cross-graph aggregations (`core/link-counter` projects `linksOutCount` / `linksInCount` this way).
766
789
 
package/cli-contract.md CHANGED
@@ -56,8 +56,8 @@ Genuinely per-user, per-machine preferences live in a **single file**
56
56
  at `~/.skill-map/settings.json`, validated against
57
57
  [`user-settings.schema.json`](./schemas/user-settings.schema.json).
58
58
  The file holds preferences that have no project meaning (the update-
59
- check toggle + its throttle bookkeeping today; future locale, theme,
60
- etc.). Constraints:
59
+ check toggle + its throttle bookkeeping, and the telemetry consent
60
+ flag today; future locale, theme, etc.). Constraints:
61
61
 
62
62
  - **One file, no `.local` partner**: values here are already
63
63
  per-machine, so the project / project-local split has no meaning.
@@ -69,13 +69,41 @@ etc.). Constraints:
69
69
  (only preferences whose value is meaningless inside a project).
70
70
  Anything that belongs to a project goes in
71
71
  `<cwd>/.skill-map/settings.json` instead.
72
- - **Closed list of writers**: today only the update-check store
73
- (`src/cli/util/update-check-store.ts` in the reference impl)
74
- reads and writes the file. New user-scope features extend that
75
- module rather than opening new home access points.
72
+ - **Closed list of writers**: a single user-settings store module
73
+ (`src/cli/util/user-settings-store.ts` in the reference impl) is the
74
+ only reader / writer of the file. Every user-scope feature (the
75
+ update-check toggle, the telemetry consent flag) goes through it
76
+ rather than opening new home access points.
76
77
 
77
78
  Everything else under `$HOME` MUST NOT be touched.
78
79
 
80
+ ### Telemetry consent
81
+
82
+ skill-map sends nothing off the machine by default. Opt-in, anonymous
83
+ **error reporting** is the one documented exception, governed in full by
84
+ [`telemetry.md`](./telemetry.md). The operator-facing contract:
85
+
86
+ - **Default OFF.** The `telemetry.errorsEnabled` flag in
87
+ `~/.skill-map/settings.json` is absent until the operator decides. Absent
88
+ or `false` means no telemetry SDK is loaded and nothing is sent, on every
89
+ surface (CLI, BFF, UI), with zero added latency.
90
+ - **Consent prompt (interactive terminals only, second eligible run).** The
91
+ CLI MAY show a one-time consent prompt (yes (default) / no / details),
92
+ but NOT on the operator's first eligible run: that run only records
93
+ `telemetry.firstRunAt` and stays silent so the prompt does not stack on
94
+ the first-`sm scan` provider-lens prompt. The next eligible run shows it,
95
+ persists the choice, and stamps `telemetry.promptedAt` (so it is never
96
+ shown again). When stdout is not a TTY (CI, pipes), nothing is asked or
97
+ recorded and the state stays OFF.
98
+ - **Kill switch.** `SKILL_MAP_TELEMETRY=0` forces OFF everywhere regardless
99
+ of the persisted flag. There is no env value that forces ON.
100
+ - **No `sm config` key.** The flag is per-machine, so it lives in the
101
+ user-settings file, not in project config. `sm config` writes project-local
102
+ settings only and MUST NOT surface this key. Consent is changed after the
103
+ first run through the Settings UI (persisted via the BFF), mirroring how
104
+ the update-check toggle works. A future `sm telemetry` verb
105
+ family MAY expose CLI status / toggling; it is not part of this level.
106
+
79
107
  ### Active provider lens
80
108
 
81
109
  The project sees its filesystem through exactly one **active provider lens** at any time. The lens is persisted as `activeProvider` in `<cwd>/.skill-map/settings.json` (see [`project-config.schema.json`](./schemas/project-config.schema.json#/properties/activeProvider) and the [`architecture.md` §Active Provider Lens](./architecture.md#active-provider-lens) section for the full architectural rationale).
@@ -204,7 +232,7 @@ Exit: 0 if all green, 1 if warnings, 2 if any `error`-level problem.
204
232
  Self-describing introspection.
205
233
 
206
234
  - `human` (default): pretty terminal output.
207
- - `md`: canonical markdown for documentation sites. Implementations MUST NOT hand-maintain equivalent markdown; `context/cli-reference.md` (in the reference impl) is regenerated from this output in CI.
235
+ - `md`: canonical markdown for documentation sites. Implementations MUST NOT hand-maintain equivalent markdown; it is generated on demand from this output.
208
236
  - `json`: structured surface dump. Shape:
209
237
 
210
238
  ```json
@@ -285,6 +313,8 @@ The watcher subscribes to the same roots that `sm scan` walks and respects `.ski
285
313
 
286
314
  **Node cap** (`--max-nodes <N>`): on `sm scan` and `sm watch` (alias `sm scan --watch`), a hard cap on the number of files the walker accepts after `.skillmapignore` filtering, before extractors run. Default comes from `scan.maxNodes` (default 256). The flag is a full override of the setting and is **bidirectional**: it can raise the cap (`--max-nodes 1000` on a 312-file repo) or lower it (`--max-nodes 100` cuts deeper than the default). When the walker reaches the cap, additional files are dropped in stable provider-walker order and the scan is marked oversized in `scan_meta` (columns `recommended_node_limit` and `override_max_nodes`), the resulting `ScanResult` envelope carries `recommendedNodeLimit` and `overrideMaxNodes` so the UI raises a persistent banner pointing at the `.skillmapignore` editor in Settings → Project. The CLI prints a human-mode notice naming both escapes: edit `.skillmapignore` (preferred, trims permanently) or re-run with `--max-nodes <N>` (force, graph quality may degrade past the recommended limit). `sm refresh` operates on a single already-classified node, so the cap does not apply there. Validation: integer ≥ 1, anything else exits `2` operational.
287
315
 
316
+ **Schema-drift rebuild (pre-1.0)**: before persisting, `sm scan` and `sm watch` compare `scan_meta.scanned_by_version` against the running CLI. A minor or major difference means the local cache predates a schema change, so the DB is deleted and rebuilt from scratch by this run (`.sm` sidecars are untouched, they are the source of truth). On an interactive terminal the rebuild is confirmed first; `--yes` (and every non-interactive caller: piped stdin, CI, the BFF, the watcher) rebuilds without prompting. Declining aborts the scan (exit `2`) without deleting anything. Patch-level differences are compatible and never trigger a rebuild. Read-only verbs keep the version-skew advisory instead of rebuilding. See [`db-schema.md` §Schema drift (pre-1.0)](./db-schema.md#schema-drift-pre-10).
317
+
288
318
  Exit: 0 on clean (or clean watcher shutdown), 1 if error-severity issues exist (one-shot scan only, the watcher does not flip exit code based on per-batch issues), 2 on operational error.
289
319
 
290
320
  ---
@@ -658,7 +688,7 @@ The `/ws` endpoint is the live-events channel for the SPA. Clients connect once
658
688
  ### Introspection
659
689
 
660
690
  - `sm help --format json`, structured CLI surface dump.
661
- - `sm help --format md`, canonical markdown, CI-enforced for the reference impl's `context/cli-reference.md`.
691
+ - `sm help --format md`, canonical markdown, generated on demand (not a committed artifact).
662
692
 
663
693
  These two formats are NORMATIVE: any change to verbs, flags, or exit codes MUST reflect in `--format json` output immediately. Third-party consumers rely on this.
664
694
 
@@ -742,7 +772,6 @@ The `done in …` stderr line, its format grammar, and the `elapsedMs` field con
742
772
  - [`job-lifecycle.md`](./job-lifecycle.md), state machine behind `sm job` verbs.
743
773
  - [`job-events.md`](./job-events.md), event stream emitted via `--json` and `--stream-output`.
744
774
  - [`db-schema.md`](./db-schema.md), tables behind `sm db` verbs.
745
- - [`../context/cli-reference.md`](../context/cli-reference.md), auto-generated reference from `sm help --format md`.
746
775
  - [`conformance/`](./conformance/README.md), test suite exercising CLI behavior.
747
776
 
748
777
  ---
@@ -51,7 +51,7 @@ Fixtures are read-only inputs. Cases declare what to invoke and what to assert.
51
51
 
52
52
  ## Case format
53
53
 
54
- Cases are validated against [`conformance-case.schema.json`](../schemas/conformance-case.schema.json). That file is the normative shape; this section is the human-readable walkthrough. Include `"$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json"` in every case file for IDE support.
54
+ Cases are validated against [`conformance-case.schema.json`](../schemas/conformance-case.schema.json). That file is the normative shape; this section is the human-readable walkthrough. Include `"$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json"` in every case file for IDE support.
55
55
 
56
56
  A case is a JSON document with this shape:
57
57
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
2
+ "$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
3
3
  "id": "extractor-emits-signal",
4
4
  "description": "Signal IR resolver phase, end-to-end. A body that contains a single `[text](path)` markdown link MUST flow through the Signal IR resolver (Phase 2 of the active-lens migration): `core/markdown-link` emits a single-candidate Signal, the resolver materialises the winning candidate as a Link, and the result lands in `scan.links` with the same shape a direct `emitLink` call would have produced. Locks the contract that the Signal IR path coexists with the direct-emit path and produces indistinguishable Link rows.",
5
5
  "fixture": "signal-ir-single-signal",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
2
+ "$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
3
3
  "id": "kernel-empty-boot",
4
4
  "description": "With every Provider, extractor, and analyzer disabled, scanning an empty scope MUST return a valid, zero-filled ScanResult. Enforces the kernel boot invariant from architecture.md.",
5
5
  "setup": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
2
+ "$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
3
3
  "id": "no-global-scope",
4
4
  "description": "Skill-map operates exclusively on the project scope. Implementations MUST NOT expose a `-g/--global` flag (the historical opt-in for a global / user scope) on any verb. Passing the flag to any verb MUST be rejected as an unknown option (exit `2`, usage error) without writing any state. Guards the principle declared in `cli-contract.md` §Scope is always project-local.",
5
5
  "invoke": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
2
+ "$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
3
3
  "id": "orphan-markdown-fallback",
4
4
  "description": "spec 0.18.0 universal markdown fallback. A `.md` file no vendor-specific Provider classifies (e.g. `ARCHITECTURE.md` at the project root) MUST be picked up by the built-in `core/markdown` Provider, classified as kind `markdown`, and attributed to the `markdown` provider id. The orchestrator's path-dedup ensures vendor Providers retain priority on files inside their territory (`.claude/agents/reviewer.md` here stays with `claude` as `agent`). Locks the contract that markdown is provider-agnostic and the kernel emits no privileged kinds.",
5
5
  "fixture": "orphan-markdown",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
2
+ "$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
3
3
  "id": "plugin-missing-ui-rejected",
4
4
  "description": "A drop-in Provider plugin whose `kinds[*]` entry omits the required `ui` block (Step 14.5.d) MUST be rejected by the loader with a clear `missing required property 'ui'` diagnostic on stderr, AND `sm scan` MUST exit cleanly with the rest of the pipeline (built-in Claude Provider) still running. Locks the contract that one bad plugin does not take down the scan.",
5
5
  "fixture": "plugin-missing-ui",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
2
+ "$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
3
3
  "id": "sidecar-end-to-end",
4
4
  "description": "Step 9.6.6 — co-located `.sm` sidecar end-to-end. Scanning a fixture that carries a stale sidecar (wrong `identity.{bodyHash,frontmatterHash}`) plus an orphan sidecar (no sibling `.md`) MUST surface `sidecar.status` on the node and emit both `annotation-stale` (per stale node) and `annotation-orphan` (per orphan `.sm`) issues from the built-in core analyzers.",
5
5
  "fixture": "sidecar-end-to-end",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
2
+ "$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
3
3
  "id": "signal-collision-detection",
4
4
  "description": "Signal IR resolver phase, range-overlap collision. A body that contains `[@./api.md](./api.md)` triggers a cross-extractor range overlap: `core/markdown-link` matches the whole bracketed-and-parenthesised span; `claude/at-directive` matches the `@./api.md` token INSIDE the bracket text. The two byte ranges overlap (the at-directive range is a strict subset of the markdown-link range). The kernel resolver picks ONE winner per the four-step tiebreak (`kind-priority` -> `higher-confidence` -> `longer-range` -> `earlier-declaration`); markdown-link wins on confidence (1.0 vs 0.85). The resolver materialises the winner as a Link, marks the loser's `resolution.outcome === 'rejected'` with `rejectedBy` naming the winner, and the built-in `core/signal-collision` analyzer surfaces the rejection as ONE `warn` issue attached to the source node. Locks the contract that range-overlap collisions surface to the operator instead of being silently merged.",
5
5
  "fixture": "signal-ir-collision",
@@ -38,7 +38,7 @@ This file is hand-maintained. A CI check before spec release compares the schema
38
38
  | 27 | `annotations.schema.json` | `sidecar-end-to-end` | 🟢 covered | Curated catalog of 15 conventional skill-map annotation fields (versioning, supersession, provenance, lifecycle, taxonomy, display, docs). `additionalProperties: true` so users / plugins extend without coordination; the `unknown-field` Tier-1 analyzer shipped in Step 9.6.6 emits warnings on truly unrecognized keys. Step 9.6.2 (2026-05-05) wired the kernel reader; Step 9.6.6 (2026-05-06) flips this row 🟢 via `sidecar-end-to-end`, which asserts that an `annotations.version: 7` value round-trips through `state_scan_nodes.annotations_json` and surfaces in the node's `sidecar.annotations` overlay AND in the denormalised `Node.version` column. Structural sample at `fixtures/sidecar-example/agent-example.sm`. Catalog trimmed from 31 to 15 fields on 2026-05-07 after UX review. |
39
39
  | 28 | `bump-report.schema.json` |, | 🔴 missing | Report shape produced by the built-in deterministic `bump` Action (Step 9.6.3, Decision #125). Extends `report-base-deterministic.schema.json` (row 29), the deterministic counterpart to `report-base.schema.json` (which is LLM-specific via `confidence` + `safety`). Three concrete shapes: success-with-write, silent-no-op (under `force`), and refusal (`fresh`). Direct conformance case lands together with the `sm bump` CLI verb in Step 9.6.4, it'll exercise all three branches via `sm bump --json` against a primed fixture. Implementation tests at `src/test/bump-action.test.ts` cover the runtime behaviour today. |
40
40
  | 29 | `report-base-deterministic.schema.json` |, (indirect via row 28) | 🟡 partial | Deterministic counterpart to `report-base.schema.json`; every deterministic Action's report extends this base. Direct contract case still pending, landed when first conformance case directly validates a deterministic report against this schema. |
41
- | 30 | `view-slots.schema.json` |, | 🔴 missing | Closed catalog of 15 view slots + the `IViewContribution` manifest declaration shape + per-slot payload schemas. Cases required (3): (a) `plugin-view-contributions-valid`, a plugin manifest declaring contributions of every slot validates; (b) `plugin-view-contributions-invalid-slot`, a manifest referencing a slot not in the catalog rejects with `invalid-manifest`; (c) `plugin-view-contributions-payload-mismatch`, an extractor emitting an off-shape payload triggers `extension.error` and drops silently. Implementation lands with the kernel surface in Phase 2 of the UI contributions plan; conformance fixtures land alongside. |
41
+ | 30 | `view-slots.schema.json` |, | 🔴 missing | Closed catalog of 14 view slots + the `IViewContribution` manifest declaration shape + per-slot payload schemas. Cases required (3): (a) `plugin-view-contributions-valid`, a plugin manifest declaring contributions of every slot validates; (b) `plugin-view-contributions-invalid-slot`, a manifest referencing a slot not in the catalog rejects with `invalid-manifest`; (c) `plugin-view-contributions-payload-mismatch`, an extractor emitting an off-shape payload triggers `extension.error` and drops silently. Implementation lands with the kernel surface in Phase 2 of the UI contributions plan; conformance fixtures land alongside. |
42
42
  | 31 | `input-types.schema.json` |, | 🔴 missing | Closed catalog of 10 input-types for plugin settings + the `ISettingDeclaration` discriminated-union manifest shape. Cases required (2): (a) `plugin-settings-valid`, a plugin manifest declaring at least one setting of each input-type validates; (b) `plugin-settings-invalid-type`, a manifest referencing a `type` not in the catalog rejects with `invalid-manifest`. Lands together with the spec/CLI surface for `sm plugins config <id>`. |
43
43
  | 32 | `refresh-report.schema.json` |, | 🔴 missing | Machine-readable output of `sm refresh <node.path> --json` and `sm refresh --stale --json`. Reports the count of enrichment rows persisted across targeted nodes (universal enrichment layer per `architecture.md` §A.8). Direct conformance case pending: seed a fixture with one Provider-classified node, run `sm refresh <node> --json`, assert the envelope validates and `refreshed >= 0`. Implementation tests at `src/test/node-enrichments.test.ts` cover the runtime behaviour today. |
44
44
  | 33 | `plugins-doctor.schema.json` |, | 🔴 missing | Machine-readable output of `sm plugins doctor --json`. Aggregates per-status counts plus structured issue / warning lists. Direct conformance case pending: prime a scope with one healthy + one invalid-manifest drop-in plugin, run `sm plugins doctor --json`, assert the envelope validates and the invalid plugin appears under `issues[]`. Implementation tests at `src/test/plugins-cli.test.ts` cover the runtime behaviour. |
@@ -1,7 +1,7 @@
1
1
  # Sample sidecar matching agent-example.md.
2
2
  # Validates against:
3
- # - https://skill-map.dev/spec/v0/sidecar.schema.json (root shape)
4
- # - https://skill-map.dev/spec/v0/annotations.schema.json (annotations block)
3
+ # - https://skill-map.ai/spec/v0/sidecar.schema.json (root shape)
4
+ # - https://skill-map.ai/spec/v0/annotations.schema.json (annotations block)
5
5
  # Hashes are sha256 over the kernel's canonical forms (body bytes; canonical YAML
6
6
  # of the frontmatter via `js-yaml` dump with sortKeys+noCompatMode). Regenerate
7
7
  # when agent-example.md changes (the fixture is meant to be drift-free).
@@ -31,7 +31,7 @@ annotations:
31
31
  - typescript
32
32
  - quality
33
33
  hidden: false
34
- docsUrl: https://skill-map.dev/examples/code-reviewer
34
+ docsUrl: https://skill-map.ai/examples/code-reviewer
35
35
 
36
36
  settings: {}
37
37
 
package/db-schema.md CHANGED
@@ -161,7 +161,7 @@ No indexes (single row).
161
161
 
162
162
  Fine-grained cache breadcrumbs for the incremental scan path. One row per `(node_path, extractor_id)` recording the body hash the Extractor saw the last time it ran against that node. Replace-all on every `sm scan` so rows for Extractors that were uninstalled since the last scan disappear automatically.
163
163
 
164
- The orchestrator consults this table on `sm scan --changed`: a node-level cache hit (body+frontmatter unchanged) is upgraded to a full skip ONLY when every currently-registered Extractor (filtered by `applicableKinds`) has a row matching the prior body hash. A new Extractor registered between scans is detected by the absence of its row and runs over the cached node WITHOUT requiring a full cache invalidation. Without this table the cache silently bypassed any Extractor newly registered between scans, leaving its emissions missing on the next `--changed` pass; the same machinery is what a future Action-issued probabilistic enrichment revision will leverage to reuse paid LLM output across unchanged bodies.
164
+ The orchestrator consults this table on `sm scan --changed`: a node-level cache hit (body+frontmatter unchanged) is upgraded to a full skip ONLY when every currently-registered Extractor (filtered by its `precondition`) has a row matching the prior body hash. A new Extractor registered between scans is detected by the absence of its row and runs over the cached node WITHOUT requiring a full cache invalidation. Without this table the cache silently bypassed any Extractor newly registered between scans, leaving its emissions missing on the next `--changed` pass; the same machinery is what a future Action-issued probabilistic enrichment revision will leverage to reuse paid LLM output across unchanged bodies.
165
165
 
166
166
  | Column | Type | Constraint |
167
167
  |---|---|---|
@@ -222,7 +222,7 @@ Phase 3 / View contribution system. Per-node typed payloads emitted by extractor
222
222
  | `plugin_id` | TEXT | NOT NULL | Owning plugin namespace per spec § A.6. |
223
223
  | `extension_id` | TEXT | NOT NULL | Extension id within the plugin. |
224
224
  | `node_path` | TEXT | NOT NULL | FK semantically to `scan_nodes.path`; orphan-swept on persist when the parent node disappears. |
225
- | `contribution_id` | TEXT | NOT NULL | Manifest Record key under `extension.viewContributions[<contributionId>]`. |
225
+ | `contribution_id` | TEXT | NOT NULL | Manifest Record key under `extension.ui[<contributionId>]` (the runtime catalog keeps the historical name `viewContributions`). |
226
226
  | `slot` | TEXT | NOT NULL | Closed-enum-by-spec slot name; mirror of `view-slots.schema.json#/$defs/SlotName`. Kept open at the SQL layer (no CHECK) so catalog evolution does not need a DDL migration; `sm plugins upgrade` handles renames at the manifest layer. |
227
227
  | `payload_json` | TEXT | NOT NULL | JSON-serialised payload, already validated against the slot's payload schema (`view-slots.schema.json#/$defs/payloads/<slot>`) at emit time. Off-shape payloads emit `extension.error` and drop silently. |
228
228
  | `emitted_at` | INTEGER | NOT NULL | Unix milliseconds. |
@@ -246,19 +246,18 @@ NOT analogous to `state_plugin_kvs` (which is plugin-managed). Belongs to the `s
246
246
 
247
247
  ### `scan_node_tags`
248
248
 
249
- Tags · dual-source. One row per `(node_path, tag, source)` triple, projected at persist time from BOTH `frontmatter.tags` (with `source='author'`) and `sidecar.annotations.tags` (with `source='user'`). Drives `sm list --tag <name>` and the UI's tag-faceted search; the `(tag)` index keeps "find all nodes with tag X" `O(log n)`.
249
+ Tags. One row per `(node_path, tag)` pair, projected at persist time from `sidecar.annotations.tags`. Tags are a skill-map concept (no vendor carries `tags` in frontmatter), so the sidecar is the single source. Drives `sm list --tag <name>` and the UI's tag-faceted search; the `(tag)` index keeps "find all nodes with tag X" `O(log n)`.
250
250
 
251
251
  | Column | Type | Constraint |
252
252
  |---|---|---|
253
253
  | `node_path` | TEXT | NOT NULL | FK semantically to `scan_nodes.path`; orphan-swept on persist when the parent node disappears. |
254
254
  | `tag` | TEXT | NOT NULL | Free-form; case-preserving. Empty strings rejected upstream by the schema's `minLength: 1` on each item. |
255
- | `source` | TEXT | NOT NULL CHECK (source IN ('author','user')) | Hard split: `'author'` = `frontmatter.tags`, `'user'` = `sidecar.annotations.tags`. The same tag string MAY appear under both sources for the same node (the PK accepts the pair); `sm list --tag X` returns the node once via DISTINCT, the UI renders both chips with their attribution. |
256
255
 
257
- Primary key: `(node_path, tag, source)`. Indexes: `ix_scan_node_tags_tag` (search by tag), `ix_scan_node_tags_node_path` (per-node lookup, e.g. inspector projection).
256
+ Primary key: `(node_path, tag)`. Indexes: `ix_scan_node_tags_tag` (search by tag), `ix_scan_node_tags_node_path` (per-node lookup, e.g. inspector projection).
258
257
 
259
- **Persistence, replace-all per scan.** Every persisted scan rebuilds the table for the live node set: rows whose `node_path` is NOT in `livePaths` are dropped (orphan sweep, same as the contributions table); rows for nodes in the live set are wiped and re-inserted from the projected source state. Cached nodes' tag rows are projected from the cached `node.frontmatter.tags` / `node.sidecar.annotations.tags` (both already in memory), so the rebuild is cheap regardless of cache hit / miss. Storage is small, a 50-node project with avg 3 tags/node is ~150 rows ≈ 7.5 KB.
258
+ **Persistence, replace-all per scan.** Every persisted scan rebuilds the table for the live node set: rows whose `node_path` is NOT in `livePaths` are dropped (orphan sweep, same as the contributions table); rows for nodes in the live set are wiped and re-inserted from the projected sidecar state. Cached nodes' tag rows are projected from the cached `node.sidecar.annotations.tags` (already in memory), so the rebuild is cheap regardless of cache hit / miss. Storage is small, a 50-node project with avg 3 tags/node is ~150 rows ≈ 7.5 KB.
260
259
 
261
- The wire shape on `/api/nodes` joins this table to project `node.tags = { byAuthor: string[], byUser: string[] }`. The kernel `Node` interface (TypeScript) does NOT carry `tags`, consumers walking the canonical sources read `node.frontmatter.tags` and `node.sidecar.annotations.tags` directly (consistent with the post-decision-#2 posture).
260
+ The wire shape on `/api/nodes` joins this table to project `node.tags = string[]`. The kernel `Node` interface (TypeScript) does NOT carry `tags`, consumers walking the canonical source read `node.sidecar.annotations.tags` directly (consistent with the post-decision-#2 posture).
262
261
 
263
262
  ---
264
263
 
@@ -464,6 +463,21 @@ The kernel ALSO maintains `PRAGMA user_version` (or the engine equivalent) as a
464
463
 
465
464
  ---
466
465
 
466
+ ## Schema drift (pre-1.0)
467
+
468
+ The project DB is a derived cache: every `scan_*` row is regenerable, and the operator's authored data lives in `.sm` sidecars, not in the DB. While the kernel stays in `0.Y.Z` (see [`versioning.md` §Pre-1.0](./versioning.md#pre-10)) it does NOT ship incremental migrations to carry an existing DB across a schema change. Instead, a write-side open (`sm scan`, `sm watch`, and the BFF watcher) compares `scan_meta.scanned_by_version` against the running CLI version:
469
+
470
+ - **Same `major.minor`** (patch differences ignored): the cache is compatible. The open proceeds untouched.
471
+ - **Any minor or major difference**: the on-disk schema is treated as drifted. The entire DB file (plus its `-wal` / `-shm` sidecars) is deleted and recreated from the current `001_initial.sql`; the scan then repopulates it. No backup is written (the cache is derived). `state_*` and `config_*` are wiped along with `scan_*`; pre-1.0 they are accepted as transient. `.sm` sidecars are never touched.
472
+
473
+ The rebuild is confirmed interactively on a TTY `sm scan` unless `--yes` is passed; non-interactive callers (piped stdin, CI, the BFF scan route, the watcher) rebuild without prompting. Declining the prompt aborts the scan (exit `2`) without deleting anything.
474
+
475
+ Read-side verbs (`sm check`, `sm list`, `sm show`, ...) do NOT rebuild. They keep the version-skew advisory (warn on an older DB, refuse on a newer or different-major DB) so a read never silently discards the cache.
476
+
477
+ This is a pre-1.0 affordance. The first `1.0.0` replaces it with real up-only migrations (see §Migrations): drift detection by version becomes drift repair by migration, and `state_*` / `config_*` stop being disposable.
478
+
479
+ ---
480
+
467
481
  ## Plugin storage
468
482
 
469
483
  Two modes declared in `plugin.json` (see [`schemas/plugins-registry.schema.json`](./schemas/plugins-registry.schema.json)).