@skill-map/spec 0.6.1 → 0.7.1

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 CHANGED
@@ -1,5 +1,318 @@
1
1
  # Spec changelog
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 0463a0f: Step 9.4 — plugin author guide + reference plugin + diagnostics polish.
8
+ **Step 9 fully closed** with this changeset.
9
+
10
+ ### Spec — plugin author guide (additive prose)
11
+
12
+ New document at `spec/plugin-author-guide.md` covering:
13
+
14
+ - Discovery roots (`<project>/.skill-map/plugins/`,
15
+ `~/.skill-map/plugins/`, `--plugin-dir <path>`).
16
+ - Manifest fields with the normative schema reference.
17
+ - `specCompat` strategy — narrow ranges pre-`v1.0.0`, `^1.0.0`
18
+ recommendation post-`v1.0.0`.
19
+ - The six extension kinds with one minimal worked example each
20
+ (detector, rule, renderer in full; adapter / audit / action flagged
21
+ for later expansion alongside Step 10).
22
+ - Storage choice (KV vs Dedicated) cross-linking `plugin-kv-api.md`
23
+ and the Step 9.2 triple-protection rule.
24
+ - Execution modes (deterministic / probabilistic) cross-linking
25
+ `architecture.md`.
26
+ - Testkit usage with `runDetectorOnFixture`, `runRuleOnGraph`,
27
+ `runRendererOnGraph`, `makeFakeRunner`.
28
+ - The five plugin statuses (`loaded` / `disabled` / `incompatible-spec`
29
+ / `invalid-manifest` / `load-error`) and how to read them.
30
+ - Stability section (document is stable; widening additions are minor
31
+ bumps; breaking edits are major).
32
+
33
+ `spec/package.json#files` updated to ship the new doc; `spec/index.json`
34
+ regenerated (57 → 58 hashed files). `coverage.md` unchanged because the
35
+ guide is prose, not a schema.
36
+
37
+ ### Reference plugin — `examples/hello-world/`
38
+
39
+ Smallest viable plugin in the principal repo (Arquitecto's pick: in
40
+ the main repo, not separate). One detector (`hello-world-greet`)
41
+ emitting `references` links per `@greet:<name>` token in node bodies.
42
+ Includes:
43
+
44
+ - `plugin.json` declaring one extension and pinning `specCompat: ^1.0.0`.
45
+ - `extensions/greet-detector.mjs` — runtime instance with both
46
+ manifest fields and the `detect` method.
47
+ - `README.md` — what it does, file layout, three-step "try it
48
+ locally" recipe, what's intentionally missing (storage,
49
+ multi-extension, probabilistic mode), pointers for production-grade
50
+ patterns.
51
+ - `test/greet-detector.test.mjs` — four-assertion test using
52
+ `@skill-map/testkit`, runnable via `node --test` with no build step.
53
+
54
+ Verified end-to-end: the example plugin loads cleanly under
55
+ `sm plugins list`, scans contribute its links to the persisted graph,
56
+ and the testkit-based test passes. The example is **not** registered
57
+ as a workspace — it's intentionally standalone so users can copy it.
58
+
59
+ ### CLI — diagnostics polish on `PluginLoader.reason`
60
+
61
+ Each failure-mode reason string now carries an actionable hint:
62
+
63
+ - `invalid-manifest` (JSON parse): names the manifest path, suggests
64
+ validating the JSON.
65
+ - `invalid-manifest` (AJV): names the manifest path AND points at
66
+ `spec/schemas/plugins-registry.schema.json#/$defs/PluginManifest`.
67
+ - `invalid-manifest` (specCompat not a valid range): suggests a range
68
+ shape (`"^1.0.0"`).
69
+ - `incompatible-spec`: suggests two remediations (update the plugin's
70
+ `specCompat`, or pin sm to a compatible spec version).
71
+ - `load-error` (extension file not found): includes the absolute
72
+ resolved path, pointer to `plugin.json#/extensions`.
73
+ - `load-error` (default export missing kind): lists the valid kinds.
74
+ - `load-error` (unknown kind): lists the valid kinds.
75
+ - `load-error` (extension manifest schema fails): names the
76
+ per-kind schema (`spec/schemas/extensions/<kind>.schema.json`).
77
+
78
+ 6 new tests under `test/plugin-loader.test.ts` (`Step 9.4 diagnostics
79
+ polish` describe block) assert each hint shape is present without
80
+ pinning the full text. Test count 437 → **443 cli + 30 testkit = 473**.
81
+
82
+ ### Step 9 closed
83
+
84
+ The four sub-steps — 9.1 (plugin runtime wiring), 9.2 (plugin
85
+ migrations + triple protection), 9.3 (`@skill-map/testkit` workspace),
86
+ 9.4 (author guide + reference plugin + diagnostics polish) — together
87
+ turn `skill-map` plugins from "discovered but inert" into a
88
+ first-class authoring surface with documentation, tests, and a
89
+ working reference. Next step: **Step 10 — job subsystem + first
90
+ probabilistic extension** (wave 2 begins).
91
+
92
+ ## 0.7.0
93
+
94
+ ### Minor Changes
95
+
96
+ - d730094: Spec — Execution modes (deterministic / probabilistic) lifted to a first-class architectural property
97
+
98
+ Frames a meta-property of skill-map that was previously implicit and scattered:
99
+ **every analytical extension is one of two modes** — `deterministic` (pure code,
100
+ runs in scan-time pipelines) or `probabilistic` (invokes an LLM through
101
+ `RunnerPort`, runs only as queued jobs). The dual-mode capability now spans four
102
+ of the six extension kinds; Adapter and Renderer remain locked to deterministic
103
+ because they sit at the system boundaries (filesystem and graph-to-string) where
104
+ non-determinism would break boot reproducibility and snapshot diffing.
105
+
106
+ **Spec changes:**
107
+
108
+ - `architecture.md` — new top-level section **§Execution modes** before
109
+ §Extension kinds. Defines the two modes, the per-kind capability matrix
110
+ (Detector / Rule / Action dual-mode by manifest declaration; Audit dual-mode
111
+ with mode **derived** from `composes[]`; Adapter / Renderer deterministic-only),
112
+ the runtime separation (`deterministic` runs in `sm scan` / `sm check`;
113
+ `probabilistic` runs only via `sm job submit <kind>:<id>`), and the
114
+ `RunnerPort` injection contract for probabilistic extensions.
115
+ - `architecture.md` §Extension kinds — table updated: each row clarifies the
116
+ mode posture (Adapter / Renderer marked deterministic-only; Detector / Rule /
117
+ Action marked dual-mode; Audit marked derived-mode).
118
+ - `architecture.md` §Stability — new clause: execution modes and the per-kind
119
+ capability matrix are stable as of v1.0.0; adding a third mode, changing
120
+ which kinds are dual-mode, or changing the audit's derivation rule is a major
121
+ bump.
122
+
123
+ **Schema changes:**
124
+
125
+ - `schemas/extensions/detector.schema.json`:
126
+ - New optional `mode` field (`deterministic` | `probabilistic`, default
127
+ `deterministic`). Omitting is equivalent to deterministic — keeps existing
128
+ detectors valid without an update.
129
+ - Description updated to spell out the dual-mode contract.
130
+ - `schemas/extensions/rule.schema.json`:
131
+ - Same shape: new optional `mode` field with default `deterministic`.
132
+ - Description rewritten — the previous "Rules MUST be deterministic" claim
133
+ moved into the deterministic-mode contract; probabilistic rules are now
134
+ explicitly allowed and run only as queued jobs.
135
+ - `schemas/extensions/action.schema.json`:
136
+ - **Breaking** — `mode` enum renamed: `local` → `deterministic`,
137
+ `invocation-template` → `probabilistic`. Pre-1.0; no consumers depend on
138
+ the old values (no third-party action plugins shipped). Description, the
139
+ two `if/then` branches, and the `expectedDurationSeconds` /
140
+ `promptTemplateRef` field descriptions updated accordingly.
141
+ - **Bug fix** — the schema previously declared `allOf` twice at the root
142
+ (lines 6–8 and 71–80); the second silently overrode the first, dropping
143
+ `$ref: base.schema.json`. Both blocks are now merged into a single `allOf`
144
+ so the action schema actually composes the base shape.
145
+ - `schemas/extensions/audit.schema.json`:
146
+ - Description rewritten — the "deterministic workflow" claim is replaced by
147
+ the **derived-mode** rule: the audit's effective mode is computed from
148
+ `composes[]` at load time. If every composed primitive is deterministic,
149
+ the audit is deterministic; if any is probabilistic, the audit is
150
+ probabilistic and dispatches as a job. Declaring `mode` directly is a
151
+ load-time error.
152
+ - `composes[]` description updated to mention that each primitive's mode
153
+ participates in derivation; dangling references stay a load-time error.
154
+ - `reportSchemaRef` description updated: probabilistic audits MUST extend
155
+ `report-base.schema.json` (carries `safety` / `confidence`); deterministic
156
+ audits MAY extend it but are not required to.
157
+ - `schemas/extensions/adapter.schema.json`:
158
+ - Description updated to state explicitly that adapters are deterministic-only
159
+ and that `mode` MUST NOT appear. Recommendation for users who want
160
+ LLM-assisted classification: write a probabilistic Detector that emits
161
+ classification hints as `Link[]`.
162
+ - `schemas/extensions/renderer.schema.json`:
163
+ - Description updated to state that renderers are deterministic-only and
164
+ that `mode` MUST NOT appear. Probabilistic narrators of the graph belong
165
+ in jobs and emit Findings, not in renderer manifests.
166
+
167
+ **Why major (despite pre-1.0 minor norm):**
168
+
169
+ Renaming the `Action.mode` enum (`local` → `deterministic`,
170
+ `invocation-template` → `probabilistic`) is breaking by definition. No
171
+ third-party Actions exist yet, but the rename touches the canonical surface and
172
+ deserves the bump. New optional fields on Detector / Rule and the new derived-
173
+ mode contract on Audit are additive and would have been minor on their own.
174
+
175
+ **Implementation work intentionally NOT included here:**
176
+
177
+ - `src/extensions/built-ins.ts` and the per-extension TS files keep working
178
+ unchanged because the new `mode` is optional with `deterministic` default.
179
+ Explicitly threading `mode: 'deterministic'` through every built-in is a
180
+ follow-up.
181
+ - `RunnerPort` injection through `ctx.runner` for probabilistic extensions is
182
+ spec'd here; the actual context plumbing lands with the first probabilistic
183
+ extension (Step 10 — first summarizer). `MockRunner` continues to satisfy
184
+ tests until then.
185
+ - Conformance case `extension-mode-derivation` (audit composes mixed
186
+ primitives → derives `probabilistic`) is mentioned in `architecture.md` and
187
+ pending under `spec/conformance/coverage.md` for the next release.
188
+ - ROADMAP.md rephrase of Steps 10–11 (from "summarizers" to "wave 2:
189
+ probabilistic extensions") and a positioning section in `README.md` follow
190
+ in separate commits to keep this changeset spec-only.
191
+
192
+ ### Minor Changes
193
+
194
+ - a73f3f4: Step 7.1 — File watcher (`sm watch` / `sm scan --watch`)
195
+
196
+ Long-running watcher that subscribes to the scan roots, debounces
197
+ filesystem events, and triggers an incremental scan per batch. Reuses
198
+ the existing `runScanWithRenames` pipeline, the `IIgnoreFilter` chain
199
+ (`.skill-mapignore` + `config.ignore` + bundled defaults), and the
200
+ `scan.*` non-job events from `job-events.md` — one ScanResult per
201
+ batch, emitted as ndjson under `--json`.
202
+
203
+ **Spec changes (minor)**:
204
+
205
+ - `spec/schemas/project-config.schema.json` — new `scan.watch` object
206
+ with a single key `debounceMs` (integer ≥ 0, default 300). Groups
207
+ bursts of filesystem events (editor saves, branch switches, npm
208
+ installs) into a single scan pass. Set to 0 to disable debouncing.
209
+ - `spec/cli-contract.md` §Scan — documents `sm watch [roots...]` as
210
+ the primary verb and `sm scan --watch` as the alias. Watcher
211
+ respects the same ignore chain as one-shot scans, emits one
212
+ ScanResult per batch (ndjson under `--json`), closes cleanly on
213
+ `SIGINT` / `SIGTERM`, exits 0 on clean shutdown. Exit-code rule
214
+ carved out for the watcher: per-batch error issues do not flip the
215
+ exit code (the loop keeps running); operational errors still exit 2.
216
+
217
+ No new events. No new ports. The watcher is implementation-defined
218
+ inside the kernel package; a future `WatchPort` can be added when /
219
+ if a non-Node implementation needs to swap the chokidar wrapper.
220
+
221
+ **Runtime changes (minor — new verb + new config key)**:
222
+
223
+ - `chokidar@5.0.0` pinned in `src/package.json` (single new runtime
224
+ dependency, MIT). Chokidar v5 requires Node ≥ 20.19; the project
225
+ already pins `engines.node: ">=24.0"` so this is a no-op for
226
+ consumers. Brings in `readdirp@5` as a transitive.
227
+ - `src/kernel/scan/watcher.ts` — `IFsWatcher` interface + concrete
228
+ `ChokidarWatcher` wrapping `chokidar.watch()` with the existing
229
+ `IIgnoreFilter` plumbed through, debouncer, batch coalescing,
230
+ and explicit `stop()` for clean teardown.
231
+ - `src/cli/commands/watch.ts` — new `WatchCommand`. `sm scan
232
+ --watch` delegates to the same code path so the two surfaces are
233
+ byte-aligned (no parallel implementations).
234
+ - `src/config/defaults.json` — new `scan.watch.debounceMs: 300`
235
+ default.
236
+
237
+ **Why minor (not patch)**: new public verb (`sm watch`), new public
238
+ config key (`scan.watch.debounceMs`), and a new flag on an existing
239
+ verb (`sm scan --watch`). All three are surface additions, not bug
240
+ fixes — minor under both the spec and the runtime semver policies.
241
+ No breaking changes; existing `sm scan` without `--watch` is
242
+ byte-identical to before.
243
+
244
+ **Roadmap**: Step 7 — Robustness, sub-step 7.1 (chokidar watcher).
245
+ Trigger normalization is implicit-already-landed (cabled into every
246
+ detector at Steps 3–4 with full unit tests in
247
+ `src/kernel/trigger-normalize.test.ts`); we do not write a sub-step
248
+ for it. Next sub-steps: 7.2 detector conflict resolution, 7.3 `sm
249
+ job prune` + retention enforcement.
250
+
251
+ ### Patch Changes
252
+
253
+ - a73f3f4: Step 7.2 — Detector conflict resolution
254
+
255
+ Two pieces:
256
+
257
+ 1. **New built-in rule `link-conflict`** (`src/extensions/rules/link-conflict/`).
258
+ Surfaces detector disagreement. Groups links by `(source, target)` and
259
+ emits one `warn` Issue per pair where the set of distinct `kind` values
260
+ has size ≥ 2. Agreement (single kind across multiple detectors) is
261
+ silent — by design, to avoid massive noise on real graphs.
262
+ Issue payload (`data`) carries `{ source, target, variants }` where
263
+ each `variant` is `{ kind, sources: detectorId[], confidence }`. Variant
264
+ sources are deduped + sorted; confidence is the highest across rows
265
+ of the same kind (`high` > `medium` > `low`).
266
+
267
+ This is the kernel piece of Decision #90 read-time "consumers that
268
+ need uniqueness aggregate at read time" — the rule is one such
269
+ consumer, on the alarming side. Storage stays untouched (one row
270
+ per detector, no merge, no dedup). Severity is `warn`, not `error`:
271
+ the rule cannot pick which kind is correct, so per `cli-contract.md`
272
+ §Exit codes the verb stays exit 0.
273
+
274
+ 2. **`sm show` pretty link aggregation** (`src/cli/commands/show.ts`).
275
+ The human renderer now groups `linksOut` / `linksIn` by `(endpoint,
276
+ kind, normalizedTrigger)` and prints one row per group with the
277
+ union of detector ids in a `sources:` field. The section header
278
+ reports both the raw row count and the unique-after-grouping count
279
+ (`Links out (12, 9 unique)`). When N > 1 detector emits the same
280
+ logical link, the row also gets a `(×N)` suffix.
281
+
282
+ `--json` output is byte-identical to before — raw rows, no merge.
283
+ Storage is byte-identical to before. The grouping is purely a
284
+ read-time presentation choice for human eyes.
285
+
286
+ **Spec changes (patch)**:
287
+
288
+ - `spec/cli-contract.md` §Browse — `sm show` row clarifies that pretty
289
+ output groups identical-shape links and that `--json` emits raw rows.
290
+ Patch (not minor) because the JSON contract is unchanged; the human
291
+ output format is non-normative anyway.
292
+
293
+ **Runtime changes (minor — new rule + new presentation)**:
294
+
295
+ - New rule `link-conflict` registered in `src/extensions/built-ins.ts`.
296
+ - `sm show` pretty output groups links + reports unique counts.
297
+
298
+ **UI inspector aggregation deferred to Step 13**: the current Flavor A
299
+ inspector renders the `Relations` card from `node.frontmatter.metadata.{
300
+ related, requires, supersedes, provides, conflictsWith}` directly — it
301
+ does NOT consume `linksOut` / `linksIn` rows from `scan_links`. There
302
+ is no link table to aggregate today. When Step 13's Flavor B lands (Hono
303
+ BFF + WS + full link panel from scan), the aggregation logic from
304
+ `src/cli/commands/show.ts` will need to be ported.
305
+
306
+ **Roadmap**: Step 7 — Robustness, sub-step 7.2 (detector conflict
307
+ resolution). Closes one of the three remaining frentes; 7.3 (`sm job
308
+ prune` + retention) still pending. Decision #90 unchanged: storage
309
+ keeps raw per-detector rows. The `related` vs LLM-amplification
310
+ discussion is documented in `.tmp/skill-map-related-test/` (status
311
+ quo retained — fields stay opt-in under `metadata.*`; revisit if
312
+ real-world amplification appears).
313
+
314
+ **Tests**: 327 → 335 (+8 new for the rule, no regressions).
315
+
3
316
  ## 0.6.1
