@skill-map/spec 0.19.0 → 0.21.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 (36) hide show
  1. package/CHANGELOG.md +607 -6
  2. package/README.md +6 -6
  3. package/architecture.md +129 -57
  4. package/cli-contract.md +71 -25
  5. package/conformance/README.md +2 -2
  6. package/conformance/cases/kernel-empty-boot.json +2 -2
  7. package/conformance/cases/sidecar-end-to-end.json +3 -3
  8. package/conformance/coverage.md +5 -5
  9. package/conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm +1 -1
  10. package/conformance/fixtures/sidecar-example/agent-example.md +1 -1
  11. package/db-schema.md +22 -18
  12. package/index.json +36 -36
  13. package/interfaces/security-scanner.md +2 -2
  14. package/job-events.md +12 -12
  15. package/job-lifecycle.md +1 -1
  16. package/package.json +1 -1
  17. package/plugin-author-guide.md +131 -82
  18. package/plugin-kv-api.md +6 -6
  19. package/prompt-preamble.md +1 -1
  20. package/schemas/annotations.schema.json +4 -4
  21. package/schemas/api/rest-envelope.schema.json +4 -4
  22. package/schemas/conformance-case.schema.json +2 -2
  23. package/schemas/extensions/analyzer.schema.json +43 -0
  24. package/schemas/extensions/base.schema.json +5 -5
  25. package/schemas/extensions/extractor.schema.json +1 -1
  26. package/schemas/extensions/hook.schema.json +6 -4
  27. package/schemas/issue.schema.json +6 -6
  28. package/schemas/link.schema.json +2 -2
  29. package/schemas/plugins-registry.schema.json +1 -1
  30. package/schemas/project-config.schema.json +19 -1
  31. package/schemas/sidecar.schema.json +2 -2
  32. package/schemas/summaries/agent.schema.json +1 -1
  33. package/schemas/summaries/command.schema.json +1 -1
  34. package/schemas/summaries/hook.schema.json +1 -1
  35. package/schemas/{view-contracts.schema.json → view-slots.schema.json} +93 -55
  36. package/schemas/extensions/rule.schema.json +0 -43
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/annotations.schema.json",
4
4
  "title": "Annotations",
5
- "description": "Catalog of conventional annotation fields skill-map ships out of the box, written into the `annotations:` block of a sidecar (`<basename>.sm`). Every field is OPTIONAL — a sidecar with an empty `annotations: {}` is valid. Schema is `additionalProperties: true` so users / plugins can add custom keys without coordination; the built-in `unknown-field` rule emits a warning on unrecognized keys (typo guard). The curated catalog is the load-bearing 13 fields below — versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`, `requires`, `conflictsWith`, `related`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), docs (`docsUrl`). The activity timestamp lives in the reserved `audit:` block (`audit.lastBumpedAt`), not in `annotations:`. Plugins that want first-class custom keys with their own validation declare `annotationContributions` in their manifest (see Step 9.6.6).",
5
+ "description": "Catalog of conventional annotation fields skill-map ships out of the box, written into the `annotations:` block of a sidecar (`<basename>.sm`). Every field is OPTIONAL — a sidecar with an empty `annotations: {}` is valid. Schema is `additionalProperties: true` so users / plugins can add custom keys without coordination; the built-in `unknown-field` analyzer emits a warning on unrecognized keys (typo guard). The curated catalog is the load-bearing 13 fields below — versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`, `requires`, `conflictsWith`, `related`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), docs (`docsUrl`). The activity timestamp lives in the reserved `audit:` block (`audit.lastBumpedAt`), not in `annotations:`. Plugins that want first-class custom keys with their own validation declare `annotationContributions` in their manifest (see Step 9.6.6).",
6
6
  "type": "object",
7
7
  "additionalProperties": true,
