@skill-map/spec 0.53.0 → 0.55.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 +32 -0
- package/README.md +12 -10
- package/architecture.md +154 -150
- package/cli-contract.md +139 -143
- package/conformance/README.md +9 -9
- package/conformance/coverage.md +5 -5
- package/db-schema.md +72 -72
- package/index.json +19 -18
- package/interfaces/security-scanner.md +25 -25
- package/job-events.md +43 -43
- package/job-lifecycle.md +32 -36
- package/package.json +2 -1
- package/plugin-author-guide.md +97 -125
- package/plugin-kv-api.md +22 -23
- package/plugin-quickstart.md +96 -0
- package/prompt-preamble.md +6 -6
- package/schemas/extensions/action.schema.json +6 -0
- package/schemas/project-config.schema.json +4 -0
- package/telemetry.md +120 -136
- package/versioning.md +12 -12
package/plugin-author-guide.md
CHANGED
|
@@ -2,85 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
How to ship a third-party `skill-map` plugin: directory layout, manifest fields, the six extension kinds, storage choice, version compatibility, dual-mode posture, and how to unit-test the result against the kernel's public types.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
*In a hurry? The [Plugin quickstart](./plugin-quickstart.md) gets a working plugin in three steps; this guide is the full contract.*
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
This guide is **descriptive prose, not the normative contract**. The normative pieces live in the JSON Schemas under [`schemas/`](./schemas/) and in [`architecture.md`](./architecture.md); every claim here is cross-linked. When this guide disagrees with a schema, the schema wins; on system behaviour, `architecture.md` wins. Deep per-system contracts (extension semantics, resolver phase, persistence sweeps, isolation model) are NOT restated here, follow the links.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
> **Status.** Pre-1.0 (`spec` is in `0.y.z`). The author surface is still settling; breaking changes ship as **minor** bumps per [`versioning.md`](./versioning.md) until the first `1.0.0`. The shape here matches the manifest schemas as of the structure-as-truth refactor (the kernel derives `id` / `kind` / the Provider kind catalog from disk, so they are no longer manifest fields).
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
---
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
my-plugin/
|
|
15
|
-
├── plugin.json ← plugin metadata (required)
|
|
16
|
-
└── extractors/ ← one folder per extension kind
|
|
17
|
-
└── my-extractor/
|
|
18
|
-
├── index.js ← extension entry (required)
|
|
19
|
-
├── text.ts ← user-facing strings (optional)
|
|
20
|
-
└── my-extractor.test.ts ← tests live next to the code (optional)
|
|
21
|
-
```
|
|
13
|
+
## Plugin lifecycle at a glance
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
`<plugin-dir>/<kind>s/<name>/index.{js,mjs,ts}` for each known kind
|
|
25
|
-
(`providers`, `extractors`, `analyzers`, `actions`, `formatters`,
|
|
26
|
-
`hooks`). **The folder layout IS the source of truth**: the plugin id comes from the
|
|
27
|
-
top-level dir, the kind from the subfolder name, the extension id from the
|
|
28
|
-
extension folder name. The manifest does NOT declare an
|
|
29
|
-
`extensions[]` array, and an extension file does NOT declare its own `id` or `kind`
|
|
30
|
-
(a manifest carrying either is rejected as `invalid-manifest`).
|
|
31
|
-
|
|
32
|
-
**Co-located files convention**: any siblings of `index.{js,mjs,ts}`
|
|
33
|
-
that the kernel does NOT recognise as an entry point are author
|
|
34
|
-
files. Two names are blessed by convention:
|
|
35
|
-
|
|
36
|
-
- **`text.ts`** holds the extension's externalised user-facing
|
|
37
|
-
strings. One per extension; imported by `index.ts` as `./text.js`.
|
|
38
|
-
Plain TS module, no schema, no codegen.
|
|
39
|
-
- **`<extension-name>.test.ts`** (or `.test.mjs` / `.test.js`) is
|
|
40
|
-
the colocated test suite, picked up by the workspace's test glob
|
|
41
|
-
(`plugins/**/*.test.ts`).
|
|
42
|
-
|
|
43
|
-
Both are optional. The kernel ignores everything that isn't
|
|
44
|
-
`index.{js,mjs,ts}`, so future per-extension fixtures or schemas can
|
|
45
|
-
live in the same folder without manifest plumbing.
|
|
15
|
+
Every plugin is one or more of **six extension kinds**. Five of them form one continuous **deterministic flow** (the scan: fast, reproducible, offline), each step following the arrow into the next. **Hook** is the sixth: it sits to the side and reacts to events. Two of the five, **Action** and **Analyzer**, can additionally run in a **probabilistic** mode (an async LLM job), but they stay part of the deterministic flow.
|
|
46
16
|
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
17
|
+
```text
|
|
18
|
+
THE DETERMINISTIC FLOW ( the scan: fast · reproducible · offline )
|
|
19
|
+
═══════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
files on disk
|
|
22
|
+
│
|
|
23
|
+
▼
|
|
24
|
+
┌────────────┐
|
|
25
|
+
│ PROVIDER │ decides what counts as a node, and under which lens
|
|
26
|
+
└─────┬──────┘ e.g. .claude/skills/foo/SKILL.md → a Claude skill
|
|
27
|
+
▼
|
|
28
|
+
┌────────────┐
|
|
29
|
+
│ EXTRACTOR │ reads one node and pulls out its references and signals
|
|
30
|
+
└─────┬──────┘ e.g. an @architect mention → a link to that agent
|
|
31
|
+
▼
|
|
32
|
+
┌────────────┐
|
|
33
|
+
│ ANALYZER │ looks across the whole graph and flags problems
|
|
34
|
+
└─────┬──────┘ e.g. a link to a missing file → an Issue
|
|
35
|
+
▼
|
|
36
|
+
┌────────────┐
|
|
37
|
+
│ ACTION │ acts on a node (still on the deterministic flow); can
|
|
38
|
+
└─────┬──────┘ also run as an LLM job. e.g. Bump · Summarize (LLM)
|
|
39
|
+
▼
|
|
40
|
+
┌────────────┐
|
|
41
|
+
│ FORMATTER │ turns the finished graph into an output format
|
|
42
|
+
└────────────┘ e.g. the whole graph → an ASCII tree ( sm graph )
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
Off to the side, reacting to the whole lifecycle (never blocks it):
|
|
46
|
+
|
|
47
|
+
┌────────────┐
|
|
48
|
+
│ HOOK │ watches events and reacts with a side effect
|
|
49
|
+
└────────────┘ e.g. after a scan finishes → notify Slack
|
|
50
|
+
fires on: boot · scan · extractor/analyzer/action · job · shutdown
|
|
77
51
|
```
|
|
78
52
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
Drop the directory under `<cwd>/.skill-map/plugins/` and
|
|
82
|
-
`sm plugins list` picks it up. A folder/kind mismatch (e.g. an extractor placed
|
|
83
|
-
under `analyzers/`) surfaces as `invalid-manifest`.
|
|
53
|
+
Full per-kind contract, methods, modes, and one example each, lives in [The six extension kinds](#the-six-extension-kinds) below and in [`architecture.md` §Extension kinds](./architecture.md#extension-kinds).
|
|
84
54
|
|
|
85
55
|
---
|
|
86
56
|
|
|
@@ -88,16 +58,16 @@ under `analyzers/`) surfaces as `invalid-manifest`.
|
|
|
88
58
|
|
|
89
59
|
The kernel scans one root: `<cwd>/.skill-map/plugins/`, committed-with-the-repo plugins. There is no implicit user-level discovery (see [`cli-contract.md` §Scope is always project-local](./cli-contract.md)): plugins live with the project that uses them.
|
|
90
60
|
|
|
91
|
-
A plugin is any direct child directory of that root containing a `plugin.json`. Nested directories are not searched recursively. Pass `--plugin-dir <path>` to replace the default root with a custom directory (mostly for testing, or
|
|
61
|
+
A plugin is any direct child directory of that root containing a `plugin.json`. Nested directories are not searched recursively. Pass `--plugin-dir <path>` to replace the default root with a custom directory (mostly for testing, or a plugin set the operator opts into).
|
|
92
62
|
|
|
93
|
-
After every change to
|
|
63
|
+
After every change to `plugins/`, run `sm plugins list` to see each plugin's load status. The seven statuses are documented under [Diagnostics](#diagnostics).
|
|
94
64
|
|
|
95
65
|
### Plugin id uniqueness
|
|
96
66
|
|
|
97
67
|
The plugin `id` is the **directory name** (`<root>/<id>/plugin.json`), not a manifest field, and is **globally unique** across every active discovery root. The kernel enforces this in two places:
|
|
98
68
|
|
|
99
69
|
1. **Directory name IS the id.** A manifest carrying an `id` key is rejected as `invalid-manifest`. Same-root collisions are impossible by construction (a filesystem cannot host two siblings with the same name).
|
|
100
|
-
2. **Cross-root id collisions are blocked, both sides.** If two plugins from different roots (project + `--plugin-dir`) share a directory name, **both** receive status `id-collision`.
|
|
70
|
+
2. **Cross-root id collisions are blocked, both sides.** If two plugins from different roots (project + `--plugin-dir`) share a directory name, **both** receive status `id-collision`. No precedence rule, neither loads its extensions; the user renames one and reruns.
|
|
101
71
|
|
|
102
72
|
`sm plugins list` shows the conflict; `sm plugins doctor` exits `1` whenever any `id-collision` is present.
|
|
103
73
|
|
|
@@ -105,7 +75,7 @@ The plugin `id` is the **directory name** (`<root>/<id>/plugin.json`), not a man
|
|
|
105
75
|
|
|
106
76
|
Every extension is identified in the registry, and in any cross-extension reference, by its **qualified id** `<plugin-id>/<extension-id>`. The plugin id (the directory name) is therefore also the **namespace** for every extension the plugin ships.
|
|
107
77
|
|
|
108
|
-
|
|
78
|
+
Examples from the reference impl's built-in extensions:
|
|
109
79
|
|
|
110
80
|
| Extension | Short id (folder name) | Qualified id (in the registry) |
|
|
111
81
|
|---|---|---|
|
|
@@ -126,13 +96,13 @@ Built-ins split between two namespaces:
|
|
|
126
96
|
|
|
127
97
|
### Extension id shape
|
|
128
98
|
|
|
129
|
-
The convention
|
|
99
|
+
The convention on every built-in extension id is **`<domain>-<detail>`** (general to specific): the leftmost segment names the entity the extension reasons about (`node`, `link`, `annotation`, `reference`, `name`, ...), the rest narrows the behaviour. Examples: `annotation-orphan`, `link-counter`, `node-stability`, `name-reserved`, `reference-broken`. Even Actions live under their entity domain (`node-bump`, `node-set-tags`) rather than verb-style ids, so the catalog reads as a structured list.
|
|
130
100
|
|
|
131
|
-
Authors are not required to follow this, but it makes `sm plugins list` self-grouping. In the extension file, declare only the short id-bearing **folder name**, not a prefixed id; the loader composes `<plugin-id>/<short-id>` from `plugin.json` (the directory name) and the extension folder. Any
|
|
101
|
+
Authors are not required to follow this, but it makes `sm plugins list` self-grouping. In the extension file, declare only the short id-bearing **folder name**, not a prefixed id; the loader composes `<plugin-id>/<short-id>` from `plugin.json` (the directory name) and the extension folder. Any cross-extension reference (`precondition.analyzerIds`, ...) uses the qualified id of the target.
|
|
132
102
|
|
|
133
103
|
### Toggle model
|
|
134
104
|
|
|
135
|
-
Every extension is independently toggle-able by its qualified id `<plugin>/<ext-id>` (e.g. `claude/at-directive`, `core/reference-broken`). The **plugin row is a presentational grouping**, not the granular toggle target:
|
|
105
|
+
Every extension is independently toggle-able by its qualified id `<plugin>/<ext-id>` (e.g. `claude/at-directive`, `core/reference-broken`); this is the only model (no `granularity` manifest field). The **plugin row is a presentational grouping**, not the granular toggle target: `sm plugins list` and the Settings UI show a row per plugin, each extension listed underneath with its own enabled / disabled state.
|
|
136
106
|
|
|
137
107
|
Two id shapes resolve at the toggle surface:
|
|
138
108
|
|
|
@@ -141,15 +111,13 @@ Two id shapes resolve at the toggle surface:
|
|
|
141
111
|
- Single-extension plugin (`openai`, `antigravity`, `agent-skills`): applies directly, no prompt.
|
|
142
112
|
- Multi-extension plugin (`claude`, `core`): requires `--yes` OR an interactive TTY confirm. CI / pipe contexts must pass `--yes`.
|
|
143
113
|
|
|
144
|
-
`--all` is the cascade variant:
|
|
145
|
-
|
|
146
|
-
Resolution order per id: DB override (`config_plugins`) > `settings.json#/plugins/<id>/enabled` > installed default. The installed default is `true` for ordinary extensions and `false` for extensions declaring `stability: 'experimental'` or `stability: 'deprecated'` (they ship disabled until the operator opts in; see [Extension manifests](#extension-manifests)). Persisted toggle keys are always qualified `<plugin>/<ext>` ids (the bundle macro path expands at write time).
|
|
114
|
+
`--all` is the cascade variant: expands to every extension in every discovered plugin under the same `--yes` / TTY-confirm gate.
|
|
147
115
|
|
|
148
|
-
|
|
116
|
+
Resolution order per id: DB override (`config_plugins`) > `settings.json#/plugins/<id>/enabled` > installed default. The installed default is `true` for ordinary extensions and `false` for extensions declaring `stability: 'experimental'` or `stability: 'deprecated'` (they ship disabled until the operator opts in; see [Extension manifests](#extension-manifests)). Persisted toggle keys are always qualified `<plugin>/<ext>` ids (the bundle macro expands at write time).
|
|
149
117
|
|
|
150
118
|
### Extractor / Analyzer / Action `precondition`, narrow the pipeline
|
|
151
119
|
|
|
152
|
-
An Extractor, Analyzer, or Action MAY declare an optional `precondition` block. When declared, the kernel runs the extension **only** against nodes that satisfy every declared sub-filter, fail-fast (no context built, no method call)
|
|
120
|
+
An Extractor, Analyzer, or Action MAY declare an optional `precondition` block. When declared, the kernel runs the extension **only** against nodes that satisfy every declared sub-filter, fail-fast (no context built, no method call), wasting zero CPU on nodes it cannot process. The shape is shared across the three kinds:
|
|
153
121
|
|
|
154
122
|
```ts
|
|
155
123
|
precondition?: {
|
|
@@ -171,7 +139,7 @@ Prefer `precondition.kind` over `precondition.provider` when the filter is reall
|
|
|
171
139
|
|
|
172
140
|
**Unknown qualified kinds are non-blocking.** A `precondition.kind` naming a kind no installed Provider declares (typo, missing Provider plugin) still loads with status `enabled`; `sm plugins doctor` surfaces an informational `precondition-kind-unknown` warning without promoting its exit code, the matching Provider may arrive later.
|
|
173
141
|
|
|
174
|
-
Use case, a deterministic frontmatter-tag extractor that only makes sense for skills
|
|
142
|
+
Use case, a deterministic frontmatter-tag extractor that only makes sense for skills.
|
|
175
143
|
|
|
176
144
|
```javascript
|
|
177
145
|
export default {
|
|
@@ -198,11 +166,11 @@ export default {
|
|
|
198
166
|
|
|
199
167
|
### Module top-level side effects survive load timeouts
|
|
200
168
|
|
|
201
|
-
The plugin loader wraps every `import()` in an `AbortController`-backed timeout (5s in the reference impl).
|
|
169
|
+
The plugin loader wraps every `import()` in an `AbortController`-backed timeout (5s in the reference impl). On fire, the loader marks the plugin `load-error` and proceeds.
|
|
202
170
|
|
|
203
|
-
**Node cannot cancel an in-flight `import()`**: once the runtime evaluates the module, every top-level line WILL run, even after the loader gave up
|
|
171
|
+
**Node cannot cancel an in-flight `import()`**: once the runtime evaluates the module, every top-level line WILL run, even after the loader gave up (a top-level `setInterval`, `fetch`, filesystem write).
|
|
204
172
|
|
|
205
|
-
The contract is therefore: **do NOT do work at module top level**. Place every side effect inside an extension's lifecycle method (`extract`, `on`, `run`, ...) so it runs under the loop the kernel
|
|
173
|
+
The contract is therefore: **do NOT do work at module top level**. Place every side effect inside an extension's lifecycle method (`extract`, `on`, `run`, ...) so it runs under the loop the kernel drives, and only when the load succeeded. A failed compat check does not protect you, the loader imports the module before checking `specCompat`. For module-level state (e.g. a compiled regex), memoise it lazily inside the lifecycle method.
|
|
206
174
|
|
|
207
175
|
---
|
|
208
176
|
|
|
@@ -219,17 +187,19 @@ Required fields (normative shape in [`schemas/plugins-registry.schema.json#/$def
|
|
|
219
187
|
|
|
220
188
|
Optional fields: `storage` (`{ mode: 'kv' }` or `{ mode: 'dedicated', tables, migrations }`), `author`, `license` (SPDX), `homepage`, `repository`.
|
|
221
189
|
|
|
222
|
-
**Structure-as-truth.** The plugin id is the directory name, NOT a manifest field; a manifest carrying `id` is rejected. The manifest does NOT list extensions, the kernel discovers each by walking `<plugin-dir>/<kind>s/<name>/index.{js,mjs,ts}`. A Provider's kind catalog lives on disk at `<plugin>/kinds/<kindName>/{schema.json, kind.json}` (see [
|
|
190
|
+
**Structure-as-truth.** The plugin id is the directory name, NOT a manifest field; a manifest carrying `id` is rejected. The manifest does NOT list extensions, the kernel discovers each by walking `<plugin-dir>/<kind>s/<name>/index.{js,mjs,ts}`. A Provider's kind catalog lives on disk at `<plugin>/kinds/<kindName>/{schema.json, kind.json}` (see [Providers](#providers)).
|
|
191
|
+
|
|
192
|
+
**Files by convention.** Siblings of `index.{js,mjs,ts}` that the kernel does not recognise as an entry point are author files. Two names are blessed: **`text.ts`** holds the extension's externalised user-facing strings (one per extension, imported by `index.ts` as `./text.js`; plain TS, no schema, no codegen), and **`<extension-name>.test.ts`** (or `.test.mjs` / `.test.js`) is the colocated test suite, picked up by the workspace test glob (`plugins/**/*.test.ts`). Both optional. The kernel ignores everything that is not `index.{js,mjs,ts}`, so future per-extension fixtures or schemas live in the same folder without manifest plumbing.
|
|
223
193
|
|
|
224
194
|
### `specCompat` strategy
|
|
225
195
|
|
|
226
|
-
Pre-`v1.0.0`, narrow ranges are the defensive default: minor bumps MAY carry breaking changes per [`versioning.md`](./versioning.md), so a plugin spanning minor boundaries can load
|
|
196
|
+
Pre-`v1.0.0`, narrow ranges are the defensive default: minor bumps MAY carry breaking changes per [`versioning.md`](./versioning.md), so a plugin spanning minor boundaries can load then crash at first use against a changed schema. Pin to the minor you tested (`"^0.40.0"` resolves any `0.40.x`; `">=0.40.0 <0.41.0"` is the explicit form). After v1.0.0, `"^1.0.0"` is recommended for most plugins.
|
|
227
197
|
|
|
228
198
|
---
|
|
229
199
|
|
|
230
200
|
## The six extension kinds
|
|
231
201
|
|
|
232
|
-
The kernel knows six categories. Each has a JSON Schema under [`schemas/extensions/`](./schemas/extensions/); the kernel validates every manifest against the schema for its declared kind at load time. The full per-kind behavioural contract lives in [`architecture.md` §Extension kinds](./architecture.md#extension-kinds)
|
|
202
|
+
The kernel knows six categories. Each has a JSON Schema under [`schemas/extensions/`](./schemas/extensions/); the kernel validates every manifest against the schema for its declared kind at load time. The full per-kind behavioural contract lives in [`architecture.md` §Extension kinds](./architecture.md#extension-kinds); this section is the author-facing summary plus one example per kind.
|
|
233
203
|
|
|
234
204
|
| Kind | Method | Receives | Returns | Mode |
|
|
235
205
|
|---|---|---|---|---|
|
|
@@ -240,9 +210,9 @@ The kernel knows six categories. Each has a JSON Schema under [`schemas/extensio
|
|
|
240
210
|
| `formatter` | `format(ctx)` | full graph | `string` | deterministic only |
|
|
241
211
|
| `hook` | `on(ctx)` | a curated lifecycle event payload | `void` (side effects) | **deterministic only** |
|
|
242
212
|
|
|
243
|
-
The runtime instance you `export default` includes both the manifest fields (`version`, `description`, plus kind-specific metadata) AND the runtime method. The kernel strips function-typed properties before AJV-validating the manifest, so the method lives
|
|
213
|
+
The runtime instance you `export default` includes both the manifest fields (`version`, `description`, plus kind-specific metadata) AND the runtime method. The kernel strips function-typed properties before AJV-validating the manifest, so the method lives beside metadata.
|
|
244
214
|
|
|
245
|
-
Base manifest fields shared by every kind (normative shape in [`schemas/extensions/base.schema.json`](./schemas/extensions/base.schema.json)): `version` (required for external plugins), `description` (required), and the optional `stability`, `order`, `annotation`, `settings`. `stability` (`'experimental' | 'beta' | 'stable' | 'deprecated'`, default `stable`) is a lifecycle label: the non-default values render as a badge next to the extension in `sm plugins list <id>` / `sm plugins show` and the Settings plugins panel.
|
|
215
|
+
Base manifest fields shared by every kind (normative shape in [`schemas/extensions/base.schema.json`](./schemas/extensions/base.schema.json)): `version` (required for external plugins), `description` (required), and the optional `stability`, `order`, `annotation`, `settings`. `stability` (`'experimental' | 'beta' | 'stable' | 'deprecated'`, default `stable`) is a lifecycle label: the non-default values render as a badge next to the extension in `sm plugins list <id>` / `sm plugins show` and the Settings plugins panel. Presentation-only for `beta` and `stable`, but `experimental` and `deprecated` additionally flip the extension's installed default to DISABLED: it does not load (does not run, does not register, toggle shows off) until the operator opts in via `sm plugins enable <plugin>/<ext>`, the Settings toggle, or a `settings.json` / `config_plugins` override. The opt-in wins over the installed default, so a `deprecated` extension can be kept running during a migration. A stable extension omits the field; declaring `stability: 'stable'` is valid but renders nothing.
|
|
246
216
|
|
|
247
217
|
### Extractors
|
|
248
218
|
|
|
@@ -250,14 +220,14 @@ Pure single-node analysis. **Never** read another node, the graph, or the databa
|
|
|
250
220
|
|
|
251
221
|
`extract(ctx) → void`. Output flows through callbacks the kernel binds onto `ctx`:
|
|
252
222
|
|
|
253
|
-
- **`ctx.emitLink(link)`**, append a `Link`. The kernel validates `link.kind` against the **global closed enum** (`invokes`, `references`, `mentions`, `points`); off-enum kinds drop as `extension.error`. Confidence is declared per emit (default `'medium'`). URL-shaped targets are partitioned into `node.externalRefsCount` and never persisted. (
|
|
223
|
+
- **`ctx.emitLink(link)`**, append a `Link`. The kernel validates `link.kind` against the **global closed enum** (`invokes`, `references`, `mentions`, `points`); off-enum kinds drop as `extension.error`. Confidence is declared per emit (default `'medium'`). URL-shaped targets are partitioned into `node.externalRefsCount` and never persisted. (No per-extractor `emitsLinkKinds` allowlist anymore.)
|
|
254
224
|
- **`ctx.enrichNode(partial)`**, merge kernel-curated properties onto the node's enrichment layer (persisted into `node_enrichments`). **Strictly separate from the author frontmatter**, which is immutable from any Extractor. Use it for inferred facts (computed titles, summaries) the author did not write.
|
|
255
225
|
- **`ctx.emitContribution(id, payload)`**, view contributions (see [View contributions](#view-contributions)).
|
|
256
226
|
- **`ctx.store`**, plugin-scoped persistence, present only when `plugin.json` declares `storage.mode`. See [`plugin-kv-api.md`](./plugin-kv-api.md).
|
|
257
227
|
|
|
258
228
|
You can read `ctx.node.sidecar.*` freely: the per-`(node, extractor)` cache hashes the sidecar `annotations` block alongside the body, so a `.sm`-only edit invalidates the cached run automatically.
|
|
259
229
|
|
|
260
|
-
> **Pick a syntax that doesn't collide with built-ins.** `core/at-directive` claims `@`, `core/slash-command` claims `/`, both with LLM-aligned semantics (and both strip fenced code blocks + inline backticks before matching). `core/backtick-path` is the deliberate inverse: it matches relative `.md` paths ONLY inside those stripped code regions, so
|
|
230
|
+
> **Pick a syntax that doesn't collide with built-ins.** `core/at-directive` claims `@`, `core/slash-command` claims `/`, both with LLM-aligned semantics (and both strip fenced code blocks + inline backticks before matching). `core/backtick-path` is the deliberate inverse: it matches relative `.md` paths ONLY inside those stripped code regions, so it cannot overlap the prose-side extractors. A new extractor matching one of those prefixes fires on the same input and emits a competing link; when both resolve to the same node that surfaces as `reference-redundant` (`name-collision` is reserved for two nodes declaring the same resolvable `name`, not for overlapping invocation forms). The example below uses a wikilink-style `[[ref:<name>]]` pattern to side-step the overlap. See [`architecture.md` §Extractor · trigger normalization](./architecture.md#extractor--trigger-normalization) for the normalization pipeline.
|
|
261
231
|
|
|
262
232
|
```javascript
|
|
263
233
|
export default {
|
|
@@ -283,7 +253,7 @@ export default {
|
|
|
283
253
|
|
|
284
254
|
Cross-node reasoning over the merged graph; runs after every Provider and extractor. Dual-mode (`mode: 'deterministic'` default, `'probabilistic'` opt-in). Deterministic analyzers run synchronously inside `sm scan` / `sm check`; probabilistic ones dispatch as jobs and NEVER participate in the deterministic scan pipeline. Optional `precondition` and `ui`. Spec at [`schemas/extensions/analyzer.schema.json`](./schemas/extensions/analyzer.schema.json).
|
|
285
255
|
|
|
286
|
-
The analyzer↔action relationship is declared from the **Action** side via `precondition.analyzerIds` (Modelo B);
|
|
256
|
+
The analyzer↔action relationship is declared from the **Action** side via `precondition.analyzerIds` (Modelo B); no `recommendedActions` field on the Analyzer.
|
|
287
257
|
|
|
288
258
|
```javascript
|
|
289
259
|
export default {
|
|
@@ -310,7 +280,7 @@ export default {
|
|
|
310
280
|
|
|
311
281
|
### Score-phase analyzers
|
|
312
282
|
|
|
313
|
-
An analyzer that declares `phase: 'score'` runs in the kernel's write-capable phase, BEFORE every read-only (`detect` / `aggregate`) analyzer. It is the only place a plugin may adjust link confidence. Declare the phase in the manifest and call `ctx.adjustConfidence(link, op)` from `evaluate` (the callback is present ONLY in the `score` phase; guard for `undefined` so the same code is inert
|
|
283
|
+
An analyzer that declares `phase: 'score'` runs in the kernel's write-capable phase, BEFORE every read-only (`detect` / `aggregate`) analyzer. It is the only place a plugin may adjust link confidence. Declare the phase in the manifest and call `ctx.adjustConfidence(link, op)` from `evaluate` (the callback is present ONLY in the `score` phase; guard for `undefined` so the same code is inert outside it):
|
|
314
284
|
|
|
315
285
|
```javascript
|
|
316
286
|
// analyzers/demote-mentions/index.js → phase: 'score'
|
|
@@ -339,9 +309,9 @@ The `op` is one of four kinds:
|
|
|
339
309
|
| `floor` | Raise to at least `value`. | raises only |
|
|
340
310
|
| `ceil` | Lower to at most `value`. | lowers only |
|
|
341
311
|
|
|
342
|
-
`link` MUST be one of `ctx.links` (matched by object identity). The kernel seeds a **1.0 baseline** on every link, then **folds** every op contributed to that link (across all scorers) into the final `link.confidence`, deterministically and order-independently: from the 1.0 baseline it applies `set` (last in canonical order wins), then sums `delta`, then `floor` (raise), then `ceil` (cap), and clamps to `[0,1]`
|
|
312
|
+
`link` MUST be one of `ctx.links` (matched by object identity). The kernel seeds a **1.0 baseline** on every link, then **folds** every op contributed to that link (across all scorers) into the final `link.confidence`, deterministically and order-independently: from the 1.0 baseline it applies `set` (last in canonical order wins), then sums `delta`, then `floor` (raise), then `ceil` (cap), and clamps to `[0,1]` once at the end (so a `-0.4` then `+0.4` round-trips to the base instead of clipping mid-fold). Across scorers the ops sort by `(pluginId, extensionId)`, so two scans always produce the same value and adjustment ordering. Each applied op is attributed to your plugin / extension and persisted to the `scan_link_scores` audit table (the "why is this link at X?" trail).
|
|
343
313
|
|
|
344
|
-
The kernel **dogfoods this exact API** through two built-in score-phase detectors, each co-locating its penalty `delta` with the finding it reports: `core/name-reserved` (reserved → `delta -0.9` → 0.1, alongside its warns) and `core/reference-broken` (broken → `delta -0.5` → 0.5, alongside its errors). A clean-resolved link keeps the 1.0 baseline (no built-in op).
|
|
314
|
+
The kernel **dogfoods this exact API** through two built-in score-phase detectors, each co-locating its penalty `delta` with the finding it reports: `core/name-reserved` (reserved → `delta -0.9` → 0.1, alongside its warns) and `core/reference-broken` (broken → `delta -0.5` → 0.5, alongside its errors). A clean-resolved link keeps the 1.0 baseline (no built-in op). The pattern to copy: **detect, report, AND score in one `phase: 'score'` evaluate**, so disabling a rule drops both effects together (no report, no confidence move, the link falls back to baseline). Your scorer composes ON TOP of that baseline: same phase, same links, ops folded with the built-ins'. To subtract, use a negative `delta`; to RAISE, a positive `delta` or a `floor`; to cap, a `ceil`. See [`architecture.md` §Analyzer phases](./architecture.md#analyzer-phases) for the normative fold semantics.
|
|
345
315
|
|
|
346
316
|
### Formatters
|
|
347
317
|
|
|
@@ -365,9 +335,9 @@ export default {
|
|
|
365
335
|
|
|
366
336
|
### Hooks
|
|
367
337
|
|
|
368
|
-
Declarative subscribers to a curated set of kernel lifecycle events. **Deterministic-only**: a hook reacts to events and cannot mutate the pipeline, block emission, or alter outputs. Errors are caught by the dispatcher (logged as `extension.error` with `kind: 'hook-error'`) and NEVER block the main flow. LLM-dependent reactions are modeled as a deterministic Hook that enqueues a probabilistic Action via `ctx.queue('<plugin>/<action>', payload)`. Spec at [`schemas/extensions/hook.schema.json`](./schemas/extensions/hook.schema.json);
|
|
338
|
+
Declarative subscribers to a curated set of kernel lifecycle events. **Deterministic-only**: a hook reacts to events and cannot mutate the pipeline, block emission, or alter outputs. Errors are caught by the dispatcher (logged as `extension.error` with `kind: 'hook-error'`) and NEVER block the main flow. LLM-dependent reactions are modeled as a deterministic Hook that enqueues a probabilistic Action via `ctx.queue('<plugin>/<action>', payload)`. Spec at [`schemas/extensions/hook.schema.json`](./schemas/extensions/hook.schema.json); triggers at [`architecture.md` §Hook · curated trigger set](./architecture.md#hook--curated-trigger-set).
|
|
369
339
|
|
|
370
|
-
The ten hookable triggers (any other
|
|
340
|
+
The ten hookable triggers (any other yields `invalid-manifest`): eight pipeline-driven, `scan.started`, `scan.completed`, `extractor.completed`, `analyzer.completed`, `action.completed`, `job.spawning`, `job.completed`, `job.failed`, plus two CLI-process-driven, `boot` (before verb routing) and `shutdown` (after the verb's exit code resolves).
|
|
371
341
|
|
|
372
342
|
```javascript
|
|
373
343
|
export default {
|
|
@@ -394,9 +364,9 @@ export default {
|
|
|
394
364
|
|
|
395
365
|
### Providers
|
|
396
366
|
|
|
397
|
-
Recognise a platform and declare a kind catalog. The catalog lives **on disk** (structure-as-truth): each kind under `<plugin>/kinds/<kindName>/` ships exactly two files, `schema.json` (the kind's frontmatter JSON Schema, MUST extend [`schemas/frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) via `allOf` + `$ref`) and `kind.json` (per-kind metadata, today `{ ui: { label, color, colorDark?, emoji?, icon? } }`, validated against [`provider-kind.schema.json`](./schemas/extensions/provider-kind.schema.json)). The kernel derives the supported kind set from the `kinds/` directory listing;
|
|
367
|
+
Recognise a platform and declare a kind catalog. The catalog lives **on disk** (structure-as-truth): each kind under `<plugin>/kinds/<kindName>/` ships exactly two files, `schema.json` (the kind's frontmatter JSON Schema, MUST extend [`schemas/frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) via `allOf` + `$ref`) and `kind.json` (per-kind metadata, today `{ ui: { label, color, colorDark?, emoji?, icon? } }`, validated against [`provider-kind.schema.json`](./schemas/extensions/provider-kind.schema.json)). The kernel derives the supported kind set from the `kinds/` directory listing; no inline `kinds` map and no `defaultRefreshAction` field.
|
|
398
368
|
|
|
399
|
-
The Provider manifest
|
|
369
|
+
The Provider manifest declares a top-level `presentation` block (its own identity in the lens dropdown / topbar / per-card chip, distinct from its kinds' `ui`), plus optional `detect`, `roots`, `gatedByActiveLens`, `read`, and `resolverRules`. The walker hardcodes the paths it scans within the project (`.claude/`, `.codex/`, ...); the kernel never extends the scan into `$HOME`. Spec at [`schemas/extensions/provider.schema.json`](./schemas/extensions/provider.schema.json); full behaviour (dispatch order, the universal markdown fallback, resolution / reservedNames / identifiers) in [`architecture.md` §Extension kinds](./architecture.md#extension-kinds).
|
|
400
370
|
|
|
401
371
|
```text
|
|
402
372
|
my-provider/
|
|
@@ -411,16 +381,18 @@ my-provider/
|
|
|
411
381
|
|
|
412
382
|
Operate on one or more nodes. Dual-mode (`mode` optional, default `'deterministic'`). Files-by-convention: every Action carries `<action-dir>/report.schema.json`; probabilistic Actions additionally carry `<action-dir>/prompt.md`. Probabilistic estimates go in `probExpectedDurationSeconds` (drives job TTL). Optional `precondition` (including `analyzerIds`, the Modelo B link). Spec at [`schemas/extensions/action.schema.json`](./schemas/extensions/action.schema.json).
|
|
413
383
|
|
|
384
|
+
An Action whose `invoke()` returns a sidecar write (`writes: [{ kind: 'sidecar', ... }]`) MUST declare the capability on its manifest as `writes: ['sidecar']`. Consumers gate on the declaration without invoking: when a project sets `allowSidecarWriters: false`, the scan composer drops every Action declaring `sidecar` (so its `inspector.action.button` never renders) and the sidecar store refuses the write. Omit the field for read-only / report-only Actions.
|
|
385
|
+
|
|
414
386
|
An Action has two independent surfaces:
|
|
415
387
|
|
|
416
388
|
- **`invoke(input, ctx)`**, the on-demand executor the user triggers (deterministic in-process code, or a probabilistic rendered prompt the runner executes). Unit-test deterministic ones by calling `invoke(input, ctx)` with a fake context; probabilistic ones still need a live kernel until Step 10 lands the job subsystem.
|
|
417
|
-
- **`project(ctx)`** (optional), a deterministic, side-effect-free, scan-time method with read-only graph access (`ctx.nodes`, `ctx.links`) plus `ctx.emitContribution(nodePath, ref, payload)`. Use it to self-project the Action's own UI affordance, typically an `inspector.action.button` declared in the manifest `ui` map (see [View contributions](#view-contributions)), computing the per-node `enabled` / prompt `options` from the live graph.
|
|
389
|
+
- **`project(ctx)`** (optional), a deterministic, side-effect-free, scan-time method with read-only graph access (`ctx.nodes`, `ctx.links`) plus `ctx.emitContribution(nodePath, ref, payload)`. Use it to self-project the Action's own UI affordance, typically an `inspector.action.button` declared in the manifest `ui` map (see [View contributions](#view-contributions)), computing the per-node `enabled` / prompt `options` from the live graph. It stays deterministic even when `invoke` is probabilistic, and runs every scan (same cost as an analyzer's emit). This is how built-in buttons like Set stability / Bump are produced: the dispatching Action owns its button, no separate "projector" analyzer. Unit-test it by calling `project(ctx)` with a fake `{ nodes, links, emitContribution }` and asserting the captured payload.
|
|
418
390
|
|
|
419
391
|
---
|
|
420
392
|
|
|
421
393
|
## Frontmatter validation, three-tier model
|
|
422
394
|
|
|
423
|
-
The kernel validates frontmatter on a graduated dial; tighter is opt-in. The policy lives in **analyzers**, not the JSON Schemas
|
|
395
|
+
The kernel validates frontmatter on a graduated dial; tighter is opt-in. The policy lives in **analyzers**, not the JSON Schemas: schemas stay shape-only ([`base.schema.json`](./schemas/frontmatter/base.schema.json) declares `additionalProperties: true`) so authors extend their own nodes without forking the spec. Per-kind schemas live with the **Provider** that emits the kind.
|
|
424
396
|
|
|
425
397
|
| Tier | Mechanism | Behaviour on unknown / non-conforming fields |
|
|
426
398
|
|---|---|---|
|
|
@@ -432,7 +404,7 @@ Tier 1 is normative: the kernel ships the analyzer out of the box. To keep an un
|
|
|
432
404
|
|
|
433
405
|
### Why no "schema-extender" plugin kind
|
|
434
406
|
|
|
435
|
-
To make custom frontmatter keys first-class, write a deterministic **Analyzer** that reads the keys from `node.frontmatter` (Tier 0
|
|
407
|
+
To make custom frontmatter keys first-class, write a deterministic **Analyzer** that reads the keys from `node.frontmatter` (Tier 0 exposes them), validates against your domain shape, and emits Issues. A "schema-extender" kind would force every consumer to re-resolve the active schema set per scan; the analyzer-driven approach keeps the parser one-pass and the validation surface composable. For a CI-blocking check, the analyzer emits at `severity: 'error'` directly (`--strict` / `scan.strict` apply only to the kernel's own frontmatter warnings).
|
|
436
408
|
|
|
437
409
|
---
|
|
438
410
|
|
|
@@ -446,7 +418,7 @@ A plugin that persists state declares `storage` in its manifest. Two modes, both
|
|
|
446
418
|
{ "storage": { "mode": "kv" } }
|
|
447
419
|
```
|
|
448
420
|
|
|
449
|
-
Backed by the kernel-owned `state_plugin_kvs` table. `ctx.store` exposes `get` / `set` / `list` / `delete`. No migrations, ready immediately. Pick KV
|
|
421
|
+
Backed by the kernel-owned `state_plugin_kvs` table. `ctx.store` exposes `get` / `set` / `list` / `delete`. No migrations, ready immediately. Pick KV for a small map (< ~1 MB, simple key lookup or prefix list); 90% of plugins fit.
|
|
450
422
|
|
|
451
423
|
### Mode B, Dedicated
|
|
452
424
|
|
|
@@ -469,7 +441,7 @@ The plugin owns SQL tables prefixed `plugin_<normalizedId>_*`. Migrations live u
|
|
|
469
441
|
- **Mode A**: `storage.schema` (single value-shape) validates every `ctx.store.set(key, value)`.
|
|
470
442
|
- **Mode B**: `storage.schemas` (sparse map, table → schema path) validates `ctx.store.write(table, row)` for the named tables; tables absent from the map accept any shape.
|
|
471
443
|
|
|
472
|
-
A schema file missing / unparseable / AJV-rejected at load flips the plugin to `load-error`. A write violating its declared schema throws synchronously, naming the plugin, table, and AJV errors. Skip validation for free-form payloads (cache rows, counters)
|
|
444
|
+
A schema file missing / unparseable / AJV-rejected at load flips the plugin to `load-error`. A write violating its declared schema throws synchronously, naming the plugin, table, and AJV errors. Skip validation for free-form payloads (cache rows, counters), friction with no payoff.
|
|
473
445
|
|
|
474
446
|
---
|
|
475
447
|
|
|
@@ -483,11 +455,11 @@ A `probabilistic` Analyzer / Action receives `ctx.runner` (a `RunnerPort`) and d
|
|
|
483
455
|
|
|
484
456
|
## Annotation contribution
|
|
485
457
|
|
|
486
|
-
>
|
|
458
|
+
> A plugin that writes a first-class field into a node's co-located `.sm` sidecar declares it via the optional `annotation` block on its extension manifest. The kernel validates it at load time, surfaces the runtime catalog via `kernel.getRegisteredAnnotationKeys()` (consumed by the BFF / UI for autocomplete), and treats two plugins claiming the same root-exclusive key as a fatal startup error. Normative contract: [`architecture.md` §Annotation system → Plugin contributions](./architecture.md#plugin-contributions).
|
|
487
459
|
|
|
488
460
|
### Manifest shape
|
|
489
461
|
|
|
490
|
-
`annotation` is a **single** declaration per extension; **the contributed key is the extension's id** (its folder name). An extension
|
|
462
|
+
`annotation` is a **single** declaration per extension; **the contributed key is the extension's id** (its folder name). An extension needing several keys splits into several extensions, one per key. The block declares an inline JSON Schema for the value plus two policy fields:
|
|
491
463
|
|
|
492
464
|
```js
|
|
493
465
|
// my-plugin/extractors/last-reviewed-at/index.js → contributes key `last-reviewed-at`
|
|
@@ -529,7 +501,7 @@ auditor: # plugin 'auditor', same key, different namespac
|
|
|
529
501
|
last-reviewed-at: 2026-05-05T18:30:00Z
|
|
530
502
|
```
|
|
531
503
|
|
|
532
|
-
A top-level (root) key requires `location: 'root'` AND `ownership: 'exclusive'`. The pair travels together: `.sm` writes deep-merge per the `SidecarStore` contract, so a shared root key would route non-deterministically. Use root sparingly
|
|
504
|
+
A top-level (root) key requires `location: 'root'` AND `ownership: 'exclusive'`. The pair travels together: `.sm` writes deep-merge per the `SidecarStore` contract, so a shared root key would route non-deterministically. Use root sparingly; each root contribution reserves that name across the whole installed-plugin surface.
|
|
533
505
|
|
|
534
506
|
```js
|
|
535
507
|
// compliance-plugin/analyzers/compliance/index.js → contributes root key `compliance`
|
|
@@ -556,7 +528,7 @@ export default {
|
|
|
556
528
|
- **`shared`** (default): multiple plugins MAY write the same key; each gets its own namespaced block, last-write-wins per `(plugin, key)` in `FilesystemSidecarStore.applyPatch`.
|
|
557
529
|
- **`exclusive`**: only this plugin may write the key. The kernel rejects any other plugin claiming the same `(key, location: 'root')` tuple. `exclusive` + `namespaced` is permitted but redundant (the namespace already isolates).
|
|
558
530
|
|
|
559
|
-
Two plugins claiming the same `(key, location: 'root', ownership: 'exclusive')` tuple is a **fatal startup error**: `loadPluginRuntime` throws `AnnotationContributionConflictError`, the host exits non-zero, the kernel does NOT boot. This is the only fatal path on the plugin-load surface (every other failure is per-plugin and the kernel keeps booting on the survivors), because otherwise annotated `.sm` files
|
|
531
|
+
Two plugins claiming the same `(key, location: 'root', ownership: 'exclusive')` tuple is a **fatal startup error**: `loadPluginRuntime` throws `AnnotationContributionConflictError`, the host exits non-zero, the kernel does NOT boot. This is the only fatal path on the plugin-load surface (every other failure is per-plugin and the kernel keeps booting on the survivors), because otherwise annotated `.sm` files route non-deterministically.
|
|
560
532
|
|
|
561
533
|
### Typo guard and runtime catalog
|
|
562
534
|
|
|
@@ -568,7 +540,7 @@ The runtime catalog is reachable via `kernel.getRegisteredAnnotationKeys()` (eac
|
|
|
568
540
|
|
|
569
541
|
## View contributions
|
|
570
542
|
|
|
571
|
-
> Lets plugins surface per-node data in the UI **without shipping any UI code**. You pick a **slot** by name from a closed kernel catalog; the slot fixes both
|
|
543
|
+
> Lets plugins surface per-node data in the UI **without shipping any UI code**. You pick a **slot** by name from a closed kernel catalog; the slot fixes both renderer and payload shape. You declare per-node emissions in the extension manifest's `ui` map and emit payloads at scan time via `ctx.emitContribution(...)`. Normative contract: [`architecture.md` §View contribution system](./architecture.md#view-contribution-system).
|
|
572
544
|
|
|
573
545
|
### What you NEVER write
|
|
574
546
|
|
|
@@ -600,7 +572,7 @@ Inside an extractor or analyzer manifest, declare a `ui` map (sibling of `annota
|
|
|
600
572
|
}
|
|
601
573
|
```
|
|
602
574
|
|
|
603
|
-
In TypeScript, declare each contribution as a module-level const with `satisfies IViewContribution` and build `ui` by shorthand.
|
|
575
|
+
In TypeScript, declare each contribution as a module-level const with `satisfies IViewContribution` and build `ui` by shorthand. Emit by passing the SAME object by reference (see [Emit path](#emit-path)) for a typed payload:
|
|
604
576
|
|
|
605
577
|
```ts
|
|
606
578
|
import type { IViewContribution } from '@skill-map/cli';
|
|
@@ -619,7 +591,7 @@ export default {
|
|
|
619
591
|
};
|
|
620
592
|
```
|
|
621
593
|
|
|
622
|
-
The `ui` **key** (kebab-case per the manifest schema) is the contribution id; the const's variable name is incidental,
|
|
594
|
+
The `ui` **key** (kebab-case per the manifest schema) is the contribution id; the const's variable name is incidental, since the kernel matches an emission to its declaration by object identity, not by name. Plain `.js` plugins use the same shape without `satisfies` (runtime check, not compile-time).
|
|
623
595
|
|
|
624
596
|
Field reference (full schema in [`schemas/view-slots.schema.json`](./schemas/view-slots.schema.json) at `$defs/IViewContribution`):
|
|
625
597
|
|
|
@@ -651,7 +623,7 @@ A bare name without a prefix (`"search"`) is rejected at load. Emoji is the cros
|
|
|
651
623
|
|
|
652
624
|
### Slot catalog (closed, 14 slots)
|
|
653
625
|
|
|
654
|
-
The kernel ships exactly these 14 slots. Each fixes a renderer + a payload shape; the **per-slot semantics, edge cases, and exact payload schemas are the canonical reference in [`view-slots.md`](./view-slots.md)** (and [`schemas/view-slots.schema.json`](./schemas/view-slots.schema.json) at `$defs/payloads/<slot>`). Read those before emitting. Adding a slot
|
|
626
|
+
The kernel ships exactly these 14 slots. Each fixes a renderer + a payload shape; the **per-slot semantics, edge cases, and exact payload schemas are the canonical reference in [`view-slots.md`](./view-slots.md)** (and [`schemas/view-slots.schema.json`](./schemas/view-slots.schema.json) at `$defs/payloads/<slot>`). Read those before emitting. Adding a slot needs a spec / UI / scaffolder round-trip.
|
|
655
627
|
|
|
656
628
|
| Slot | Renderer |
|
|
657
629
|
|---|---|
|
|
@@ -672,7 +644,7 @@ The kernel ships exactly these 14 slots. Each fixes a renderer + a payload shape
|
|
|
672
644
|
|
|
673
645
|
### Inspector grouping and `order`
|
|
674
646
|
|
|
675
|
-
The six `inspector.body.panel.*` contributions are not rendered in a shared drawer. The inspector groups them **one collapsible section per plugin**, titled by the plugin id (host-applied from the trusted contribution `pluginId`, never the payload) and **collapsed by default**. A plugin's bricks only
|
|
647
|
+
The six `inspector.body.panel.*` contributions are not rendered in a shared drawer. The inspector groups them **one collapsible section per plugin**, titled by the plugin id (host-applied from the trusted contribution `pluginId`, never the payload) and **collapsed by default**. A plugin's bricks only land in its own section; it cannot contribute into another plugin's space.
|
|
676
648
|
|
|
677
649
|
Two optional, inspector-only `order` hints (both `number`, default `100`) control layout:
|
|
678
650
|
|
|
@@ -685,7 +657,7 @@ Two optional, inspector-only `order` hints (both `number`, default `100`) contro
|
|
|
685
657
|
|
|
686
658
|
### Chip vs Issue
|
|
687
659
|
|
|
688
|
-
For analyzers, a per-node card surfaces a finding through two independent channels: the `Issue` returned by `evaluate(ctx)` feeds the aggregated stats and the scan / check exit code; a view contribution to a card slot is **purely presentational** (its `severity` controls only the chip's own colour, never the count, never the exit code). The colour rule
|
|
660
|
+
For analyzers, a per-node card surfaces a finding through two independent channels: the `Issue` returned by `evaluate(ctx)` feeds the aggregated stats and the scan / check exit code; a view contribution to a card slot is **purely presentational** (its `severity` controls only the chip's own colour, never the count, never the exit code). The colour rule (when a chip may paint `warn` / `danger`) and the reserved status of `graph.node.alert` are documented in [`view-slots.md` §Chip vs Issue](./view-slots.md). Breaking it produces misleading cards and is caught in code review, not by the schema.
|
|
689
661
|
|
|
690
662
|
### Emit path
|
|
691
663
|
|
|
@@ -698,9 +670,9 @@ ctx.emitContribution(total, { value });
|
|
|
698
670
|
ctx.emitContribution(nodePath, breakdown, { bars: [...] });
|
|
699
671
|
```
|
|
700
672
|
|
|
701
|
-
Pass the contribution **object you declared in `ui`, by reference** (the `const` above), not a string id. The kernel recovers the contribution id (the `ui` key) by object identity and looks up the declared slot to validate the payload against `view-slots.schema.json#/$defs/payloads/<slot>`. The payload argument is typed from `ref.slot` (`SlotPayload<C['slot']>`), so a wrong-shape payload is a **compile error** in TypeScript. At runtime, a ref
|
|
673
|
+
Pass the contribution **object you declared in `ui`, by reference** (the `const` above), not a string id. The kernel recovers the contribution id (the `ui` key) by object identity and looks up the declared slot to validate the payload against `view-slots.schema.json#/$defs/payloads/<slot>`. The payload argument is typed from `ref.slot` (`SlotPayload<C['slot']>`), so a wrong-shape payload is a **compile error** in TypeScript. At runtime, a ref not among your declared `ui` objects (a spread copy, an inline literal) or an off-shape payload emits an `extension.error` and drops, same posture as `emitLink`. For `topbar.nav.start`, analyzers use `ctx.emitScopeContribution(ref, payload)` (reserved in the spec; the runtime callback lands when the first scope-level adopter arrives).
|
|
702
674
|
|
|
703
|
-
To surface the same data in two surfaces, declare two contributions (one per slot) and emit twice
|
|
675
|
+
To surface the same data in two surfaces, declare two contributions (one per slot) and emit twice; no broadcast.
|
|
704
676
|
|
|
705
677
|
---
|
|
706
678
|
|
|
@@ -732,9 +704,9 @@ The kernel exposes resolved settings via `ctx.settings.<settingId>`. Settings ar
|
|
|
732
704
|
|
|
733
705
|
### Setting values and the operator
|
|
734
706
|
|
|
735
|
-
The manifest declares the *shape* (label, type, default); the **operator** supplies the *values*. Non-`secret` values live in the project config under `plugins.<pluginId>.extensions.<extId>.settings.<settingId>` (the extension id is the leaf folder name, not the qualified `<plugin>/<ext>` id, the plugin is already the parent key), so a team can commit them in `settings.json` or keep a per-checkout override in `settings.local.json`. The
|
|
707
|
+
The manifest declares the *shape* (label, type, default); the **operator** supplies the *values*. Non-`secret` values live in the project config under `plugins.<pluginId>.extensions.<extId>.settings.<settingId>` (the extension id is the leaf folder name, not the qualified `<plugin>/<ext>` id, the plugin is already the parent key), so a team can commit them in `settings.json` or keep a per-checkout override in `settings.local.json`. The settings resolver builds the runtime `ctx.settings` object from each declared setting's `default`, overlaying the merged config value, validating against the input-type's value schema; a value failing validation drops back to the default with a warning (the scan never crashes on bad settings). `project-config.schema.json` keeps the `settings` object permissive (`additionalProperties: true`); per-type validation is the resolver's job, since the static schema cannot know which type a given `settingId` picked.
|
|
736
708
|
|
|
737
|
-
`secret` settings are the exception on WHERE they land: the kernel forces them into project-local `settings.local.json` (gitignored), never the committed `settings.json`, so a token never travels via the shared repo. There is **no encryption** (the value is plain text on the local machine); the only protection is "does not leave the checkout". An optional `envVar` lets CI inject the value without writing it to disk
|
|
709
|
+
`secret` settings are the exception on WHERE they land: the kernel forces them into project-local `settings.local.json` (gitignored), never the committed `settings.json`, so a token never travels via the shared repo. There is **no encryption** (the value is plain text on the local machine); the only protection is "does not leave the checkout". An optional `envVar` lets CI inject the value without writing it to disk. See `input-types.schema.json#/$defs/Setting_Secret`.
|
|
738
710
|
|
|
739
711
|
The operator reads and writes values through the CLI (UI form is the parallel surface):
|
|
740
712
|
|
|
@@ -748,7 +720,7 @@ A write lands in `settings.json` by default (or `settings.local.json` when the l
|
|
|
748
720
|
|
|
749
721
|
### Catalog version
|
|
750
722
|
|
|
751
|
-
The slot + input-type catalog evolves on its own cadence. `catalogCompat` (required in the manifest) is the semver range you tested against, independent of `specCompat`. A mismatch surfaces as `incompatible-catalog`; resolution is `sm plugins upgrade <id>`, which runs registered migrations from the kernel's closed registry. When auto-migration is impossible (a slot you used was removed), the upgrade verb fails loud and
|
|
723
|
+
The slot + input-type catalog evolves on its own cadence. `catalogCompat` (required in the manifest) is the semver range you tested against, independent of `specCompat`. A mismatch surfaces as `incompatible-catalog`; resolution is `sm plugins upgrade <id>`, which runs registered migrations from the kernel's closed registry. When auto-migration is impossible (a slot you used was removed), the upgrade verb fails loud and the manifest needs a manual edit.
|
|
752
724
|
|
|
753
725
|
---
|
|
754
726
|
|
|
@@ -778,13 +750,13 @@ test('emits one reference per [[ref:<name>]] token', async () => {
|
|
|
778
750
|
});
|
|
779
751
|
```
|
|
780
752
|
|
|
781
|
-
Analyzers take a `ctx` with `nodes`, `links`, and (if you assert on view contributions) an `emitContribution` spy,
|
|
753
|
+
Analyzers take a `ctx` with `nodes`, `links`, and (if you assert on view contributions) an `emitContribution` spy, returning the issue array. Formatters take `{ nodes, links, issues }` and return a string. For probabilistic Actions, shape a fake `ctx.runner` that records the calls your test cares about. The public TypeScript types (`IExtractor`, `IAnalyzer`, `IFormatter`, the matching `*Context` types, `Node`, `Link`, `Issue`, ...) re-export from `@skill-map/cli`.
|
|
782
754
|
|
|
783
755
|
---
|
|
784
756
|
|
|
785
757
|
## Diagnostics
|
|
786
758
|
|
|
787
|
-
`sm plugins list` shows every discovered plugin with one of **seven** statuses.
|
|
759
|
+
`sm plugins list` shows every discovered plugin with one of **seven** statuses. First thing to check when a plugin doesn't behave.
|
|
788
760
|
|
|
789
761
|
| Status | Meaning | Common cause |
|
|
790
762
|
|---|---|---|
|
|
@@ -798,7 +770,7 @@ Analyzers take a `ctx` with `nodes`, `links`, and (if you assert on view contrib
|
|
|
798
770
|
|
|
799
771
|
`sm plugins doctor` runs the full load pass and exits `1` if any plugin is in a non-`loaded` / non-`disabled` state. Wire it into CI.
|
|
800
772
|
|
|
801
|
-
Beyond load status, `sm plugins doctor` also reports **runtime contribution errors from the last scan**: view contributions rejected at emit time (an undeclared ref, or a payload that fails the slot's schema) are persisted per scan and surfaced in a "Runtime contribution errors (last scan)" section grouped by plugin, and any present
|
|
773
|
+
Beyond load status, `sm plugins doctor` also reports **runtime contribution errors from the last scan**: view contributions rejected at emit time (an undeclared ref, or a payload that fails the slot's schema) are persisted per scan and surfaced in a "Runtime contribution errors (last scan)" section grouped by plugin, and any present promote the exit code to `1`. A plugin can be `loaded` (clean manifest) yet still have runtime rejections: a healthy `list` status does not mean your chips rendered. The same errors appear per-plugin in the Settings plugin panel (a warning badge plus a collapsible diagnostics list). Re-run `sm scan` after a fix to clear.
|
|
802
774
|
|
|
803
775
|
---
|
|
804
776
|
|