4
317
 
5
318
  ### Patch Changes
package/architecture.md CHANGED
@@ -115,18 +115,66 @@ No extension is privileged. The Claude adapter ships bundled with the reference
115
115
 
116
116
  ---
117
117
 
118
+ ## Execution modes
119
+
120
+ Every analytical extension in skill-map is one of two **modes**:
121
+
122
+ - **`deterministic`** — pure code. Same input → same output, every run.
123
+ - **`probabilistic`** — calls an LLM through the kernel's `RunnerPort`. Output may vary across runs; cost and latency are non-trivial.
124
+
125
+ Mode is a property of the extension as a whole, not of an individual call. **An extension is one mode or the other; it cannot switch at runtime.** If a plugin author needs both flavors of the same idea (regex-based AND LLM-based "find suspicious imports"), they ship two extensions with distinct ids.
126
+
127
+ ### Which kinds support which modes
128
+
129
+ | Kind | Modes | How mode is set |
130
+ |---|---|---|
131
+ | **Detector** | deterministic / probabilistic | declared in manifest (`mode` field, optional; defaults to `deterministic`) |
132
+ | **Rule** | deterministic / probabilistic | declared in manifest (`mode` field, optional; defaults to `deterministic`) |
133
+ | **Action** | deterministic / probabilistic | declared in manifest (`mode` field, **required** — no default) |
134
+ | **Audit** | deterministic / probabilistic | derived from `composes[]` (see below) |
135
+ | **Adapter** | deterministic-only | implicit; `mode` field MUST NOT appear |
136
+ | **Renderer** | deterministic-only | implicit; `mode` field MUST NOT appear |
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.
139
+
140
+ ### Audit · derived mode
141
+
142
+ An audit is a **composer**: it declares which primitives it runs and the kernel handles dispatch. The audit manifest does NOT carry a `mode` field. Instead it declares `composes[]` — the rule and action references the audit executes in sequence. At load time the kernel resolves each entry and computes the audit's **effective mode**:
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.
150
+
151
+ ### When each mode runs
152
+
153
+ - **Deterministic extensions** run synchronously inside the standard kernel pipelines (`sm scan`, `sm check`, `sm list`). Fast, free, reproducible. CI-safe.
154
+ - **Probabilistic extensions** never run during `sm scan`. They are dispatched as **jobs** via `sm job submit <kind>:<id>`. Jobs are async, queued, persisted under `state_jobs`, and resume on next boot. The same scan snapshot can be re-analyzed by probabilistic extensions on demand without re-walking the filesystem.
155
+
156
+ This separation is normative: a probabilistic extension cannot register a hook that fires from `sm scan`. The kernel rejects it at load time.
157
+
158
+ ### How probabilistic extensions invoke the LLM
159
+
160
+ The kernel exposes the LLM through the `RunnerPort` (see §Ports above). Reference impl: `ClaudeCliRunner`. Tests: `MockRunner`. Other adapters (OpenAI, local Ollama, etc.) implement the same port without spec changes.
161
+
162
+ A probabilistic extension receives the runner in its invocation context alongside `ctx.store`. The extension never imports a specific LLM SDK — the runner contract is what the spec normalizes; wire format and model selection are adapter concerns.
163
+
164
+ ---
165
+
118
166
  ## Extension kinds