8
8
  "properties": {
@@ -19,7 +19,7 @@
19
19
  "supersedes": {
20
20
  "type": "array",
21
21
  "items": { "type": "string", "minLength": 1 },
22
- "description": "Paths (relative to scope root) of nodes this node replaces. Consumed by the built-in `superseded` rule and surfaces in `sm list --superseded`."
22
+ "description": "Paths (relative to scope root) of nodes this node replaces. Consumed by the built-in `superseded` analyzer and surfaces in `sm list --superseded`."
23
23
  },
24
24
  "supersededBy": {
25
25
  "type": "string",
@@ -29,7 +29,7 @@
29
29
  "requires": {
30
30
  "type": "array",
31
31
  "items": { "type": "string", "minLength": 1 },
32
- "description": "Paths (relative to scope root) of nodes this node depends on. Surfaces in the dependency graph; the `broken-ref` rule flags missing targets."
32
+ "description": "Paths (relative to scope root) of nodes this node depends on. Surfaces in the dependency graph; the `broken-ref` analyzer flags missing targets."
33
33
  },
34
34
  "conflictsWith": {
35
35
  "type": "array",
@@ -39,7 +39,7 @@
39
39
  "related": {
40
40
  "type": "array",
41
41
  "items": { "type": "string", "minLength": 1 },
42
- "description": "Paths (relative to scope root) of conceptually related nodes. Soft link for navigation; no strong semantics, no rule enforcement."
42
+ "description": "Paths (relative to scope root) of conceptually related nodes. Soft link for navigation; no strong semantics, no analyzer enforcement."
43
43
  },
44
44
  "authors": {
45
45
  "type": "array",
@@ -106,16 +106,16 @@
106
106
  },
107
107
  "contributionsRegistry": {
108
108
  "type": "object",
109
- "description": "Catalog of registered view contributions active in the current scope, keyed by qualified contribution id `<pluginId>/<extensionId>/<contributionId>`. Built once per server boot from every enabled extension's `viewContributions` map and embedded into every payload-bearing envelope so the UI can fetch the catalog without a separate request. Sentinel envelopes (`health`, `scan`, `graph`), action-result envelopes (`sidecar.bumped`), and the catalog envelopes themselves (`annotations.registered`, `contributions.registered`) are exempt. Mirror of `kindRegistry`, parallel surface. Each entry references a contract by name from the closed catalog at `view-contracts.schema.json#/$defs/ContractName`; the UI consults its own contract→slot mapping (slots are UI-only) to render. Per-node payloads are delivered separately on `node` envelopes (single via `item.contributions`) or list envelopes (`items[].contributions`, only when `limit ≤ bff.maxBulkContributions`, default 200).",
109
+ "description": "Catalog of registered view contributions active in the current scope, keyed by qualified contribution id `<pluginId>/<extensionId>/<contributionId>`. Built once per server boot from every enabled extension's `viewContributions` map and embedded into every payload-bearing envelope so the UI can fetch the catalog without a separate request. Sentinel envelopes (`health`, `scan`, `graph`), action-result envelopes (`sidecar.bumped`), and the catalog envelopes themselves (`annotations.registered`, `contributions.registered`) are exempt. Mirror of `kindRegistry`, parallel surface. Each entry references a slot by name from the closed catalog at `view-slots.schema.json#/$defs/SlotName`; the slot fixes both the renderer and the payload shape. Per-node payloads are delivered separately on `node` envelopes (single via `item.contributions`) or list envelopes (`items[].contributions`, only when `limit ≤ bff.maxBulkContributions`, default 200).",
110
110
  "additionalProperties": {
111
111
  "type": "object",
112
- "required": ["pluginId", "extensionId", "contributionId", "contract"],
112
+ "required": ["pluginId", "extensionId", "contributionId", "slot"],
113
113
  "additionalProperties": false,
114
114
  "properties": {
115
115
  "pluginId": { "type": "string", "minLength": 1 },
116
116
  "extensionId": { "type": "string", "minLength": 1 },
117
117
  "contributionId": { "type": "string", "minLength": 1 },
118
- "contract": { "$ref": "../view-contracts.schema.json#/$defs/ContractName" },
118
+ "slot": { "$ref": "../view-slots.schema.json#/$defs/SlotName" },
119
119
  "label": { "type": "string", "maxLength": 64 },
120
120
  "tooltip": { "type": "string", "maxLength": 256 },
121
121
  "icon": { "type": "string", "maxLength": 64 },
@@ -243,7 +243,7 @@
243
243
  }
244
244
  },
245
245
  {
246
- "description": "View-contributions-catalog envelope — `items` + `counts.total` only, no `filters` / `kindRegistry` / `contributionsRegistry` / `returned`. Used by `GET /api/contributions/registered`. Mirror of `annotations.registered`. The catalog ships in entirety; `counts.total` doubles as `items.length`. Each item is an `IRegisteredViewContribution` shape: `{ pluginId, extensionId, contributionId, contract, label?, tooltip?, icon?, emptyText?, emitWhenEmpty? }`.",
246
+ "description": "View-contributions-catalog envelope — `items` + `counts.total` only, no `filters` / `kindRegistry` / `contributionsRegistry` / `returned`. Used by `GET /api/contributions/registered`. Mirror of `annotations.registered`. The catalog ships in entirety; `counts.total` doubles as `items.length`. Each item is an `IRegisteredViewContribution` shape: `{ pluginId, extensionId, contributionId, slot, label?, tooltip?, icon?, emptyText?, emitWhenEmpty? }`.",
247
247
  "required": ["items", "counts"],
248
248
  "properties": {
249
249
  "kind": { "const": "contributions.registered" },
@@ -39,9 +39,9 @@
39
39
  "type": "boolean",
40
40
  "description": "When true, the runner injects `SKILL_MAP_DISABLE_ALL_EXTRACTORS=1` into the child process environment, dropping every Extractor extension before scan composition."
41
41
  },
42
- "disableAllRules": {
42
+ "disableAllAnalyzers": {
43
43
  "type": "boolean",
44
- "description": "When true, the runner injects `SKILL_MAP_DISABLE_ALL_RULES=1` into the child process environment, dropping every Rule extension before scan composition."
44
+ "description": "When true, the runner injects `SKILL_MAP_DISABLE_ALL_ANALYZERS=1` into the child process environment, dropping every Analyzer extension before scan composition."
45
45
  },
46
46
  "priorScans": {
47
47
  "type": "array",
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://skill-map.dev/spec/v0/extensions/analyzer.schema.json",
4
+ "title": "ExtensionAnalyzer",
5
+ "description": "Manifest shape for an `Analyzer` extension. An analyzer consumes the full graph (nodes + links) after all extractors have run, emits `Issue[]`, and MAY emit view contributions to project findings into the UI. Analyzers are dual-mode: `deterministic` analyzers MUST be byte-for-byte reproducible (same graph in → same issues out; time, random, and network are forbidden) and run synchronously inside `sm check` / `sm scan`. `probabilistic` analyzers invoke an LLM through the kernel's `RunnerPort` and execute only as queued jobs (`sm job submit analyzer:<id>`); their output MAY vary across runs and they NEVER participate in `sm scan`. See `architecture.md` §Execution modes for the full contract.",
6
+ "allOf": [
7
+ { "$ref": "base.schema.json" }
8
+ ],
9
+ "type": "object",
10
+ "required": ["id", "kind", "version", "emitsAnalyzerIds", "defaultSeverity"],
11
+ "unevaluatedProperties": false,
12
+ "properties": {
13
+ "kind": { "const": "analyzer" },
14
+ "mode": {
15
+ "type": "string",
16
+ "enum": ["deterministic", "probabilistic"],
17
+ "default": "deterministic",
18
+ "description": "`deterministic` (default): pure code, byte-for-byte reproducible, runs during `sm check` and `sm scan`. `probabilistic`: invokes an LLM via `ctx.runner` and runs only as a queued job; never participates in scan-time pipelines. The kernel rejects probabilistic analyzers that try to register scan-time hooks at load time. Omitting the field is equivalent to declaring `deterministic`."
19
+ },
20
+ "emitsAnalyzerIds": {
21
+ "type": "array",
22
+ "description": "List of `analyzer_id` values this analyzer may emit on issues. Typically a singleton (`trigger-collision` → emits `trigger-collision`). An analyzer emitting an `analyzer_id` not in this list → kernel logs `analyzer-id-violation` but keeps the issue (forward compatibility).",
23
+ "minItems": 1,
24
+ "items": { "type": "string" }
25
+ },
26
+ "defaultSeverity": {
27
+ "type": "string",
28
+ "enum": ["error", "warn", "info"],
29
+ "description": "Severity attached by default to emitted issues. Analyzers MAY override per-issue."
30
+ },
31
+ "consumes": {
32
+ "type": "string",
33
+ "enum": ["nodes", "links", "both"],
34
+ "default": "both",
35
+ "description": "Which slices of the graph the analyzer reads. The kernel MAY pass a restricted view when this is a strict subset."
36
+ },
37
+ "configurable": {
38
+ "type": "boolean",
39
+ "default": false,
40
+ "description": "If true, the analyzer reads its own config from `config_preferences` under the key `analyzers.<id>.<field>`. Implementations MAY surface these in `sm config`."
41
+ }
42
+ }
43
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/base.schema.json",
4
4
  "title": "ExtensionBase",
5
- "description": "Base manifest shape common to every extension kind. Kind-specific schemas (`provider`, `extractor`, `rule`, `action`, `formatter`, `hook`) extend this via `allOf` and add a discriminant `kind` literal plus kind-specific fields. camelCase keys throughout. Closed-content enforcement (unknown keys = bug) lives on the kind schemas via `unevaluatedProperties: false`; those see base's evaluated keys through the `allOf` composition. Adding closure here too would force every kind schema to re-list every base key, which is the footgun the spec used to trip on before 2026-04-22.",
5
+ "description": "Base manifest shape common to every extension kind. Kind-specific schemas (`provider`, `extractor`, `analyzer`, `action`, `formatter`, `hook`) extend this via `allOf` and add a discriminant `kind` literal plus kind-specific fields. camelCase keys throughout. Closed-content enforcement (unknown keys = bug) lives on the kind schemas via `unevaluatedProperties: false`; those see base's evaluated keys through the `allOf` composition. Adding closure here too would force every kind schema to re-list every base key, which is the footgun the spec used to trip on before 2026-04-22.",
6
6
  "type": "object",
7
7
  "required": ["id", "kind", "version"],
8
8
  "properties": {
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "kind": {
15
15
  "type": "string",
16
- "enum": ["provider", "extractor", "rule", "action", "formatter", "hook"],
16
+ "enum": ["provider", "extractor", "analyzer", "action", "formatter", "hook"],
17
17
  "description": "Discriminant. MUST match the file exporting this manifest; kind mismatch → load-error."
18
18
  },
19
19
  "version": {
@@ -62,17 +62,17 @@
62
62
  }
63
63
  }
64
64
  },
65
- "description": "Plugin-contributed annotation keys. Each entry declares an inline JSON Schema for the value the extension writes into a sidecar. Keys default to the plugin's `<plugin-id>:` namespace; opt-in to top-level via `location: 'root'` (requires `ownership: 'exclusive'`). The kernel exposes the runtime catalog via `kernel.getRegisteredAnnotationKeys()` for UI autocomplete; the built-in `core/unknown-field` Rule emits a warning on truly unrecognized keys (typo guard). See `plugin-author-guide.md` §Annotation contributions for the worked examples."
65
+ "description": "Plugin-contributed annotation keys. Each entry declares an inline JSON Schema for the value the extension writes into a sidecar. Keys default to the plugin's `<plugin-id>:` namespace; opt-in to top-level via `location: 'root'` (requires `ownership: 'exclusive'`). The kernel exposes the runtime catalog via `kernel.getRegisteredAnnotationKeys()` for UI autocomplete; the built-in `core/unknown-field` Analyzer emits a warning on truly unrecognized keys (typo guard). See `plugin-author-guide.md` §Annotation contributions for the worked examples."
66
66
  },
67
67
  "viewContributions": {
68
68
  "type": "object",
69
69
  "additionalProperties": {
70
- "$ref": "../view-contracts.schema.json#/$defs/IViewContribution"
70
+ "$ref": "../view-slots.schema.json#/$defs/IViewContribution"
71
71
  },
72
72
  "propertyNames": {
73
73
  "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
74
74
  },
75
- "description": "Plugin-contributed view contributions. Each entry declares one rendering surface in the UI by picking a `contract` name from the closed catalog at `view-contracts.schema.json#/$defs/ContractName`. The kernel validates the manifest at load (`invalid-manifest` on unknown contract); the plugin emits per-node payloads via `ctx.emitContribution(<contributionId>, payload)` during scan; the runtime validates payloads against the contract's payload schema in `view-contracts.schema.json#/$defs/payloads/<contract>`; off-contract payloads emit `extension.error` and drop silently (mirror of `emitLink` off-contract drop). The kernel exposes the runtime catalog via `kernel.getRegisteredViewContributions()`; the BFF surfaces it at `GET /api/contributions/registered`. The plugin author NEVER picks a slot — slot mapping is a UI decision (see `ROADMAP.md` §UI contribution system). See `plugin-author-guide.md` §View contributions for worked examples."
75
+ "description": "Plugin-contributed view contributions. Each entry declares one rendering surface in the UI by picking a `slot` name from the closed catalog at `view-slots.schema.json#/$defs/SlotName`. The kernel validates the manifest at load (`invalid-manifest` on unknown slot); the plugin emits per-node payloads via `ctx.emitContribution(<contributionId>, payload)` during scan; the runtime validates payloads against the slot's payload schema in `view-slots.schema.json#/$defs/payloads/<slot>`; off-shape payloads emit `extension.error` and drop silently (mirror of `emitLink` off-contract drop). The kernel exposes the runtime catalog via `kernel.getRegisteredViewContributions()`; the BFF surfaces it at `GET /api/contributions/registered`. The plugin author picks ONE slot — that is the only choice; the slot fixes both the renderer and the payload shape. See `plugin-author-guide.md` §View contributions for worked examples."
76
76
  }
77
77
  }
78
78
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/extractor.schema.json",
4
4
  "title": "ExtensionExtractor",
5
- "description": "Manifest shape for an `Extractor` extension. An extractor consumes a parsed node (frontmatter + body) and emits output through three context-supplied callbacks rather than returning a value: `ctx.emitLink(link)` writes to the kernel's `links` table (validated against `emitsLinkKinds` before persistence), `ctx.enrichNode(partial)` merges author-canonical properties into the kernel's enrichment layer (separate from the author-supplied frontmatter), and `ctx.store` persists into the plugin's own KV namespace or dedicated tables. The runtime method is `extract(ctx) → void`. Extractors run in isolation: they MUST NOT read other nodes, the graph, or the DB. Cross-node reasoning lives in Rules. Extractors are deterministic-only: pure code, runs synchronously inside `sm scan`, same input → same output every run. LLM-driven enrichment of a node is an Action concern (queued as a job), not an Extractor concern. See `architecture.md` §Execution modes for the full contract. Renamed from `detector` in spec 0.8.x — the FindBugs/SpotBugs lineage of \"detector\" connoted bug finding, while this kind extracts signals (relations, enrichments, custom data); ENRE (Entity Relationship Extractor) is the closer precedent.",
5
+ "description": "Manifest shape for an `Extractor` extension. An extractor consumes a parsed node (frontmatter + body) and emits output through three context-supplied callbacks rather than returning a value: `ctx.emitLink(link)` writes to the kernel's `links` table (validated against `emitsLinkKinds` before persistence), `ctx.enrichNode(partial)` merges author-canonical properties into the kernel's enrichment layer (separate from the author-supplied frontmatter), and `ctx.store` persists into the plugin's own KV namespace or dedicated tables. The runtime method is `extract(ctx) → void`. Extractors run in isolation: they MUST NOT read other nodes, the graph, or the DB. Cross-node reasoning lives in Analyzers. Extractors are deterministic-only: pure code, runs synchronously inside `sm scan`, same input → same output every run. LLM-driven enrichment of a node is an Action concern (queued as a job), not an Extractor concern. See `architecture.md` §Execution modes for the full contract. Renamed from `detector` in spec 0.8.x — the FindBugs/SpotBugs lineage of \"detector\" connoted bug finding, while this kind extracts signals (relations, enrichments, custom data); ENRE (Entity Relationship Extractor) is the closer precedent.",
6
6
  "allOf": [
7
7
  { "$ref": "base.schema.json" }
8
8
  ],
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/hook.schema.json",
4
4
  "title": "ExtensionHook",
5
- "description": "Manifest shape for a `Hook` extension. Subscribes declaratively to a curated set of kernel lifecycle events. Dual-mode (deterministic / probabilistic). Hooks react to events; they cannot block or alter the main pipeline. Probabilistic hooks are deferred to the job subsystem and never run in-scan. The set of hookable triggers is intentionally small — eight events out of the full job-events catalog. Other events (per-node `scan.progress`, `model.delta`, `run.*`, internal job lifecycle) are deliberately not hookable: too verbose for a reactive surface, internal to the runner, or covered elsewhere. Declaring a trigger outside the hookable set yields `invalid-manifest` at load time. See `architecture.md` §Hook · curated trigger set for the per-trigger payload contracts.",
5
+ "description": "Manifest shape for a `Hook` extension. Subscribes declaratively to a curated set of kernel lifecycle events. Dual-mode (deterministic / probabilistic). Hooks react to events; they cannot block or alter the main pipeline. Probabilistic hooks are deferred to the job subsystem and never run in-scan. The set of hookable triggers is intentionally small — ten events out of the full job-events catalog. Eight are pipeline-driven (emitted from inside `runScan`); two (`boot`, `shutdown`) are CLI-process-driven (emitted by the driving binary before / after the verb runs, fire-and-forget so `process.exit` is never blocked). Other events (per-node `scan.progress`, `model.delta`, `run.*`, internal job lifecycle) are deliberately not hookable: too verbose for a reactive surface, internal to the runner, or covered elsewhere. Declaring a trigger outside the hookable set yields `invalid-manifest` at load time. See `architecture.md` §Hook · curated trigger set for the per-trigger payload contracts.",
6
6
  "type": "object",
7
7
  "required": ["id", "kind", "version", "triggers"],
8
8
  "unevaluatedProperties": false,
@@ -22,20 +22,22 @@
22
22
  "items": {
23
23
  "type": "string",
24
24
  "enum": [
25
+ "boot",
25
26
  "scan.started",
26
27
  "scan.completed",
27
28
  "extractor.completed",
28
- "rule.completed",
29
+ "analyzer.completed",
29
30
  "action.completed",
30
31
  "job.spawning",
31
32
  "job.completed",
32
- "job.failed"
33
+ "job.failed",
34
+ "shutdown"
33
35
  ]
34
36
  }
35
37
  },
36
38
  "filter": {
37
39
  "type": "object",
38
- "description": "Optional declarative filter applied to the event payload before invoking `on(ctx)`. Keys are payload field paths (e.g. `extractorId`, `ruleId`, `actionId`); values are the literal expected match. Cross-field validation against the declared `triggers` is performed at load time when the host implementation supports it; an unknown field for every declared trigger yields `invalid-manifest`. Absence of `filter` means \"invoke on every event of every declared trigger\"."
40
+ "description": "Optional declarative filter applied to the event payload before invoking `on(ctx)`. Keys are payload field paths (e.g. `extractorId`, `analyzerId`, `actionId`); values are the literal expected match. Cross-field validation against the declared `triggers` is performed at load time when the host implementation supports it; an unknown field for every declared trigger yields `invalid-manifest`. Absence of `filter` means \"invoke on every event of every declared trigger\"."
39
41
  }
40
42
  },
41
43
  "allOf": [
@@ -2,15 +2,15 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/issue.schema.json",
4
4
  "title": "Issue",
5
- "description": "Deterministic finding emitted by a rule when evaluating the graph. Not to be confused with `Finding`, which is probabilistic (LLM-produced).",
5
+ "description": "Deterministic finding emitted by a analyzer when evaluating the graph. Not to be confused with `Finding`, which is probabilistic (LLM-produced).",
6
6
  "type": "object",
7
- "required": ["ruleId", "severity", "nodeIds", "message"],
7
+ "required": ["analyzerId", "severity", "nodeIds", "message"],
8
8
  "additionalProperties": false,
9
9
  "properties": {
10
- "ruleId": {
10
+ "analyzerId": {
11
11
  "type": "string",
12
12
  "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
13
- "description": "Kebab-case identifier of the rule that emitted this issue (e.g. `trigger-collision`, `broken-ref`, `superseded`)."
13
+ "description": "Kebab-case identifier of the analyzer that emitted this issue (e.g. `trigger-collision`, `broken-ref`, `superseded`)."
14
14
  },
15
15
  "severity": {
16
16
  "type": "string",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "nodeIds": {
21
21
  "type": "array",
22
- "description": "`node.path` values involved in this issue. Most rules emit 1 or 2; trigger-collision may emit N. Field name uses `id` generically to remain stable across future identifier changes.",
22
+ "description": "`node.path` values involved in this issue. Most analyzers emit 1 or 2; trigger-collision may emit N. Field name uses `id` generically to remain stable across future identifier changes.",
23
23
  "minItems": 1,
24
24
  "items": { "type": "string" }
25
25
  },
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "data": {
49
49
  "type": "object",
50
- "description": "Rule-specific structured payload (e.g. the colliding trigger string, the missing target). Free-form.",
50
+ "description": "Analyzer-specific structured payload (e.g. the colliding trigger string, the missing target). Free-form.",
51
51
  "additionalProperties": true
52
52
  }
53
53
  }
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "target": {
15
15
  "type": "string",
16
- "description": "`node.path` of the destination. MAY point to a missing node; rules detect broken refs."
16
+ "description": "`node.path` of the destination. MAY point to a missing node; analyzers detect broken refs."
17
17
  },
18
18
  "kind": {
19
19
  "type": "string",
@@ -23,7 +23,7 @@
23
23
  "confidence": {
24
24
  "type": "string",
25
25
  "enum": ["high", "medium", "low"],
26
- "description": "Extractor's self-assessed confidence. Rules MAY filter by confidence."
26
+ "description": "Extractor's self-assessed confidence. Analyzers MAY filter by confidence."
27
27
  },
28
28
  "sources": {
29
29
  "type": "array",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "catalogCompat": {
31
31
  "type": "string",
32
- "description": "Optional semver range against the kernel's view-contracts + input-types catalog version (e.g. `^1.0.0`). Independent from `specCompat` because the catalog evolves on its own cadence (new contracts ship as minor bumps; rename/remove ships as catalog-major bumps that trigger `sm plugins upgrade`). Absent = the plugin declares no view contributions or settings AND opts out of catalog-version checking; `sm plugins doctor` will warn if such a plugin actually emits via `viewContributions` or `settings`. Mismatch surfaces as `incompatible-catalog` plugin status."
32
+ "description": "Optional semver range against the kernel's view-slots + input-types catalog version (e.g. `^1.0.0`). Independent from `specCompat` because the catalog evolves on its own cadence (new slots ship as minor bumps; rename/remove ships as catalog-major bumps that trigger `sm plugins upgrade`). Absent = the plugin declares no view contributions or settings AND opts out of catalog-version checking; `sm plugins doctor` will warn if such a plugin actually emits via `viewContributions` or `settings`. Mismatch surfaces as `incompatible-catalog` plugin status."
33
33
  },
34
34
  "description": {
35
35
  "type": "string"
@@ -57,6 +57,20 @@
57
57
  "description": "Milliseconds to wait after the last filesystem event before triggering an incremental scan. Groups bursts (editor saves, branch switches, package installs) into a single scan pass. Default 300. Set to 0 to disable debouncing — every filesystem event triggers a scan immediately."
58
58
  }
59
59
  }
60
+ },
61
+ "includeHome": {
62
+ "type": "boolean",
63
+ "description": "**Privacy-sensitive, project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`) — opens disk access outside the project. Default false. When true, `sm scan` (without `-g`) appends every active Provider's `explorationDir` resolved against `~` (typically `~/.claude`, `~/.gemini`, `~/.agents`) to the scan roots. Files there are walked, parsed, and indexed as nodes alongside the project content. Reference impl: `sm config set scan.includeHome true` requires `--yes` to confirm; the Settings UI's Project section requires an explicit confirm dialog. The scan emits a stderr line listing the HOME paths it added so the operator sees the expanded surface. **Stripped with a warning when found in the committed `project` layer** so a teammate's `~/` is never scanned because of a shared checkout."
64
+ },
65
+ "extraRoots": {
66
+ "type": "array",
67
+ "items": { "type": "string" },
68
+ "description": "**Privacy-sensitive, project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`) when entries point outside the project — opens disk access there. Default `[]`. Additional directories appended to the scan roots; same parsing / indexing as the project root. Paths starting with `~` resolve against the user home; relative paths resolve against the project root. Reference impl gates writes that introduce out-of-project paths behind `--yes` (CLI) and a confirm dialog (UI), per the same analyzers as `includeHome`. **Stripped with a warning when found in the committed `project` layer** — paths are inherently per-machine and must not travel via the shared repo."
69
+ },
70
+ "referencePaths": {
71
+ "type": "array",
72
+ "items": { "type": "string" },
73
+ "description": "**Privacy-sensitive, project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`) when entries point outside the project — opens read-only disk access for link validation only. Default `[]`. Directories walked in parallel by the scan to collect existing absolute paths into a side set; the kernel passes the set to analyzers via `IAnalyzerContext.referenceablePaths` so `core/broken-ref` can resolve a link against the filesystem when the in-graph lookup misses. Files under these paths are NOT parsed and NOT indexed as nodes — the only effect is suppressing `broken-ref` warnings for targets that exist on disk outside the scan. Same write-gate analyzers as `extraRoots`. **Stripped with a warning when found in the committed `project` layer**."
60
74
  }
61
75
  }
62
76
  },
@@ -130,10 +144,14 @@
130
144
  "locale": { "type": "string", "description": "BCP-47 tag. Default `en`." }
131
145
  }
132
146
  },
147
+ "allowEditSmFiles": {
148
+ "type": "boolean",
149
+ "description": "**Project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`). Grants this project permission to create / modify `.sm` annotation sidecars next to source files. Default `false`. The first time a verb or BFF route attempts a `.sm` write while this is `false`, the kernel raises `EConsentRequiredError`. The CLI surfaces it as an interactive `confirm()` prompt (or `--yes` bypass); the BFF returns 412 `confirm-required` so the UI can open a `ConfirmationService` dialog. On accept the flag is persisted to `<cwd>/.skill-map/settings.local.json` (gitignored, per-checkout) and never asked again. On decline the operation aborts WITHOUT persisting the rejection — the next attempt re-asks. **Stripped with a warning when found in the committed `project` layer** (`<cwd>/.skill-map/settings.json`) — each developer consents independently."
150
+ },
133
151
  "updateCheck": {
134
152
  "type": "object",
135
153
  "additionalProperties": false,
136
- "description": "Controls the once-per-day notification when a newer @skill-map/cli release is published on npm. Disabled in CI, when SM_NO_UPDATE_CHECK=1, when stderr is not a TTY, or when the project DB is missing.",
154
+ "description": "Controls the once-per-day notification when a newer @skill-map/cli release is published on npm. Disabled in CI, when SM_NO_UPDATE_CHECK=1, when stderr is not a TTY, or when the project DB is missing. **User-scope only**: this key SHOULD live in `~/.skill-map/settings.json` and the reference implementation forces user-scope reads via `core/config/helper:USER_ONLY_KEYS` — a project-layer entry from an older install continues to validate but is silently ignored at read time. `sm config set` rejects writes to the project layer for this key (rerun with `-g`); the Settings UI's General section persists toggles to the user layer.",
137
155
  "properties": {
138
156
  "enabled": {
139
157
  "type": "boolean",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/sidecar.schema.json",
4
4
  "title": "Sidecar",
5
- "description": "Root shape of a co-located YAML sidecar (`<basename>.sm` next to `<basename>.md`). The `.sm` file IS the annotations file — every key under it is, conceptually, an annotation on the node. The YAML root organizes those annotations into structural blocks: `identity` (anchor + drift-detection hashes), `annotations` (the curated catalog of conventional fields), `audit` (timestamps), `settings` (reserved), and arbitrary `<plugin-id>:` namespaces for plugin-contributed data. Vendor file (`<basename>.md`) stays untouched. Schema is `additionalProperties: true` so plugins can add namespaces without coordination; the built-in `unknown-field` rule warns on truly unrecognized root keys (typo guard). Format is YAML — comments via `#`, multiline strings via `|` / `>`, permissive types per the YAML 1.2 spec. See `architecture.md` §Annotation system and ROADMAP §Step 9.6 for the design rationale.",
5
+ "description": "Root shape of a co-located YAML sidecar (`<basename>.sm` next to `<basename>.md`). The `.sm` file IS the annotations file — every key under it is, conceptually, an annotation on the node. The YAML root organizes those annotations into structural blocks: `identity` (anchor + drift-detection hashes), `annotations` (the curated catalog of conventional fields), `audit` (timestamps), `settings` (reserved), and arbitrary `<plugin-id>:` namespaces for plugin-contributed data. Vendor file (`<basename>.md`) stays untouched. Schema is `additionalProperties: true` so plugins can add namespaces without coordination; the built-in `unknown-field` analyzer warns on truly unrecognized root keys (typo guard). Format is YAML — comments via `#`, multiline strings via `|` / `>`, permissive types per the YAML 1.2 spec. See `architecture.md` §Annotation system and ROADMAP §Step 9.6 for the design rationale.",
6
6
  "type": "object",
7
7
  "required": ["identity"],
8
8
  "additionalProperties": true,
@@ -70,7 +70,7 @@
70
70
  "frontmatterHash": {
71
71
  "type": "string",
72
72
  "pattern": "^[a-f0-9]{64}$",
73
- "description": "sha256 of the canonical frontmatter (per the kernel's `canonicalFrontmatter` rule), hex-encoded lowercase, captured at the moment this sidecar was last bumped. Compared against the current frontmatter hash to detect drift."
73
+ "description": "sha256 of the canonical frontmatter (per the kernel's `canonicalFrontmatter` analyzer), hex-encoded lowercase, captured at the moment this sidecar was last bumped. Compared against the current frontmatter hash to detect drift."
74
74
  },
75
75
  "resolvedAs": {
76
76
  "type": "object",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "toolsObserved": {
29
29
  "type": "array",
30
- "description": "Tool names the summarizer observed referenced in the body. MAY differ from `metadata.tools` / frontmatter.tools; rules can emit drift issues.",
30
+ "description": "Tool names the summarizer observed referenced in the body. MAY differ from `metadata.tools` / frontmatter.tools; analyzers can emit drift issues.",
31
31
  "items": { "type": "string" }
32
32
  },
33
33
  "interactionStyle": {
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "argsObserved": {
24
24
  "type": "array",
25
- "description": "Argument structure as inferred from the body. MAY differ from declared `frontmatter.args`; rules can emit drift issues.",
25
+ "description": "Argument structure as inferred from the body. MAY differ from declared `frontmatter.args`; analyzers can emit drift issues.",
26
26
  "items": {
27
27
  "type": "object",
28
28
  "required": ["name"],
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "triggerInferred": {
20
20
  "type": "string",
21
- "description": "Event or condition the summarizer inferred from the body. MAY differ from declared `frontmatter.event`/`condition`; rules can emit drift issues."
21
+ "description": "Event or condition the summarizer inferred from the body. MAY differ from declared `frontmatter.event`/`condition`; analyzers can emit drift issues."
22
22
  },
23
23
  "sideEffects": {
24
24
  "type": "array",