@skill-map/spec 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +777 -3
  2. package/README.md +11 -13
  3. package/architecture.md +118 -35
  4. package/cli-contract.md +38 -25
  5. package/conformance/README.md +43 -14
  6. package/conformance/cases/kernel-empty-boot.json +3 -3
  7. package/conformance/coverage.md +20 -24
  8. package/db-schema.md +61 -6
  9. package/index.json +35 -77
  10. package/interfaces/security-scanner.md +1 -1
  11. package/job-events.md +75 -1
  12. package/package.json +1 -1
  13. package/plugin-author-guide.md +409 -51
  14. package/schemas/conformance-case.schema.json +14 -5
  15. package/schemas/execution-record.schema.json +8 -8
  16. package/schemas/extensions/action.schema.json +2 -2
  17. package/schemas/extensions/base.schema.json +5 -5
  18. package/schemas/extensions/extractor.schema.json +48 -0
  19. package/schemas/extensions/formatter.schema.json +29 -0
  20. package/schemas/extensions/hook.schema.json +44 -0
  21. package/schemas/extensions/provider.schema.json +51 -0
  22. package/schemas/extensions/rule.schema.json +1 -1
  23. package/schemas/frontmatter/base.schema.json +2 -2
  24. package/schemas/link.schema.json +4 -4
  25. package/schemas/node.schema.json +4 -4
  26. package/schemas/plugins-registry.schema.json +19 -4
  27. package/schemas/project-config.schema.json +2 -2
  28. package/schemas/scan-result.schema.json +3 -3
  29. package/conformance/cases/basic-scan.json +0 -17
  30. package/conformance/cases/orphan-detection.json +0 -22
  31. package/conformance/cases/rename-high.json +0 -21
  32. package/conformance/fixtures/minimal-claude/agents/reviewer.md +0 -16
  33. package/conformance/fixtures/minimal-claude/commands/status.md +0 -17
  34. package/conformance/fixtures/minimal-claude/hooks/pre-commit.md +0 -13
  35. package/conformance/fixtures/minimal-claude/notes/architecture.md +0 -11
  36. package/conformance/fixtures/minimal-claude/skills/hello.md +0 -22
  37. package/conformance/fixtures/orphan-after/skills/keep.md +0 -13
  38. package/conformance/fixtures/orphan-before/skills/keep.md +0 -13
  39. package/conformance/fixtures/orphan-before/skills/lonely.md +0 -13
  40. package/conformance/fixtures/rename-high-after/skills/bar.md +0 -14
  41. package/conformance/fixtures/rename-high-before/skills/foo.md +0 -14
  42. package/schemas/extensions/adapter.schema.json +0 -40
  43. package/schemas/extensions/audit.schema.json +0 -47
  44. package/schemas/extensions/detector.schema.json +0 -41
  45. package/schemas/extensions/renderer.schema.json +0 -29
  46. package/schemas/frontmatter/agent.schema.json +0 -17
  47. package/schemas/frontmatter/command.schema.json +0 -39
  48. package/schemas/frontmatter/hook.schema.json +0 -29
  49. package/schemas/frontmatter/note.schema.json +0 -11
  50. package/schemas/frontmatter/skill.schema.json +0 -37
@@ -31,12 +31,21 @@
31
31
  "description": "Pre-invocation toggles and ordered staging steps. All toggles default to `false`. `priorScans`, when present, run BEFORE the main `invoke` so a case can establish a prior snapshot the heuristic-driven verbs (e.g. `sm scan` rename detection) can react to.",
32
32
  "additionalProperties": false,