119
167
 
120
168
  Six kinds, all first-class, all loaded through the same registry. Each kind has a JSON Schema describing its manifest shape under [`schemas/extensions/`](./schemas/extensions/). Implementations MUST validate every extension manifest against the schema for its declared kind at load time; validation failure → the extension is skipped with status `invalid-manifest`.
121
169
 
122
170
  | Kind | Role | Input | Output |
123
171
  |---|---|---|---|
124
- | **Adapter** | Recognizes a platform. Decides which files are nodes and what kind they are. Declares per-kind `defaultRefreshAction` (an action id that drives the probabilistic-refresh surface). | Filesystem walk results, candidate path. | `{ kind, adapter } \| null`. |
125
- | **Detector** | Extracts signals from a node body. | Parsed node (frontmatter + body). | `Link[]`. |
126
- | **Rule** | Evaluates the graph. | Full graph (nodes + links). | `Issue[]`. |
127
- | **Action** | Operates on one or more nodes. Two modes: `local` (code) or `invocation-template` (LLM prompt). | Node(s), optional args. | Local: report JSON. Template: rendered prompt that a runner executes. |
128
- | **Audit** | Deterministic workflow that composes rules and actions. Produces a structured report. | Graph + optional scope filter. | Audit report (hardcoded shape, kind-specific). |
129
- | **Renderer** | Serializes the graph. | Graph + optional filter. | String (ASCII / Mermaid / DOT / JSON / user-defined). |
172
+ | **Adapter** | Recognizes a platform. Decides which files are nodes and what kind they are. Declares per-kind `defaultRefreshAction` (an action id that drives the probabilistic-refresh surface). Deterministic-only. | Filesystem walk results, candidate path. | `{ kind, adapter } \| null`. |
173
+ | **Detector** | Extracts signals from a node body. Dual-mode: `deterministic` runs in scan, `probabilistic` runs in jobs. | Parsed node (frontmatter + body). | `Link[]`. |
174
+ | **Rule** | Evaluates the graph. Dual-mode: `deterministic` runs in `sm check`, `probabilistic` runs in jobs. | Full graph (nodes + links). | `Issue[]`. |
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
+ | **Audit** | Workflow that composes rules and actions. Effective mode is derived from `composes[]` — deterministic if all composed primitives are deterministic, probabilistic otherwise. Produces a structured report. | Graph + optional scope filter. | Audit report (hardcoded shape, kind-specific). |
177
+ | **Renderer** | Serializes the graph. Deterministic-only. | Graph + optional filter. | String (ASCII / Mermaid / DOT / JSON / user-defined). |
130
178
 
131
179
  ### Adapter · `defaultRefreshAction`
132
180
 
@@ -267,6 +315,8 @@ The **port list** is stable as of spec v1.0.0. Adding a sixth port is a major bu
267
315
 
268
316
  The **extension kind list** (6 kinds) is stable as of spec v1.0.0. Adding a seventh kind is a major bump.
269
317
 
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, changing which kinds are dual-mode, or changing the audit's mode-derivation rule is a major bump. Renaming or repurposing the mode enum values is a major bump.
319
+
270
320
  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.
271
321
 
272
322
  The **Detector · 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
@@ -166,11 +166,15 @@ Keys are dot-paths (`jobs.minimumTtlSeconds`, `scan.tokenize`). Unknown keys →
166
166
  | `sm scan` | Full scan. Truncates `scan_*` and repopulates. |
167
167
  | `sm scan -n <node.path>` | Partial scan: one node. |
168
168
  | `sm scan --changed` | Incremental: only files changed since last scan (mtime heuristic). |
169
+ | `sm scan --watch` | Long-running: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`. |
169
170
  | `sm scan --compare-with <path>` | Delta report: compare current state with a saved scan dump. Does not modify the DB. |
171
+ | `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. |
170
172
 
