@skill-map/spec 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +607 -6
- package/README.md +6 -6
- package/architecture.md +129 -57
- package/cli-contract.md +71 -25
- package/conformance/README.md +2 -2
- package/conformance/cases/kernel-empty-boot.json +2 -2
- package/conformance/cases/sidecar-end-to-end.json +3 -3
- package/conformance/coverage.md +5 -5
- package/conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm +1 -1
- package/conformance/fixtures/sidecar-example/agent-example.md +1 -1
- package/db-schema.md +22 -18
- package/index.json +36 -36
- 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 +131 -82
- package/plugin-kv-api.md +6 -6
- package/prompt-preamble.md +1 -1
- package/schemas/annotations.schema.json +4 -4
- package/schemas/api/rest-envelope.schema.json +4 -4
- package/schemas/conformance-case.schema.json +2 -2
- package/schemas/extensions/analyzer.schema.json +43 -0
- package/schemas/extensions/base.schema.json +5 -5
- package/schemas/extensions/extractor.schema.json +1 -1
- package/schemas/extensions/hook.schema.json +6 -4
- package/schemas/issue.schema.json +6 -6
- package/schemas/link.schema.json +2 -2
- package/schemas/plugins-registry.schema.json +1 -1
- package/schemas/project-config.schema.json +19 -1
- package/schemas/sidecar.schema.json +2 -2
- 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/{view-contracts.schema.json → view-slots.schema.json} +93 -55
- package/schemas/extensions/rule.schema.json +0 -43
package/plugin-author-guide.md
CHANGED
|
@@ -69,8 +69,8 @@ After every change to the `plugins/` folder, run `sm plugins list` to see the lo
|
|
|
69
69
|
|
|
70
70
|
The `id` declared in `plugin.json` is **globally unique** across every active discovery root. The kernel enforces this in two places:
|
|
71
71
|
|
|
72
|
-
1. **Directory name MUST equal manifest id.** A plugin lives at `<root>/<id>/plugin.json`. If `basename(<plugin-dir>) !== manifest.id`, discovery surfaces the plugin with status `invalid-manifest` and a reason naming both names. This
|
|
73
|
-
2. **Cross-root id collisions are blocked, both sides.** If two plugins from different roots (project + global, or any combination of `--plugin-dir`) declare the same `id`, **both** receive status `id-collision`. There is no precedence
|
|
72
|
+
1. **Directory name MUST equal manifest id.** A plugin lives at `<root>/<id>/plugin.json`. If `basename(<plugin-dir>) !== manifest.id`, discovery surfaces the plugin with status `invalid-manifest` and a reason naming both names. This analyzer eliminates same-root collisions by construction (a filesystem cannot host two siblings with the same name).
|
|
73
|
+
2. **Cross-root id collisions are blocked, both sides.** If two plugins from different roots (project + global, or any combination of `--plugin-dir`) declare the same `id`, **both** receive status `id-collision`. There is no precedence analyzer — neither plugin loads its extensions; the user resolves the conflict by renaming one and rerunning. Coherent with the spec analyzer that no extension is privileged.
|
|
74
74
|
|
|
75
75
|
`sm plugins list` shows the conflict; `sm plugins doctor` exits `1` whenever any `id-collision` is present.
|
|
76
76
|
|
|
@@ -88,14 +88,14 @@ Concrete examples for the reference impl's bundled extensions:
|
|
|
88
88
|
| At-directive extractor | `at-directive` | `core/at-directive` |
|
|
89
89
|
| Markdown-link extractor | `markdown-link` | `core/markdown-link` |
|
|
90
90
|
| External-URL counter | `external-url-counter` | `core/external-url-counter` |
|
|
91
|
-
| Broken-ref
|
|
92
|
-
| Trigger-collision
|
|
91
|
+
| Broken-ref analyzer | `broken-ref` | `core/broken-ref` |
|
|
92
|
+
| Trigger-collision analyzer | `trigger-collision` | `core/trigger-collision` |
|
|
93
93
|
| ASCII formatter | `ascii` | `core/ascii` |
|
|
94
|
-
| Validate-all
|
|
94
|
+
| Validate-all analyzer | `validate-all` | `core/validate-all` |
|
|
95
95
|
|
|
96
96
|
Built-ins split between two namespaces:
|
|
97
97
|
|
|
98
|
-
- **`core/`** — kernel-internal primitives, platform-agnostic. Owns every built-in
|
|
98
|
+
- **`core/`** — kernel-internal primitives, platform-agnostic. Owns every built-in analyzer (including `validate-all`), the ASCII formatter, and the cross-vendor extractors (`annotations`, `slash`, `at-directive`, `markdown-link`, `external-url-counter`) any Provider can rely on.
|
|
99
99
|
- **`claude/`** — the Claude Code Provider bundle: the Provider that classifies `.claude/{agents,commands,skills}` paths and parses their frontmatter. Vendor-specific bundles (`gemini`, `agent-skills`) follow the same shape — Provider only — since the syntax their nodes use is shared with Claude and lives in `core`.
|
|
100
100
|
|
|
101
101
|
For your own plugin, the `id` you declare in `plugin.json` is the namespace for every extension the plugin contains. If your manifest declares `id: "my-plugin"` and your extension file declares `id: "foo-extractor"`, the kernel registers it as `my-plugin/foo-extractor`. You do **not** write the qualifier yourself — the loader injects it.
|
|
@@ -111,7 +111,7 @@ What this means in practice:
|
|
|
111
111
|
The kernel guards against two foot-guns:
|
|
112
112
|
|
|
113
113
|
- If the extension file injects a `pluginId` field that doesn't match `plugin.json#/id`, the loader emits `invalid-manifest` with a directed reason. The composed qualifier MUST come from `plugin.json` — there is no second source of truth.
|
|
114
|
-
- The kebab-case pattern on the extension `id` deliberately forbids `/`. This keeps the
|
|
114
|
+
- The kebab-case pattern on the extension `id` deliberately forbids `/`. This keeps the analyzer "the qualifier always lives in the plugin id, never in the extension id" enforced by AJV.
|
|
115
115
|
|
|
116
116
|
For built-ins, the reference impl's `src/extensions/built-ins.ts` declares each extension's `pluginId` (`core` or `claude`) explicitly — built-ins do not have a `plugin.json`, so the bundle declaration IS the source of truth for their namespace.
|
|
117
117
|
|
|
@@ -127,7 +127,7 @@ Every plugin and every built-in bundle declares a **granularity** that controls
|
|
|
127
127
|
Built-in mapping:
|
|
128
128
|
|
|
129
129
|
- **`claude`** / **`gemini`** / **`agent-skills`** — `granularity: 'bundle'`. Each vendor Provider bundle is enabled or disabled as a whole; today every such bundle ships only its Provider, so the toggle flips classification + frontmatter parsing for that platform.
|
|
130
|
-
- **`core`** — `granularity: 'extension'`. `sm plugins disable core/superseded` flips just the supersession
|
|
130
|
+
- **`core`** — `granularity: 'extension'`. `sm plugins disable core/superseded` flips just the supersession analyzer; every other core extension (every other analyzer, the ASCII formatter, the cross-vendor extractors) stays live.
|
|
131
131
|
|
|
132
132
|
Per-verb behaviour:
|
|
133
133
|
|
|
@@ -151,7 +151,7 @@ In your own plugin's `plugin.json`, set `granularity` only when you opt into the
|
|
|
151
151
|
"specCompat": "^1.0.0",
|
|
152
152
|
"granularity": "extension",
|
|
153
153
|
"extensions": [
|
|
154
|
-
"./extensions/orphan-skill-
|
|
154
|
+
"./extensions/orphan-skill-analyzer.js",
|
|
155
155
|
"./extensions/csv-formatter.js"
|
|
156
156
|
]
|
|
157
157
|
}
|
|
@@ -252,7 +252,7 @@ The kernel knows six categories. Three are dual-mode (deterministic or probabili
|
|
|
252
252
|
|---|---|---|---|---|
|
|
253
253
|
| `provider` | `walk(roots, opts)` | filesystem roots | `IRawNode[]` | deterministic only |
|
|
254
254
|
| `extractor` | `extract(ctx)` | one node + body + frontmatter + callbacks | `void` (output via `ctx.emitLink` / `ctx.enrichNode` / `ctx.store`) | deterministic only |
|
|
255
|
-
| `
|
|
255
|
+
| `analyzer` | `evaluate(ctx)` | full graph | `Issue[]` | dual-mode |
|
|
256
256
|
| `action` | `run(ctx)` | one or more nodes | execution record | dual-mode |
|
|
257
257
|
| `formatter` | `format(ctx)` | full graph | `string` | deterministic only |
|
|
258
258
|
| `hook` | `on(ctx)` | a curated lifecycle event payload | `void` (reactions are side effects) | dual-mode |
|
|
@@ -261,7 +261,7 @@ The runtime instance you `export default` from an extension file MUST include bo
|
|
|
261
261
|
|
|
262
262
|
### Extractors
|
|
263
263
|
|
|
264
|
-
Pure single-node analysis. **Never** read another node, the graph, or the database — cross-node reasoning is for
|
|
264
|
+
Pure single-node analysis. **Never** read another node, the graph, or the database — cross-node reasoning is for analyzers. Spec at [`schemas/extensions/extractor.schema.json`](./schemas/extensions/extractor.schema.json).
|
|
265
265
|
|
|
266
266
|
The runtime method is `extract(ctx) → void`. Output flows through three callbacks the kernel binds onto the context:
|
|
267
267
|
|
|
@@ -271,6 +271,8 @@ The runtime method is `extract(ctx) → void`. Output flows through three callba
|
|
|
271
271
|
|
|
272
272
|
Extractors are deterministic-only and never see `ctx.runner`. If an Extractor needs LLM-derived data on a node, that workload belongs in an Action — see [`architecture.md` §Execution modes](./architecture.md#execution-modes).
|
|
273
273
|
|
|
274
|
+
You can read `ctx.node.sidecar.*` freely — the kernel's per-`(node, extractor)` cache hashes the sidecar `annotations` block alongside the `.md` body, so a `.sm`-only edit invalidates the cached run automatically. No manifest flag, no opt-in: just read what you need.
|
|
275
|
+
|
|
274
276
|
> **Pick a syntax that doesn't collide with built-ins.** The built-in `at-directive` extractor fires on any `@token`; the built-in `slash` extractor fires on any `/token`. A new extractor 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.
|
|
275
277
|
|
|
276
278
|
```javascript
|
|
@@ -303,16 +305,16 @@ export default {
|
|
|
303
305
|
```
|
|
304
306
|
|
|
305
307
|
|
|
306
|
-
###
|
|
308
|
+
### Analyzers
|
|
307
309
|
|
|
308
|
-
Cross-node reasoning over the merged graph. Run after every Provider and extractor has completed. Spec at [`schemas/extensions/
|
|
310
|
+
Cross-node reasoning over the merged graph. Run after every Provider and extractor has completed. Spec at [`schemas/extensions/analyzer.schema.json`](./schemas/extensions/analyzer.schema.json).
|
|
309
311
|
|
|
310
|
-
|
|
312
|
+
Analyzers are dual-mode (`deterministic` default; `probabilistic` opt-in via the manifest). Deterministic analyzers run synchronously inside `sm scan` / `sm check` — same CI-safe baseline as today. Probabilistic analyzers are dispatched as queued jobs via the kernel's `RunnerPort`; they NEVER participate in the deterministic scan-time pipeline. Until the job subsystem ships at Step 10 the dispatch is stubbed: `sm scan` always skips probabilistic analyzers silently, and `sm check` exposes them via the opt-in `--include-prob` flag — the verb loads the plugin runtime, finds the registered prob analyzers (filtered by `--analyzers` and `-n` if set), and emits a stderr advisory naming them. The flag default is unchanged: deterministic-only, CI-safe. The `--async` companion is reserved for the future encoding (returns job ids without waiting once jobs land); today it is a no-op the advisory simply mentions. The flag does NOT extend to `sm scan` or `sm list`.
|
|
311
313
|
|
|
312
314
|
```javascript
|
|
313
315
|
export default {
|
|
314
316
|
id: 'orphan-skill',
|
|
315
|
-
kind: '
|
|
317
|
+
kind: 'analyzer',
|
|
316
318
|
version: '1.0.0',
|
|
317
319
|
description: 'Flags skill nodes with zero inbound links.',
|
|
318
320
|
evaluate(ctx) {
|
|
@@ -323,7 +325,7 @@ export default {
|
|
|
323
325
|
return ctx.nodes
|
|
324
326
|
.filter((n) => n.kind === 'skill' && (inboundCount.get(n.path) ?? 0) === 0)
|
|
325
327
|
.map((n) => ({
|
|
326
|
-
|
|
328
|
+
analyzerId: 'orphan-skill',
|
|
327
329
|
severity: 'info',
|
|
328
330
|
message: `Skill ${n.path} has no inbound references.`,
|
|
329
331
|
nodeIds: [n.path],
|
|
@@ -366,7 +368,7 @@ The eight hookable triggers (declaring any other event yields `invalid-manifest`
|
|
|
366
368
|
1. `scan.started` — pre-scan setup (one per scan).
|
|
367
369
|
2. `scan.completed` — post-scan reaction (one per scan).
|
|
368
370
|
3. `extractor.completed` — aggregated per-Extractor outputs.
|
|
369
|
-
4. `
|
|
371
|
+
4. `analyzer.completed` — aggregated per-Analyzer outputs.
|
|
370
372
|
5. `action.completed` — Action executed on a node.
|
|
371
373
|
6. `job.spawning` — pre-spawn of runner subprocess (Step 10).
|
|
372
374
|
7. `job.completed` — most common trigger (Step 10).
|
|
@@ -400,7 +402,7 @@ export default {
|
|
|
400
402
|
|
|
401
403
|
> **Mode semantics.** Default `mode: 'deterministic'` runs `on(ctx)` in-process during the dispatch of the matching event, synchronously between the event's emission and the next pipeline step. `mode: 'probabilistic'` enqueues the hook as a job; until the job subsystem ships at Step 10, probabilistic hooks load but skip dispatch with a stderr advisory.
|
|
402
404
|
|
|
403
|
-
> **What hooks CANNOT do.** Hooks REACT to events; they cannot block emission, mutate the graph, alter Extractor /
|
|
405
|
+
> **What hooks CANNOT do.** Hooks REACT to events; they cannot block emission, mutate the graph, alter Extractor / Analyzer output, or enrich nodes. For graph mutations use `extractor.enrichNode`; for graph reasoning use a Analyzer; for periodic background work use a probabilistic Action wrapped in a hook that submits the job. The single-responsibility split keeps the kernel's deterministic baseline stable.
|
|
404
406
|
|
|
405
407
|
### Providers / Actions
|
|
406
408
|
|
|
@@ -452,15 +454,15 @@ Every Provider declares two required top-level fields beyond the manifest base:
|
|
|
452
454
|
|
|
453
455
|
## Frontmatter validation — three-tier model
|
|
454
456
|
|
|
455
|
-
The kernel validates frontmatter on a graduated dial; tighter is opt-in. The model is normative — every conforming implementation MUST honour the three tiers — but the policy lives in **
|
|
457
|
+
The kernel validates frontmatter on a graduated dial; tighter is opt-in. The model is normative — every conforming implementation MUST honour the three tiers — but the policy lives in **analyzers**, not the JSON Schemas. The schemas stay shape-only ([`schemas/frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) declares `additionalProperties: true` deliberately) so that authors can extend their own nodes without forking the spec. Per-kind frontmatter schemas live with the **Provider** that emits the kind (declared via `provider.kinds[<kind>].schema`); spec only ships the universal `base`.
|
|
456
458
|
|
|
457
459
|
| Tier | Mechanism | Behavior on unknown / non-conforming fields |
|
|
458
460
|
|---|---|---|
|
|
459
|
-
| **0 — Default permissive** | `additionalProperties: true` on `base.schema.json` and on every per-kind frontmatter schema declared by an installed Provider. | Field passes silently, persists in `node.frontmatter`, and is available to every extension (extractors,
|
|
460
|
-
| **1 — Built-in `unknown-field`
|
|
461
|
+
| **0 — Default permissive** | `additionalProperties: true` on `base.schema.json` and on every per-kind frontmatter schema declared by an installed Provider. | Field passes silently, persists in `node.frontmatter`, and is available to every extension (extractors, analyzers, actions, formatters). |
|
|
462
|
+
| **1 — Built-in `unknown-field` analyzer** | Deterministic Analyzer shipped with the kernel. Always active. | Emits an Issue with `severity: 'warn'` for every key outside the documented catalog (base + the matched kind's schema). |
|
|
461
463
|
| **2 — Strict mode** | [`schemas/project-config.schema.json`](./schemas/project-config.schema.json) `scan.strict: true` (team default in `settings.json`); also via `--strict` on `sm scan`. | Promotes **all** frontmatter warnings to `severity: 'error'`. They persist in the DB; `sm check` then exits `1` on the next read. CI fails. |
|
|
462
464
|
|
|
463
|
-
> Tier 1 is normative behavior — the kernel ships the
|
|
465
|
+
> Tier 1 is normative behavior — the kernel ships the analyzer out-of-the-box. Disabling it is not a supported configuration; an unknown key that you want to keep is either (a) moved under `metadata.*` (the spec permits free-form keys there), or (b) carried as-is at the cost of a persistent `warn`-severity issue (informational unless you run Tier 2).
|
|
464
466
|
|
|
465
467
|
### Worked example — same node, three tiers
|
|
466
468
|
|
|
@@ -476,20 +478,20 @@ priority: high # ← author-defined, not in any schema
|
|
|
476
478
|
---
|
|
477
479
|
```
|
|
478
480
|
|
|
479
|
-
**Tier 0 (default permissive — no project config, default scan).** The field validates fine. `node.frontmatter.priority === 'high'` for any extractor /
|
|
481
|
+
**Tier 0 (default permissive — no project config, default scan).** The field validates fine. `node.frontmatter.priority === 'high'` for any extractor / analyzer / action that reads the node. No issues raised by the schema itself.
|
|
480
482
|
|
|
481
|
-
**Tier 1 (always-active `unknown-field`
|
|
483
|
+
**Tier 1 (always-active `unknown-field` analyzer).** After `sm scan`, the analyzer emits:
|
|
482
484
|
|
|
483
485
|
```jsonc
|
|
484
486
|
{
|
|
485
|
-
"
|
|
487
|
+
"analyzerId": "unknown-field",
|
|
486
488
|
"severity": "warn",
|
|
487
|
-
"message": "Unknown frontmatter field 'priority' on skill node 'code-reviewer'. Add it to a custom
|
|
489
|
+
"message": "Unknown frontmatter field 'priority' on skill node 'code-reviewer'. Add it to a custom analyzer or move it under metadata.* if intentional.",
|
|
488
490
|
"nodeIds": ["code-reviewer.md"]
|
|
489
491
|
}
|
|
490
492
|
```
|
|
491
493
|
|
|
492
|
-
`sm scan` exits `0` (warnings do not fail the verb). The author can either move the key under `metadata.*` — where [`schemas/frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) already permits free-form keys, so the `unknown-field`
|
|
494
|
+
`sm scan` exits `0` (warnings do not fail the verb). The author can either move the key under `metadata.*` — where [`schemas/frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) already permits free-form keys, so the `unknown-field` analyzer does not match — or accept the persistent warning and add a Analyzer that consumes `priority` for whatever cross-node logic motivated the field.
|
|
493
495
|
|
|
494
496
|
**Tier 2 (strict mode).** Either `scan.strict: true` in `.skill-map/settings.json`, or `sm scan --strict` on the CLI. The same `unknown-field` warning is now persisted at `severity: 'error'`. `sm scan --strict` exits `1` when the issue is created; `sm check` (which reads from the DB) also exits `1` thereafter. CI breaks until the field is reconciled.
|
|
495
497
|
|
|
@@ -505,15 +507,15 @@ The CLI flag wins when both are set (see the `--strict` description on `sm scan`
|
|
|
505
507
|
|
|
506
508
|
### Why no "schema-extender" plugin kind
|
|
507
509
|
|
|
508
|
-
A reasonable next thought is: "I want my plugin to widen the frontmatter schema so my custom keys are first-class." The spec deliberately rejects that route. The accepted path is to write a deterministic **
|
|
510
|
+
A reasonable next thought is: "I want my plugin to widen the frontmatter schema so my custom keys are first-class." The spec deliberately rejects that route. The accepted path is to write a deterministic **Analyzer** that:
|
|
509
511
|
|
|
510
512
|
1. Reads the candidate keys from `node.frontmatter` (which Tier 0 already exposes).
|
|
511
513
|
2. Validates them against whatever shape your domain expects (regex, enum, cross-node consistency).
|
|
512
514
|
3. Emits Issues for violations.
|
|
513
515
|
|
|
514
|
-
The trade-off is intentional: a "schema-extender" kind would force every consumer (the kernel, the storage layer, every other plugin, the UI) to re-resolve the active schema set per scan. A
|
|
516
|
+
The trade-off is intentional: a "schema-extender" kind would force every consumer (the kernel, the storage layer, every other plugin, the UI) to re-resolve the active schema set per scan. A Analyzer-driven approach keeps the kernel's parser one-pass and the validation surface composable — the union of every author's analyzers is the project's policy.
|
|
515
517
|
|
|
516
|
-
If the
|
|
518
|
+
If the analyzer needs to be CI-blocking, the analyzer itself emits the Issue at `severity: 'error'`. `--strict` / `scan.strict` apply only to the kernel's own frontmatter-shape and `unknown-field` warnings; plugin-authored analyzers pick their own severity directly.
|
|
517
519
|
|
|
518
520
|
---
|
|
519
521
|
|
|
@@ -606,7 +608,7 @@ The kernel validates the row passed to `ctx.store.write(table, row)` against the
|
|
|
606
608
|
|
|
607
609
|
## Execution modes
|
|
608
610
|
|
|
609
|
-
|
|
611
|
+
Analyzer / Action / Hook declare `mode` in the manifest. Action's `mode` is required; Analyzer and Hook default to `deterministic`. Provider / Extractor / Formatter must NOT declare `mode` — they are deterministic-only by spec.
|
|
610
612
|
|
|
611
613
|
```jsonc
|
|
612
614
|
// extractor — deterministic by spec, no mode field
|
|
@@ -630,7 +632,7 @@ The full per-kind capability matrix lives in [`architecture.md` §Execution mode
|
|
|
630
632
|
npm install --save-dev @skill-map/testkit
|
|
631
633
|
```
|
|
632
634
|
|
|
633
|
-
The testkit ships builders, per-kind context factories, in-memory KV / runner fakes, and high-level `runExtractorOnFixture` / `
|
|
635
|
+
The testkit ships builders, per-kind context factories, in-memory KV / runner fakes, and high-level `runExtractorOnFixture` / `runAnalyzerOnGraph` / `runFormatterOnGraph` helpers. Most plugin tests reduce to one line per assertion.
|
|
634
636
|
|
|
635
637
|
```javascript
|
|
636
638
|
import { test } from 'node:test';
|
|
@@ -649,7 +651,7 @@ test('emits one reference per [[ref:<name>]] token', async () => {
|
|
|
649
651
|
});
|
|
650
652
|
```
|
|
651
653
|
|
|
652
|
-
For
|
|
654
|
+
For analyzer tests, `runAnalyzerOnGraph(analyzer, { context: { nodes, links } })` returns the issue array. For formatter tests, `runFormatterOnGraph(formatter, { context: { nodes, links, issues } })` returns the formatted string.
|
|
653
655
|
|
|
654
656
|
For probabilistic extensions, `makeFakeRunner()` queues canned responses and records every call:
|
|
655
657
|
|
|
@@ -673,11 +675,11 @@ Full surface in `@skill-map/testkit/index.ts`.
|
|
|
673
675
|
| Status | Meaning | Common cause |
|
|
674
676
|
|---|---|---|
|
|
675
677
|
| `loaded` | manifest valid, specCompat satisfied, every extension imported and validated. | — |
|
|
676
|
-
| `disabled` | user toggled it off via `sm plugins disable` or `settings.json#/plugins/<id>/enabled`. Manifest parsed; extensions not imported. | Intentional. |
|
|
678
|
+
| `disabled` | user toggled it off via `sm plugins disable` or `settings.json#/plugins/<id>/enabled`. Manifest parsed; extensions not imported. The plugin's `scan_contributions` rows are purged eagerly so its UI chips disappear immediately; plugin-managed KV / dedicated-table state is preserved (see `plugin-kv-api.md`). | Intentional. |
|
|
677
679
|
| `incompatible-spec` | manifest parsed but `semver.satisfies` failed against the installed spec. | Plugin built against an older / newer spec. |
|
|
678
680
|
| `invalid-manifest` | `plugin.json` missing, unparseable, AJV-fails, OR the directory name does not equal the manifest id. | Typo, missing required field, wrong shape, mismatched directory name. |
|
|
679
681
|
| `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. |
|
|
680
|
-
| `id-collision` | two plugins reachable from different roots declared the same `id`. Both collided plugins receive this status; no precedence
|
|
682
|
+
| `id-collision` | two plugins reachable from different roots declared the same `id`. Both collided plugins receive this status; no precedence analyzer applies. | Project-local plugin and a user-global plugin (or two `--plugin-dir` plugins) sharing an id. Rename one and rerun. |
|
|
681
683
|
|
|
682
684
|
`sm plugins doctor` runs the full load pass and exits 1 if any plugin is in a non-`loaded` / non-`disabled` state (so any of `incompatible-spec` / `invalid-manifest` / `load-error` / `id-collision` trips it). Wire it into CI to catch breakage early.
|
|
683
685
|
|
|
@@ -715,7 +717,7 @@ Field-by-field:
|
|
|
715
717
|
| `location` | `'namespaced'` \| `'root'` | `'namespaced'` | Where the key lands inside the sidecar (see below). |
|
|
716
718
|
| `ownership` | `'shared'` \| `'exclusive'` | `'shared'` | Conflict policy. REQUIRED to be `'exclusive'` when `location: 'root'`. |
|
|
717
719
|
|
|
718
|
-
The `schema` field is **inline** — an object literal in the manifest, not a `$ref` to a file. Aligns with how the extractor /
|
|
720
|
+
The `schema` field is **inline** — an object literal in the manifest, not a `$ref` to a file. Aligns with how the extractor / analyzer / action schemas already declare other inline shapes; avoids an extra path-resolution step at load time.
|
|
719
721
|
|
|
720
722
|
### Namespacing default vs root opt-in
|
|
721
723
|
|
|
@@ -742,10 +744,10 @@ auditor:
|
|
|
742
744
|
Opting into a top-level (root) key requires `location: 'root'` AND `ownership: 'exclusive'`. The pair travels together — a top-level reserved key cannot be silently shared between plugins, because `.sm` writes deep-merge per the `SidecarStore` contract and a shared root key would route non-deterministically. Use root sparingly: for every plugin that contributes a root key, the kernel reserves that name across the whole installed-plugin surface.
|
|
743
745
|
|
|
744
746
|
```js
|
|
745
|
-
// compliance-plugin/extensions/
|
|
747
|
+
// compliance-plugin/extensions/analyzer.js
|
|
746
748
|
export default {
|
|
747
749
|
id: 'compliance-checker',
|
|
748
|
-
kind: '
|
|
750
|
+
kind: 'analyzer',
|
|
749
751
|
// ...
|
|
750
752
|
annotationContributions: {
|
|
751
753
|
compliance: {
|
|
@@ -774,7 +776,7 @@ compliance:
|
|
|
774
776
|
dueAt: 2026-12-31T23:59:59Z
|
|
775
777
|
```
|
|
776
778
|
|
|
777
|
-
### Ownership
|
|
779
|
+
### Ownership analyzers
|
|
778
780
|
|
|
779
781
|
- `shared` (default) — multiple plugins MAY write the same key. Every plugin gets its own namespaced block; `last-write-wins` is per-`(plugin, key)` tuple inside `FilesystemSidecarStore.applyPatch`. Two plugins on the SAME namespaced key from the same plugin id is structurally impossible (one extension per kind per plugin id by spec), so the only collision surface is intra-extension.
|
|
780
782
|
- `exclusive` — only this plugin may write the key. The kernel rejects any other plugin that tries to claim the same `(key, location: 'root')` tuple as `exclusive`. `exclusive` + `namespaced` is permitted but redundant in practice (the namespace already isolates by plugin id); use it as documentation when you want the manifest to scream "no other plugin should ever write this".
|
|
@@ -787,13 +789,13 @@ This is the only fatal path on the plugin-load surface. Every other failure mode
|
|
|
787
789
|
|
|
788
790
|
### Tier-1 typo guard (`core/unknown-field`)
|
|
789
791
|
|
|
790
|
-
The built-in `core/unknown-field`
|
|
792
|
+
The built-in `core/unknown-field` Analyzer walks every parsed `.sm` and emits a `warn` issue per truly-unknown key. Three surfaces are checked:
|
|
791
793
|
|
|
792
794
|
1. Inside `annotations:` — keys not in `annotations.schema.json`'s curated catalog (the ~25 conventional fields). Plugins do NOT contribute to `annotations:`; that block is skill-map-curated.
|
|
793
795
|
2. At the sidecar root — keys outside the four reserved blocks (`for`, `annotations`, `settings`, `audit`) that are also NOT a registered plugin namespace `<plugin-id>:` AND NOT a registered `location: 'root'` contribution.
|
|
794
796
|
3. Inside a registered `<plugin-id>:` namespace — values that fail the schema declared by the owning plugin's `annotationContributions[<key>].schema`.
|
|
795
797
|
|
|
796
|
-
The
|
|
798
|
+
The analyzer never blocks a scan; advisories surface through the standard issue channel (CLI, UI, REST). When you ship a contribution, the loader compiles your inline schema, the runtime catalog publishes it, and `core/unknown-field` automatically validates user writes against your declaration.
|
|
797
799
|
|
|
798
800
|
### Runtime catalog accessor
|
|
799
801
|
|
|
@@ -810,28 +812,27 @@ Pure read; no side effects. Built-in catalog fields from `annotations.schema.jso
|
|
|
810
812
|
|
|
811
813
|
## View contributions
|
|
812
814
|
|
|
813
|
-
> **Status.** Sibling system to annotation contributions, designed to let plugins surface per-node data in the UI without shipping any UI code. Plugin authors pick a **
|
|
815
|
+
> **Status.** Sibling system to annotation contributions, designed to let plugins surface per-node data in the UI without shipping any UI code. Plugin authors pick a **slot** by name from a closed kernel catalog; the slot fixes both the renderer and the payload shape. Authors declare per-node emissions in their extension manifest and emit payloads at scan time via `ctx.emitContribution(id, payload)`. See [`architecture.md`](./architecture.md) §View contribution system for the normative contract.
|
|
814
816
|
|
|
815
817
|
### What it solves
|
|
816
818
|
|
|
817
|
-
Today, the only way a plugin can surface UI is implicit: extractors emit `Link` (rendered by the kernel-built `linked-nodes-panel`),
|
|
819
|
+
Today, the only way a plugin can surface UI is implicit: extractors emit `Link` (rendered by the kernel-built `linked-nodes-panel`), analyzers emit `Issue` (rendered by the kernel-built issues panel), providers ship `kinds[*].ui` styling, and one-off plugins write into the sidecar via `annotationContributions`. The moment your extractor wants to surface anything else — a counter on each card, a stat breakdown panel in the inspector, a tree showing parsed structure, a per-node tag — there is no path. View contributions fill that gap. You declare what to surface and where; the kernel validates the payload against the slot's shape and the UI renders.
|
|
818
820
|
|
|
819
821
|
### What you NEVER write
|
|
820
822
|
|
|
821
823
|
- HTML, CSS, JavaScript, or Angular components.
|
|
822
824
|
- JSON Schema for your contributions or your settings.
|
|
823
|
-
- The slot id where your contribution appears (slots are UI-only).
|
|
824
825
|
- The renderer component that draws your contribution.
|
|
825
826
|
|
|
826
827
|
You DO write:
|
|
827
828
|
|
|
828
|
-
- The `
|
|
829
|
+
- The `slot` name (one of 15 closed-catalog values). The slot you pick fixes both where the data renders and what payload shape the kernel will accept.
|
|
829
830
|
- Optional `label`, `tooltip`, `icon`, `emptyText`, `emitWhenEmpty` per contribution.
|
|
830
831
|
- The per-node payload your `extract(ctx)` emits via `ctx.emitContribution(...)`.
|
|
831
832
|
|
|
832
833
|
### Manifest shape
|
|
833
834
|
|
|
834
|
-
Inside any extension manifest (`IExtractor`, `
|
|
835
|
+
Inside any extension manifest (`IExtractor`, `IAnalyzer`, ...), declare a `viewContributions` map next to `annotationContributions`. Each key is your local contribution id; the value picks a slot.
|
|
835
836
|
|
|
836
837
|
```jsonc
|
|
837
838
|
{
|
|
@@ -839,12 +840,12 @@ Inside any extension manifest (`IExtractor`, `IRule`, ...), declare a `viewContr
|
|
|
839
840
|
"kind": "extractor",
|
|
840
841
|
"viewContributions": {
|
|
841
842
|
"breakdown": {
|
|
842
|
-
"
|
|
843
|
+
"slot": "inspector.body.panel.breakdown",
|
|
843
844
|
"label": "Keyword hits",
|
|
844
845
|
"emptyText": "No matches."
|
|
845
846
|
},
|
|
846
847
|
"total": {
|
|
847
|
-
"
|
|
848
|
+
"slot": "card.footer.left",
|
|
848
849
|
"icon": "🔍",
|
|
849
850
|
"label": "kw",
|
|
850
851
|
"emitWhenEmpty": false
|
|
@@ -853,35 +854,55 @@ Inside any extension manifest (`IExtractor`, `IRule`, ...), declare a `viewContr
|
|
|
853
854
|
}
|
|
854
855
|
```
|
|
855
856
|
|
|
856
|
-
Field reference (full schema in [`schemas/view-
|
|
857
|
+
Field reference (full schema in [`schemas/view-slots.schema.json`](./schemas/view-slots.schema.json) at `$defs/IViewContribution`):
|
|
857
858
|
|
|
858
859
|
| Field | Required | Notes |
|
|
859
860
|
|---|---|---|
|
|
860
|
-
| `
|
|
861
|
+
| `slot` | yes | One of the 15 catalog names (see below). Unknown name → `invalid-manifest` at load. |
|
|
861
862
|
| `label` | no | Short human-readable label. English-only per [`AGENTS.md`](../AGENTS.md) (`Externalized texts, not internationalized`). |
|
|
862
863
|
| `tooltip` | no | Hover tooltip on the chip / panel header. |
|
|
863
|
-
| `icon` | no | Single string.
|
|
864
|
+
| `icon` | no, but required for counter slots and `card.title.right` | Single prefix-discriminated string. Emoji renders as text; `pi-foo` / `pi pi-foo` → PrimeIcons; `fa-solid fa-foo` / `fa-regular fa-foo` / `fa-brands fa-foo` → FontAwesome (full pass-through); `fa-foo` → defaults to `fa-solid fa-foo`. Bare names without prefix are rejected at load. See [Icon string forms](#icon-string-forms) below. |
|
|
864
865
|
| `emptyText` | no | Text shown when payload is empty AND `emitWhenEmpty: true`. |
|
|
865
866
|
| `emitWhenEmpty` | no, default `false` | When `false`, kernel drops empty payloads silently so the slot stays clean. |
|
|
866
867
|
|
|
867
|
-
|
|
868
|
+
#### Icon string forms
|
|
869
|
+
|
|
870
|
+
Four valid shapes, prefix-discriminated by the UI resolver:
|
|
871
|
+
|
|
872
|
+
```jsonc
|
|
873
|
+
{ "icon": "🔍" } // emoji — renders as text
|
|
874
|
+
{ "icon": "pi-search" } // PrimeIcons — equivalent to "pi pi-search"
|
|
875
|
+
{ "icon": "pi pi-search" } // PrimeIcons — full class string accepted
|
|
876
|
+
{ "icon": "fa-solid fa-magnifying-glass" } // FontAwesome — explicit family, pass-through
|
|
877
|
+
{ "icon": "fa-regular fa-star" } // FontAwesome — outlined variant
|
|
878
|
+
{ "icon": "fa-brands fa-github" } // FontAwesome — brand glyph
|
|
879
|
+
{ "icon": "fa-magnifying-glass" } // FontAwesome shorthand — defaults to `fa-solid`
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
Anything else (e.g. bare `"search"` without a prefix) is rejected at manifest load with `invalid-manifest`. Pick the family that fits the visual; emoji is the cross-platform safe choice when you do not care about variant. FontAwesome Free's `regular` set is limited — only a handful of icons (e.g. `fa-star`, `fa-sun`, `fa-moon`, `fa-circle-up`) have outlined variants. PrimeIcons covers more generic UI glyphs.
|
|
883
|
+
|
|
884
|
+
### Slot catalog (closed)
|
|
868
885
|
|
|
869
|
-
The kernel ships exactly these
|
|
886
|
+
The kernel ships exactly these 15 slots. Each slot fixes a renderer + a payload shape; multiple slots may share a payload shape (e.g. all counter slots accept `{ value }`). Adding a slot requires a spec / UI / scaffolder round-trip — discuss in [`ROADMAP.md`](../ROADMAP.md) before opening a PR.
|
|
870
887
|
|
|
871
|
-
|
|
|
888
|
+
| Slot | Payload shape | Renderer |
|
|
872
889
|
|---|---|---|
|
|
873
|
-
| `
|
|
874
|
-
| `
|
|
875
|
-
| `
|
|
876
|
-
| `
|
|
877
|
-
| `node
|
|
878
|
-
| `
|
|
879
|
-
| `
|
|
880
|
-
| `
|
|
881
|
-
| `
|
|
882
|
-
| `
|
|
883
|
-
|
|
884
|
-
|
|
890
|
+
| `card.title.right` | `{ icon?, severity?, tooltip? }` | icon marker (manifest icon required) |
|
|
891
|
+
| `card.subtitle.left` | `{ value: integer ≥ 0, severity?, tooltip? }` | counter chip (manifest icon required) |
|
|
892
|
+
| `card.footer.left` | `{ value: integer ≥ 0, severity?, tooltip? }` | counter chip (manifest icon required) |
|
|
893
|
+
| `card.footer.right` | `{ value: integer ≥ 0, severity?, tooltip? }` | counter chip (manifest icon required) |
|
|
894
|
+
| `graph.node.alert` | `{ icon?, severity?, count?, tooltip? }` | graph corner badge |
|
|
895
|
+
| `inspector.header.badge.counter` | `{ value: integer ≥ 0, severity?, tooltip? }` | counter chip (manifest icon required) |
|
|
896
|
+
| `inspector.header.badge.tag` | `{ label, severity?, tooltip? }` | tag chip |
|
|
897
|
+
| `inspector.body.panel.breakdown` | `{ entries: Array<{ label, value, tooltip? }> }` (≤ 20) | bar chart panel |
|
|
898
|
+
| `inspector.body.panel.records` | `{ columns: ≤6, rows: ≤50 }` | table panel |
|
|
899
|
+
| `inspector.body.panel.tree` | recursive `{ label, marker?, children? }` (depth ≤ 6, total ≤ 200) | tree panel |
|
|
900
|
+
| `inspector.body.panel.key-values` | `{ entries: Array<{ key, value, tooltip? }> }` (≤ 50) | definition list panel |
|
|
901
|
+
| `inspector.body.panel.link-list` | `{ entries: Array<{ path, label?, kind? }> }` (≤ 100) | clickable list panel |
|
|
902
|
+
| `inspector.body.panel.markdown` | `{ markdown }` (≤ 4096 chars, sanitized) | sanitized markdown panel |
|
|
903
|
+
| `topbar.nav.start` | `{ value, label?, severity?, tooltip? }` | scope chip |
|
|
904
|
+
|
|
905
|
+
Per-slot semantics, edge cases, and exact payload schemas live in [`view-slots.md`](./view-slots.md) (catalog reference) and [`schemas/view-slots.schema.json`](./schemas/view-slots.schema.json) at `$defs/payloads/<slot>`. Read those before emitting.
|
|
885
906
|
|
|
886
907
|
### Emit path
|
|
887
908
|
|
|
@@ -895,11 +916,39 @@ ctx.emitContribution('breakdown', {
|
|
|
895
916
|
ctx.emitContribution('total', { value: total });
|
|
896
917
|
```
|
|
897
918
|
|
|
898
|
-
The first argument is the manifest Record key (`'breakdown'` or `'total'` above), NOT the
|
|
919
|
+
The first argument is the manifest Record key (`'breakdown'` or `'total'` above), NOT the slot name. The kernel composes the qualified id from your plugin id, extension id, and this Record key, and looks up the slot you declared in the manifest to validate the payload.
|
|
920
|
+
|
|
921
|
+
The kernel validates the payload against the slot's payload schema in `view-slots.schema.json#/$defs/payloads/<slot>`. Off-shape payloads emit an `extension.error` event and drop silently — same posture as `emitLink` rejecting links not in your `emitsLinkKinds`.
|
|
922
|
+
|
|
923
|
+
For `topbar.nav.start`, analyzers use `ctx.emitScopeContribution(id, payload)` (extractors do not see this method — scope-level emission lives in analyzer context).
|
|
924
|
+
|
|
925
|
+
### Multi-slot rendering
|
|
926
|
+
|
|
927
|
+
Want the same data in two surfaces? Declare two contributions, each pointing at a different slot. There is no broadcast — the slot you pick is the slot the data renders in.
|
|
928
|
+
|
|
929
|
+
```jsonc
|
|
930
|
+
"viewContributions": {
|
|
931
|
+
"mentionsFooter": {
|
|
932
|
+
"slot": "card.footer.left",
|
|
933
|
+
"icon": "@",
|
|
934
|
+
"label": "mentions"
|
|
935
|
+
},
|
|
936
|
+
"mentionsBadge": {
|
|
937
|
+
"slot": "inspector.header.badge.counter",
|
|
938
|
+
"icon": "@",
|
|
939
|
+
"label": "mentions"
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
Then emit twice (typically with the same value):
|
|
899
945
|
|
|
900
|
-
|
|
946
|
+
```ts
|
|
947
|
+
ctx.emitContribution('mentionsFooter', { value: count });
|
|
948
|
+
ctx.emitContribution('mentionsBadge', { value: count });
|
|
949
|
+
```
|
|
901
950
|
|
|
902
|
-
|
|
951
|
+
This is intentional: one source of truth per surface, no surprise duplication when a renderer changes its mind about which slots to draw in.
|
|
903
952
|
|
|
904
953
|
### Settings
|
|
905
954
|
|
|
@@ -950,13 +999,13 @@ The kernel exposes resolved settings to extractors via `ctx.settings.<settingId>
|
|
|
950
999
|
|
|
951
1000
|
### Catalog version
|
|
952
1001
|
|
|
953
|
-
The catalog of
|
|
1002
|
+
The catalog of slots and input-types evolves on its own cadence. Declare a semver range in your manifest:
|
|
954
1003
|
|
|
955
1004
|
```jsonc
|
|
956
1005
|
{ "catalogCompat": "^1.0.0" }
|
|
957
1006
|
```
|
|
958
1007
|
|
|
959
|
-
Independent of `specCompat` (the spec version range). Mismatch surfaces as `incompatible-catalog` plugin status; resolution is `sm plugins upgrade <id>`, which runs registered migrations from the kernel's closed migration registry. When auto-migration is impossible (a
|
|
1008
|
+
Independent of `specCompat` (the spec version range). Mismatch surfaces as `incompatible-catalog` plugin status; resolution is `sm plugins upgrade <id>`, which runs registered migrations from the kernel's closed migration registry. When auto-migration is impossible (a slot you used was removed entirely), the upgrade verb fails loud (CLI exit ≠ 0 + console message) and your manifest needs a manual edit.
|
|
960
1009
|
|
|
961
1010
|
`catalogCompat` is **optional**: omit it if your plugin declares no `viewContributions` and no `settings`. The doctor verb (`sm plugins doctor`) warns if such a plugin actually emits via `viewContributions` or declares `settings`.
|
|
962
1011
|
|
|
@@ -1008,12 +1057,12 @@ export const extractor = {
|
|
|
1008
1057
|
|
|
1009
1058
|
viewContributions: {
|
|
1010
1059
|
breakdown: {
|
|
1011
|
-
|
|
1060
|
+
slot: 'inspector.body.panel.breakdown',
|
|
1012
1061
|
label: 'Keyword hits',
|
|
1013
1062
|
emptyText: 'No matches.',
|
|
1014
1063
|
},
|
|
1015
1064
|
total: {
|
|
1016
|
-
|
|
1065
|
+
slot: 'card.footer.left',
|
|
1017
1066
|
icon: '🔍',
|
|
1018
1067
|
label: 'kw',
|
|
1019
1068
|
emitWhenEmpty: false,
|
|
@@ -1052,7 +1101,7 @@ After `sm scan`, the UI surfaces:
|
|
|
1052
1101
|
- A `🔍 N` chip on every node's card (when `total > 0`).
|
|
1053
1102
|
- A "Keyword hits" panel in the inspector body for every node, with a horizontal bar chart per keyword.
|
|
1054
1103
|
|
|
1055
|
-
The plugin author wrote zero UI code, zero CSS, zero HTML, zero JSON Schema, and
|
|
1104
|
+
The plugin author wrote zero UI code, zero CSS, zero HTML, zero JSON Schema, and zero renderer logic.
|
|
1056
1105
|
|
|
1057
1106
|
### Scaffolder
|
|
1058
1107
|
|
|
@@ -1062,18 +1111,18 @@ Hand-writing the manifest is supported but discouraged. Run:
|
|
|
1062
1111
|
sm plugins create
|
|
1063
1112
|
```
|
|
1064
1113
|
|
|
1065
|
-
The scaffolder walks you through the closed catalogs (settings + view
|
|
1114
|
+
The scaffolder walks you through the closed catalogs (settings + view contribution slots) and emits a complete plugin directory with manifest, extension stub, test scaffold, and README. Hand-writing remains valid because the spec is the source of truth, but the scaffolder catches invalid slot picks at author time, while a hand-written manifest only fails at load time.
|
|
1066
1115
|
|
|
1067
1116
|
Companion verbs:
|
|
1068
1117
|
|
|
1069
|
-
- `sm plugins doctor` — surfaces `incompatible-catalog`, `invalid-manifest`, deprecated-
|
|
1118
|
+
- `sm plugins doctor` — surfaces `incompatible-catalog`, `invalid-manifest`, deprecated-slot usage.
|
|
1070
1119
|
- `sm plugins upgrade <id>` — applies catalog migrations registered in the kernel.
|
|
1071
|
-
- `sm plugins
|
|
1120
|
+
- `sm plugins slots list` — prints the catalog (slots + input-types), flags deprecated entries.
|
|
1072
1121
|
|
|
1073
1122
|
### Watch out for
|
|
1074
1123
|
|
|
1075
|
-
- **
|
|
1076
|
-
- **Don't write JSON Schema.** Settings use `type` from the input-type catalog; view contributions use `
|
|
1124
|
+
- **Pick exactly one slot per contribution.** The slot determines both the renderer and the payload shape. If you want the same data in two surfaces (e.g. card chip + inspector badge), declare two contributions in the manifest, one per slot, and emit twice.
|
|
1125
|
+
- **Don't write JSON Schema.** Settings use `type` from the input-type catalog; view contributions use `slot` from the slot catalog.
|
|
1077
1126
|
- **Don't mutate payloads after emission.** The kernel validates and serializes at emit time; a plugin holding a reference to the emitted payload and mutating it later has undefined behavior.
|
|
1078
1127
|
- **Don't emit HTML.** `node-markdown` accepts markdown with a sanitized allow-list; `[innerHTML]` bindings in the renderer are lint-banned (see [`context/view-contributions.md`](../context/view-contributions.md)).
|
|
1079
1128
|
- **Don't try to read another plugin's contributions.** The BFF rejects cross-plugin reads at the route level.
|
|
@@ -1084,7 +1133,7 @@ Companion verbs:
|
|
|
1084
1133
|
|
|
1085
1134
|
- [`architecture.md`](./architecture.md) — extension contract, ports, execution modes.
|
|
1086
1135
|
- [`plugin-kv-api.md`](./plugin-kv-api.md) — Storage Mode A normative API.
|
|
1087
|
-
- [`db-schema.md`](./db-schema.md) — table catalog and migration
|
|
1136
|
+
- [`db-schema.md`](./db-schema.md) — table catalog and migration analyzers (Mode B).
|
|
1088
1137
|
- [`schemas/plugins-registry.schema.json`](./schemas/plugins-registry.schema.json) — normative manifest shape.
|
|
1089
1138
|
- [`schemas/extensions/*.schema.json`](./schemas/extensions) — per-kind manifest schemas.
|
|
1090
1139
|
|
|
@@ -1094,8 +1143,8 @@ Companion verbs:
|
|
|
1094
1143
|
|
|
1095
1144
|
- Document status: **stable** as of spec v1.0.0. Future minor revisions add new sections (e.g. richer testkit coverage when actions gain helpers); breaking edits to the documented surface require a major bump per [`versioning.md`](./versioning.md).
|
|
1096
1145
|
- The six plugin statuses (`loaded` / `disabled` / `incompatible-spec` / `invalid-manifest` / `load-error` / `id-collision`) are stable; adding a seventh status is a minor bump.
|
|
1097
|
-
- The structural
|
|
1098
|
-
- The cross-root id-collision
|
|
1146
|
+
- The structural analyzer **directory name MUST equal manifest id** is stable; relaxing it (allowing mismatch) is a major bump.
|
|
1147
|
+
- The cross-root id-collision analyzer (both sides blocked, no precedence) is stable; introducing precedence (e.g. project root wins over global) is a major bump.
|
|
1099
1148
|
- The `granularity` field on `PluginManifest` is stable as introduced. The two values (`bundle` / `extension`) are stable. Adding a third value is a minor bump; changing the default away from `bundle` is a major bump (every existing plugin manifest would silently flip toggle semantics).
|
|
1100
1149
|
- The optional `applicableKinds` field on the Extractor manifest is stable as introduced. Adding a wildcard syntax (`'*'`) is a minor bump (additive, the existing "absent = all kinds" semantics keeps holding); changing the default away from "applies to every kind" or making the field required is a major bump. Promoting the unknown-kinds doctor warning to a hard load error is a major bump (today's contract is "load OK, surface as warning").
|
|
1101
1150
|
- The recommended `specCompat` strategy is descriptive prose; revising the recommendation does not require a spec bump as long as the schema stays unchanged.
|
package/plugin-kv-api.md
CHANGED
|
@@ -102,7 +102,7 @@ Errors MUST NOT leak backend-specific details (SQL strings, file paths) to plugi
|
|
|
102
102
|
|
|
103
103
|
## Mode B: dedicated tables
|
|
104
104
|
|
|
105
|
-
Mode B is governed by [`db-schema.md`](./db-schema.md) (catalog
|
|
105
|
+
Mode B is governed by [`db-schema.md`](./db-schema.md) (catalog analyzers + triple protection). This section restates the API surface.
|
|
106
106
|
|
|
107
107
|
### Declaration
|
|
108
108
|
|
|
@@ -147,7 +147,7 @@ Mode B plugins MAY call `db.transaction(async (tx) => { ... })`. The kernel prov
|
|
|
147
147
|
- Index and constraint prefixes are similarly injected.
|
|
148
148
|
- A failing plugin migration disables only that plugin (`status: load-error`); other plugins and the kernel continue.
|
|
149
149
|
|
|
150
|
-
See [`db-schema.md`](./db-schema.md) for the normative migration
|
|
150
|
+
See [`db-schema.md`](./db-schema.md) for the normative migration analyzers.
|
|
151
151
|
|
|
152
152
|
---
|
|
153
153
|
|
|
@@ -172,7 +172,7 @@ A plugin MUST declare **exactly one** storage mode. Mixing modes in the same plu
|
|
|
172
172
|
|
|
173
173
|
---
|
|
174
174
|
|
|
175
|
-
## Visibility
|
|
175
|
+
## Visibility analyzers
|
|
176
176
|
|
|
177
177
|
- A plugin MUST NOT read or write rows outside its scope. Mode A: the accessor is scoped. Mode B: the validator enforces the prefix.
|
|
178
178
|
- The kernel MAY expose read-only introspection for diagnostics (e.g., `sm plugins show <id> --storage` lists key counts). This is authoritative, not a plugin-level API.
|
|
@@ -184,7 +184,7 @@ A plugin MUST declare **exactly one** storage mode. Mixing modes in the same plu
|
|
|
184
184
|
|
|
185
185
|
- Mode A rows are stored in `state_plugin_kvs` and are backed up with `sm db backup`.
|
|
186
186
|
- Mode B rows live in the plugin's dedicated tables, prefixed `plugin_<id>_`, and are likewise backed up.
|
|
187
|
-
- `sm plugins disable <id>` does NOT drop the plugin's data — disabled plugins keep their KV rows and dedicated tables. `sm plugins forget <id>` (deferred to post-`v1.0`) is the verb that wipes.
|
|
187
|
+
- `sm plugins disable <id>` does NOT drop the plugin's data — disabled plugins keep their KV rows and dedicated tables. (`scan_contributions` rows ARE purged eagerly on disable — see `db-schema.md` § `scan_contributions` — because those are scan-derived and would otherwise keep rendering in the UI until the next scan. The KV / dedicated-table data is plugin-managed and survives toggle cycles so re-enabling restores state.) `sm plugins forget <id>` (deferred to post-`v1.0`) is the verb that wipes everything.
|
|
188
188
|
- `sm db reset` (no modifier) drops only `scan_*`. Plugin KV rows (mode A) and plugin-dedicated tables (mode B) are **preserved** — the reset is non-destructive to plugin storage.
|
|
189
189
|
- `sm db reset --state` drops `state_*` AND every `plugin_<normalized_id>_*` table, which includes `state_plugin_kvs` (mode A) AND the plugin-dedicated tables (mode B). The CLI MUST require interactive confirmation unless `--yes` is passed.
|
|
190
190
|
- `sm db reset --hard` deletes the DB file entirely, destroying all plugin storage regardless of mode.
|
|
@@ -203,8 +203,8 @@ Post-v1.0 work: signed manifest, sandboxed worker-thread isolation, per-plugin D
|
|
|
203
203
|
|
|
204
204
|
## See also
|
|
205
205
|
|
|
206
|
-
- [`db-schema.md`](./db-schema.md) — table catalog, migration
|
|
207
|
-
- [`architecture.md`](./architecture.md) — extension contract
|
|
206
|
+
- [`db-schema.md`](./db-schema.md) — table catalog, migration analyzers, triple protection for mode B.
|
|
207
|
+
- [`architecture.md`](./architecture.md) — extension contract analyzers and `ctx.store` injection via the kernel.
|
|
208
208
|
|
|
209
209
|
---
|
|
210
210
|
|
package/prompt-preamble.md
CHANGED
|
@@ -143,7 +143,7 @@ This preamble is a **mitigation**, not a guarantee. A determined attacker can st
|
|
|
143
143
|
2. It gives the model a structured place to report suspected injections, so consumers can act (flag the node, re-run with a different model, refuse to summarize).
|
|
144
144
|
3. It makes injection attempts visible (via the `safety` field in reports) so that deterministic rules can surface patterns over the graph.
|
|
145
145
|
|
|
146
|
-
Defense-in-depth: the deterministic
|
|
146
|
+
Defense-in-depth: the deterministic analyzer `injection-pattern` (shipped as a built-in analyzer in the default plugin pack) scans node bodies for known injection patterns independently of the LLM. Neither layer is sufficient alone.
|
|
147
147
|
|
|
148
148
|
---
|
|
149
149
|
|