@skill-map/spec 0.18.0 → 0.20.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 +680 -1
- package/README.md +6 -6
- package/architecture.md +244 -41
- package/cli-contract.md +48 -20
- package/conformance/README.md +2 -2
- package/conformance/cases/kernel-empty-boot.json +2 -2
- package/conformance/cases/orphan-markdown-fallback.json +22 -0
- package/conformance/cases/plugin-missing-ui-rejected.json +2 -1
- package/conformance/cases/sidecar-end-to-end.json +3 -4
- package/conformance/coverage.md +8 -6
- package/conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md +6 -0
- package/conformance/fixtures/orphan-markdown/ARCHITECTURE.md +10 -0
- package/conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm +2 -2
- package/conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.sm +1 -1
- package/conformance/fixtures/sidecar-example/agent-example.md +1 -1
- package/conformance/fixtures/sidecar-example/agent-example.sm +1 -1
- package/db-schema.md +68 -23
- package/index.json +47 -42
- package/interfaces/security-scanner.md +2 -2
- package/job-events.md +12 -12
- package/job-lifecycle.md +1 -1
- package/package.json +1 -1
- package/plugin-author-guide.md +374 -69
- package/plugin-kv-api.md +5 -5
- package/prompt-preamble.md +1 -1
- package/schemas/annotations.schema.json +5 -9
- package/schemas/api/rest-envelope.schema.json +55 -11
- package/schemas/conformance-case.schema.json +2 -2
- package/schemas/extensions/analyzer.schema.json +43 -0
- package/schemas/extensions/base.schema.json +14 -4
- package/schemas/extensions/extractor.schema.json +3 -10
- package/schemas/extensions/hook.schema.json +6 -4
- package/schemas/extensions/provider.schema.json +1 -1
- package/schemas/frontmatter/base.schema.json +6 -1
- package/schemas/input-types.schema.json +260 -0
- package/schemas/issue.schema.json +6 -6
- package/schemas/link.schema.json +2 -2
- package/schemas/node.schema.json +1 -19
- package/schemas/plugins-registry.schema.json +14 -2
- package/schemas/project-config.schema.json +25 -0
- package/schemas/sidecar.schema.json +6 -6
- package/schemas/summaries/agent.schema.json +1 -1
- package/schemas/summaries/command.schema.json +1 -1
- package/schemas/summaries/hook.schema.json +1 -1
- package/schemas/summaries/markdown.schema.json +1 -1
- package/schemas/view-slots.schema.json +335 -0
- package/schemas/extensions/rule.schema.json +0 -43
package/db-schema.md
CHANGED
|
@@ -41,7 +41,7 @@ Every kernel table belongs to exactly one zone, identified by a mandatory name p
|
|
|
41
41
|
|
|
42
42
|
## Naming conventions (normative)
|
|
43
43
|
|
|
44
|
-
These
|
|
44
|
+
These analyzers apply to every kernel table and to every plugin-authored table under its prefix.
|
|
45
45
|
|
|
46
46
|
- **Tables**: `snake_case`, plural. Zone prefix REQUIRED. Example: `scan_nodes`, `state_jobs`.
|
|
47
47
|
- **Columns**: `snake_case`. Primary key column is always `id`.
|
|
@@ -57,7 +57,7 @@ These rules apply to every kernel table and to every plugin-authored table under
|
|
|
57
57
|
- **Constraints**: `fk_`, `uq_`, `ck_` prefixes.
|
|
58
58
|
- **SQL keywords**: UPPERCASE. Identifiers lowercase.
|
|
59
59
|
|
|
60
|
-
The kernel MUST reject any plugin migration that violates these
|
|
60
|
+
The kernel MUST reject any plugin migration that violates these analyzers at validation time (see `plugin-kv-api.md`).
|
|
61
61
|
|
|
62
62
|
Domain types exposed to driving adapters use `camelCase`. The SQLite reference impl uses Kysely's `CamelCasePlugin` to bridge `snake_case ↔ camelCase` at the port boundary.
|
|
63
63
|
|
|
@@ -72,7 +72,7 @@ One row per detected node, matching [`schemas/node.schema.json`](./schemas/node.
|
|
|
72
72
|
| Column | Type | Constraint | Notes |
|
|
73
73
|
|---|---|---|---|
|
|
74
74
|
| `path` | TEXT | PRIMARY KEY | Relative path from scope root. Canonical node identifier. |
|
|
75
|
-
| `kind` | TEXT | NOT NULL | Open-by-design (`node.schema.json#/properties/kind`): the value is whatever the classifying Provider declares. Built-in
|
|
75
|
+
| `kind` | TEXT | NOT NULL | Open-by-design (`node.schema.json#/properties/kind`): the value is whatever the classifying Provider declares. Built-in catalogs: `claude` ships `skill` / `agent` / `command`; `gemini` ships `agent` / `skill`; `agent-skills` ships `skill`; `core/markdown` ships the format-named generic fallback `markdown` (universal — picks up any `.md` no vendor Provider claims, see `architecture.md` §Provider · dispatch order). External Providers MAY emit their own. |
|
|
76
76
|
| `provider` | TEXT | NOT NULL | Provider extension id. |
|
|
77
77
|
| `title` | TEXT | NULL | |
|
|
78
78
|
| `description` | TEXT | NULL | |
|
|
@@ -117,12 +117,12 @@ Indexes: `ix_scan_links_source_path`, `ix_scan_links_target_path`, `ix_scan_link
|
|
|
117
117
|
|
|
118
118
|
### `scan_issues`
|
|
119
119
|
|
|
120
|
-
One row per
|
|
120
|
+
One row per analyzer-emitted issue, matching [`schemas/issue.schema.json`](./schemas/issue.schema.json).
|
|
121
121
|
|
|
122
122
|
| Column | Type | Constraint | Notes |
|
|
123
123
|
|---|---|---|---|
|
|
124
124
|
| `id` | INTEGER | PRIMARY KEY AUTOINCREMENT | |
|
|
125
|
-
| `
|
|
125
|
+
| `analyzer_id` | TEXT | NOT NULL | |
|
|
126
126
|
| `severity` | TEXT | NOT NULL, CHECK in (`error`, `warn`, `info`) | |
|
|
127
127
|
| `node_ids_json` | TEXT | NOT NULL | JSON array. |
|
|
128
128
|
| `link_indices_json` | TEXT | NULL | JSON array of `scan_links.id`. |
|
|
@@ -131,7 +131,7 @@ One row per rule-emitted issue, matching [`schemas/issue.schema.json`](./schemas
|
|
|
131
131
|
| `fix_json` | TEXT | NULL | |
|
|
132
132
|
| `data_json` | TEXT | NULL | |
|
|
133
133
|
|
|
134
|
-
Indexes: `
|
|
134
|
+
Indexes: `ix_scan_issues_analyzer_id`, `ix_scan_issues_severity`.
|
|
135
135
|
|
|
136
136
|
### `scan_meta`
|
|
137
137
|
|
|
@@ -159,7 +159,7 @@ No indexes (single row).
|
|
|
159
159
|
|
|
160
160
|
Fine-grained cache breadcrumbs for the incremental scan path. One row per `(node_path, extractor_id)` recording the body hash the Extractor saw the last time it ran against that node. Replace-all on every `sm scan` so rows for Extractors that were uninstalled since the last scan disappear automatically.
|
|
161
161
|
|
|
162
|
-
The orchestrator consults this table on `sm scan --changed`: a node-level cache hit (body+frontmatter unchanged) is upgraded to a full skip ONLY when every currently-registered Extractor (filtered by `applicableKinds`) has a row matching the prior body hash. A new Extractor registered between scans is detected by the absence of its row and runs over the cached node WITHOUT requiring a full cache invalidation. Without this table the cache silently bypassed any Extractor newly registered between scans
|
|
162
|
+
The orchestrator consults this table on `sm scan --changed`: a node-level cache hit (body+frontmatter unchanged) is upgraded to a full skip ONLY when every currently-registered Extractor (filtered by `applicableKinds`) has a row matching the prior body hash. A new Extractor registered between scans is detected by the absence of its row and runs over the cached node WITHOUT requiring a full cache invalidation. Without this table the cache silently bypassed any Extractor newly registered between scans, leaving its emissions missing on the next `--changed` pass; the same machinery is what a future Action-issued probabilistic enrichment revision will leverage to reuse paid LLM output across unchanged bodies.
|
|
163
163
|
|
|
164
164
|
| Column | Type | Constraint |
|
|
165
165
|
|---|---|---|
|
|
@@ -170,45 +170,90 @@ The orchestrator consults this table on `sm scan --changed`: a node-level cache
|
|
|
170
170
|
|
|
171
171
|
Primary key: `(node_path, extractor_id)`. Indexes: `ix_scan_extractor_runs_node`, `ix_scan_extractor_runs_extractor`.
|
|
172
172
|
|
|
173
|
-
**Source-attribution interaction.** `scan_links.sources_json` carries the *short* extractor id the author wrote (e.g. `'slash'`); this table keys on the *qualified* form (`'
|
|
173
|
+
**Source-attribution interaction.** `scan_links.sources_json` carries the *short* extractor id the author wrote (e.g. `'slash'`); this table keys on the *qualified* form (`'core/slash'`). When a cached link is reshaped on reuse the orchestrator strips short ids whose owning Extractor is no longer registered (audit trail accuracy: a removed extractor must not stay attributed); links whose sole source is an uninstalled Extractor disappear; links whose sources include a missing-but-still-registered Extractor are dropped so the missing Extractor can re-emit fresh.
|
|
174
174
|
|
|
175
175
|
### `node_enrichments`
|
|
176
176
|
|
|
177
177
|
Universal enrichment layer (A.8). Stores `ctx.enrichNode(partial)` outputs separately from the author-supplied frontmatter on `scan_nodes.frontmatter_json`, which the Extractor pipeline NEVER mutates.
|
|
178
178
|
|
|
179
|
-
One row per `(node_path, extractor_id)` pair an Extractor enriched.
|
|
179
|
+
One row per `(node_path, extractor_id)` pair an Extractor enriched. Extractors are deterministic-only; rows are simply overwritten via PRIMARY KEY conflict on the next re-extract through the A.9 cache.
|
|
180
180
|
|
|
181
181
|
| Column | Type | Constraint |
|
|
182
182
|
|---|---|---|
|
|
183
183
|
| `node_path` | TEXT | NOT NULL | FK semantically to `scan_nodes.path`; replaced when a rename heuristic fires (mirrors the `state_*` FK migration). |
|
|
184
184
|
| `extractor_id` | TEXT | NOT NULL | Qualified id `<plugin_id>/<id>` per spec § A.6. |
|
|
185
|
-
| `body_hash_at_enrichment` | TEXT | NOT NULL | The `node.body_hash` the Extractor saw when it produced this enrichment.
|
|
185
|
+
| `body_hash_at_enrichment` | TEXT | NOT NULL | The `node.body_hash` the Extractor saw when it produced this enrichment. Always equal to the live body hash for Extractor writes; reserved for future Action-issued probabilistic enrichments where stale tracking is meaningful. |
|
|
186
186
|
| `value_json` | TEXT | NOT NULL | JSON-serialised `Partial<Node>` — the cumulative merge of every `enrichNode(...)` call the Extractor made for this node within its `extract()` invocation. |
|
|
187
|
-
| `stale` | INTEGER | NOT NULL DEFAULT 0, CHECK in (0, 1) | `
|
|
187
|
+
| `stale` | INTEGER | NOT NULL DEFAULT 0, CHECK in (0, 1) | Reserved. Always `0` in this revision (Extractors are deterministic; re-running is free). The flag and its index are kept for the future Action-prob enrichment revision where queued LLM jobs must preserve paid output across body changes. |
|
|
188
188
|
| `enriched_at` | INTEGER | NOT NULL | Unix milliseconds — when the Extractor produced this enrichment. Drives the read-time merge order (`ASC` → last-write-wins per field) inside `mergeNodeWithEnrichments`. |
|
|
189
|
-
| `is_probabilistic` | INTEGER | NOT NULL DEFAULT 0, CHECK in (0, 1) |
|
|
189
|
+
| `is_probabilistic` | INTEGER | NOT NULL DEFAULT 0, CHECK in (0, 1) | Reserved. Always `0` for Extractor writes (Extractors are deterministic-only). Reserved for the future Action-prob enrichment revision where the writer's mode is denormalised onto the row so the stale-flag query stays single-table. |
|
|
190
190
|
|
|
191
|
-
Primary key: `(node_path, extractor_id)`. Indexes: `ix_node_enrichments_node`, `ix_node_enrichments_stale`.
|
|
191
|
+
Primary key: `(node_path, extractor_id)`. Indexes: `ix_node_enrichments_node`, `ix_node_enrichments_stale`. The `_stale` index is dormant in this revision (every row has `stale = 0`); it is preserved so the future Action-prob revision can ship without a schema migration.
|
|
192
192
|
|
|
193
193
|
**Persistence flow** (per `sm scan`):
|
|
194
194
|
|
|
195
195
|
1. **Rename migration** — for every `RenameOp` from the rename heuristic, update `node_enrichments.node_path` from `op.from` to `op.to` so the audit trail tracks the file like `state_*` rows do.
|
|
196
196
|
2. **Drop-on-disappear** — delete every row whose `node_path` is no longer in the live node set.
|
|
197
|
-
3. **Upsert** — for every `(node_path, extractor_id)` pair the orchestrator emitted in this scan, upsert with `stale = 0` and the current `body_hash`. The PRIMARY KEY conflict refreshes `body_hash_at_enrichment` / `value_json` / `enriched_at`
|
|
198
|
-
4. **Stale flagging** —
|
|
197
|
+
3. **Upsert** — for every `(node_path, extractor_id)` pair the orchestrator emitted in this scan, upsert with `stale = 0`, `is_probabilistic = 0`, and the current `body_hash`. The PRIMARY KEY conflict refreshes `body_hash_at_enrichment` / `value_json` / `enriched_at` on every re-run.
|
|
198
|
+
4. **Stale flagging** — no-op in this revision (Extractors are deterministic-only; the sweep finds nothing to flag). The step is preserved in the persistence flow so the future Action-prob revision slots in without reshaping the contract.
|
|
199
199
|
|
|
200
|
-
**Read-side `node.merged` view.**
|
|
200
|
+
**Read-side `node.merged` view.** Analyzers / `sm check` / `sm export` consume `node.frontmatter` directly (deterministic CI-safe baseline). UI / future opt-in consumers call `mergeNodeWithEnrichments(node, enrichments)` which:
|
|
201
201
|
|
|
202
202
|
1. Filters `enrichments` to rows targeting this node AND not flagged stale.
|
|
203
203
|
2. Sorts by `enriched_at` ASC.
|
|
204
204
|
3. Spread-merges each `value` over the author frontmatter (last-write-wins per field).
|
|
205
205
|
|
|
206
|
-
Stale row visibility is opt-in via `mergeNodeWithEnrichments(node, enrichments, { includeStale: true })`
|
|
206
|
+
Stale row visibility is opt-in via `mergeNodeWithEnrichments(node, enrichments, { includeStale: true })` and is a no-op today (no rows are stale-flagged); the flag is preserved for the future Action-prob revision noted above.
|
|
207
207
|
|
|
208
208
|
**Refresh verbs** (see [`cli-contract.md` §Scan](./cli-contract.md#scan)):
|
|
209
209
|
|
|
210
|
-
- `sm refresh <node.path>` re-runs Extractors against a single node and upserts their enrichment rows.
|
|
211
|
-
- `sm refresh --stale` batches the granular form across every node carrying at least one stale row
|
|
210
|
+
- `sm refresh <node.path>` re-runs Extractors against a single node and upserts their enrichment rows. Extractors are deterministic-only — they always run for real and persist.
|
|
211
|
+
- `sm refresh --stale` batches the granular form across every node carrying at least one stale row; in this revision the stale set is always empty so the verb prints a "nothing to do" advisory and exits `0`.
|
|
212
|
+
|
|
213
|
+
### `scan_contributions`
|
|
214
|
+
|
|
215
|
+
Phase 3 / View contribution system. Per-node typed payloads emitted by extractors via `ctx.emitContribution(id, payload)` (and analyzers via `ctx.emitScopeContribution(id, payload)` for scope-level slots). One row per `(plugin_id, extension_id, node_path, contribution_id)` tuple.
|
|
216
|
+
|
|
217
|
+
| Column | Type | Constraint |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| `plugin_id` | TEXT | NOT NULL | Owning plugin namespace per spec § A.6. |
|
|
220
|
+
| `extension_id` | TEXT | NOT NULL | Extension id within the plugin. |
|
|
221
|
+
| `node_path` | TEXT | NOT NULL | FK semantically to `scan_nodes.path`; orphan-swept on persist when the parent node disappears. |
|
|
222
|
+
| `contribution_id` | TEXT | NOT NULL | Manifest Record key under `extension.viewContributions[<contributionId>]`. |
|
|
223
|
+
| `slot` | TEXT | NOT NULL | Closed-enum-by-spec slot name; mirror of `view-slots.schema.json#/$defs/SlotName`. Kept open at the SQL layer (no CHECK) so catalog evolution does not need a DDL migration; `sm plugins upgrade` handles renames at the manifest layer. |
|
|
224
|
+
| `payload_json` | TEXT | NOT NULL | JSON-serialised payload, already validated against the slot's payload schema (`view-slots.schema.json#/$defs/payloads/<slot>`) at emit time. Off-shape payloads emit `extension.error` and drop silently. |
|
|
225
|
+
| `emitted_at` | INTEGER | NOT NULL | Unix milliseconds. |
|
|
226
|
+
|
|
227
|
+
Primary key: `(plugin_id, extension_id, node_path, contribution_id)`. Indexes: `ix_scan_contributions_node_path` (inspector lazy-fetch + orphan sweep), `ix_scan_contributions_plugin_id` (catalog sweep + `purgeByPlugin`).
|
|
228
|
+
|
|
229
|
+
**Persistence — orphan + catalog + per-tuple sweep + upsert (NOT pure replace-all).** The watcher's cached pass leaves the contributions buffer empty for cached nodes — the orchestrator skips `extract()` when the per-(node, extractor) cache hits, so no `emitContribution` fires. A naive wipe-all would silently drop the prior valid rows on every watcher boot. The persist runs four passes inside the same tx as the rest of the scan zone:
|
|
230
|
+
|
|
231
|
+
1. **Orphan sweep** — drops every row whose `node_path` is NOT in the current live node set (`livePaths` derived from `result.nodes`). Disappeared nodes lose their contributions automatically.
|
|
232
|
+
2. **Catalog sweep** — drops every row whose qualified id `(pluginId, extensionId, contributionId)` is NOT in the registered runtime catalog (`registeredContributionKeys` collected via `collectRegisteredContributionKeys(composed)`). Uninstalled plugins, disabled bundles, and removed contributions lose their rows on the next scan.
|
|
233
|
+
3. **Per-tuple sweep** — for every `(pluginId, extensionId, node_path)` tuple in `freshlyRunTuples` (extension actually ran against that node this scan: extractor cache miss, OR analyzer), drop any row carrying that triple whose `contribution_id` is NOT refreshed by the buffer. Catches the "extractor used to emit, now does not" case without touching cached-extractor rows. Tuple format: `<pluginId>/<extensionId>/<nodePath>`.
|
|
234
|
+
4. **Upsert** — `INSERT ... ON CONFLICT DO UPDATE SET payload_json = excluded.payload_json, slot = excluded.slot` for every row in the buffer. PK conflict refreshes `payload_json` + `slot` + `emitted_at`.
|
|
235
|
+
|
|
236
|
+
Cached nodes' rows survive untouched — they're neither orphaned (still in the live set) nor uninstalled (still in the catalog) nor in `freshlyRunTuples` (extractor short-circuited via the per-(node, extractor) cache) nor in the buffer (no re-emit). The next time the body changes, the orchestrator re-runs the extractor, the tuple lands in the freshly-run set, and either the upsert refreshes the row or the per-tuple sweep drops it.
|
|
237
|
+
|
|
238
|
+
**Backwards-compat fallbacks.** `IPersistOptions.livePaths`, `IPersistOptions.registeredContributionKeys`, `IPersistOptions.freshlyRunTuples` are all optional. Absent / empty `livePaths` falls back to wipe-all (legacy behaviour). Absent / empty `registeredContributionKeys` skips the catalog sweep (rows for disabled plugins linger until next purge). Absent / empty `freshlyRunTuples` skips the per-tuple sweep (rows that should have been dropped because an extractor stopped emitting linger until the node body, the extractor registration, or the node existence changes again — older callers preserve the pre-fix behaviour).
|
|
239
|
+
|
|
240
|
+
NOT analogous to `state_plugin_kvs` (which is plugin-managed). Belongs to the `scan_*` family — sweep semantics replace pure replace-all but the data is still scan-derived.
|
|
241
|
+
|
|
242
|
+
### `scan_node_tags`
|
|
243
|
+
|
|
244
|
+
Tags · dual-source. One row per `(node_path, tag, source)` triple, projected at persist time from BOTH `frontmatter.tags` (with `source='author'`) and `sidecar.annotations.tags` (with `source='user'`). Drives `sm list --tag <name>` and the UI's tag-faceted search; the `(tag)` index keeps "find all nodes with tag X" `O(log n)`.
|
|
245
|
+
|
|
246
|
+
| Column | Type | Constraint |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| `node_path` | TEXT | NOT NULL | FK semantically to `scan_nodes.path`; orphan-swept on persist when the parent node disappears. |
|
|
249
|
+
| `tag` | TEXT | NOT NULL | Free-form; case-preserving. Empty strings rejected upstream by the schema's `minLength: 1` on each item. |
|
|
250
|
+
| `source` | TEXT | NOT NULL CHECK (source IN ('author','user')) | Hard split: `'author'` = `frontmatter.tags`, `'user'` = `sidecar.annotations.tags`. The same tag string MAY appear under both sources for the same node (the PK accepts the pair); `sm list --tag X` returns the node once via DISTINCT, the UI renders both chips with their attribution. |
|
|
251
|
+
|
|
252
|
+
Primary key: `(node_path, tag, source)`. Indexes: `ix_scan_node_tags_tag` (search by tag), `ix_scan_node_tags_node_path` (per-node lookup, e.g. inspector projection).
|
|
253
|
+
|
|
254
|
+
**Persistence — replace-all per scan.** Every persisted scan rebuilds the table for the live node set: rows whose `node_path` is NOT in `livePaths` are dropped (orphan sweep, same as the contributions table); rows for nodes in the live set are wiped and re-inserted from the projected source state. Cached nodes' tag rows are projected from the cached `node.frontmatter.tags` / `node.sidecar.annotations.tags` (both already in memory), so the rebuild is cheap regardless of cache hit / miss. Storage is small — a 50-node project with avg 3 tags/node is ~150 rows ≈ 7.5 KB.
|
|
255
|
+
|
|
256
|
+
The wire shape on `/api/nodes` joins this table to project `node.tags = { byAuthor: string[], byUser: string[] }`. The kernel `Node` interface (TypeScript) does NOT carry `tags` — consumers walking the canonical sources read `node.frontmatter.tags` and `node.sidecar.annotations.tags` directly (consistent with the post-decision-#2 posture).
|
|
212
257
|
|
|
213
258
|
---
|
|
214
259
|
|
|
@@ -477,12 +522,12 @@ Implementations MUST apply a rename heuristic at scan time **before** committing
|
|
|
477
522
|
- Emit no issue. Log at `info` level.
|
|
478
523
|
3. Remaining pairs where `newPath.frontmatterHash == deletedPath.frontmatterHash` (body differs, frontmatter is a perfect match) → classify as **medium-confidence rename**. The kernel MUST:
|
|
479
524
|
- Apply the same FK migration.
|
|
480
|
-
- Emit an issue with `
|
|
481
|
-
4. Any `deletedPath` left without a match after steps 2–3 becomes an **orphan**: the kernel emits an issue with `
|
|
525
|
+
- Emit an issue with `analyzerId: auto-rename-medium` (severity `warn`) pointing to both paths. The issue's `data` MUST include `{ from: <old.path>, to: <new.path>, confidence: "medium" }` so `sm orphans undo-rename <new.path>` can read the prior path without user input.
|
|
526
|
+
4. Any `deletedPath` left without a match after steps 2–3 becomes an **orphan**: the kernel emits an issue with `analyzerId: orphan` (severity `info`) and keeps the `state_*` rows referencing the dead path untouched until the user runs `sm orphans reconcile <dead.path> --to <new.path>` or accepts the orphan.
|
|
482
527
|
|
|
483
528
|
Matching is 1-to-1: once a `newPath` is claimed as the rename target of some `deletedPath`, no other deletion can match it in the same scan. Ambiguity (two deletions share a body hash with the same new path) → fall back to the orphan path for all candidates, with issue `auto-rename-ambiguous` listing every conflict. `auto-rename-ambiguous` issues MUST populate `data` with `{ to: <new.path>, candidates: [<old.path.a>, <old.path.b>, ...] }`; in this case `sm orphans undo-rename` requires the user to pass `--from <old.path>` to disambiguate.
|
|
484
529
|
|
|
485
|
-
Note on casing: `bodyHash` / `frontmatterHash` / `
|
|
530
|
+
Note on casing: `bodyHash` / `frontmatterHash` / `analyzerId` / `data` are the domain-object field names (per `node.schema.json` and `issue.schema.json`). The SQLite reference impl stores the same values in `body_hash` / `frontmatter_hash` / `analyzer_id` / `data_json` columns; the storage adapter bridges the two (see §Naming conventions above). The heuristic is specified against the domain types, not the columns.
|
|
486
531
|
|
|
487
532
|
The heuristic runs inside the scan transaction, so either all renames land or none do. `sm scan` is the only surface that triggers automatic rename detection. Two manual verbs exist for cases the heuristic missed or got wrong:
|
|
488
533
|
|
|
@@ -510,7 +555,7 @@ Failures are reported with suggested remediation (e.g., "run `sm db migrate`", "
|
|
|
510
555
|
|
|
511
556
|
## See also
|
|
512
557
|
|
|
513
|
-
- [`architecture.md`](./architecture.md) — `StoragePort` interface definition and dependency
|
|
558
|
+
- [`architecture.md`](./architecture.md) — `StoragePort` interface definition and dependency analyzers.
|
|
514
559
|
- [`plugin-kv-api.md`](./plugin-kv-api.md) — `ctx.store` accessor for mode A / mode B persistence.
|
|
515
560
|
- [`job-lifecycle.md`](./job-lifecycle.md) — atomic claim and TTL/reap semantics that drive `state_jobs`.
|
|
516
561
|
- [`cli-contract.md`](./cli-contract.md) — `sm db` verb surface (reset, backup, restore, migrate).
|
package/index.json
CHANGED
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"path": "schemas/extensions/extractor.schema.json"
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
|
-
"id": "extensions/
|
|
75
|
-
"path": "schemas/extensions/
|
|
74
|
+
"id": "extensions/analyzer",
|
|
75
|
+
"path": "schemas/extensions/analyzer.schema.json"
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
"id": "extensions/action",
|
|
@@ -174,64 +174,69 @@
|
|
|
174
174
|
}
|
|
175
175
|
]
|
|
176
176
|
},
|
|
177
|
-
"specPackageVersion": "0.
|
|
177
|
+
"specPackageVersion": "0.20.0",
|
|
178
178
|
"integrity": {
|
|
179
179
|
"algorithm": "sha256",
|
|
180
180
|
"files": {
|
|
181
|
-
"CHANGELOG.md": "
|
|
182
|
-
"README.md": "
|
|
183
|
-
"architecture.md": "
|
|
184
|
-
"cli-contract.md": "
|
|
185
|
-
"conformance/README.md": "
|
|
186
|
-
"conformance/cases/kernel-empty-boot.json": "
|
|
187
|
-
"conformance/cases/
|
|
188
|
-
"conformance/cases/
|
|
189
|
-
"conformance/
|
|
181
|
+
"CHANGELOG.md": "c1207870c14e59ad7a34dc709e19cb4dbfe4f9fa797b85614ba2466ef8a6dd95",
|
|
182
|
+
"README.md": "b551522ab0c7f5ef702e9ea4d4f67fd7ad838b080d85975c2834d8d40af14a00",
|
|
183
|
+
"architecture.md": "181f54e12cff7b2a86e6a741520391ed828799b5b59725028eb4947b819066a7",
|
|
184
|
+
"cli-contract.md": "af43179f3b363801fde1ddbbaede2185eaec6a7f42b89a748e98e505799663e2",
|
|
185
|
+
"conformance/README.md": "70e3101104765ef359d5322d0a7c9248d2157f78a510fb2cc8005b4eba3173d6",
|
|
186
|
+
"conformance/cases/kernel-empty-boot.json": "2a5be9c93143d07a16d998df09dcc8fa4ea2d2f9a0bff6417573ed5a770352c1",
|
|
187
|
+
"conformance/cases/orphan-markdown-fallback.json": "8ef6e49b7e6532bd845d9f54974a16e537cf98d355f0c5e4f4fb06abac3adcc5",
|
|
188
|
+
"conformance/cases/plugin-missing-ui-rejected.json": "bdebee810436e6be88edf2fe38ddc6939fd3f53e6a12dc1d66da051c4922f1e9",
|
|
189
|
+
"conformance/cases/sidecar-end-to-end.json": "24a73e7c857709d001cf7013b8fe5ccad4027e064b39533dda33697d80b56e7a",
|
|
190
|
+
"conformance/coverage.md": "45208fd74c5b548962025307d489deb91eaeedc57c0b10ff7c941631851b6f07",
|
|
191
|
+
"conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md": "7f062731106f2d9811e4fffcf6ab44b8dfff4cfb16536a469514cc0664e832bf",
|
|
192
|
+
"conformance/fixtures/orphan-markdown/ARCHITECTURE.md": "d6b6e18d4b963b26a292de73348c3396fd4710ab4c4bdd6cf094e581f99ec8d6",
|
|
190
193
|
"conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json": "4d78af6f12faa9d131e2a19f1dbb8f250baacc525978f3a8c858932b95da4ff6",
|
|
191
194
|
"conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/provider.js": "d1f4898b43201d24f048171ce84d433b68694457452fbc64498857f5da3e9bbb",
|
|
192
195
|
"conformance/fixtures/plugin-missing-ui/notes/example.md": "55767f0aa1b6774546a99f28c58e7b732aa9cfa5dfce8d0326470f7f622f577e",
|
|
193
196
|
"conformance/fixtures/preamble-v1.txt": "1e0aeef224b64477bdc13a949c3ad402e68249caf499ecdba1302371677c068b",
|
|
194
|
-
"conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm": "
|
|
197
|
+
"conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm": "3102ff10a0f08f60c014f82409d45ad4faf2cefa04d652a87676d3557ad64944",
|
|
195
198
|
"conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.md": "cb3a95777cba530d47e6040c5601b6dcd34b5fc653dd69f183369eb6bdd956b5",
|
|
196
|
-
"conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.sm": "
|
|
197
|
-
"conformance/fixtures/sidecar-example/agent-example.md": "
|
|
198
|
-
"conformance/fixtures/sidecar-example/agent-example.sm": "
|
|
199
|
-
"db-schema.md": "
|
|
200
|
-
"interfaces/security-scanner.md": "
|
|
201
|
-
"job-events.md": "
|
|
202
|
-
"job-lifecycle.md": "
|
|
203
|
-
"plugin-author-guide.md": "
|
|
204
|
-
"plugin-kv-api.md": "
|
|
205
|
-
"prompt-preamble.md": "
|
|
206
|
-
"schemas/annotations.schema.json": "
|
|
207
|
-
"schemas/api/rest-envelope.schema.json": "
|
|
199
|
+
"conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.sm": "cb04f7f3103b4218b09fd4da92f7ea429588b04c1dac6a9547ce362263b11224",
|
|
200
|
+
"conformance/fixtures/sidecar-example/agent-example.md": "741131403e8c9580d0b7a8c2446cb4502d01f80053b7a2092663de92431aaa82",
|
|
201
|
+
"conformance/fixtures/sidecar-example/agent-example.sm": "41200387e74a120c554a34dfabc50dd2151067a1c6599695c59412d8eab38bb4",
|
|
202
|
+
"db-schema.md": "3e3ffdb245e68675f5a366c2ba8ad038e0233ce34105e9276e92c02cc9cdf69f",
|
|
203
|
+
"interfaces/security-scanner.md": "aefe9f02f190615ba18649df03c1bdd79d98691039563c659e90f34362e5f1d5",
|
|
204
|
+
"job-events.md": "b223bf0e576cbd481688e163ab3ce0a6e952a8a4a3912f1342237b664984e388",
|
|
205
|
+
"job-lifecycle.md": "1d9c42632f8e77ef58ff47ae6d9680e7ed5939760627c75253aab8c80f728fd1",
|
|
206
|
+
"plugin-author-guide.md": "7286ab5be91cdf2ca9f1132c64c44d9a353ee7a4bc3473f8eeeda0c57edd2a6a",
|
|
207
|
+
"plugin-kv-api.md": "3e932e74ad27ce4e7e6218cbbddd2437c810d12f90b1590ef2313019d9b7d82f",
|
|
208
|
+
"prompt-preamble.md": "4860c310ccf2823870d318993ad8f067571799dade90bddb6634c3dbedd636b7",
|
|
209
|
+
"schemas/annotations.schema.json": "b3a9aa66de17058ccfd890ea9ff1b9ee315a0877e9dd4a58fd8b76e26a99d00e",
|
|
210
|
+
"schemas/api/rest-envelope.schema.json": "0f33b58e885cd0d74682a534d24765edee88fc35a63c03e987f73bdad451c892",
|
|
208
211
|
"schemas/bump-report.schema.json": "c2d853715d5f50098567bc23382a4e81baf78d589c6e1baf67d3b841e7f7d8ae",
|
|
209
|
-
"schemas/conformance-case.schema.json": "
|
|
212
|
+
"schemas/conformance-case.schema.json": "f6d4c9fb92e79cb516eeeb9d042223572a3bd5ff8e7871a0becce13916f20cf6",
|
|
210
213
|
"schemas/execution-record.schema.json": "9628fa557cb856402f3a5f1d1167c609e46a197c850fe8171abfddd46c1028a8",
|
|
211
214
|
"schemas/extensions/action.schema.json": "262272175c06a2e33c08f819a45c3ef8260276c91a9d0542fdffc932aeb32db7",
|
|
212
|
-
"schemas/extensions/
|
|
213
|
-
"schemas/extensions/
|
|
215
|
+
"schemas/extensions/analyzer.schema.json": "6272e5959f3c94e109e6116f7ed6b5ae35e4e3aea821a3c30742d11c5ab5838f",
|
|
216
|
+
"schemas/extensions/base.schema.json": "528083fb7db8bd064147224999e1bb3959ee2061863f55f48e928c27222cf957",
|
|
217
|
+
"schemas/extensions/extractor.schema.json": "a859a53a7a5b009b1fe20d322bc1a8ff62e4b91ef938e98b1c80c802bd734b37",
|
|
214
218
|
"schemas/extensions/formatter.schema.json": "2ab092aa37ae349c69b93071ed4f0e131affb7bb5799516ca82c721262631b36",
|
|
215
|
-
"schemas/extensions/hook.schema.json": "
|
|
216
|
-
"schemas/extensions/provider.schema.json": "
|
|
217
|
-
"schemas/
|
|
218
|
-
"schemas/frontmatter/base.schema.json": "88d906ec15e94543b919dd22b4ea4053e40024dc14068e6c7df09662db2f3350",
|
|
219
|
+
"schemas/extensions/hook.schema.json": "a55cec50f6fda5b924de86359b910d22548d0a5bb61b2051edb82a80d3b36a2b",
|
|
220
|
+
"schemas/extensions/provider.schema.json": "077c0c079e3965cee667019f76ee1e180d6b1f4162767d868bccc912e8dfbf89",
|
|
221
|
+
"schemas/frontmatter/base.schema.json": "ec4abde950c31639974fc078e6bdc74ed48da4d2c0a996f5248684406910a178",
|
|
219
222
|
"schemas/history-stats.schema.json": "23f472d1de06d23fc775aabba821f8375f347af4dc8d89ba567980d61a11f9de",
|
|
220
|
-
"schemas/
|
|
223
|
+
"schemas/input-types.schema.json": "f1f51ccda746ea3c8a404757f60c89e403619e88ec4137a50af100ec89f8f4b5",
|
|
224
|
+
"schemas/issue.schema.json": "0bbc1783ad07cb5c3c2399d7a560f57314a9ff76ed061b4a198ddf7ce74dad78",
|
|
221
225
|
"schemas/job.schema.json": "ffbdd51c54b487c44eb57fabd07f624ac1030c14ef69b46933c154092853a84c",
|
|
222
|
-
"schemas/link.schema.json": "
|
|
223
|
-
"schemas/node.schema.json": "
|
|
224
|
-
"schemas/plugins-registry.schema.json": "
|
|
225
|
-
"schemas/project-config.schema.json": "
|
|
226
|
+
"schemas/link.schema.json": "7fc429d03aca7e4c0b9a28241712c1aa2a5275870cea5ed938c2f97e8cccb081",
|
|
227
|
+
"schemas/node.schema.json": "2ede4385e796cbf416c494d810dcb6d6036b35e71561efee46f5675bf0a015fe",
|
|
228
|
+
"schemas/plugins-registry.schema.json": "678f476cf460d0b5876a92e72e0d572b6db265dd9fad6e95db553c56f77db5d9",
|
|
229
|
+
"schemas/project-config.schema.json": "f6479bc73aa58821128965a5cea957cdd979cbaa4b942d76a251218cacfdeafa",
|
|
226
230
|
"schemas/report-base-deterministic.schema.json": "6f8b38c097994ee87e0639935c42b5e85d8ea4244959ca397978171b0d7d2222",
|
|
227
231
|
"schemas/report-base.schema.json": "a1021e9a59b4df9f99cd92454d797e88469766e7d49f52d231c4645ffdfdad8f",
|
|
228
232
|
"schemas/scan-result.schema.json": "d1a8782e198bc9bb92dad247437aefa1b02f92ff8dca8562eaf2348fd7c5cf0c",
|
|
229
|
-
"schemas/sidecar.schema.json": "
|
|
230
|
-
"schemas/summaries/agent.schema.json": "
|
|
231
|
-
"schemas/summaries/command.schema.json": "
|
|
232
|
-
"schemas/summaries/hook.schema.json": "
|
|
233
|
-
"schemas/summaries/markdown.schema.json": "
|
|
233
|
+
"schemas/sidecar.schema.json": "119f71e943a3d9305e25e26e1d8b3b525a391af0a7547d8f8a79de96b016e07c",
|
|
234
|
+
"schemas/summaries/agent.schema.json": "bf540f9a804f2b43756ab33b7deb0462620d26e88cc9379c75a5f87d3b1b47d8",
|
|
235
|
+
"schemas/summaries/command.schema.json": "c26f6965f77c5058608feb5e7b9f807395de8e015b0dea5efcdb44cb1820551a",
|
|
236
|
+
"schemas/summaries/hook.schema.json": "58420ec485e152fdd21fa3d87337ad74b0d81a48d3b83dd072d4a2d196f78573",
|
|
237
|
+
"schemas/summaries/markdown.schema.json": "33e2a1a11ec08a860c0c220609235c6fbdfda9ce19b6d65238f467f132ed4e54",
|
|
234
238
|
"schemas/summaries/skill.schema.json": "f01bab92c51d64ee23e61587e42cf0dc5b37a2f518f5b12b3d1d456390338aa8",
|
|
239
|
+
"schemas/view-slots.schema.json": "44e329a2d0fff8f4ed6b3c92209661b5dae39927742fb6121ce71584941d27d6",
|
|
235
240
|
"versioning.md": "996e62006423edc01151a6f7869605f76c5e1454cc30b38d9f616925b5bcfb64"
|
|
236
241
|
}
|
|
237
242
|
}
|
|
@@ -147,7 +147,7 @@ A `category` value SHOULD be one of these for interoperability:
|
|
|
147
147
|
- `injection-risk` — pattern likely to enable prompt injection, SQL injection, command injection.
|
|
148
148
|
- `license-violation` — incompatible license terms for a dependency or referenced asset.
|
|
149
149
|
- `outdated` — version pinned well below current, not exploited but due for upgrade.
|
|
150
|
-
- `policy-violation` — organization-level
|
|
150
|
+
- `policy-violation` — organization-level analyzer (naming, banned words, required disclaimer).
|
|
151
151
|
|
|
152
152
|
Vendors MAY introduce their own category with the prefix `vendor:<slug>` (e.g. `vendor:socket:supply-chain`). Consumers that don't understand a vendor category MUST treat it as opaque but still display it.
|
|
153
153
|
|
|
@@ -158,7 +158,7 @@ Vendors MAY introduce their own category with the prefix `vendor:<slug>` (e.g. `
|
|
|
158
158
|
- Scanners are invoked through the standard job system: `sm job submit security-snyk -n <node.path>` or `sm job submit security-snyk --all`.
|
|
159
159
|
- The report is persisted through the normal action report mechanism ([`state_executions`](../db-schema.md)`.report_path` points to the JSON file).
|
|
160
160
|
- `sm findings --security` aggregates findings from reports whose action id starts with `security-`, merging across scanners, deduplicating by `finding.id`.
|
|
161
|
-
- Implementations MAY also surface findings at scan time via a companion
|
|
161
|
+
- Implementations MAY also surface findings at scan time via a companion Analyzer (e.g. `security-findings-stale` flags nodes whose last security scan is older than a threshold). This is recommended but not normative.
|
|
162
162
|
|
|
163
163
|
---
|
|
164
164
|
|
package/job-events.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Canonical event stream emitted during job execution. Every implementation MUST emit these events in the order described, with the shapes defined below. Consumers include the CLI pretty printer, the `--json` ndjson output, the Server's WebSocket broadcaster, and any third-party integration.
|
|
4
4
|
|
|
5
|
-
This document is **normative**. The set of event types, their payload shapes, and their ordering
|
|
5
|
+
This document is **normative**. The set of event types, their payload shapes, and their ordering analyzers are stable contracts.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -291,7 +291,7 @@ Emitted once at the end of `sm job run`, after the last job event.
|
|
|
291
291
|
|
|
292
292
|
---
|
|
293
293
|
|
|
294
|
-
## Ordering
|
|
294
|
+
## Ordering analyzers
|
|
295
295
|
|
|
296
296
|
For each job, the normative order is:
|
|
297
297
|
|
|
@@ -401,25 +401,25 @@ Emitted once per registered Extractor, after the full walk completes. Aggregated
|
|
|
401
401
|
|
|
402
402
|
> **Hookable** — see [`architecture.md` §Hook · curated trigger set](./architecture.md#hook--curated-trigger-set). Per-Extractor metrics, audit. Filter by `data.extractorId` to scope to a single Extractor.
|
|
403
403
|
|
|
404
|
-
#### `
|
|
404
|
+
#### `analyzer.completed`
|
|
405
405
|
|
|
406
|
-
Emitted once per registered
|
|
406
|
+
Emitted once per registered Analyzer, after every issue has been validated.
|
|
407
407
|
|
|
408
408
|
```json
|
|
409
409
|
{
|
|
410
|
-
"type": "
|
|
410
|
+
"type": "analyzer.completed",
|
|
411
411
|
"timestamp": 1745159455950,
|
|
412
412
|
"runId": "...",
|
|
413
413
|
"jobId": null,
|
|
414
414
|
"data": {
|
|
415
|
-
"
|
|
415
|
+
"analyzerId": "core/superseded"
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
```
|
|
419
419
|
|
|
420
|
-
`
|
|
420
|
+
`analyzerId` is the qualified extension id.
|
|
421
421
|
|
|
422
|
-
> **Hookable** — see [`architecture.md` §Hook · curated trigger set](./architecture.md#hook--curated-trigger-set). Per-
|
|
422
|
+
> **Hookable** — see [`architecture.md` §Hook · curated trigger set](./architecture.md#hook--curated-trigger-set). Per-Analyzer alerting, downstream tooling. Filter by `data.analyzerId`.
|
|
423
423
|
|
|
424
424
|
#### `action.completed`
|
|
425
425
|
|
|
@@ -456,7 +456,7 @@ Emitted by the scan after `scan.completed` when the new scan's issue set differs
|
|
|
456
456
|
"runId": "...",
|
|
457
457
|
"jobId": null,
|
|
458
458
|
"data": {
|
|
459
|
-
"
|
|
459
|
+
"analyzerId": "trigger-collision",
|
|
460
460
|
"severity": "warn",
|
|
461
461
|
"nodeIds": ["skills/a.md", "skills/b.md"],
|
|
462
462
|
"message": "..."
|
|
@@ -475,13 +475,13 @@ Emitted when an issue present in the previous scan is absent from the new one.
|
|
|
475
475
|
"runId": "...",
|
|
476
476
|
"jobId": null,
|
|
477
477
|
"data": {
|
|
478
|
-
"
|
|
478
|
+
"analyzerId": "broken-ref",
|
|
479
479
|
"nodeIds": ["skills/c.md"]
|
|
480
480
|
}
|
|
481
481
|
}
|
|
482
482
|
```
|
|
483
483
|
|
|
484
|
-
Issue diffing is keyed on `(
|
|
484
|
+
Issue diffing is keyed on `(analyzerId, nodeIds sorted, message)` — same key → same issue. A payload change on the same key emits no event; consumers re-read full issue detail from `sm check` when needed.
|
|
485
485
|
|
|
486
486
|
---
|
|
487
487
|
|
|
@@ -523,6 +523,6 @@ Consumers MUST ignore unknown fields (forward compatibility).
|
|
|
523
523
|
|
|
524
524
|
The envelope (`type`, `timestamp`, `runId`, `jobId`, `data`) is stable. Adding an envelope field is a major bump because every consumer would need to handle it.
|
|
525
525
|
|
|
526
|
-
The **non-job event families** (`scan.*`, `issue.*`, `extractor.completed`, `
|
|
526
|
+
The **non-job event families** (`scan.*`, `issue.*`, `extractor.completed`, `analyzer.completed`, `action.completed`) are marked **experimental** across spec v0.x. They ship alongside the WebSocket broadcaster at Step 13 of the reference impl; shapes may tighten before a stable tag lands. Once promoted to `stable` (a minor spec bump), the same add/remove/rename semantics as the job events apply.
|
|
527
527
|
|
|
528
528
|
The **Hook curated trigger set** (eight hookable lifecycle events; see [`architecture.md` §Hook · curated trigger set](./architecture.md#hook--curated-trigger-set)) is itself stable as of the same minor in which it lands: adding a hookable trigger is a minor bump, removing or renaming one is a major bump. The curation policy ("a hook subscribes only to a deliberately small set") is normative — surface noise reduction is the entire point.
|
package/job-lifecycle.md
CHANGED
|
@@ -242,7 +242,7 @@ Config controls (`jobs.retention.completed`, `jobs.retention.failed`):
|
|
|
242
242
|
|
|
243
243
|
## See also
|
|
244
244
|
|
|
245
|
-
- [`architecture.md`](./architecture.md) — `RunnerPort` definition; driving-adapter peer
|
|
245
|
+
- [`architecture.md`](./architecture.md) — `RunnerPort` definition; driving-adapter peer analyzer for Skill agents.
|
|
246
246
|
- [`job-events.md`](./job-events.md) — canonical event stream emitted during job execution.
|
|
247
247
|
- [`prompt-preamble.md`](./prompt-preamble.md) — verbatim preamble prepended to every rendered job content row.
|
|
248
248
|
- [`db-schema.md`](./db-schema.md) — `state_jobs` and `state_executions` table catalogs.
|