171
- `--json` output conforms to `schemas/scan-result.schema.json`.
173
+ `--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.
172
174
 
173
- Exit: 0 on clean, 1 if error-severity issues exist, 2 on operational error.
175
+ The watcher subscribes to the same roots that `sm scan` walks and respects `.skill-mapignore` plus `config.ignore` exactly as the one-shot scan does. Filesystem events are grouped using `scan.watch.debounceMs` (default 300ms) before the watcher re-runs the incremental scan and persists. `SIGINT` / `SIGTERM` close the watcher cleanly. Exit code on clean shutdown is 0.
176
+
177
+ Exit: 0 on clean (or clean watcher shutdown), 1 if error-severity issues exist (one-shot scan only — the watcher does not flip exit code based on per-batch issues), 2 on operational error.
174
178
 
175
179
  ---
176
180
 
@@ -179,7 +183,7 @@ Exit: 0 on clean, 1 if error-severity issues exist, 2 on operational error.
179
183
  | Command | Purpose |
180
184
  |---|---|
181
185
  | `sm list [--kind <k>] [--issue] [--sort-by ...] [--limit N]` | Tabular listing. `--json` emits an array conforming to `node.schema.json`. |
182
- | `sm show <node.path>` | Node detail: weight (bytes/tokens triple-split), frontmatter, links in/out, issues, findings, summary. `--json` emits a detail object. |
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 detector 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 detector (`scan_links` is unchanged) — the grouping is purely a read-time presentation choice. |
183
187
  | `sm check` | Print all current issues. Equivalent to `sm scan --json \| jq '.issues'` but faster (reads from DB). |
184
188
  | `sm findings [--kind ...] [--since ...] [--threshold <n>]` | Probabilistic findings (injection, stale summaries, low confidence). `--json` emits an array of finding objects. |
185
189
  | `sm graph [--format ascii\|mermaid\|dot]` | Render the full graph via the named renderer. |
@@ -12,7 +12,7 @@ This file is hand-maintained. A CI check before spec release compares the schema
12
12
  | 2 | `link.schema.json` | — | 🔴 missing | Needs fixture with at least one `invokes` + `references` + `mentions` link, both `high`/`medium`/`low` confidence. |
13
13
  | 3 | `issue.schema.json` | — | 🔴 missing | Needs fixture triggering `trigger-collision` + `broken-ref` + `superseded`. |
14
14
  | 4 | `scan-result.schema.json` | `basic-scan`, `kernel-empty-boot` | 🟢 covered | Zero-filled (empty-boot) + populated (minimal-claude) both asserted. |
15
- | 5 | `execution-record.schema.json` | — | 🔴 missing | Blocked by Step 5 (history). Needs a case that runs a `local` action and inspects `state_executions` via `sm history --json`. |
15
+ | 5 | `execution-record.schema.json` | — | 🔴 missing | Blocked by Step 5 (history). Needs a case that runs a `deterministic` action and inspects `state_executions` via `sm history --json`. |
16
16
  | 6 | `project-config.schema.json` | — | 🔴 missing | Case: init a scope, write a partial `.skill-map/settings.json` (optionally with a `.skill-map/settings.local.json` overlay), assert effective config after the layered merge. |
17
17
  | 7 | `plugins-registry.schema.json` | — | 🔴 missing | Two sub-cases required: (a) `PluginManifest` validation via `sm plugins show --json`; (b) aggregate `PluginsRegistry` via `sm plugins list --json`. |
18
18
  | 8 | `job.schema.json` | — | 🔴 missing | Blocked by Step 10 (job system). Needs a case that submits a local action (no LLM), inspects `sm job show --json`. |
@@ -33,8 +33,8 @@ This file is hand-maintained. A CI check before spec release compares the schema
33
33
  | 23 | `extensions/adapter.schema.json` | — | 🔴 missing | Case: the `claude` adapter manifest validates; a crafted invalid manifest (missing `defaultRefreshAction`) fails with `invalid-manifest`. |
34
34
  | 24 | `extensions/detector.schema.json` | — | 🔴 missing | Case: `frontmatter` + `slash` + `at-directive` detector manifests validate; a detector emitting a disallowed `emitsLinkKinds` value fails. |
35
35
  | 25 | `extensions/rule.schema.json` | — | 🔴 missing | Case: `trigger-collision`, `broken-ref`, `superseded` manifests validate. |
36
- | 26 | `extensions/action.schema.json` | — | 🔴 missing | Case: a `local` action manifest validates; an `invocation-template` action WITHOUT `promptTemplateRef` fails. |
37
- | 27 | `extensions/audit.schema.json` | — | 🔴 missing | Case: `validate-all` audit manifest validates; an audit referencing a non-existent rule id in `composes` fails at load with `invalid-manifest`. |
36
+ | 26 | `extensions/action.schema.json` | — | 🔴 missing | Case: a `deterministic` action manifest validates; a `probabilistic` action WITHOUT `promptTemplateRef` fails. |
37
+ | 27 | `extensions/audit.schema.json` | — | 🔴 missing | Case: `validate-all` audit manifest validates; an audit referencing a non-existent rule id in `composes` fails at load with `invalid-manifest`; an audit declaring `mode` directly fails at load. |
38
38
  | 28 | `extensions/renderer.schema.json` | — | 🔴 missing | Case: `ascii` renderer manifest validates. |
39
39
  | 29 | `history-stats.schema.json` | — | 🔴 missing | Blocked by Step 5 (history). Case: seed `state_executions` with a deterministic fixture, run `sm history stats --json --since <T0> --until <T1> --period month --top 5`, assert the document validates and that `totals.executionsCount == sum(perAction.executionsCount)` and `errorRates.global == totals.failedCount / totals.executionsCount`. Percentiles (`p95`/`p99`) intentionally omitted in v1 — add later as a minor bump without breaking consumers. |
40
40
 
@@ -48,6 +48,7 @@ These have their own conformance cases even though they are not JSON Schemas.
48
48
  |---|---|---|---|---|
49
49
  | A | Preamble verbatim text | `preamble-bitwise-match` | 🟠 deferred | Deferred to Step 10 (needs `sm job preview` to render a job file). Fixture: `fixtures/preamble-v1.txt` (already present, byte-identical to `prompt-preamble.md` source). |
50
50
  | B | Kernel empty-boot invariant | `kernel-empty-boot` | 🟢 covered | All extensions disabled → empty ScanResult. |
51
+ | C | Audit mode derivation | `extension-mode-derivation` | 🟠 deferred | Deferred to Step 10 (audit's effective mode is derived from `composes[]` at load time; full validation requires the job subsystem to verify dispatch routing). Sub-cases: (1) audit composing only deterministic primitives → effective mode `deterministic`, runs synchronously inside `sm audit <id>`; (2) audit composing at least one probabilistic primitive → effective mode `probabilistic`, dispatches as a job; (3) audit declaring `mode` directly in the manifest → load-time error `invalid-manifest`; (4) audit composing a dangling reference → load-time error `invalid-manifest`. See `architecture.md` §Execution modes. |
51
52
  | C | Atomic-claim race safety | — | 🔴 missing | Blocked by Step 10. Two concurrent `sm job claim` invocations against a single queued row — exactly one MUST succeed. |
52
53
  | D | Duplicate detection | — | 🔴 missing | Blocked by Step 10. Two `sm job submit` with same `(action, version, node, contentHash)` — second exits 3. |
53
54
  | E | `--force` bypass | — | 🔴 missing | Blocked by Step 10. |
package/index.json CHANGED
@@ -190,20 +190,20 @@
190
190
  }
191
191
  ]
192
192
  },
193
- "specPackageVersion": "0.6.1",
193
+ "specPackageVersion": "0.7.1",
194
194
  "integrity": {
195
195
  "algorithm": "sha256",
196
196
  "files": {
197
- "CHANGELOG.md": "73e7db22a362dfe6b1d7aa8f456d57d2106936b831c7de6fc9b44c9f7f9642a2",
197
+ "CHANGELOG.md": "11a026e881126ac96703de9e3e4e3ddd9ebf7b776ba4d2197ed8c68dce5e6d98",
198
198
  "README.md": "8bd57e02d9a9d3f0a4efd18c0f0bd1f4bbe13eb206add0317659e48eab435e7e",
199
- "architecture.md": "99f9d6a1a90e6c96d3c8a6f36c2650da4a1af0a1bc21173ea8eb2c492008539a",
200
- "cli-contract.md": "bab14bb72ddd8a57e00808f7f12741c63a33da99055b278e4407ab9b4bb7e2c1",
199
+ "architecture.md": "0ebaacef9e57206bc0dde27ff44a02e0a7def9ae9ceba2f27053b31ff708708b",
200
+ "cli-contract.md": "12ca455496d48a61fc83888808433acf1470f09c261cf1161375b01f0f3f85c4",
201
201
  "conformance/README.md": "79c5e63f18a368951dc9f3e31e9bf9574de3f8b97150b2d75365d4febd8eb6dc",
202
202
  "conformance/cases/basic-scan.json": "24623da0cad8c8c54b3ff9b09820ea1276fe8b8f0fc680bf6e8abeb4edb8e424",
203
203
  "conformance/cases/kernel-empty-boot.json": "175524674b14d993d29f10080d7697074b3a2eee25b359ff903344d73c6acc98",
204
204
  "conformance/cases/orphan-detection.json": "7fea6e866d775d09cadb70ccd764f6c8317ca61316c6d187a97cb2466db4e19e",
205
205
  "conformance/cases/rename-high.json": "f23513893e25fc4259db06a497906137de981da775d8ab2ef262554d54af5f27",
206
- "conformance/coverage.md": "ef98b87b70c46d7deb9853a8015c3e366a296088a70e13e4ffe223d91b9b4622",
206
+ "conformance/coverage.md": "a9580457cd868638676a450ace478438f832d057ab9c3ad64c088366afc07b7a",
207
207
  "conformance/fixtures/minimal-claude/agents/reviewer.md": "d0dd681ba63838301e480116aa09825329f01832b0116de5c5476fdd8a5dcf54",
208
208
  "conformance/fixtures/minimal-claude/commands/status.md": "3f36e053fd1c059ffd902f84a55be8a458c26072f97cb37dd7e97314ae2a9bf5",
209
209
  "conformance/fixtures/minimal-claude/hooks/pre-commit.md": "ec9cec8ac4ce34d40ec055ffd90e8f06ea3e5764d6ec3ee84e0d97de71b930c7",
@@ -216,20 +216,21 @@
216
216
  "conformance/fixtures/rename-high-after/skills/bar.md": "16f7678829c7702f8ebaeef920a891756da198466a1884badd8d8b4a7d1bab6a",
217
217
  "conformance/fixtures/rename-high-before/skills/foo.md": "16f7678829c7702f8ebaeef920a891756da198466a1884badd8d8b4a7d1bab6a",
218
218
  "db-schema.md": "002224f629403a247c0243d4b242c1e35e28bd93073ea53137ec1d30084d9bd7",
219
- "interfaces/security-scanner.md": "81dc3dc2c439a75f4603b6d52e714f44ac564032c8aa424385ebbf4502adae3e",
219
+ "interfaces/security-scanner.md": "e46d33d6e39b15672c8f7350f1cbd4755534510fe57c679c2b1d0be57577d818",
220
220
  "job-events.md": "08796b7fbeb55e5b03cf3bc394224e70a23438a4d15a46ad1d70121c2c68b967",
221
221
  "job-lifecycle.md": "1fe88b1a2ed204e41bb41ac172fbb3e912dccd0dd8a1f8ea8e21a681b336d6ee",
222
+ "plugin-author-guide.md": "d8ddba9d47eed4ff973862cb3af5e22b693bb5bede3275df8817bbcebcd7689c",
222
223
  "plugin-kv-api.md": "04b2178f46fb88adeae9240df9c9e1761b660396072001dac32cd402e11a2d7d",
223
224
  "prompt-preamble.md": "23a8eff0477fbbc46192a27781bc781bda4202bb9c669b7a7a002b0d668146b0",
224
225
  "schemas/conformance-case.schema.json": "d69c501bbca079da0ca87685eb4cbdbc2e405334469fc937929ca9134e01a2b3",
225
226
  "schemas/execution-record.schema.json": "ec0f3acf1d0ce099c059d73eb434936bfd1bcf12023693bd572efb2a7352faa6",
226
- "schemas/extensions/action.schema.json": "c7520d3cefecf75d27d3e04473821fd6e5dc5a7924eede147f74275ba6caccad",
227
- "schemas/extensions/adapter.schema.json": "429b865e738664bb437ac62690a2d7282ce992339fbb300417c73625f5cdb7c8",
228
- "schemas/extensions/audit.schema.json": "9ec2c68584707696423a1d617bc1e003cf8ee96a2c67b2f008f6647b2927c86c",
227
+ "schemas/extensions/action.schema.json": "63736f3efe33e35abcaa12de6d746c405e9bf0927b999bc0d49de3ba948d5831",
228
+ "schemas/extensions/adapter.schema.json": "819a696d4379262b8b1df96a16bc56bc46df60339ddddf4a9d92752dd008d682",
229
+ "schemas/extensions/audit.schema.json": "58b1895fd447cee7d5ed9e8c9139ecd6b0fe11d439903c30ec82f34ece14b24b",
229
230
  "schemas/extensions/base.schema.json": "c832a8c9976a7ddc70b8f9226a54de14aa3e85d71bc77ed7a8671a77d599c0e4",
230
- "schemas/extensions/detector.schema.json": "077b9cccb0bd3d58ca53d61d59c609aa42709225d187e341412a857ab341462f",
231
- "schemas/extensions/renderer.schema.json": "2ec52545c85bb5e36d0f4f67c155b0e1656468b62a1045d2eb268255202306f0",
232
- "schemas/extensions/rule.schema.json": "dd957deaafd41699309cb073a4620e4e8e45d3ba15541adba0e693e6d85cdf76",
231
+ "schemas/extensions/detector.schema.json": "a693c17b7e75bcf37eb87f84eea30e89d7aae179b5b89ef5a1cff330c333c029",
232
+ "schemas/extensions/renderer.schema.json": "187e3498d0f3bddb49b9793bca9601fe461ff8d23625069e4c5c8ba18acbb81a",
233
+ "schemas/extensions/rule.schema.json": "75e5adababcf1f0c5c6aaf8009795d49e7a7e196cee13a58940a076429d0be5e",
233
234
  "schemas/frontmatter/agent.schema.json": "0e63d7692efb29facccc69472fff48a25f44934618346bfc09738864c6917787",
234
235
  "schemas/frontmatter/base.schema.json": "e68fbb85d3e873c4897af776eaf873860bd6e86b5abc1799e801d35c4f7937cf",
235
236
  "schemas/frontmatter/command.schema.json": "7b8463ce9c83edd2e3073dd4cd1bbeec4b42e53b03b48bc9a59e540136c2de89",
@@ -242,7 +243,7 @@
242
243
  "schemas/link.schema.json": "3e92f5c9def61a857a2c7b22846d82b988157de083463615144ddc92403a489e",
243
244
  "schemas/node.schema.json": "14f345fac450f5728c895d1b878e0015eabb9d72ba9da4a8d2236c82933d3fcf",
244
245
  "schemas/plugins-registry.schema.json": "92b2052bd06e366709dd6e1449d99408999e33707c4007afc7662980e73c3ef1",
245
- "schemas/project-config.schema.json": "a37acdd6198e38dfc429161d92988170ddac91c6e98969e0aaaa8d717f5b9ba3",
246
+ "schemas/project-config.schema.json": "74f8f2ba2c4897ee47a5cc08e27ec3898dc0a938fe7e3823f33f6c5005724d1f",
246
247
  "schemas/report-base.schema.json": "a1021e9a59b4df9f99cd92454d797e88469766e7d49f52d231c4645ffdfdad8f",
247
248
  "schemas/scan-result.schema.json": "5efe9b1954c5e729c4b55dbc4dd51263d97967d16c0b3cea398877ace74d37b7",
248
249
  "schemas/summaries/agent.schema.json": "3d22558eeb170e00c4fc32018a810d27333cc632c9e528ff386100cfdfded087",
@@ -49,7 +49,7 @@ The Action receives a standard invocation: a single node, or (via `--all`) a set
49
49
 
50
50
  i.e. applies to every node. A scanner MAY narrow to specific kinds if the vendor's check only applies to, for example, shell-hook content.
51
51
 
52
- Scanners are **local-mode** Actions by default: no LLM involvement. The Action runs its own logic (HTTP request to a vendor API, local regex scan, dependency check) and writes a report. Scanners MAY also be `invocation-template` Actions if the scanner relies on model analysis — the same report shape applies.
52
+ Scanners are **deterministic-mode** Actions by default: no LLM involvement. The Action runs its own logic (HTTP request to a vendor API, local regex scan, dependency check) and writes a report. Scanners MAY also be `probabilistic` Actions if the scanner relies on model analysis — the same report shape applies.
53
53
 
54
54
  ---
55
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-map/spec",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "JSON Schemas, prose contracts, and conformance suite for the skill-map specification.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,6 +38,7 @@
38
38
  "prompt-preamble.md",
39
39
  "db-schema.md",
40
40
  "plugin-kv-api.md",
41
+ "plugin-author-guide.md",
41
42
  "interfaces/",
42
43
  "schemas/",
43
44
  "conformance/",
@@ -0,0 +1,335 @@
1
+ # Plugin author guide
2
+
3
+ How to ship a third-party `skill-map` plugin: directory layout, manifest fields, the six extension kinds, storage choice, version compatibility, dual-mode posture, and how to test the result with `@skill-map/testkit`.
4
+
5
+ This guide is **descriptive prose**, not the normative contract. The normative pieces live in the schemas and the architecture document — every claim here is cross-linked to its source. When the two disagree, [`architecture.md`](./architecture.md) wins.
6
+
7
+ > **Status.** Ships with spec v1.0.0. The author surface is intended to stay stable through the v1.x line; widening (new extension kind, new storage mode) is a minor bump per [`versioning.md`](./versioning.md).
8
+
9
+ ---
10
+
11
+ ## Quick start
12
+
13
+ ```text
14
+ my-plugin/
15
+ ├── plugin.json ← manifest (required)
16
+ └── extensions/
17
+ └── detector.mjs ← one file per declared extension
18
+ ```
19
+
20
+ ```jsonc
21
+ // my-plugin/plugin.json
22
+ {
23
+ "id": "my-plugin",
24
+ "version": "1.0.0",
25
+ "specCompat": "^1.0.0",
26
+ "extensions": ["./extensions/detector.mjs"]
27
+ }
28
+ ```
29
+
30
+ ```javascript
31
+ // my-plugin/extensions/detector.mjs
32
+ export default {
33
+ id: 'my-detector',
34
+ kind: 'detector',
35
+ version: '1.0.0',
36
+ emitsLinkKinds: ['references'],
37
+ defaultConfidence: 'high',
38
+ scope: 'body',
39
+ detect(ctx) {
40
+ // ctx.node, ctx.body, ctx.frontmatter — return Link[]
41
+ return [];
42
+ },
43
+ };
44
+ ```
45
+
46
+ Drop the directory under one of the discovery roots and `sm plugins list` will pick it up.
47
+
48
+ ---
49
+
50
+ ## Discovery
51
+
52
+ The kernel scans two roots, in this order:
53
+
54
+ 1. `<project>/.skill-map/plugins/` — committed-with-the-repo plugins.
55
+ 2. `~/.skill-map/plugins/` — user-level plugins available across every project.
56
+
57
+ A plugin is any direct child directory containing a `plugin.json`. Nested directories are not searched recursively. Pass `--plugin-dir <path>` to override both roots (mostly for testing).
58
+
59
+ After every change to the `plugins/` folder, run `sm plugins list` to see the load status of each. The five statuses are documented under [Diagnostics](#diagnostics) below.
60
+
61
+ ---
62
+
63
+ ## Manifest
64
+
65
+ Required fields (see [`schemas/plugins-registry.schema.json#/$defs/PluginManifest`](./schemas/plugins-registry.schema.json) for the normative shape):
66
+
67
+ | Field | Type | Notes |
68
+ |---|---|---|
69
+ | `id` | kebab-case string | Globally unique. Pattern: `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`. |
70
+ | `version` | semver | Plugin version, independent of `specCompat`. |
71
+ | `specCompat` | semver range | Spec versions this plugin is compatible with. Checked via `semver.satisfies(specVersion, this)` at load time. |
72
+ | `extensions` | string[] | Relative paths to extension files. Each file's default export is the extension's runtime instance. `minItems: 1`. |
73
+
74
+ Optional fields:
75
+
76
+ | Field | Type | Notes |
77
+ |---|---|---|
78
+ | `description` | string | One-line summary shown in `sm plugins list`. |
79
+ | `storage` | object | `{ "mode": "kv" }` or `{ "mode": "dedicated", "tables": [...], "migrations": [...] }`. Absent means the plugin does not persist state. |
80
+ | `author` | string | Free-form. |
81
+ | `license` | string | SPDX identifier. |
82
+ | `homepage` | string | URL. |
83
+ | `repository` | string | URL. |
84
+
85
+ ### `specCompat` strategy
86
+
87
+ Pre-`v1.0.0` of the spec, narrow ranges are the defensive default — minor bumps **MAY** carry breaking changes per [`versioning.md`](./versioning.md). A plugin that spans minor boundaries can load successfully and crash at first use against a changed schema.
88
+
89
+ After the spec hits v1.0.0, the recommended ranges are:
90
+
91
+ - `"^1.0.0"` — most plugins. Loads against any v1.x.
92
+ - `">=1.0.0 <2.0.0"` — equivalent, more explicit.
93
+ - A pre-release pin (`"^1.0.0-beta.5"`) — only when you depend on a feature added between minors.
94
+
95
+ Authors who explicitly review each minor's changelog **MAY** widen across the next major (`"^1.0.0 || ^2.0.0"`) at their own risk.
96
+
97
+ ---
98
+
99
+ ## The six extension kinds
100
+
101
+ The kernel knows six categories. Four are dual-mode (deterministic or probabilistic per [`architecture.md` §Execution modes](./architecture.md)); two are deterministic-only because they sit at the system boundaries.
102
+
103
+ | Kind | Method | Receives | Returns | Mode |
104
+ |---|---|---|---|---|
105
+ | `adapter` | `walk(roots, opts)` | filesystem roots | `IRawNode[]` | deterministic only |
106
+ | `detector` | `detect(ctx)` | one node + body + frontmatter | `Link[]` | dual-mode |
107
+ | `rule` | `evaluate(ctx)` | full graph | `Issue[]` | dual-mode |
108
+ | `action` | `run(ctx)` | one or more nodes | execution record | dual-mode |
109
+ | `audit` | `audit(ctx)` | full graph | `TAuditReport` | derived (from `composes[]`) |
110
+ | `renderer` | `render(ctx)` | full graph | `string` | deterministic only |
111
+
112
+ The runtime instance you `export default` from an extension file MUST include both the manifest fields (id, kind, version, plus kind-specific metadata) AND the runtime method. The kernel strips function-typed properties before AJV-validating the manifest shape, so `detect` / `evaluate` / etc. live alongside metadata without confusing the schema.
113
+
114
+ ### Detectors
115
+
116
+ Pure single-node analysis. **Never** read another node, the graph, or the database — cross-node reasoning is for rules. Spec at [`schemas/extensions/detector.schema.json`](./schemas/extensions/detector.schema.json).
117
+
118
+ > **Pick a syntax that doesn't collide with built-ins.** The built-in `at-directive` detector fires on any `@token`; the built-in `slash` detector fires on any `/token`. A new detector that also matches one of those prefixes will likely fire on the same input, and if the two emit different `target` shapes the kernel raises a `trigger-collision` error. The example below uses a wikilink-style `[[ref:<name>]]` pattern to side-step this; reserve `@` and `/` for the built-ins.
119
+
120
+ ```javascript
121
+ import { normalizeTrigger } from '@skill-map/cli';
122
+
123
+ export default {
124
+ id: 'ref-detector',
125
+ kind: 'detector',
126
+ version: '1.0.0',
127
+ description: 'Detects [[ref:<name>]] tokens in the body.',
128
+ stability: 'experimental',
129
+ emitsLinkKinds: ['references'],
130
+ defaultConfidence: 'medium',
131
+ scope: 'body',
132
+ detect(ctx) {
133
+ const matches = [...ctx.body.matchAll(/\[\[ref:([a-z0-9-]+)\]\]/gi)];
134
+ return matches.map((m) => ({
135
+ source: ctx.node.path,
136
+ target: m[1],
137
+ kind: 'references',
138
+ confidence: 'medium',
139
+ sources: ['ref-detector'],
140
+ trigger: { originalTrigger: m[0], normalizedTrigger: m[0].toLowerCase() },
141
+ }));
142
+ },
143
+ };
144
+ ```
145
+
146
+ ### Rules
147
+
148
+ Cross-node reasoning over the merged graph. Run after every adapter and detector has completed. Spec at [`schemas/extensions/rule.schema.json`](./schemas/extensions/rule.schema.json).
149
+
150
+ ```javascript
151
+ export default {
152
+ id: 'orphan-skill',
153
+ kind: 'rule',
154
+ version: '1.0.0',
155
+ description: 'Flags skill nodes with zero inbound links.',
156
+ evaluate(ctx) {
157
+ const inboundCount = new Map();
158
+ for (const link of ctx.links) {
159
+ inboundCount.set(link.target, (inboundCount.get(link.target) ?? 0) + 1);
160
+ }
161
+ return ctx.nodes
162
+ .filter((n) => n.kind === 'skill' && (inboundCount.get(n.path) ?? 0) === 0)
163
+ .map((n) => ({
164
+ ruleId: 'orphan-skill',
165
+ severity: 'info',
166
+ message: `Skill ${n.path} has no inbound references.`,
167
+ nodeIds: [n.path],
168
+ }));
169
+ },
170
+ };
171
+ ```
172
+
173
+ ### Renderers
174
+
175
+ Graph-to-string serializers. Invoked by `sm graph --format <name>`. Output **MUST** be byte-deterministic for the same input graph (the snapshot-test suite relies on this). Spec at [`schemas/extensions/renderer.schema.json`](./schemas/extensions/renderer.schema.json).
176
+
177
+ ```javascript
178
+ export default {
179
+ id: 'csv-renderer',
180
+ kind: 'renderer',
181
+ version: '1.0.0',
182
+ format: 'csv',
183
+ contentType: 'text/csv',
184
+ render(ctx) {
185
+ const rows = ['source,target,kind,confidence'];
186
+ for (const link of ctx.links) {
187
+ rows.push([link.source, link.target, link.kind, link.confidence].join(','));
188
+ }
189
+ return rows.join('\n');
190
+ },
191
+ };
192
+ ```
193
+
194
+ ### Adapters / Audits / Actions
195
+
196
+ These ship later in the v1.x line as bundled built-ins; the spec already pins their manifest shapes. Until the testkit grows full helpers for them (planned alongside Step 10), authors are encouraged to test them with a live kernel via `sm scan` against a fixture directory rather than in unit tests.
197
+
198
+ ---
199
+
200
+ ## Storage
201
+
202
+ A plugin that needs to persist state declares `storage` in its manifest. Two modes; each is documented in full at [`plugin-kv-api.md`](./plugin-kv-api.md).
203
+
204
+ ### Mode A — KV
205
+
206
+ ```jsonc
207
+ { "storage": { "mode": "kv" } }
208
+ ```
209
+
210
+ Backed by the kernel-owned `state_plugin_kvs` table. The plugin gets `ctx.store` with `get` / `set` / `list` / `delete`. No migrations to write, ready immediately.
211
+
212
+ Pick KV when your state is a small map (less than ~1 MB total, simple key lookup or prefix list). 90 % of plugins fit.
213
+
214
+ ### Mode B — Dedicated
215
+
216
+ ```jsonc
217
+ {
218
+ "storage": {
219
+ "mode": "dedicated",
220
+ "tables": ["plugin_my_plugin_items", "plugin_my_plugin_history"],
221
+ "migrations": ["./migrations/001_init.sql"]
222
+ }
223
+ }
224
+ ```
225
+
226
+ The plugin owns SQL tables prefixed `plugin_<normalizedId>_*`. Migrations live under `<plugin-dir>/migrations/NNN_<name>.sql` and apply through `sm db migrate` (mixed with kernel migrations, after them).
227
+
228
+ Pick Dedicated when you need indexes, joins, or relational shape.
229
+
230
+ #### Triple protection
231
+
232
+ Every DDL or DML object a plugin migration creates / alters / drops MUST live in the `plugin_<normalizedId>_*` namespace. The kernel enforces this in three places:
233
+
234
+ 1. **Discovery (Layer 1)**: every pending migration file is parsed and validated before any of them run. A bad file aborts the whole batch with no DB writes.
235
+ 2. **Apply (Layer 2)**: the same validator re-runs immediately before `db.exec(sql)`, defending against TOCTOU edits between discovery and apply.
236
+ 3. **Catalog assertion (Layer 3)**: `sqlite_master` is swept after each plugin's batch commits; any new object outside the prefix is reported as an intrusion (exit 2).
237
+
238
+ Forbidden in plugin migrations: `BEGIN` / `COMMIT` / `ROLLBACK` / `SAVEPOINT` / `PRAGMA` / `ATTACH` / `DETACH` / `VACUUM` / `REINDEX` / `ANALYZE`. The runner wraps each migration in its own transaction. Schema qualifiers other than `main.` are also rejected.
239
+
240
+ ---
241
+
242
+ ## Execution modes
243
+
244
+ Detector / Rule / Action declare `mode` in the manifest with default `deterministic`. Audit forbids `mode` — the kernel derives it from `composes[]` at load time. Adapter / Renderer must NOT declare `mode`.
245
+
246
+ ```jsonc
247
+ // deterministic detector — default, runs in sm scan
248
+ { "kind": "detector", "id": "my-detector", "mode": "deterministic", ... }
249
+ ```
250
+
251
+ ```jsonc
252
+ // probabilistic action — runs only as a queued job, dispatched via `sm job submit action:my-action`
253
+ { "kind": "action", "id": "my-action", "mode": "probabilistic", ... }
254
+ ```
255
+
256
+ A `probabilistic` extension receives `ctx.runner` (a `RunnerPort`) and dispatches its work to the configured LLM runner (CLI, Skill Agent, or in-process per [`architecture.md`](./architecture.md)). It MUST NOT register scan-time hooks; the kernel rejects probabilistic extensions that do.
257
+
258
+ The full per-kind capability matrix lives in [`architecture.md` §Execution modes](./architecture.md).
259
+
260
+ ---
261
+
262
+ ## Testing with `@skill-map/testkit`
263
+
264
+ ```bash
265
+ npm install --save-dev @skill-map/testkit
266
+ ```
267
+
268
+ The testkit ships builders, per-kind context factories, in-memory KV / runner fakes, and high-level `runDetectorOnFixture` / `runRuleOnGraph` / `runRendererOnGraph` helpers. Most plugin tests reduce to one line per assertion.
269
+
270
+ ```javascript
271
+ import { test } from 'node:test';
272
+ import { strictEqual } from 'node:assert';
273
+ import { runDetectorOnFixture, node } from '@skill-map/testkit';
274
+
275
+ import detector from '../extensions/detector.mjs';
276
+
277
+ test('emits one reference per [[ref:<name>]] token', async () => {
278
+ const links = await runDetectorOnFixture(detector, {
279
+ body: 'Talk to [[ref:architect]] or [[ref:sre]].',
280
+ context: { node: node({ path: 'a.md' }) },
281
+ });
282
+ strictEqual(links.length, 2);
283
+ strictEqual(links[0].target, 'architect');
284
+ });
285
+ ```
286
+
287
+ For rule tests, `runRuleOnGraph(rule, { context: { nodes, links } })` returns the issue array. For renderer tests, `runRendererOnGraph(renderer, { context: { nodes, links, issues } })` returns the rendered string.
288
+
289
+ For probabilistic extensions, `makeFakeRunner()` queues canned responses and records every call:
290
+
291
+ ```javascript
292
+ import { makeFakeRunner } from '@skill-map/testkit';
293
+
294
+ const runner = makeFakeRunner();
295
+ runner.queue({ text: '5 nodes summarized' });
296
+ const result = await myAction.run({ runner, ... });
297
+ strictEqual(runner.history[0].action, 'skill-summarizer');
298
+ ```
299
+
300
+ Full surface in `@skill-map/testkit/index.ts`.
301
+
302
+ ---
303
+
304
+ ## Diagnostics
305
+
306
+ `sm plugins list` shows every discovered plugin with one of five statuses. When a plugin doesn't behave the way you expect, this is the first thing to check.
307
+
308
+ | Status | Meaning | Common cause |
309
+ |---|---|---|
310
+ | `loaded` | manifest valid, specCompat satisfied, every extension imported and validated. | — |
311
+ | `disabled` | user toggled it off via `sm plugins disable` or `settings.json#/plugins/<id>/enabled`. Manifest parsed; extensions not imported. | Intentional. |
312
+ | `incompatible-spec` | manifest parsed but `semver.satisfies` failed against the installed spec. | Plugin built against an older / newer spec. |
313
+ | `invalid-manifest` | `plugin.json` missing, unparseable, or AJV-fails. | Typo, missing required field, wrong shape. |
314
+ | `load-error` | manifest passed but an extension module failed to import or its default export failed schema validation. | Missing `kind` field, wrong `kind` for the file, runtime import error. |
315
+
316
+ `sm plugins doctor` runs the full load pass and exits 1 if any plugin is in a non-`loaded` / non-`disabled` state. Wire it into CI to catch breakage early.
317
+
318
+ ---
319
+
320
+ ## See also
321
+
322
+ - [`architecture.md`](./architecture.md) — extension contract, ports, execution modes.
323
+ - [`plugin-kv-api.md`](./plugin-kv-api.md) — Storage Mode A normative API.
324
+ - [`db-schema.md`](./db-schema.md) — table catalog and migration rules (Mode B).
325
+ - [`schemas/plugins-registry.schema.json`](./schemas/plugins-registry.schema.json) — normative manifest shape.
326
+ - [`schemas/extensions/*.schema.json`](./schemas/extensions) — per-kind manifest schemas.
327
+
328
+ ---
329
+
330
+ ## Stability
331
+
332
+ - Document status: **stable** as of spec v1.0.0. Future minor revisions add new sections (e.g. richer testkit coverage when actions / audits gain helpers); breaking edits to the documented surface require a major bump per [`versioning.md`](./versioning.md).
333
+ - The five plugin statuses (`loaded` / `disabled` / `incompatible-spec` / `invalid-manifest` / `load-error`) are stable; adding a sixth status is a minor bump.
334
+ - The recommended `specCompat` strategy is descriptive prose; revising the recommendation does not require a spec bump as long as the schema stays unchanged.
335
+ - The example code blocks track the public TypeScript surface of `@skill-map/cli`; bumping their imports follows the cli's own semver.
@@ -2,10 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/action.schema.json",
4
4
  "title": "ExtensionAction",