33
33
  "properties": {
34
- "disableAllAdapters": { "type": "boolean" },
35
- "disableAllDetectors": { "type": "boolean" },
36
- "disableAllRules": { "type": "boolean" },
34
+ "disableAllProviders": {
35
+ "type": "boolean",
36
+ "description": "When true, the runner injects `SKILL_MAP_DISABLE_ALL_PROVIDERS=1` into the child process environment, dropping every Provider extension (built-in and user-plugin) before scan composition."
37
+ },
38
+ "disableAllExtractors": {
39
+ "type": "boolean",
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
+ },
42
+ "disableAllRules": {
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."
45
+ },
37
46
  "priorScans": {
38
47
  "type": "array",
39
- "description": "Ordered staging scans, each running before the next. For step N, the runner first replaces the scope's adapter content with `priorScans[N].fixture`, then invokes `sm scan` with the supplied flags. After the last step, the runner copies the top-level `fixture` (overwriting again) and runs the main `invoke`. The DB persists across all steps because `.skill-map/` is preserved between fixture swaps.",
48
+ "description": "Ordered staging scans, each running before the next. For step N, the runner first replaces the scope's Provider content with `priorScans[N].fixture`, then invokes `sm scan` with the supplied flags. After the last step, the runner copies the top-level `fixture` (overwriting again) and runs the main `invoke`. The DB persists across all steps because `.skill-map/` is preserved between fixture swaps.",
40
49
  "items": {
41
50
  "type": "object",
42
51
  "required": ["fixture"],
@@ -63,7 +72,7 @@
63
72
  "properties": {
64
73
  "verb": {
65
74
  "type": "string",
66
- "description": "First-level CLI verb (`scan`, `list`, `show`, `check`, `findings`, `graph`, `export`, `audit`, `job`, `record`, …)."
75
+ "description": "First-level CLI verb (`scan`, `list`, `show`, `check`, `findings`, `graph`, `export`, `job`, `record`, …)."
67
76
  },
68
77
  "sub": {
69
78
  "type": "string",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/execution-record.schema.json",
4
4
  "title": "ExecutionRecord",
5
- "description": "A single row in the execution history (`state_executions`). One record per action or audit invocation — regardless of whether the runner was CLI, Skill, or in-process.",
5
+ "description": "A single row in the execution history (`state_executions`). One record per action invocation — regardless of whether the runner was CLI, Skill, or in-process.",
6
6
  "type": "object",
7
7
  "required": ["id", "kind", "extensionId", "extensionVersion", "status", "startedAt", "finishedAt"],
8
8
  "additionalProperties": false,
@@ -13,12 +13,12 @@
13
13
  },
14
14
  "kind": {
15
15
  "type": "string",
16
- "enum": ["action", "audit"],
17
- "description": "Which extension kind was executed."
16
+ "enum": ["action"],
17
+ "description": "Which extension kind was executed. The enum has a single value today (the audit kind was removed pre-1.0); the field is preserved as a forward-compatibility lever."
18
18
  },
19
19
  "extensionId": {
20
20
  "type": "string",
21
- "description": "Id of the action or audit that ran (e.g. `skill-summarizer`, `validate-all`)."
21
+ "description": "Id of the action that ran (e.g. `skill-summarizer`)."
22
22
  },
23
23
  "extensionVersion": {
24
24
  "type": "string",
@@ -26,13 +26,13 @@
26
26
  },
27
27
  "nodeIds": {
28
28
  "type": "array",
29
- "description": "Target `node.path` values. Empty for audits that evaluate the whole graph.",
29
+ "description": "Target `node.path` values. Empty for whole-graph actions.",
30
30
  "items": { "type": "string" }
31
31
  },
32
32
  "contentHash": {
33
33
  "type": ["string", "null"],
34
34
  "pattern": "^[a-f0-9]{64}$",
35
- "description": "Duplicate-prevention hash used at submit time: sha256(actionId + actionVersion + bodyHash + frontmatterHash + promptTemplateHash). Null for audits."
35
+ "description": "Duplicate-prevention hash used at submit time: sha256(actionId + actionVersion + bodyHash + frontmatterHash + promptTemplateHash). May be null for in-process actions that don't memoize."
36
36
  },
37
37
  "status": {
38
38
  "type": "string",
@@ -51,7 +51,7 @@
51
51
  "runner": {
52
52
  "type": ["string", "null"],
53
53
  "enum": ["cli", "skill", "in-process", null],
54
- "description": "Which runner executed this job. Null for audits."
54
+ "description": "Which runner executed this job. Null when the action runs synchronously without dispatching."
55
55
  },
56
56
  "startedAt": {
57
57
  "type": "integer",
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "jobId": {
84
84
  "type": ["string", "null"],
85
- "description": "Originating job id when applicable. Null for audits and in-process actions."
85
+ "description": "Originating job id when applicable. Null for in-process actions."
86
86
  }
87
87
  }
88
88
  }
@@ -36,10 +36,10 @@
36
36
  "items": { "type": "string", "enum": ["skill", "agent", "command", "hook", "note"] },
37
37
  "description": "Node kinds this action accepts. Omitted → any kind."
38
38
  },
39
- "adapter": {
39
+ "provider": {
40
40
  "type": "array",
41
41
  "items": { "type": "string" },
42
- "description": "Adapter ids whose nodes this action accepts. Omitted → any adapter."
42
+ "description": "Provider ids whose nodes this action accepts. Omitted → any Provider."
43
43
  },
44
44
  "stability": {
45
45
  "type": "array",
@@ -2,23 +2,23 @@
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 (`adapter`, `detector`, `rule`, `action`, `audit`, `renderer`) 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`, `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.",
6
6
  "type": "object",
7
7
  "required": ["id", "kind", "version"],
8
8
  "properties": {
9
9
  "id": {
10
10
  "type": "string",
11
11
  "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
12
- "description": "Kebab-case identifier unique within the extension's kind across all loaded plugins. The built-in default set uses bare ids (`claude`, `frontmatter`, `trigger-collision`). Third-party extensions SHOULD namespace with their plugin id (`my-plugin/foo-detector`) to avoid collisions."
12
+ "description": "Kebab-case identifier unique within the extension's kind across the same plugin. The kernel registers every extension under the **qualified** id `<plugin-id>/<id>` (e.g. `core/frontmatter`, `claude/slash`, `hello-world/greet`) — see `architecture.md` §Extension kinds and `plugin-author-guide.md` §Qualified extension ids. Authors declare only the short id here; the qualifier is composed by the loader from the manifest's `id` (or, for built-ins, from the bundle declaration in `built-ins.ts`). The pattern is intentionally constrained to a single kebab-case segment without the `/` separator: the qualifier always lives in the plugin id, never in the extension id."
13
13
  },
14
14
  "kind": {
15
15
  "type": "string",
16
- "enum": ["adapter", "detector", "rule", "action", "audit", "renderer"],
16
+ "enum": ["provider", "extractor", "rule", "action", "formatter", "hook"],
17
17
  "description": "Discriminant. MUST match the file exporting this manifest; kind mismatch → load-error."
18
18
  },
19
19
  "version": {
20
20
  "type": "string",
21
- "description": "Extension semver. Bumped independently from the plugin version; frozen into `state_executions.extension_version` on every run for audit reproducibility."
21
+ "description": "Extension semver. Bumped independently from the plugin version; frozen into `state_executions.extension_version` on every run for reproducibility."
22
22
  },
23
23
  "description": {
24
24
  "type": "string",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "preconditions": {
34
34
  "type": "array",
35
- "description": "Free-form predicates the kernel evaluates before offering this extension. Canonical keys: `kind=<node-kind>`, `adapter=<adapter-id>`, `frontmatter.<path>=<value>`. Unknown keys are skipped with a warning, not rejected.",
35
+ "description": "Free-form predicates the kernel evaluates before offering this extension. Canonical keys: `kind=<node-kind>`, `provider=<provider-id>`, `frontmatter.<path>=<value>`. Unknown keys are skipped with a warning, not rejected.",
36
36
  "items": { "type": "string" }
37
37
  },
38
38
  "entry": {
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://skill-map.dev/spec/v0/extensions/extractor.schema.json",
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 dual-mode: `deterministic` extractors run synchronously inside `sm scan`; `probabilistic` extractors invoke an LLM through the kernel's `RunnerPort` (exposed as `ctx.runner`) and execute only as queued jobs (never during scan). 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
+ "allOf": [
7
+ { "$ref": "base.schema.json" }
8
+ ],
9
+ "type": "object",
10
+ "required": ["id", "kind", "version", "emitsLinkKinds", "defaultConfidence"],
11
+ "unevaluatedProperties": false,
12
+ "properties": {
13
+ "kind": { "const": "extractor" },
14
+ "mode": {
15
+ "type": "string",
16
+ "enum": ["deterministic", "probabilistic"],
17
+ "default": "deterministic",
18
+ "description": "`deterministic` (default): pure code, runs synchronously during `sm scan`. Same input → same output, every run. `probabilistic`: invokes an LLM via `ctx.runner` and runs only as a queued job (`sm job submit extractor:<id>`); never participates in `sm scan`. The kernel rejects probabilistic extractors that try to register scan-time hooks at load time. Omitting the field is equivalent to declaring `deterministic`."
19
+ },
20
+ "emitsLinkKinds": {
21
+ "type": "array",
22
+ "description": "Subset of `Link.kind` values this extractor is allowed to emit through `ctx.emitLink(...)`. Emitting an unlisted kind at runtime → kernel rejects the link and logs `extractor-kind-violation`.",
23
+ "minItems": 1,
24
+ "items": {
25
+ "type": "string",
26
+ "enum": ["invokes", "references", "mentions", "supersedes"]
27
+ }
28
+ },
29
+ "defaultConfidence": {
30
+ "type": "string",
31
+ "enum": ["high", "medium", "low"],
32
+ "description": "Confidence attached to emitted links by default. Extractors MAY override per-link at emission time."
33
+ },
34
+ "scope": {
35
+ "type": "string",
36
+ "enum": ["frontmatter", "body", "both"],
37
+ "default": "both",
38
+ "description": "Which part of the node this extractor consumes. The kernel passes only the declared scope to the extractor — a `frontmatter` extractor that tries to read `body` receives an empty string."
39
+ },
40
+ "applicableKinds": {
41
+ "type": "array",
42
+ "minItems": 1,
43
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
44
+ "uniqueItems": true,
45
+ "description": "Optional opt-in filter. If declared, the extractor runs only on nodes whose kind is in this list. Absent = applies to all kinds (default). No wildcards — the absence of the field already means \"every kind\". Empty array is invalid (`minItems: 1`). Unknown kinds (not declared by any installed Provider) load OK but emit a warning in `sm plugins doctor` — the Provider may arrive later. The kernel filters fail-fast: nodes whose kind is excluded never see `extract()`, so a probabilistic extractor wastes zero LLM cost on inapplicable nodes."
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://skill-map.dev/spec/v0/extensions/formatter.schema.json",
4
+ "title": "ExtensionFormatter",
5
+ "description": "Manifest shape for a `Formatter` extension. A formatter serializes the graph (or a filtered subgraph) into a string in a declared format. Formatters are invoked by `sm graph --format <format>` and `sm export`. Formatters are deterministic-only — they sit at the graph-to-string boundary and their output MUST be byte-deterministic for the same input graph (the snapshot-test suite relies on this). The `mode` field MUST NOT appear in formatter manifests. Probabilistic narrators of the graph are a valid product but they live in jobs and emit Findings, not in formatters.",
6
+ "allOf": [
7
+ { "$ref": "base.schema.json" }
8
+ ],
9
+ "type": "object",
10
+ "required": ["id", "kind", "version", "formatId"],
11
+ "unevaluatedProperties": false,
12
+ "properties": {
13
+ "kind": { "const": "formatter" },
14
+ "formatId": {
15
+ "type": "string",
16
+ "description": "Format identifier consumed by `sm graph --format <name>`. Built-in set: `ascii`, `mermaid`, `dot`, `json`. Third-party formatters MAY register new format ids; collisions are a load-time error. Distinct from the runtime method `format(ctx)` which produces the serialized output."
17
+ },
18
+ "contentType": {
19
+ "type": "string",
20
+ "description": "MIME-like hint used by the Server when streaming formatted output over HTTP (e.g. `text/plain`, `image/svg+xml`, `application/json`). Advisory.",
21
+ "default": "text/plain"
22
+ },
23
+ "supportsFilter": {
24
+ "type": "boolean",
25
+ "default": true,
26
+ "description": "If true, the formatter accepts the `--filter` expression used by `sm export`. If false, `sm export --format <this>` rejects `--filter` with exit 2."
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://skill-map.dev/spec/v0/extensions/hook.schema.json",
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.",
6
+ "type": "object",
7
+ "required": ["id", "kind", "version", "triggers"],
8
+ "unevaluatedProperties": false,
9
+ "properties": {
10
+ "kind": { "const": "hook" },
11
+ "mode": {
12
+ "type": "string",
13
+ "enum": ["deterministic", "probabilistic"],
14
+ "default": "deterministic",
15
+ "description": "`deterministic`: the hook's `on(ctx)` runs in-process during the dispatch of the matching event, synchronously between the event's emission and the next pipeline step. `probabilistic`: the hook is enqueued as a job (handled by the job subsystem; lands at Step 10). A probabilistic hook is loaded but not dispatched in-scan — the kernel surfaces a stderr advisory and skips it until the job subsystem ships."
16
+ },
17
+ "triggers": {
18
+ "type": "array",
19
+ "minItems": 1,
20
+ "uniqueItems": true,
21
+ "description": "List of lifecycle events this hook subscribes to. Each entry MUST be one of the hookable triggers below. Declaring an unknown event (e.g. `scan.progress`, `model.delta`) is rejected at load time with `invalid-manifest` — the curated set is the contract.",
22
+ "items": {
23
+ "type": "string",
24
+ "enum": [
25
+ "scan.started",
26
+ "scan.completed",
27
+ "extractor.completed",
28
+ "rule.completed",
29
+ "action.completed",
30
+ "job.spawning",
31
+ "job.completed",
32
+ "job.failed"
33
+ ]
34
+ }
35
+ },
36
+ "filter": {
37
+ "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\"."
39
+ }
40
+ },
41
+ "allOf": [
42
+ { "$ref": "base.schema.json" }
43
+ ]
44
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://skill-map.dev/spec/v0/extensions/provider.schema.json",
4
+ "title": "ExtensionProvider",
5
+ "description": "Manifest shape for a `Provider` extension. A Provider declares its own universe: the platform it recognises (Claude Code, Codex, Gemini, Obsidian vault, generic MD), the catalog of node `kind`s it emits, the per-kind frontmatter schema each kind follows, and the filesystem directory (`explorationDir`) where its content lives. The catalog lives in the `kinds` map, keyed by kind name. Each map entry declares the relative path to the kind's frontmatter schema (resolved against the Provider's directory) and the qualified `defaultRefreshAction` id the UI's probabilistic-refresh surface dispatches for that kind. Spec only ships `frontmatter/base.schema.json` (universal); per-kind schemas live with their owning Provider so that adding a new platform is purely additive — no spec bump needed to introduce kinds. Exactly zero or one Provider MUST match any given file; multiple matches → the kernel emits an issue `provider-ambiguous` and the file is left unclassified. Providers are deterministic-only — they sit at the filesystem boundary and run during boot; probabilistic classification would make boot slow, costly, and non-reproducible. The `mode` field MUST NOT appear in Provider manifests. If you need LLM-assisted classification, write a probabilistic Extractor that emits classification hints via `ctx.emitLink`. Distinct from the **hexagonal-architecture** 'adapter' (`RunnerPort.adapter`, `StoragePort.adapter`, etc.), which is an internal driven-adapter implementing a port — Providers live in the extension surface, hexagonal adapters live in `src/kernel/adapters/`. Stability: stable as of spec v1.0.0 except where noted.",
6
+ "allOf": [
7
+ { "$ref": "base.schema.json" }
8
+ ],
9
+ "type": "object",
10
+ "required": ["id", "kind", "version", "kinds", "explorationDir"],
11
+ "unevaluatedProperties": false,
12
+ "properties": {
13
+ "kind": { "const": "provider" },
14
+ "explorationDir": {
15
+ "type": "string",
16
+ "minLength": 1,
17
+ "description": "Filesystem directory (relative to user home or project root) where this Provider's content lives. Required. Examples: '~/.claude' for the Claude Provider; '~/.cursor' for a hypothetical Cursor Provider. The kernel walks this directory during boot/scan to discover nodes; the Provider's `globs` (if declared) refines what to match inside. `sm doctor` validates the directory exists; missing directory yields a non-blocking warning."
18
+ },
19
+ "roots": {
20
+ "type": "array",
21
+ "description": "Path globs (relative to scope root) that this Provider SHOULD be consulted for. Advisory — the kernel walks all roots and consults every Provider regardless, but this field lets `sm doctor` warn when no file matched a specific Provider (i.e. the Provider was loaded for a platform that isn't in this scope).",
22
+ "items": { "type": "string" }
23
+ },
24
+ "kinds": {
25
+ "type": "object",
26
+ "description": "Catalog of node kinds this Provider emits. Keyed by kind name (e.g. `skill`, `agent`, `note`). Each entry declares the relative path to the kind's frontmatter schema (the kernel resolves it against the Provider's package directory) and the qualified `defaultRefreshAction` id the UI dispatches when the user requests a probabilistic refresh on a node of that kind. The map MUST be non-empty: a Provider that emits no kinds is meaningless. Kind names follow camelCase / lowerCase convention; the spec does not constrain the value space (a Cursor Provider could declare `rule`, an Obsidian Provider could declare `daily`).",
27
+ "minProperties": 1,
28
+ "propertyNames": {
29
+ "type": "string",
30
+ "pattern": "^[a-z][a-zA-Z0-9]*$"
31
+ },
32
+ "additionalProperties": {
33
+ "type": "object",
34
+ "required": ["schema", "defaultRefreshAction"],
35
+ "additionalProperties": false,
36
+ "properties": {
37
+ "schema": {
38
+ "type": "string",
39
+ "minLength": 1,
40
+ "description": "Path to the kind's frontmatter JSON Schema, relative to the Provider's package directory. The schema MUST extend `frontmatter/base.schema.json` (declared in spec) via `allOf` + `$ref` to base's `$id`. The kernel resolves the path at boot time and registers the schema with AJV for validation during scan."
41
+ },
42
+ "defaultRefreshAction": {
43
+ "type": "string",
44
+ "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*/[a-z][a-z0-9]*(-[a-z0-9]+)*$",
45
+ "description": "Qualified action id (`<plugin-id>/<action-id>`) the UI's probabilistic-refresh surface (`🧠 prob`) dispatches for nodes of this kind. The action MUST exist in the registry by the time the graph is queried; a dangling reference disables the Provider with status `invalid-manifest`."
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -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/rule.schema.json",
4
4
  "title": "ExtensionRule",
5
- "description": "Manifest shape for a `Rule` extension. A rule consumes the full graph (nodes + links) after all detectors have run and emits `Issue[]`. Rules are dual-mode: `deterministic` rules 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` rules invoke an LLM through the kernel's `RunnerPort` and execute only as queued jobs (`sm job submit rule:<id>`); their output MAY vary across runs and they NEVER participate in `sm scan`. See `architecture.md` §Execution modes for the full contract.",
5
+ "description": "Manifest shape for a `Rule` extension. A rule consumes the full graph (nodes + links) after all extractors have run and emits `Issue[]`. Rules are dual-mode: `deterministic` rules 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` rules invoke an LLM through the kernel's `RunnerPort` and execute only as queued jobs (`sm job submit rule:<id>`); their output MAY vary across runs and they NEVER participate in `sm scan`. See `architecture.md` §Execution modes for the full contract.",
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/frontmatter/base.schema.json",
4
4
  "title": "FrontmatterBase",
5
- "description": "Base shape of the YAML frontmatter block on every node, across all kinds. Kind-specific schemas (`skill`, `agent`, `command`, `hook`, `note`) extend this via `allOf`. camelCase keys throughout. `additionalProperties` is intentionally permissive; the deterministic `unknown-field` rule emits warnings for keys outside the catalog so that the JSON Schema remains shape-only and policy lives in rules.",
5
+ "description": "Base shape of the YAML frontmatter block on every node, across all kinds. Universal — common to every Provider's nodes regardless of platform. Per-kind schemas live in the Provider that emits the kind (declared via `provider.kinds[<kind>].schema`) and extend this base via `allOf` + `$ref` to its `$id`. camelCase keys throughout. `additionalProperties` is intentionally permissive; the deterministic `unknown-field` rule emits warnings for keys outside the catalog so that the JSON Schema remains shape-only and policy lives in rules.",
6
6
  "type": "object",
7
7
  "required": ["name", "description", "metadata"],
8
8
  "additionalProperties": true,
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "type": {
21
21
  "type": "string",
22
- "description": "User-declared kind hint. Optional; the adapter may use it to tie-break classification. Free-form string so that new kinds introduced by adapters don't require a spec bump."
22
+ "description": "User-declared kind hint. Optional; the Provider may use it to tie-break classification. Free-form string so that new kinds introduced by Providers don't require a spec bump."
23
23
  },
24
24
  "author": {
25
25
  "type": "string",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/link.schema.json",
4
4
  "title": "Link",
5
- "description": "Directed relation between two nodes, produced by one or more detectors during a scan.",
5
+ "description": "Directed relation between two nodes, produced by one or more extractors during a scan.",
6
6
  "type": "object",
7
7
  "required": ["source", "target", "kind", "confidence", "sources"],
8
8
  "additionalProperties": false,
@@ -23,11 +23,11 @@
23
23
  "confidence": {
24
24
  "type": "string",
25
25
  "enum": ["high", "medium", "low"],
26
- "description": "Detector's self-assessed confidence. Rules MAY filter by confidence."
26
+ "description": "Extractor's self-assessed confidence. Rules MAY filter by confidence."
27
27
  },
28
28
  "sources": {
29
29
  "type": "array",
30
- "description": "Detector ids that produced this link. At least one. Multiple detectors may converge on the same link; kernel merges them.",
30
+ "description": "Extractor ids that produced this link. At least one. Multiple extractors may converge on the same link; kernel merges them.",
31
31
  "minItems": 1,
32
32
  "items": { "type": "string" }
33
33
  },
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "location": {
51
51
  "type": ["object", "null"],
52
- "description": "Where in the source body the link was found. Null if the detector didn't track location.",
52
+ "description": "Where in the source body the link was found. Null if the extractor didn't track location.",
53
53
  "required": ["line"],
54
54
  "additionalProperties": false,
55
55
  "properties": {
@@ -4,7 +4,7 @@
4
4
  "title": "Node",
5
5
  "description": "A single markdown file in the graph (skill, agent, command, hook, note). Identified by its relative path from the scope root.",
6
6
  "type": "object",
7
- "required": ["path", "kind", "adapter", "bodyHash", "frontmatterHash", "bytes", "linksOutCount", "linksInCount", "externalRefsCount"],
7
+ "required": ["path", "kind", "provider", "bodyHash", "frontmatterHash", "bytes", "linksOutCount", "linksInCount", "externalRefsCount"],
8
8
  "additionalProperties": false,
9
9
  "properties": {
10
10
  "path": {
@@ -14,11 +14,11 @@
14
14
  "kind": {
15
15
  "type": "string",
16
16
  "enum": ["skill", "agent", "command", "hook", "note"],
17
- "description": "Category assigned by the adapter. Stability: stable. New kinds may be added in a minor bump."
17
+ "description": "Category assigned by the Provider. Stability: stable. New kinds may be added in a minor bump."
18
18
  },
19
- "adapter": {
19
+ "provider": {
20
20
  "type": "string",
21
- "description": "Identifier of the adapter extension that classified this node (e.g. `claude`)."
21
+ "description": "Identifier of the Provider extension that classified this node (e.g. `claude`)."
22
22
  },
23
23
  "title": {
24
24
  "type": ["string", "null"],
@@ -36,6 +36,12 @@
36
36
  "minItems": 1,
37
37
  "items": { "type": "string" }
38
38
  },
39
+ "granularity": {
40
+ "type": "string",
41
+ "enum": ["bundle", "extension"],
42
+ "default": "bundle",
43
+ "description": "Toggle granularity for this plugin. `bundle` (default) — the plugin id is the only enable/disable key; the whole set of extensions follows the toggle. `extension` — each extension is independently toggle-able under its qualified id `<plugin-id>/<extension-id>`. Built-in bundles use the same field: the `claude` bundle is `bundle` (the Provider and its kind-aware extractors form a coherent provider); the `core` bundle is `extension` (every kernel built-in is removable per the spec promise that no extension is privileged). Plugin authors should keep the default unless their plugin ships several orthogonal capabilities a user might reasonably want piecemeal."
44
+ },
39
45
  "storage": {
40
46
  "type": "object",
41
47
  "description": "Persistence mode for this plugin. Absent = plugin does not persist state.",
@@ -44,7 +50,11 @@
44
50
  "required": ["mode"],
45
51
  "additionalProperties": false,
46
52
  "properties": {
47
- "mode": { "const": "kv", "description": "Shared `state_plugin_kvs` table, scoped by plugin id." }
53
+ "mode": { "const": "kv", "description": "Shared `state_plugin_kvs` table, scoped by plugin id." },
54
+ "schema": {
55
+ "type": "string",
56
+ "description": "Optional. JSON Schema (path relative to plugin root) that validates the value on every `ctx.store.set(key, value)`. Absent = permissive (no validation, status quo). The kernel AJV-compiles the schema at load time; a missing or unparseable schema file surfaces as `load-error`."
57
+ }
48
58
  }
49
59
  },
50
60
  {
@@ -53,7 +63,12 @@
53
63
  "properties": {
54
64
  "mode": { "const": "dedicated", "description": "Plugin-owned tables, prefixed `plugin_<normalizedId>_`." },
55
65
  "tables": { "type": "array", "minItems": 1, "items": { "type": "string" } },
56
- "migrations": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Relative paths to `.sql` migration files." }
66
+ "migrations": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Relative paths to `.sql` migration files." },
67
+ "schemas": {
68
+ "type": "object",
69
+ "description": "Optional. Maps each declared table to a JSON Schema (path relative to plugin root) that validates rows on write. Tables not present here accept any shape (permissive, status quo). The kernel AJV-validates `ctx.store.write(table, row)` against the declared schema and throws on shape violation. A missing or unparseable schema file surfaces as `load-error`.",
70
+ "additionalProperties": { "type": "string" }
71
+ }
57
72
  }
58
73
  }
59
74
  ]
@@ -86,8 +101,8 @@
86
101
  "manifest": { "$ref": "#/$defs/PluginManifest" },
87
102
  "status": {
88
103
  "type": "string",
89
- "enum": ["enabled", "disabled", "incompatible-spec", "invalid-manifest", "load-error"],
90
- "description": "Resolved state after discovery. `disabled` = user-disabled via config; others = automatic."
104
+ "enum": ["enabled", "disabled", "incompatible-spec", "invalid-manifest", "load-error", "id-collision"],
105
+ "description": "Resolved state after discovery. `disabled` = user-disabled via config; `id-collision` = two plugins (any combination of project / global / --plugin-dir) declared the same `id`, both blocked, no precedence; others = automatic."
91
106
  },
92
107
  "statusReason": {
93
108
  "type": ["string", "null"],
@@ -19,9 +19,9 @@
19
19
  "type": "string",
20
20
  "description": "Name of the offline tokenizer used to compute per-node token counts during scan. Default `cl100k_base`. Stored alongside each token count in `scan_nodes` so consumers know which encoder produced the numbers. Changing this invalidates prior counts on next scan."
21
21
  },
22
- "adapters": {
22
+ "providers": {
23
23
  "type": "array",
24
- "description": "Adapter ids to enable, in priority order when multiple match. Empty/absent = use all registered.",
24
+ "description": "Provider ids to enable, in priority order when multiple match. Empty/absent = use all registered.",
25
25
  "items": { "type": "string" }
26
26
  },
27
27
  "roots": {
@@ -37,9 +37,9 @@
37
37
  "minItems": 1,
38
38
  "items": { "type": "string" }
39
39
  },
40
- "adapters": {
40
+ "providers": {
41
41
  "type": "array",
42
- "description": "Adapter ids that participated in classification. Empty if no adapter matched.",
42
+ "description": "Provider ids that participated in classification. Empty if no Provider matched.",
43
43
  "items": { "type": "string" }
44
44
  },
45
45
  "nodes": {
@@ -60,7 +60,7 @@
60
60
  "additionalProperties": false,
61
61
  "properties": {
62
62
  "filesWalked": { "type": "integer", "minimum": 0 },
63
- "filesSkipped": { "type": "integer", "minimum": 0, "description": "Files walked but not classified by any adapter." },
63
+ "filesSkipped": { "type": "integer", "minimum": 0, "description": "Files walked but not classified by any Provider." },
64
64
  "nodesCount": { "type": "integer", "minimum": 0 },
65
65
  "linksCount": { "type": "integer", "minimum": 0 },
66
66
  "issuesCount": { "type": "integer", "minimum": 0 },
@@ -1,17 +0,0 @@
1
- {
2
- "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
3
- "id": "basic-scan",
4
- "description": "Scanning the minimal-claude fixture detects exactly five nodes, one per kind, with no issues.",
5
- "fixture": "minimal-claude",
6
- "invoke": {
7
- "verb": "scan",
8
- "flags": ["--json"]
9
- },
10
- "assertions": [
11
- { "type": "exit-code", "value": 0 },
12
- { "type": "json-path", "path": "$.schemaVersion", "equals": 1 },
13
- { "type": "json-path", "path": "$.stats.nodesCount", "equals": 5 },
14
- { "type": "json-path", "path": "$.stats.issuesCount", "equals": 0 },
15
- { "type": "json-path", "path": "$.nodes.length", "equals": 5 }
16
- ]
17
- }
@@ -1,22 +0,0 @@
1
- {
2
- "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
3
- "id": "orphan-detection",
4
- "description": "Deleting a file with no replacement triggers the orphan branch of the rename heuristic: exactly one issue with ruleId `orphan` is emitted, severity `info`, naming the dead path. The remaining survivor node is reported normally.",
5
- "fixture": "orphan-after",
6
- "setup": {
7
- "priorScans": [
8
- { "fixture": "orphan-before" }
9
- ]
10
- },
11
- "invoke": {
12
- "verb": "scan",
13
- "flags": ["--changed", "--json"]
14
- },
15
- "assertions": [
16
- { "type": "exit-code", "value": 0 },
17
- { "type": "json-path", "path": "$.stats.nodesCount", "equals": 1 },
18
- { "type": "json-path", "path": "$.stats.issuesCount", "equals": 1 },
19
- { "type": "json-path", "path": "$.issues[0].ruleId", "equals": "orphan" },
20
- { "type": "json-path", "path": "$.issues[0].severity", "equals": "info" }
21
- ]
22
- }
@@ -1,21 +0,0 @@
1
- {
2
- "$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
3
- "id": "rename-high",
4
- "description": "Moving a single file with identical body across the rename triggers a high-confidence auto-rename: NO issue is emitted (per spec/db-schema.md §Rename detection), the new path is the only node in the result, and the rename heuristic operates silently.",
5
- "fixture": "rename-high-after",
6
- "setup": {
7
- "priorScans": [
8
- { "fixture": "rename-high-before" }
9
- ]
10
- },
11
- "invoke": {
12
- "verb": "scan",
13
- "flags": ["--changed", "--json"]
14
- },
15
- "assertions": [
16
- { "type": "exit-code", "value": 0 },
17
- { "type": "json-path", "path": "$.stats.nodesCount", "equals": 1 },
18
- { "type": "json-path", "path": "$.stats.issuesCount", "equals": 0 },
19
- { "type": "json-path", "path": "$.nodes[0].path", "equals": "skills/bar.md" }
20
- ]
21
- }
@@ -1,16 +0,0 @@
1
- ---
2
- name: reviewer-agent
3
- description: A minimal agent that reviews supplied text for tone, clarity, and grammar.
4
- model: sonnet
5
- tools:
6
- - Read
7
- - Edit
8
- metadata:
9
- version: 1.0.0
10
- stability: stable
11
- color: blue
12
- ---
13
-
14
- # Reviewer agent
15
-
16
- Reviews supplied text and suggests edits. Does not modify files automatically; returns proposed diffs only.
@@ -1,17 +0,0 @@
1
- ---
2
- name: status
3
- description: Prints a one-line project status (branch, dirty flag, ahead/behind).
4
- args:
5
- - name: verbose
6
- type: boolean
7
- required: false
8
- description: Include extra git details.
9
- metadata:
10
- version: 1.0.0
11
- ---
12
-
13
- # /status command
14
-
15
- Prints a concise status line for the current project. Shows the branch name, whether the tree is dirty, and the commit distance from upstream.
16
-
17
- With `--verbose`, also lists modified paths.