@skill-map/spec 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +676 -3
- package/README.md +11 -13
- package/architecture.md +118 -35
- package/cli-contract.md +38 -25
- package/conformance/README.md +42 -13
- package/conformance/cases/kernel-empty-boot.json +2 -2
- package/conformance/coverage.md +19 -23
- package/db-schema.md +61 -6
- package/index.json +34 -76
- package/job-events.md +75 -1
- package/package.json +1 -1
- package/plugin-author-guide.md +409 -51
- package/schemas/conformance-case.schema.json +3 -3
- package/schemas/execution-record.schema.json +8 -8
- package/schemas/extensions/action.schema.json +2 -2
- package/schemas/extensions/base.schema.json +5 -5
- package/schemas/extensions/extractor.schema.json +48 -0
- package/schemas/extensions/formatter.schema.json +29 -0
- package/schemas/extensions/hook.schema.json +44 -0
- package/schemas/extensions/provider.schema.json +51 -0
- package/schemas/extensions/rule.schema.json +1 -1
- package/schemas/frontmatter/base.schema.json +2 -2
- package/schemas/link.schema.json +4 -4
- package/schemas/node.schema.json +4 -4
- package/schemas/plugins-registry.schema.json +19 -4
- package/schemas/project-config.schema.json +2 -2
- package/schemas/scan-result.schema.json +3 -3
- package/conformance/cases/basic-scan.json +0 -17
- package/conformance/cases/orphan-detection.json +0 -22
- package/conformance/cases/rename-high.json +0 -21
- package/conformance/fixtures/minimal-claude/agents/reviewer.md +0 -16
- package/conformance/fixtures/minimal-claude/commands/status.md +0 -17
- package/conformance/fixtures/minimal-claude/hooks/pre-commit.md +0 -13
- package/conformance/fixtures/minimal-claude/notes/architecture.md +0 -11
- package/conformance/fixtures/minimal-claude/skills/hello.md +0 -22
- package/conformance/fixtures/orphan-after/skills/keep.md +0 -13
- package/conformance/fixtures/orphan-before/skills/keep.md +0 -13
- package/conformance/fixtures/orphan-before/skills/lonely.md +0 -13
- package/conformance/fixtures/rename-high-after/skills/bar.md +0 -14
- package/conformance/fixtures/rename-high-before/skills/foo.md +0 -14
- package/schemas/extensions/adapter.schema.json +0 -40
- package/schemas/extensions/audit.schema.json +0 -47
- package/schemas/extensions/detector.schema.json +0 -41
- package/schemas/extensions/renderer.schema.json +0 -29
- package/schemas/frontmatter/agent.schema.json +0 -17
- package/schemas/frontmatter/command.schema.json +0 -39
- package/schemas/frontmatter/hook.schema.json +0 -29
- package/schemas/frontmatter/note.schema.json +0 -11
- package/schemas/frontmatter/skill.schema.json +0 -37
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ This document is the **source of truth**. The reference implementation under `..
|
|
|
7
7
|
## What this spec defines
|
|
8
8
|
|
|
9
9
|
- The **domain model**: nodes, links, issues, scan results.
|
|
10
|
-
- The **extension contract**: six extension kinds (
|
|
10
|
+
- The **extension contract**: six extension kinds (provider, extractor, rule, action, formatter, hook) with their input/output shapes.
|
|
11
11
|
- The **CLI contract**: verb set, flags, exit codes, JSON introspection.
|
|
12
12
|
- The **persistence contract**: table catalog owned by the kernel, plugin key-value API.
|
|
13
13
|
- The **job contract**: lifecycle states, event stream, prompt preamble, submit/claim/record semantics.
|
|
@@ -76,20 +76,18 @@ spec/ ← published as @skill-map/spec
|
|
|
76
76
|
│ │
|
|
77
77
|
│ ├── extensions/ ← one per extension kind; validated at plugin load
|
|
78
78
|
│ │ ├── base.schema.json ┐
|
|
79
|
-
│ │ ├──
|
|
80
|
-
│ │ ├──
|
|
81
|
-
│ │ ├── rule.schema.json │ (base +
|
|
79
|
+
│ │ ├── provider.schema.json │
|
|
80
|
+
│ │ ├── extractor.schema.json │ 6 extension schemas
|
|
81
|
+
│ │ ├── rule.schema.json │ (base + 5 kinds)
|
|
82
82
|
│ │ ├── action.schema.json │
|
|
83
|
-
│ │
|
|
84
|
-
│ │ └── renderer.schema.json ┘
|
|
83
|
+
│ │ └── formatter.schema.json ┘
|
|
85
84
|
│ │
|
|
86
85
|
│ ├── frontmatter/ ← user-authored; additionalProperties: true
|
|
87
|
-
│ │
|
|
88
|
-
│ │
|
|
89
|
-
│ │
|
|
90
|
-
│ │
|
|
91
|
-
│ │
|
|
92
|
-
│ │ └── note.schema.json ┘
|
|
86
|
+
│ │ └── base.schema.json ← universal shape; per-kind schemas live with
|
|
87
|
+
│ │ the Provider that emits the kind (e.g. the
|
|
88
|
+
│ │ built-in Claude Provider's `skill / agent /
|
|
89
|
+
│ │ command / hook / note` schemas live in
|
|
90
|
+
│ │ `src/extensions/providers/claude/schemas/`)
|
|
93
91
|
│ │
|
|
94
92
|
│ └── summaries/ ← kernel-controlled; additionalProperties: false
|
|
95
93
|
│ ├── skill.schema.json ┐
|
|
@@ -110,7 +108,7 @@ spec/ ← published as @skill-map/spec
|
|
|
110
108
|
## How to read this spec
|
|
111
109
|
|
|
112
110
|
- **Building a tool or plugin that consumes skill-map output?** Start with [`schemas/scan-result.schema.json`](./schemas/scan-result.schema.json) and [`schemas/node.schema.json`](./schemas/node.schema.json).
|
|
113
|
-
- **Building a custom
|
|
111
|
+
- **Building a custom extractor, rule, or formatter?** Read [`architecture.md`](./architecture.md), then the relevant schema under [`schemas/extensions/`](./schemas/extensions/).
|
|
114
112
|
- **Building an alternative CLI implementation?** Read [`cli-contract.md`](./cli-contract.md) and run [`conformance/`](./conformance/README.md).
|
|
115
113
|
- **Integrating a new platform (adapter)?** Read [`architecture.md`](./architecture.md) §adapters, then the Claude adapter source in `../src/extensions/adapters/claude/` as a worked example.
|
|
116
114
|
- **Shipping a job-running runner?** Read [`job-events.md`](./job-events.md), [`job-lifecycle.md`](./job-lifecycle.md), [`prompt-preamble.md`](./prompt-preamble.md).
|
package/architecture.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
Normative description of skill-map's internal boundaries: the **kernel**, the **ports** it exposes, the **adapters** that drive and serve it, and the
|
|
3
|
+
Normative description of skill-map's internal boundaries: the **kernel**, the **ports** it exposes, the **adapters** that drive and serve it, and the five **extension kinds** that live outside the kernel.
|
|
4
4
|
|
|
5
|
-
Any conforming implementation — reference or third-party — MUST respect these boundaries. The conformance suite under [`conformance/`](./conformance/README.md) enforces
|
|
5
|
+
Any conforming implementation — reference or third-party — MUST respect these boundaries. The conformance suite under [`conformance/`](./conformance/README.md) enforces the kernel-agnostic invariants; per-Provider suites (e.g. `src/extensions/providers/claude/conformance/` for the reference impl's Claude Provider) enforce the kind-catalog cases. Both are driven via `sm conformance run`.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -67,6 +67,15 @@ Discovers plugin directories, reads `plugin.json`, checks `specCompat`, dynamica
|
|
|
67
67
|
|
|
68
68
|
Operations: `discover(scopes)`, `load(pluginPath)`, `validateManifest(json)`.
|
|
69
69
|
|
|
70
|
+
The loader enforces two id-uniqueness rules during discovery (see [`plugin-author-guide.md` §Plugin id uniqueness](./plugin-author-guide.md#plugin-id-uniqueness) for the author-facing summary):
|
|
71
|
+
|
|
72
|
+
1. **Directory name == manifest id.** A plugin lives at `<root>/<id>/plugin.json`. A mismatch surfaces as status `invalid-manifest`. This rule eliminates same-root collisions by construction.
|
|
73
|
+
2. **Cross-root id collision blocks both sides.** Two plugins reachable from different roots (project + global, or any `--plugin-dir` combination) that declare the same `id` BOTH receive status `id-collision`. No precedence rule applies — coherent with §Boot invariant ("no extension is privileged"). The user resolves by renaming one of them.
|
|
74
|
+
|
|
75
|
+
In addition, the loader **qualifies every extension** with its owning plugin id before registering it. The registry stores extensions under the qualified id `<plugin-id>/<extension-id>` (e.g. `claude/slash`, `core/broken-ref`, `hello-world/greet`). Authors continue to declare the short `id` in each extension manifest; the loader composes the qualified form from `manifest.id` at load time. Built-in extensions bundled with the reference impl declare their `pluginId` directly in `built-ins.ts` — `core/` for kernel-internal primitives (rules, the formatter, the external-url-counter extractor) and `claude/` for the Claude provider bundle (the Provider and its kind-aware extractors). If a plugin author injects a `pluginId` field on an extension that disagrees with `plugin.json`'s `id`, the loader emits `invalid-manifest` with a directed reason.
|
|
76
|
+
|
|
77
|
+
Each plugin (and each built-in bundle) declares a **granularity** that controls how its extensions are toggled. `granularity: 'bundle'` (the default) means the plugin id is the only enable/disable key; `granularity: 'extension'` means each extension is independently toggle-able under its qualified id. The loader's pre-import `resolveEnabled(pluginId)` short-circuit is always coarse (bundle level) — when a granularity=`extension` bundle is partially enabled, the import work proceeds and the runtime composer (the CLI's `composeScanExtensions` / `composeFormatters` in `src/cli/util/plugin-runtime.ts`) drops the disabled extensions before they reach the orchestrator. The two built-in bundles split deliberately: `claude` is granularity=`bundle` (provider-level toggle), `core` is granularity=`extension` (every kernel built-in is removable, satisfying §Boot invariant: "no extension is privileged"). See [`plugin-author-guide.md` §Granularity — bundle vs extension](./plugin-author-guide.md#granularity--bundle-vs-extension) for the author-facing summary.
|
|
78
|
+
|
|
70
79
|
### `RunnerPort`
|
|
71
80
|
|
|
72
81
|
Executes an action against a job file. Returns a report reference (or an error) plus runner-side metrics (duration, tokens, exit code).
|
|
@@ -103,15 +112,15 @@ The kernel is the only component that MAY:
|
|
|
103
112
|
- Dispatch extension hooks.
|
|
104
113
|
|
|
105
114
|
The kernel MUST NOT:
|
|
106
|
-
- Know which
|
|
107
|
-
- Know which platform a node belongs to (that is the `
|
|
115
|
+
- Know which Provider produced an event.
|
|
116
|
+
- Know which platform a node belongs to (that is the `Provider` extension's job).
|
|
108
117
|
- Contain any platform-specific branching (e.g., `if (platform === 'claude')`).
|
|
109
118
|
|
|
110
119
|
### Boot invariant
|
|
111
120
|
|
|
112
121
|
**With all extensions removed, the kernel MUST boot and return an empty graph.** This is enforced by the conformance suite case `kernel-empty-boot`.
|
|
113
122
|
|
|
114
|
-
No extension is privileged. The Claude
|
|
123
|
+
No extension is privileged. The Claude Provider ships bundled with the reference impl but is removable, same as any third-party plugin.
|
|
115
124
|
|
|
116
125
|
---
|
|
117
126
|
|
|
@@ -128,25 +137,16 @@ Mode is a property of the extension as a whole, not of an individual call. **An
|
|
|
128
137
|
|
|
129
138
|
| Kind | Modes | How mode is set |
|
|
130
139
|
|---|---|---|
|
|
131
|
-
| **
|
|
140
|
+
| **Extractor** | deterministic / probabilistic | declared in manifest (`mode` field, optional; defaults to `deterministic`) |
|
|
132
141
|
| **Rule** | deterministic / probabilistic | declared in manifest (`mode` field, optional; defaults to `deterministic`) |
|
|
133
142
|
| **Action** | deterministic / probabilistic | declared in manifest (`mode` field, **required** — no default) |
|
|
134
|
-
| **
|
|
135
|
-
| **
|
|
136
|
-
| **
|
|
137
|
-
|
|
138
|
-
Adapter and Renderer are locked to deterministic because they sit at the **boundaries** of the system. An adapter resolves `path → kind` during boot; probabilistic classification would make the boot phase slow, costly, and non-reproducible. A renderer must produce diffable output (`sm scan` snapshots round-trip in CI). Probabilistic narrators of the graph are a valid product but they live in jobs and emit Findings, not in renderers.
|
|
143
|
+
| **Hook** | deterministic / probabilistic | declared in manifest (`mode` field, optional; defaults to `deterministic`) |
|
|
144
|
+
| **Provider** | deterministic-only | implicit; `mode` field MUST NOT appear |
|
|
145
|
+
| **Formatter** | deterministic-only | implicit; `mode` field MUST NOT appear |
|
|
139
146
|
|
|
140
|
-
|
|
147
|
+
Provider and Formatter are locked to deterministic because they sit at the **boundaries** of the system. A Provider resolves `path → kind` during boot; probabilistic classification would make the boot phase slow, costly, and non-reproducible. A formatter must produce diffable output (`sm scan` snapshots round-trip in CI). Probabilistic narrators of the graph are a valid product but they live in jobs and emit Findings, not in formatters.
|
|
141
148
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
- If every composed primitive is `deterministic` → the audit's effective mode is `deterministic`. Runs synchronously inside `sm audit <id>`.
|
|
145
|
-
- If any composed primitive is `probabilistic` → the audit's effective mode is `probabilistic`. Dispatches as a job via `sm job submit audit:<id>`.
|
|
146
|
-
|
|
147
|
-
A dangling reference in `composes[]` (the id doesn't resolve, the kind is wrong, or the primitive is disabled) is a **load-time error**. The audit is rejected with status `invalid-manifest`, not silently skipped. This matches the rule already in place for `defaultRefreshAction`. Declaring `mode` directly on an audit manifest is also a load-time error.
|
|
148
|
-
|
|
149
|
-
The effective mode is exposed to the UI and to `sm audit show <id>` so consumers can preview cost before invoking.
|
|
149
|
+
> **Naming note — `Provider` vs hexagonal `adapter`.** The extension kind formerly named `Adapter` is now `Provider`. The hexagonal-architecture term `adapter` (driving / driven adapters that implement ports — `RunnerPort.adapter`, `StoragePort.adapter`, `FilesystemPort.adapter`, `PluginLoaderPort.adapter`) is unchanged: those live in `kernel/adapters/` and are internal to the impl. A `Provider` is an **extension** authored by plugins; an **adapter** in the hexagonal sense is a **port implementation** internal to the kernel package. The two concepts share an architectural lineage (both bridge two worlds) but live in deliberately disjoint namespaces so plugin authors and impl maintainers never confuse them.
|
|
150
150
|
|
|
151
151
|
### When each mode runs
|
|
152
152
|
|
|
@@ -169,22 +169,76 @@ Six kinds, all first-class, all loaded through the same registry. Each kind has
|
|
|
169
169
|
|
|
170
170
|
| Kind | Role | Input | Output |
|
|
171
171
|
|---|---|---|---|
|
|
172
|
-
| **
|
|
173
|
-
| **
|
|
172
|
+
| **Provider** | Recognizes a platform. Declares the catalog of node `kind`s it emits via the `kinds` map; each map entry pairs the kind's frontmatter schema (path relative to the Provider's package directory) with its `defaultRefreshAction` (a qualified action id that drives the probabilistic-refresh surface). Also declares the filesystem `explorationDir` where its content lives. Deterministic-only. | Filesystem walk results, candidate path. | `{ kind, provider } \| null`. |
|
|
173
|
+
| **Extractor** | Extracts signals from a node body. Dual-mode: `deterministic` runs in scan, `probabilistic` runs in jobs. Output flows through three context callbacks (no return value): `ctx.emitLink(link)` for the kernel's `links` table, `ctx.enrichNode(partial)` for the kernel's enrichment layer (separate from the author's frontmatter), `ctx.store` for the plugin's own KV / dedicated tables. | Parsed node (frontmatter + body) + callbacks. | `void` (output via callbacks). |
|
|
174
174
|
| **Rule** | Evaluates the graph. Dual-mode: `deterministic` runs in `sm check`, `probabilistic` runs in jobs. | Full graph (nodes + links). | `Issue[]`. |
|
|
175
175
|
| **Action** | Operates on one or more nodes. Dual-mode: `deterministic` (in-process code) or `probabilistic` (rendered prompt the runner executes). | Node(s), optional args. | Deterministic: report JSON. Probabilistic: rendered prompt that a runner executes. |
|
|
176
|
-
| **
|
|
177
|
-
| **
|
|
176
|
+
| **Formatter** | Serializes the graph. Deterministic-only. | Graph + optional filter. | String (ASCII / Mermaid / DOT / JSON / user-defined). |
|
|
177
|
+
| **Hook** | Reacts declaratively to one of eight curated lifecycle events (`scan.started`, `scan.completed`, `extractor.completed`, `rule.completed`, `action.completed`, `job.spawning`, `job.completed`, `job.failed`). Dual-mode: `deterministic` runs in-process during the dispatch, `probabilistic` is enqueued as a job. Hooks REACT to events; they cannot block, mutate, or steer the pipeline. | A curated event payload (run-scoped, scan-scoped, or job-scoped) plus an optional declarative `filter` map. | `void` (reactions are side effects). |
|
|
178
|
+
|
|
179
|
+
### Provider · `kinds` catalog
|
|
180
|
+
|
|
181
|
+
Every `Provider` extension MUST declare a map `kinds: { <kind>: { schema: string, defaultRefreshAction: string } }` covering every `kind` it can classify into. Each entry has two required fields:
|
|
182
|
+
|
|
183
|
+
- **`schema`** — path to the kind's frontmatter JSON Schema, relative to the Provider's package directory. The schema MUST extend the spec's universal [`frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) via `allOf` + `$ref` to base's `$id` so cross-package resolution works without copying base into every Provider. The kernel registers each Provider's schemas with AJV at scan boot and validates each node's frontmatter against the entry that matches its classified kind.
|
|
184
|
+
- **`defaultRefreshAction`** — qualified action id (`<plugin-id>/<action-id>`) the UI's probabilistic-refresh surface (`🧠 prob`) dispatches for nodes of this kind. The referenced action MUST exist in the registry by the time the graph is queried; a dangling reference is a load-time error for the Provider (status `invalid-manifest`). Consumers dispatch `sm job submit <defaultRefreshAction> -n <nodePath>` when the user asks for a probabilistic refresh. Implementations MAY allow plugins to override the default per-node via `metadata.refreshAction`, but the Provider default is normative.
|
|
185
|
+
|
|
186
|
+
The catalog is the single source of truth for "which kinds does this Provider emit" — the `IProvider` runtime contract derives the kind set from `Object.keys(kinds)`. Spec 0.8.0 (Phase 3 of plug-in model overhaul) replaced two earlier fields (`emits: string[]` and a flat `defaultRefreshAction: { <kind>: actionId }`) with this richer map; the catalog also subsumes per-kind frontmatter schemas, which previously lived in spec under `schemas/frontmatter/<kind>.schema.json`.
|
|
187
|
+
|
|
188
|
+
### Provider · `explorationDir`
|
|
189
|
+
|
|
190
|
+
Every `Provider` extension MUST declare an `explorationDir: string` naming the filesystem directory (relative to user home or project root) where its content lives. 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` (and `sm plugins doctor`) validates the directory exists; missing directory yields a non-blocking warning so the user sees the gap without the load failing — the Provider may legitimately precede installation of its platform.
|
|
191
|
+
|
|
192
|
+
### Extractor · output callbacks
|
|
193
|
+
|
|
194
|
+
The `Extractor` runtime contract is `extract(ctx) → void`. The extractor emits its work through three callbacks the kernel binds onto `ctx`:
|
|
195
|
+
|
|
196
|
+
- `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.
|
|
197
|
+
- `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 (e.g. computed titles, summaries, signals from probabilistic extractors) without polluting what the user wrote on disk. See §Enrichment layer below for the full lifecycle (per-extractor attribution, stale tracking, refresh verbs).
|
|
198
|
+
- `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).
|
|
199
|
+
|
|
200
|
+
Probabilistic extractors additionally receive `ctx.runner` (the `RunnerPort`) for LLM dispatch.
|
|
201
|
+
|
|
202
|
+
### Extractor · enrichment layer
|
|
203
|
+
|
|
204
|
+
`ctx.enrichNode(partial)` is the only writable surface the Extractor pipeline has on a node. The author's frontmatter on `scan_nodes.frontmatter_json` is read-only from any Extractor — that contract holds for both deterministic and probabilistic extractors. Implementations MUST:
|
|
178
205
|
|
|
179
|
-
|
|
206
|
+
- Persist enrichments into a per-`(node, extractor)` table (the reference impl uses [`node_enrichments`](./db-schema.md#node_enrichments)) so attribution survives across scans.
|
|
207
|
+
- Preserve the author frontmatter byte-for-byte through every scan and refresh; the enrichment overlay is a SEPARATE store.
|
|
208
|
+
- Track stale state for probabilistic rows: when the scan loop detects `body_hash_at_enrichment != node.body_hash` for a probabilistic enrichment, mark the row stale (NOT delete it — the LLM cost is preserved). Deterministic enrichments do not need stale tracking — they regenerate via the §Extractor · fine-grained scan cache contract.
|
|
180
209
|
|
|
181
|
-
|
|
210
|
+
Read-side merge (`mergeNodeWithEnrichments` in the reference impl):
|
|
182
211
|
|
|
183
|
-
|
|
212
|
+
1. Filter to non-stale enrichments for the target node.
|
|
213
|
+
2. Sort by `enriched_at` ASC.
|
|
214
|
+
3. Spread-merge each `value` over the author frontmatter (last-write-wins per field).
|
|
184
215
|
|
|
185
|
-
|
|
216
|
+
Rules / `sm check` / `sm export` consume `node.frontmatter` directly (deterministic CI-safe baseline); enrichment consumption is opt-in by the caller. Stale visibility is also opt-in (`includeStale: true` in the merge helper) so the UI can render a "stale (last value: …)" marker without polluting the deterministic merge.
|
|
186
217
|
|
|
187
|
-
|
|
218
|
+
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).
|
|
219
|
+
|
|
220
|
+
### Extractor · `applicableKinds` filter
|
|
221
|
+
|
|
222
|
+
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 a probabilistic extractor wastes zero LLM cost — and a deterministic extractor 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.
|
|
223
|
+
|
|
224
|
+
### Extractor · fine-grained scan cache
|
|
225
|
+
|
|
226
|
+
Implementations MAY maintain a per-`(node, extractor)` cache so that on `sm scan --changed` the orchestrator can skip rerunning an Extractor against an unchanged body when that specific Extractor already ran against the same body hash. The reference impl persists the cache in [`scan_extractor_runs`](./db-schema.md#scan_extractor_runs).
|
|
227
|
+
|
|
228
|
+
The contract the cache MUST satisfy (engine-agnostic):
|
|
229
|
+
|
|
230
|
+
- A node-level cache hit (body+frontmatter unchanged) is upgraded to a full skip ONLY when every currently-registered Extractor that applies to the node's kind has a recorded run against the prior body hash.
|
|
231
|
+
- A new Extractor registered between scans MUST run on the cached node — its absence from the cache is the canonical signal. The rest of the cache (existing Extractors against the same body) is preserved.
|
|
232
|
+
- An Extractor uninstalled between scans MUST have its cache rows removed and its sole-source links dropped. Links whose `sources` mix the uninstalled Extractor's short id with a still-cached Extractor's short id MUST be reshaped: the obsolete short id is stripped from the array and the link survives with the cached attribution intact. The persisted audit trail therefore never references a removed contributor.
|
|
233
|
+
- The cache is transparent to plugin authors. An Extractor cannot opt out and cannot inspect the cache; its only obligation is to be deterministic for a given body input (probabilistic Extractors run as jobs, never in scan).
|
|
234
|
+
|
|
235
|
+
This invariant is the difference between a free and a paid scan for the probabilistic Extractor model: re-running an LLM Extractor against an unchanged body would be both expensive and non-reproducible.
|
|
236
|
+
|
|
237
|
+
### Extractor · trigger normalization
|
|
238
|
+
|
|
239
|
+
Extractors that emit invocation-style links (slashes, at-directives, command names) populate the `link.trigger` block defined in [`schemas/link.schema.json`](./schemas/link.schema.json):
|
|
240
|
+
|
|
241
|
+
- `originalTrigger` — the exact source text the extractor saw, byte-for-byte. Used only for display.
|
|
188
242
|
- `normalizedTrigger` — the output of the pipeline below. Used for equality and collision detection — the built-in `trigger-collision` rule keys on this field.
|
|
189
243
|
|
|
190
244
|
Both fields MUST be present whenever `link.trigger` is non-null. Implementations MUST produce byte-identical `normalizedTrigger` output for byte-identical input across platforms and locales.
|
|
@@ -200,7 +254,7 @@ Applied in exactly this order:
|
|
|
200
254
|
5. **Collapse whitespace** — runs of two or more spaces become one.
|
|
201
255
|
6. **Trim** — strip leading and trailing whitespace.
|
|
202
256
|
|
|
203
|
-
Characters outside the separator set that are not letters or digits (e.g. `/`, `@`, `:`, `.`) are **preserved**. Stripping them is the
|
|
257
|
+
Characters outside the separator set that are not letters or digits (e.g. `/`, `@`, `:`, `.`) are **preserved**. Stripping them is the extractor's concern, not the normalizer's — the normalizer operates on whatever the extractor classifies as "the trigger text". This keeps namespaced invocations like `/skill-map:explore` or `@my-plugin/foo` comparable in their intended form.
|
|
204
258
|
|
|
205
259
|
#### Examples
|
|
206
260
|
|
|
@@ -212,9 +266,38 @@ Characters outside the separator set that are not letters or digits (e.g. `/`, `
|
|
|
212
266
|
| ` hacer review ` | `hacer review` |
|
|
213
267
|
| `Clúster` | `cluster` |
|
|
214
268
|
| `/MyCommand` | `/mycommand` |
|
|
215
|
-
| `@
|
|
269
|
+
| `@FooExtractor` | `@fooextractor` |
|
|
216
270
|
| `skill-map:explore` | `skill map:explore` |
|
|
217
271
|
|
|
272
|
+
### Hook · curated trigger set
|
|
273
|
+
|
|
274
|
+
Hooks subscribe declaratively to a curated set of kernel lifecycle events and react to them. Reaction-only by design: a hook cannot mutate the pipeline, block emission, or alter outputs. The hookable trigger set is intentionally small — eight events out of the full [`job-events.md`](./job-events.md) catalog. Other events (per-node `scan.progress`, `model.delta`, `run.*`, `job.claimed`, `job.callback.received`) are deliberately NOT hookable: too verbose for a reactive surface, internal to the runner, or covered elsewhere. Declaring a trigger outside the curated set yields `invalid-manifest` at load time.
|
|
275
|
+
|
|
276
|
+
| Trigger | When it fires | Payload (key fields) | Hook scope |
|
|
277
|
+
|---|---|---|---|
|
|
278
|
+
| `scan.started` | Once at the start of every `sm scan` invocation. | `roots: string[]`. | Pre-scan setup (cache warm-up, telemetry init). |
|
|
279
|
+
| `scan.completed` | Once at the end of every `sm scan` invocation. | `stats: { filesWalked, nodesCount, linksCount, issuesCount, durationMs }`. | Post-scan reaction (Slack notification, CI gate, summary). |
|
|
280
|
+
| `extractor.completed` | Once per registered Extractor, after the full walk completes. Aggregated, NOT per-node. | `extractorId: string` (qualified). | Per-Extractor metrics, audit. |
|
|
281
|
+
| `rule.completed` | Once per Rule, after every issue has been validated. | `ruleId: string` (qualified). | Per-Rule alerting, downstream tooling. |
|
|
282
|
+
| `action.completed` | Once per Action invocation, after the report has been recorded. | `actionId: string` (qualified), `node`, `jobResult`. | Per-Action notification, integration glue. |
|
|
283
|
+
| `job.spawning` | Pre-spawn of a runner subprocess (job subsystem; Step 10). | `jobId`, `actionId`, spawn metadata. | Pre-flight checks, audit logging. |
|
|
284
|
+
| `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). |
|
|
285
|
+
|
|
286
|
+
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:
|
|
287
|
+
|
|
288
|
+
- `filter: { extractorId: 'core/external-url-counter' }` — invoke only when THIS extractor finishes.
|
|
289
|
+
- `filter: { actionId: 'claude/skill-summarizer' }` — invoke only for one Action.
|
|
290
|
+
- `filter: { reason: 'runner-error' }` (on `job.failed`) — invoke only when the runner crashed.
|
|
291
|
+
|
|
292
|
+
#### Mode semantics
|
|
293
|
+
|
|
294
|
+
- **Deterministic** (default): 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. Errors are caught by the dispatcher (logged through a synthetic `extension.error` event with kind `hook-error`) and NEVER block the main pipeline. A buggy hook degrades gracefully — the scan continues.
|
|
295
|
+
- **Probabilistic**: the hook is enqueued as a job. Until the job subsystem ships at Step 10, probabilistic hooks load but skip dispatch with a stderr advisory. The hook still surfaces in `sm plugins list` / `sm plugins doctor`; it just does not fire today.
|
|
296
|
+
|
|
297
|
+
#### Cross-extension impact
|
|
298
|
+
|
|
299
|
+
Hooks introduce no new persisted state and do NOT participate in the deterministic scan cache (A.9). A scan that re-runs against an unchanged corpus dispatches `scan.started` / `scan.completed` exactly as before; subscribed hooks fire on every scan regardless of cache hit / miss. Hooks that need cache-aware behaviour MUST inspect their own state via `ctx.store` (declared in their plugin's manifest).
|
|
300
|
+
|
|
218
301
|
### Contract rules
|
|
219
302
|
|
|
220
303
|
1. An extension declares its kind in its module export and its manifest. Kind mismatch → load-error.
|
|
@@ -226,7 +309,7 @@ Characters outside the separator set that are not letters or digits (e.g. `/`, `
|
|
|
226
309
|
### Locality
|
|
227
310
|
|
|
228
311
|
- **Drop-in**: extensions live inside plugins, discovered at boot from `.skill-map/plugins/<id>/` and `~/.skill-map/plugins/<id>/`.
|
|
229
|
-
- **Built-in**: the reference impl bundles a default extension set (one
|
|
312
|
+
- **Built-in**: the reference impl bundles a default extension set (one Provider, four extractors, five rules, one formatter, zero hooks). The fifth rule, `core/validate-all`, replays every scanned node and link through the authoritative spec schemas via AJV — the kernel-side guard against persisting non-conforming graph rows. The Hook kind has no built-ins at this bump; the kind exists so plugins can subscribe (concrete built-in hooks land separately when demand surfaces). These are loaded from `src/extensions/` and are indistinguishable from plugin-supplied extensions from the kernel's point of view.
|
|
230
313
|
|
|
231
314
|
---
|
|
232
315
|
|
|
@@ -313,10 +396,10 @@ This is what makes "CLI-first" a coherent rule: every CLI verb is a kernel funct
|
|
|
313
396
|
|
|
314
397
|
The **port list** is stable as of spec v1.0.0. Adding a sixth port is a major bump.
|
|
315
398
|
|
|
316
|
-
The **extension kind list** (
|
|
399
|
+
The **extension kind list** (5 kinds) is stable as of spec v1.0.0. Adding a sixth kind is a major bump.
|
|
317
400
|
|
|
318
|
-
The **execution modes** (`deterministic` / `probabilistic`) and the per-kind mode capability matrix above are stable as of spec v1.0.0. Adding a third mode
|
|
401
|
+
The **execution modes** (`deterministic` / `probabilistic`) and the per-kind mode capability matrix above are stable as of spec v1.0.0. Adding a third mode or changing which kinds are dual-mode is a major bump. Renaming or repurposing the mode enum values is a major bump.
|
|
319
402
|
|
|
320
403
|
The **dependency rules** above are stable as of spec v1.0.0. Relaxing any is a major bump; tightening (forbidding an allowed import) is a minor bump.
|
|
321
404
|
|
|
322
|
-
The **
|
|
405
|
+
The **Extractor · trigger normalization** pipeline (six steps, in order) is stable from the next spec release. Adding a new step at the end is a minor bump; reordering, removing, or changing any existing step (including the character classes in step 4) is a major bump. Implementations that produce different `normalizedTrigger` output for equivalent input are non-conforming.
|
package/cli-contract.md
CHANGED
|
@@ -62,8 +62,8 @@ All verbs use this shared table. Additional codes MAY be defined per-verb (docum
|
|
|
62
62
|
| Code | Meaning | When emitted |
|
|
63
63
|
|---|---|---|
|
|
64
64
|
| `0` | OK | Command completed, no issues at or above the configured severity threshold. |
|
|
65
|
-
| `1` | Issues found | Command completed, but deterministic issues at `error` severity exist. Applies to `sm scan`, `sm check`, `sm
|
|
66
|
-
| `2` | Operational error | Bad flags, missing DB, unreadable file, corrupt config, unhandled exception. Accompanied by an error message on stderr. |
|
|
65
|
+
| `1` | Issues found | Command completed, but deterministic issues at `error` severity exist. Applies to `sm scan`, `sm check`, `sm doctor`. |
|
|
66
|
+
| `2` | Operational error | Bad flags, missing DB, unreadable file, corrupt config, runtime / environment mismatch (e.g. wrong Node version, missing native dependency), unhandled exception. Accompanied by an error message on stderr. |
|
|
67
67
|
| `3` | Duplicate conflict | Job submission refused because an active duplicate exists (same `action + version + node + contentHash`). Returned by `sm job submit`. |
|
|
68
68
|
| `4` | Nonce mismatch | `sm record` called with an `id`/`nonce` pair that does not match. |
|
|
69
69
|
| `5` | Not found | A named resource does not exist (node id, job id, plugin id, config key). |
|
|
@@ -72,6 +72,20 @@ Codes 6–15 are reserved. Codes ≥ 16 are free for verb-specific use.
|
|
|
72
72
|
|
|
73
73
|
---
|
|
74
74
|
|
|
75
|
+
## Dry-run
|
|
76
|
+
|
|
77
|
+
A verb that exposes `-n` / `--dry-run` MUST honour the following contract:
|
|
78
|
+
|
|
79
|
+
- **No observable side effects.** The command MUST NOT mutate the database, the filesystem, the config, the network, or spawn external processes. Read-only operations needed to compute the preview (e.g. loading the prior `ScanResult`, reading existing config files, listing FS entries) ARE permitted.
|
|
80
|
+
- **No auto-provisioning.** A dry-run MUST NOT create directories, schema files, or DBs that would not exist after the command. If the operation would create a `.skill-map/` scope, dry-run only previews the creation; the directory must NOT appear on disk.
|
|
81
|
+
- **Output mirrors the live mode** — same shape, same fields, same `--json` schema — except that human-readable output explicitly indicates the dry-run state ("would persist …", "would create …", "would delete …", or a clear "(dry-run)" suffix) and machine-readable output sets a top-level `dryRun: true` field where applicable.
|
|
82
|
+
- **Exit codes mirror the live mode.** Same exit code table; the dry-run posture does not introduce new codes. A dry-run that surfaces an error severity (e.g. "scan would emit an error-severity issue") still exits `1`; a dry-run that fails to read the input still exits `2`.
|
|
83
|
+
- **Dry-run MUST NOT depend on `--yes` / `--force`.** Verbs that offer interactive confirmation for destructive operations MUST allow `--dry-run` to bypass the prompt entirely (no confirmation needed when nothing is being destroyed).
|
|
84
|
+
|
|
85
|
+
Dry-run is **per-verb opt-in**. The flag is not global; verbs that do not declare it MUST reject `--dry-run` as an unknown option (exit `2`), the same as any other unknown flag. The verb catalog below names every verb that exposes the flag and what its preview looks like.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
75
89
|
## Verb catalog
|
|
76
90
|
|
|
77
91
|
### Setup & state
|
|
@@ -85,7 +99,7 @@ Bootstrap the current scope.
|
|
|
85
99
|
- Runs migrations.
|
|
86
100
|
- Runs a first scan.
|
|
87
101
|
|
|
88
|
-
Flags: `--no-scan` (skip the first scan), `--force` (rewrite an existing config).
|
|
102
|
+
Flags: `--no-scan` (skip the first scan), `--force` (rewrite an existing config), `-n` / `--dry-run` (preview the scope provisioning — would-create lines for every directory and file the live invocation would write — without touching the filesystem; respects `--force` for the "would-overwrite" preview).
|
|
89
103
|
|
|
90
104
|
Exit: 0 on success, 2 on failure.
|
|
91
105
|
|
|
@@ -112,7 +126,7 @@ Diagnostic report:
|
|
|
112
126
|
- Orphan job files (count).
|
|
113
127
|
- Plugins in error state (list).
|
|
114
128
|
- LLM runner availability (`claude` binary on PATH, version).
|
|
115
|
-
- Detected
|
|
129
|
+
- Detected Providers that matched nothing, or whose `explorationDir` does not exist on disk (non-blocking warning).
|
|
116
130
|
|
|
117
131
|
Exit: 0 if all green, 1 if warnings, 2 if any `error`-level problem.
|
|
118
132
|
|
|
@@ -167,8 +181,10 @@ Keys are dot-paths (`jobs.minimumTtlSeconds`, `scan.tokenize`). Unknown keys →
|
|
|
167
181
|
| `sm scan -n <node.path>` | Partial scan: one node. |
|
|
168
182
|
| `sm scan --changed` | Incremental: only files changed since last scan (mtime heuristic). |
|
|
169
183
|
| `sm scan --watch` | Long-running: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`. |
|
|
170
|
-
| `sm scan
|
|
184
|
+
| `sm scan compare-with <dump> [roots...]` | Delta report: run a fresh scan in memory and compare against the saved `ScanResult` dump at `<dump>`. Read-only — does not modify the DB. Exit `0` on empty delta, `1` on any drift, `2` on operational error (missing or malformed dump, schema violation). |
|
|
171
185
|
| `sm watch [roots...]` | Long-running watcher. Same semantics as `sm scan --watch`, exposed as a top-level verb because the watcher is a loop, not a one-shot scan. |
|
|
186
|
+
| `sm refresh <node.path>` | Re-run Extractors against a single node and upsert their outputs into the universal enrichment layer (`node_enrichments`, see [`db-schema.md`](./db-schema.md#node_enrichments)). Stub state until the job subsystem ships at Step 10: deterministic Extractors run for real and persist; probabilistic Extractors emit a stderr advisory and skip without touching their stale rows. Exit `0` on success (with possible stub advisory), `2` on failure, `5` if the node is not in the persisted scan. |
|
|
187
|
+
| `sm refresh --stale` | Batch form of `sm refresh <node>` — refreshes every node carrying at least one stale probabilistic enrichment row. Same stub caveat: deterministic Extractors persist; probabilistic Extractors skip with a stderr advisory. Exit `0` (including when the stale set is empty — prints a "nothing to do" advisory). |
|
|
172
188
|
|
|
173
189
|
`--json` output conforms to `schemas/scan-result.schema.json`. `sm watch` (and `sm scan --watch`) emit one ScanResult per batch — under `--json` this is an `ndjson` stream of ScanResult documents.
|
|
174
190
|
|
|
@@ -183,10 +199,10 @@ Exit: 0 on clean (or clean watcher shutdown), 1 if error-severity issues exist (
|
|
|
183
199
|
| Command | Purpose |
|
|
184
200
|
|---|---|
|
|
185
201
|
| `sm list [--kind <k>] [--issue] [--sort-by ...] [--limit N]` | Tabular listing. `--json` emits an array conforming to `node.schema.json`. |
|
|
186
|
-
| `sm show <node.path>` | Node detail: weight (bytes/tokens triple-split), frontmatter, links in/out, issues, findings, summary. `--json` emits a detail object with the raw link rows. Pretty output groups identical-shape links (same endpoint, kind, normalized trigger) onto one line and lists the union of
|
|
187
|
-
| `sm check` | Print all current issues. Equivalent to `sm scan --json \| jq '.issues'` but faster (reads from DB). |
|
|
202
|
+
| `sm show <node.path>` | Node detail: weight (bytes/tokens triple-split), frontmatter, links in/out, issues, findings, summary. `--json` emits a detail object with the raw link rows. Pretty output groups identical-shape links (same endpoint, kind, normalized trigger) onto one line and lists the union of extractor ids in a `sources:` field; the section header reports both the raw row count and the unique-after-grouping count, e.g. `Links out (12, 9 unique)`. Storage keeps one row per extractor (`scan_links` is unchanged) — the grouping is purely a read-time presentation choice. |
|
|
203
|
+
| `sm check [-n <node.path>] [--rules <ids>] [--include-prob] [--async]` | Print all current issues. Equivalent to `sm scan --json \| jq '.issues'` but faster (reads from DB). `-n` restricts to issues whose `nodeIds` include the path; `--rules <ids>` accepts a comma-separated list of qualified or short rule ids and restricts the issue read accordingly. Default behaviour is deterministic-only (CI-safe, status quo). `--include-prob` is the opt-in flag for probabilistic Rule dispatch (spec § A.7): the verb loads the plugin runtime, finds Rules with `mode === 'probabilistic'` (filtered by `--rules` if set), and emits a stderr advisory naming the rule ids. Full prob dispatch requires the job subsystem (Step 10); until then `--include-prob` is a stub — prob rules never produce issues, never alter the exit code, and `--async` (reserved companion: returns job ids without waiting once jobs land) is a no-op the advisory simply mentions. The flag does NOT extend to `sm scan` or `sm list`. |
|
|
188
204
|
| `sm findings [--kind ...] [--since ...] [--threshold <n>]` | Probabilistic findings (injection, stale summaries, low confidence). `--json` emits an array of finding objects. |
|
|
189
|
-
| `sm graph [--format ascii\|mermaid\|dot]` | Render the full graph via the named
|
|
205
|
+
| `sm graph [--format ascii\|mermaid\|dot]` | Render the full graph via the named formatter. |
|
|
190
206
|
| `sm export <query> --format json\|md\|mermaid` | Filtered export. Query syntax is implementation-defined pre-1.0. |
|
|
191
207
|
| `sm orphans` | History rows whose target node is missing. |
|
|
192
208
|
| `sm orphans reconcile <orphan.path> --to <new.path>` | Migrate history rows from the old path to the new one after a rename. Use case: the scan's rename heuristic missed a match (semantic-only rename, body rewrite) and the user wants to stitch history manually. |
|
|
@@ -277,33 +293,22 @@ Authentication: the nonce is the sole credential. An implementation MUST reject
|
|
|
277
293
|
|
|
278
294
|
---
|
|
279
295
|
|
|
280
|
-
### Audits
|
|
281
|
-
|
|
282
|
-
| Command | Purpose |
|
|
283
|
-
|---|---|
|
|
284
|
-
| `sm audit list` | Registered audits. |
|
|
285
|
-
| `sm audit run <id>` | Execute. `--json` emits the audit report per the audit's declared shape. |
|
|
286
|
-
|
|
287
|
-
Exit: 0 if audit returns "pass"; 1 if audit returns "fail" with at least one error-severity finding; 2 on operational error.
|
|
288
|
-
|
|
289
|
-
---
|
|
290
|
-
|
|
291
296
|
### Database
|
|
292
297
|
|
|
293
298
|
See `db-schema.md` for the table catalog.
|
|
294
299
|
|
|
295
300
|
| Command | Purpose |
|
|
296
301
|
|---|---|
|
|
297
|
-
| `sm db reset` | Drop `scan_*` only. Keep `state_*` and `config_*`. Non-destructive — no confirmation required. |
|
|
298
|
-
| `sm db reset --state` | Drop `scan_*` AND `state_*` (including `state_plugin_kvs` and every `plugin_<id>_*` table). Keep `config_*`. Destructive. |
|
|
299
|
-
| `sm db reset --hard` | Delete the DB file entirely. Keep the plugins folder so the next boot re-discovers them. Destructive. |
|
|
302
|
+
| `sm db reset [-n / --dry-run]` | Drop `scan_*` only. Keep `state_*` and `config_*`. Non-destructive — no confirmation required. `--dry-run` prints the row counts that would be deleted per `scan_*` table without touching the DB. |
|
|
303
|
+
| `sm db reset --state [-n / --dry-run]` | Drop `scan_*` AND `state_*` (including `state_plugin_kvs` and every `plugin_<id>_*` table). Keep `config_*`. Destructive. `--dry-run` previews the deletion without touching the DB. |
|
|
304
|
+
| `sm db reset --hard [-n / --dry-run]` | Delete the DB file entirely. Keep the plugins folder so the next boot re-discovers them. Destructive. `--dry-run` reports the file path and size that would be deleted without unlinking it. |
|
|
300
305
|
| `sm db backup [--out <path>]` | WAL checkpoint + file copy. |
|
|
301
|
-
| `sm db restore <path
|
|
306
|
+
| `sm db restore <path> [-n / --dry-run]` | Swap the DB. Destructive. `--dry-run` validates the source file (existence, header, schema version) and reports what would be overwritten without touching the live DB. |
|
|
302
307
|
| `sm db shell` | Interactive SQL shell (implementations backed by SQLite use `sqlite3`; others use equivalent). |
|
|
303
308
|
| `sm db dump [--tables ...]` | SQL dump. |
|
|
304
309
|
| `sm db migrate [--dry-run \| --status \| --to <n> \| --kernel-only \| --plugin <id> \| --no-backup]` | Migration controls. |
|
|
305
310
|
|
|
306
|
-
Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interactive confirmation unless `--yes` (non-interactive mode for scripts) or `--force` (alias, kept for backward compatibility) is passed. `sm db reset` without a modifier is non-destructive and never prompts.
|
|
311
|
+
Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interactive confirmation unless `--yes` (non-interactive mode for scripts) or `--force` (alias, kept for backward compatibility) is passed. `sm db reset` without a modifier is non-destructive and never prompts. **`--dry-run` short-circuits the confirmation prompt entirely** (per §Dry-run rule: dry-run MUST NOT depend on `--yes` / `--force`).
|
|
307
312
|
|
|
308
313
|
---
|
|
309
314
|
|
|
@@ -322,6 +327,14 @@ Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interacti
|
|
|
322
327
|
|
|
323
328
|
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.
|
|
324
329
|
|
|
330
|
+
### Conformance
|
|
331
|
+
|
|
332
|
+
| Command | Purpose |
|
|
333
|
+
|---|---|
|
|
334
|
+
| `sm conformance run [--scope spec\|provider:<id>\|all]` | Run the conformance suite. `--scope spec` runs only the kernel-agnostic cases bundled with `@skill-map/spec` (default fixture: `preamble-v1.txt`, case: `kernel-empty-boot`). `--scope provider:<id>` runs only the named built-in Provider's suite (today: `provider:claude`). `--scope all` (default) runs every visible scope in registry order. Exit 0 on a clean sweep; exit 1 if any case failed; exit 2 on a configuration error (unknown scope, missing binary). |
|
|
335
|
+
|
|
336
|
+
Per-Provider conformance suites live next to the Provider's manifest under `<plugin-dir>/conformance/{cases,fixtures}/`. The verb discovers them by walking the built-in Provider directory (and, post-job-subsystem, the plugin loader's discovery output). External consumers — alt-impl authors, Provider authors validating their own work — drive the same suite via this verb without reaching into bespoke scripts.
|
|
337
|
+
|
|
325
338
|
---
|
|
326
339
|
|
|
327
340
|
## Machine-readable output rules
|
|
@@ -354,7 +367,7 @@ Every verb that does non-trivial work MUST report its own wall-clock duration. C
|
|
|
354
367
|
|
|
355
368
|
### Scope
|
|
356
369
|
|
|
357
|
-
**In scope**: any verb that walks the filesystem, hits the DB, spawns a subprocess, or renders a report. Examples: `sm scan`, `sm check`, `sm list`, `sm show`, `sm findings`, `sm history`, `sm history stats`, `sm graph`, `sm export`, `sm
|
|
370
|
+
**In scope**: any verb that walks the filesystem, hits the DB, spawns a subprocess, or renders a report. Examples: `sm scan`, `sm check`, `sm list`, `sm show`, `sm findings`, `sm history`, `sm history stats`, `sm graph`, `sm export`, `sm job submit`, `sm job run`, `sm job claim`, `sm job preview`, `sm record`, `sm doctor`, `sm db backup`, `sm db restore`, `sm db dump`, `sm db migrate`, `sm plugins list`, `sm plugins doctor`, `sm init`, `sm conformance run`.
|
|
358
371
|
|
|
359
372
|
**Exempt**: informational verbs that return in well under a millisecond and would clutter the output — `sm --version`, `sm --help`, `sm version`, `sm help`, `sm config get`, `sm config list`, `sm config show`.
|
|
360
373
|
|
package/conformance/README.md
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Language-neutral test suite the specification demands. A conforming implementation passes every case; failing any case is a conformance bug.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The suite splits across two ownership boundaries (Phase 5 / A.13 of spec 0.8.0):
|
|
6
|
+
|
|
7
|
+
- **Spec-owned cases** — kernel-agnostic. They live in this directory and ship with `@skill-map/spec`. Today: `kernel-empty-boot` (boot invariant) and the `preamble-bitwise-match` deferred case. The universal preamble fixture (`preamble-v1.txt`) lives here too.
|
|
8
|
+
- **Provider-owned cases** — exercise a Provider's own `kinds` catalog. They live next to the Provider's manifest, under `<plugin-dir>/conformance/`. The reference impl ships one such suite at [`src/extensions/providers/claude/conformance/`](../../src/extensions/providers/claude/conformance/) covering Claude's five kinds (`skill` / `agent` / `command` / `hook` / `note`) via cases `basic-scan`, `rename-high`, `orphan-detection`.
|
|
9
|
+
|
|
10
|
+
The shape below is normative; the case count in either bucket expands before spec-v1.0.0 (see [`../versioning.md`](../versioning.md)). See [`coverage.md`](./coverage.md) for the spec-owned matrix and the Provider's own coverage file (e.g. `src/extensions/providers/claude/conformance/coverage.md`) for the matching Provider-owned matrix.
|
|
11
|
+
|
|
12
|
+
The reference CLI exposes both buckets via `sm conformance run`:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
sm conformance run --scope spec # spec-owned cases only
|
|
16
|
+
sm conformance run --scope provider:claude # the Claude Provider's cases
|
|
17
|
+
sm conformance run --scope all # both (default)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
External consumers (alt-impl authors, Provider authors validating their own work) can drive the suite without bespoke scripting — the verb provisions the same isolated tmp scope per case as the in-process reference runner does.
|
|
6
21
|
|
|
7
22
|
---
|
|
8
23
|
|
|
@@ -12,15 +27,18 @@ This directory is **stub-level** as of spec v0.1.0. Two cases ship (`basic-scan`
|
|
|
12
27
|
spec/conformance/
|
|
13
28
|
├── README.md ← this file
|
|
14
29
|
├── fixtures/
|
|
15
|
-
│ ├── minimal-claude/ ← controlled MD corpus (5 nodes, one per kind)
|
|
16
|
-
│ │ ├── skills/hello.md
|
|
17
|
-
│ │ ├── agents/reviewer.md
|
|
18
|
-
│ │ ├── commands/status.md
|
|
19
|
-
│ │ ├── hooks/pre-commit.md
|
|
20
|
-
│ │ └── notes/architecture.md
|
|
21
30
|
│ └── preamble-v1.txt ← verbatim preamble text for bitwise-match checks
|
|
22
31
|
└── cases/
|
|
23
|
-
└──
|
|
32
|
+
└── kernel-empty-boot.json ← declarative case (see "Case format" below)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
src/extensions/providers/<id>/conformance/ ← Provider-owned, mirrors the layout
|
|
37
|
+
├── coverage.md
|
|
38
|
+
├── cases/
|
|
39
|
+
│ └── *.json
|
|
40
|
+
└── fixtures/
|
|
41
|
+
└── ...
|
|
24
42
|
```
|
|
25
43
|
|
|
26
44
|
Fixtures are read-only inputs. Cases declare what to invoke and what to assert. A conformance runner is implementation-specific code that:
|
|
@@ -45,13 +63,13 @@ A case is a JSON document with this shape:
|
|
|
45
63
|
"fixture": "string — folder under fixtures/ used as the scope root.",
|
|
46
64
|
|
|
47
65
|
"setup": {
|
|
48
|
-
"
|
|
66
|
+
"disableAllProviders": false,
|
|
49
67
|
"disableAllDetectors": false,
|
|
50
68
|
"disableAllRules": false
|
|
51
69
|
},
|
|
52
70
|
|
|
53
71
|
"invoke": {
|
|
54
|
-
"verb": "scan | list | show | check | findings | graph | export |
|
|
72
|
+
"verb": "scan | list | show | check | findings | graph | export | job | record | ...",
|
|
55
73
|
"sub": "submit | run | ...",
|
|
56
74
|
"args": ["positional", "args"],
|
|
57
75
|
"flags": ["--json", "--all", "..."]
|
|
@@ -97,10 +115,11 @@ Assertion types beyond this list MAY be proposed via spec-vX.Y.Z minor bumps. Im
|
|
|
97
115
|
|
|
98
116
|
## Current case inventory
|
|
99
117
|
|
|
118
|
+
### Spec-owned (this directory)
|
|
119
|
+
|
|
100
120
|
| Id | Verifies |
|
|
101
121
|
|---|---|
|
|
102
|
-
| `
|
|
103
|
-
| `kernel-empty-boot` | With every adapter/detector/rule disabled, scanning an empty scope returns a valid empty graph. |
|
|
122
|
+
| `kernel-empty-boot` | With every Provider/Extractor/Rule disabled, scanning an empty scope returns a valid empty graph. |
|
|
104
123
|
|
|
105
124
|
Cases explicitly referenced elsewhere in the spec (landing before v1.0):
|
|
106
125
|
|
|
@@ -108,6 +127,14 @@ Cases explicitly referenced elsewhere in the spec (landing before v1.0):
|
|
|
108
127
|
|---|---|---|
|
|
109
128
|
| `preamble-bitwise-match` | `prompt-preamble.md` | Rendered job files contain `preamble-v1.txt` byte-for-byte. Deferred to Step 10 (requires `sm job preview`). |
|
|
110
129
|
|
|
130
|
+
### Provider-owned (per `<plugin-dir>/conformance/`)
|
|
131
|
+
|
|
132
|
+
| Provider | Id | Verifies |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `claude` | `basic-scan` | Scanning the `minimal-claude` corpus detects exactly five nodes (one per kind) with no issues. Implicitly validates each per-kind schema. |
|
|
135
|
+
| `claude` | `rename-high` | High-confidence rename emits no issue; the new path is the sole node. |
|
|
136
|
+
| `claude` | `orphan-detection` | Deletion with no replacement triggers exactly one `orphan` issue (severity `info`). |
|
|
137
|
+
|
|
111
138
|
---
|
|
112
139
|
|
|
113
140
|
## Runner (reference pseudocode)
|
|
@@ -128,7 +155,9 @@ for (const caseFile of await readdir('spec/conformance/cases')) {
|
|
|
128
155
|
}
|
|
129
156
|
```
|
|
130
157
|
|
|
131
|
-
|
|
158
|
+
A Provider-owned runner mirrors the loop with a different cases / fixtures root — `<plugin-dir>/conformance/cases/` and `<plugin-dir>/conformance/fixtures/`. The reference CLI ships both as `sm conformance run` (Phase 5 / A.13); the verb resolves the spec scope via `@skill-map/spec` and discovers Provider scopes by walking each built-in plugin's `conformance/` directory.
|
|
159
|
+
|
|
160
|
+
The reference implementation's runner ships under `src/conformance/index.ts`; the verb lives at `src/cli/commands/conformance.ts` and uses the runner one case at a time.
|
|
132
161
|
|
|
133
162
|
---
|
|
134
163
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
|
|
3
3
|
"id": "kernel-empty-boot",
|
|
4
|
-
"description": "With every
|
|
4
|
+
"description": "With every Provider, detector, and rule disabled, scanning an empty scope MUST return a valid, zero-filled ScanResult. Enforces the kernel boot invariant from architecture.md.",
|
|
5
5
|
"setup": {
|
|
6
|
-
"
|
|
6
|
+
"disableAllProviders": true,
|
|
7
7
|
"disableAllDetectors": true,
|
|
8
8
|
"disableAllRules": true
|
|
9
9
|
},
|