5
- "description": "Manifest shape for an `Action` extension. An action operates on one or more nodes in one of two modes: `local` (code runs in-process, returns a report JSON directly) or `invocation-template` (kernel renders a prompt, a runner executes it, the callback closes the job). The `mode` discriminator drives which additional fields are required.",
6
- "allOf": [
7
- { "$ref": "base.schema.json" }
8
- ],
5
+ "description": "Manifest shape for an `Action` extension. An action operates on one or more nodes in one of two modes: `deterministic` (code runs in-process, returns a report JSON directly) or `probabilistic` (kernel renders a prompt, a runner executes it against an LLM, the callback closes the job). The `mode` discriminator drives which additional fields are required. See `architecture.md` §Execution modes for the cross-extension contract.",
9
6
  "type": "object",
10
7
  "required": ["id", "kind", "version", "mode", "reportSchemaRef"],
11
8
  "unevaluatedProperties": false,
@@ -13,8 +10,8 @@
13
10
  "kind": { "const": "action" },
14
11
  "mode": {
15
12
  "type": "string",
16
- "enum": ["local", "invocation-template"],
17
- "description": "`local`: the plugin's code computes the report synchronously, no job file, no runner. `invocation-template`: the kernel renders a prompt + preamble into a job file; a runner executes it; `sm record` closes the job."
13
+ "enum": ["deterministic", "probabilistic"],
14
+ "description": "`deterministic`: the plugin's code computes the report synchronously, no job file, no runner. `probabilistic`: the kernel renders a prompt + preamble into a job file; a runner executes it via `RunnerPort`; `sm record` closes the job."
18
15
  },
19
16
  "reportSchemaRef": {
20
17
  "type": "string",
@@ -23,11 +20,11 @@
23
20
  "expectedDurationSeconds": {
24
21
  "type": "integer",
25
22
  "minimum": 1,
26
- "description": "Best-effort estimate of wall-clock duration. Drives TTL (`ttl = max(expectedDurationSeconds × graceMultiplier, minimumTtlSeconds)`). Required for `invocation-template`; advisory for `local`."
23
+ "description": "Best-effort estimate of wall-clock duration. Drives TTL (`ttl = max(expectedDurationSeconds × graceMultiplier, minimumTtlSeconds)`). Required for `probabilistic`; advisory for `deterministic`."
27
24
  },
28
25
  "promptTemplateRef": {
29
26
  "type": "string",
30
- "description": "Path (relative to the extension file) to the prompt template the kernel renders at `sm job submit`. REQUIRED when `mode: invocation-template`; FORBIDDEN when `mode: local`. The template MUST NOT interpolate user text outside `<user-content>` blocks (see `prompt-preamble.md`)."
27
+ "description": "Path (relative to the extension file) to the prompt template the kernel renders at `sm job submit`. REQUIRED when `mode: probabilistic`; FORBIDDEN when `mode: deterministic`. The template MUST NOT interpolate user text outside `<user-content>` blocks (see `prompt-preamble.md`)."
31
28
  },
32
29
  "precondition": {
33
30
  "type": "object",
@@ -69,12 +66,13 @@
69
66
  }
70
67
  },
71
68
  "allOf": [
69
+ { "$ref": "base.schema.json" },
72
70
  {
73
- "if": { "properties": { "mode": { "const": "invocation-template" } } },
71
+ "if": { "properties": { "mode": { "const": "probabilistic" } } },
74
72
  "then": { "required": ["promptTemplateRef", "expectedDurationSeconds"] }
75
73
  },
76
74
  {
77
- "if": { "properties": { "mode": { "const": "local" } } },
75
+ "if": { "properties": { "mode": { "const": "deterministic" } } },
78
76
  "then": { "not": { "required": ["promptTemplateRef"] } }
79
77
  }
80
78
  ]
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/adapter.schema.json",
4
4
  "title": "ExtensionAdapter",
5
- "description": "Manifest shape for an `Adapter` extension. An adapter recognizes a platform (Claude Code, Codex, Gemini, Obsidian vault, generic MD) and classifies each candidate file into a node `kind`. Exactly zero or one adapter MUST match any given file; multiple matches → the kernel emits an issue `adapter-ambiguous` and the file is left unclassified. Stability: stable as of spec v1.0.0 except where noted.",
5
+ "description": "Manifest shape for an `Adapter` extension. An adapter recognizes a platform (Claude Code, Codex, Gemini, Obsidian vault, generic MD) and classifies each candidate file into a node `kind`. Exactly zero or one adapter MUST match any given file; multiple matches → the kernel emits an issue `adapter-ambiguous` and the file is left unclassified. Adapters 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 adapter manifests. If you need LLM-assisted classification, write a probabilistic Detector that emits classification hints as `Link[]`. Stability: stable as of spec v1.0.0 except where noted.",
6
6
  "allOf": [
7
7
  { "$ref": "base.schema.json" }
8
8
  ],
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/audit.schema.json",
4
4
  "title": "ExtensionAudit",
5
- "description": "Manifest shape for an `Audit` extension. An audit is a hardcoded, deterministic workflow that composes rules and/or local actions into a single report. Audits MUST NOT submit LLM-backed actionstheir defining property is reproducibility. An audit that needs probabilistic signal is the wrong shape; emit a `Findings` surface via LLM verbs instead.",
5
+ "description": "Manifest shape for an `Audit` extension. An audit is a hardcoded workflow that composes rules and actions into a single report. The audit's execution mode is NOT declared in the manifest it is **derived** from the modes of the primitives it composes: if every composed primitive is `deterministic`, the audit's effective mode is `deterministic` and runs synchronously inside `sm audit <id>`; if any composed primitive is `probabilistic`, the audit's effective mode is `probabilistic` and dispatches as a queued job (`sm job submit audit:<id>`). Declaring `mode` in the manifest is a load-time error. See `architecture.md` §Execution modes for the full derivation contract.",
6
6
  "allOf": [
7
7
  { "$ref": "base.schema.json" }
8
8
  ],
@@ -13,7 +13,7 @@
13
13
  "kind": { "const": "audit" },
14
14
  "composes": {
15
15
  "type": "array",
16
- "description": "Ordered list of rule ids and/or local action ids the audit executes in sequence. The kernel resolves each id in the registry at load time; a dangling reference disables the audit with status `invalid-manifest`.",
16
+ "description": "Ordered list of rule and action references the audit executes in sequence. The kernel resolves each reference in the registry at load time; a dangling reference (id not found, kind mismatch, or primitive disabled) disables the audit with status `invalid-manifest`. Each composed primitive's `mode` participates in the audit's mode derivation.",
17
17
  "minItems": 1,
18
18
  "items": {
19
19
  "type": "object",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "reportSchemaRef": {
34
34
  "type": "string",
35
- "description": "Reference to the JSON Schema of the audit's report shape. Audits do NOT extend `report-base.schema.json` they are deterministic and therefore carry no `safety` / `confidence`. Their shape is kind-specific."
35
+ "description": "Reference to the JSON Schema of the audit's report shape. Probabilistic audits MUST extend `report-base.schema.json` (carries `safety` / `confidence` per the report-base contract). Deterministic audits MAY extend it but are not required to."
36
36
  },
37
37
  "exitCodeMap": {
38
38
  "type": "object",
@@ -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/detector.schema.json",
4
4
  "title": "ExtensionDetector",
5
- "description": "Manifest shape for a `Detector` extension. A detector consumes a parsed node (frontmatter + body) and emits `Link[]` pointing to other nodes or to external URLs (the latter only if it is the designated URL-counter detector). Detectors run in isolation: they MUST NOT read other nodes, the graph, or the DB. Cross-node reasoning lives in Rules.",
5
+ "description": "Manifest shape for a `Detector` extension. A detector consumes a parsed node (frontmatter + body) and emits `Link[]` pointing to other nodes or to external URLs (the latter only if it is the designated URL-counter detector). Detectors run in isolation: they MUST NOT read other nodes, the graph, or the DB. Cross-node reasoning lives in Rules. Detectors are dual-mode: `deterministic` detectors run synchronously inside `sm scan`; `probabilistic` detectors invoke an LLM through the kernel's `RunnerPort` and execute only as queued jobs (never during scan). See `architecture.md` §Execution modes for the full contract.",
6
6
  "allOf": [
7
7
  { "$ref": "base.schema.json" }
8
8
  ],
@@ -11,6 +11,12 @@
11
11
  "unevaluatedProperties": false,
12
12
  "properties": {
13
13
  "kind": { "const": "detector" },
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 detector:<id>`); never participates in `sm scan`. The kernel rejects probabilistic detectors that try to register scan-time hooks at load time. Omitting the field is equivalent to declaring `deterministic`."
19
+ },
14
20
  "emitsLinkKinds": {
15
21
  "type": "array",
16
22
  "description": "Subset of `Link.kind` values this detector is allowed to emit. Emitting an unlisted kind at runtime → kernel rejects the link and logs `detector-kind-violation`.",
@@ -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/renderer.schema.json",
4
4
  "title": "ExtensionRenderer",
5
- "description": "Manifest shape for a `Renderer` extension. A renderer serializes the graph (or a filtered subgraph) into a string in a declared format. Renderers are invoked by `sm graph --format <format>` and `sm export`. Output MUST be byte-deterministic for the same input graph the snapshot-test suite relies on this.",
5
+ "description": "Manifest shape for a `Renderer` extension. A renderer serializes the graph (or a filtered subgraph) into a string in a declared format. Renderers are invoked by `sm graph --format <format>` and `sm export`. Renderers 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 renderer manifests. Probabilistic narrators of the graph are a valid product but they live in jobs and emit Findings, not in renderers.",
6
6
  "allOf": [
7
7
  { "$ref": "base.schema.json" }
8
8
  ],
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.dev/spec/v0/extensions/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 MUST be deterministic: same graph in → same issues out, byte-for-byte. Any source of non-determinism (time, random, network) is forbidden and is a conformance violation.",
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.",
6
6
  "allOf": [
7
7
  { "$ref": "base.schema.json" }
8
8
  ],
@@ -11,6 +11,12 @@
11
11
  "unevaluatedProperties": false,
12
12
  "properties": {
13
13
  "kind": { "const": "rule" },
14
+ "mode": {
15
+ "type": "string",
16
+ "enum": ["deterministic", "probabilistic"],
17
+ "default": "deterministic",
18
+ "description": "`deterministic` (default): pure code, byte-for-byte reproducible, runs during `sm check` and `sm scan`. `probabilistic`: invokes an LLM via `ctx.runner` and runs only as a queued job; never participates in scan-time pipelines. The kernel rejects probabilistic rules that try to register scan-time hooks at load time. Omitting the field is equivalent to declaring `deterministic`."
19
+ },
14
20
  "emitsRuleIds": {
15
21
  "type": "array",
16
22
  "description": "List of `rule_id` values this rule may emit on issues. Typically a singleton (`trigger-collision` → emits `trigger-collision`). A rule emitting a `rule_id` not in this list → kernel logs `rule-id-violation` but keeps the issue (forward compatibility).",
@@ -45,6 +45,18 @@
45
45
  "type": "integer",
46
46
  "minimum": 1,
47
47
  "description": "Files larger than this are skipped with an `info`-level log entry. Default 1048576 (1 MiB). Protects against scanning accidental binary drops or generated artefacts."
48
+ },
49
+ "watch": {
50
+ "type": "object",
51
+ "additionalProperties": false,
52
+ "description": "File-watcher knobs for `sm watch` and `sm scan --watch`. The watcher subscribes to the same roots `sm scan` walks, applies the `.skill-mapignore` filter, and triggers an incremental scan after each batch.",
53
+ "properties": {
54
+ "debounceMs": {
55
+ "type": "integer",
56
+ "minimum": 0,
57
+ "description": "Milliseconds to wait after the last filesystem event before triggering an incremental scan. Groups bursts (editor saves, branch switches, package installs) into a single scan pass. Default 300. Set to 0 to disable debouncing — every filesystem event triggers a scan immediately."
58
+ }
59
+ }
48
60
  }
49
61
  }
50
62
  },