@skill-map/spec 0.18.0 → 0.19.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 +338 -0
- package/architecture.md +220 -24
- package/cli-contract.md +16 -9
- package/conformance/cases/orphan-markdown-fallback.json +22 -0
- package/conformance/cases/plugin-missing-ui-rejected.json +2 -1
- package/conformance/cases/sidecar-end-to-end.json +1 -2
- package/conformance/coverage.md +4 -2
- package/conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md +6 -0
- package/conformance/fixtures/orphan-markdown/ARCHITECTURE.md +10 -0
- package/conformance/fixtures/sidecar-end-to-end/.claude/agents/orphan.sm +1 -1
- package/conformance/fixtures/sidecar-end-to-end/.claude/agents/stale.sm +1 -1
- package/conformance/fixtures/sidecar-example/agent-example.sm +1 -1
- package/db-schema.md +57 -13
- package/index.json +28 -23
- package/package.json +1 -1
- package/plugin-author-guide.md +303 -29
- package/schemas/annotations.schema.json +2 -6
- package/schemas/api/rest-envelope.schema.json +55 -11
- package/schemas/extensions/base.schema.json +11 -1
- package/schemas/extensions/extractor.schema.json +3 -10
- package/schemas/extensions/provider.schema.json +1 -1
- package/schemas/frontmatter/base.schema.json +6 -1
- package/schemas/input-types.schema.json +260 -0
- package/schemas/node.schema.json +1 -19
- package/schemas/plugins-registry.schema.json +14 -2
- package/schemas/project-config.schema.json +11 -0
- package/schemas/sidecar.schema.json +5 -5
- package/schemas/summaries/markdown.schema.json +1 -1
- package/schemas/view-contracts.schema.json +298 -0
package/plugin-author-guide.md
CHANGED
|
@@ -83,19 +83,20 @@ Concrete examples for the reference impl's bundled extensions:
|
|
|
83
83
|
| Extension | Short id (in the file) | Qualified id (in the registry) |
|
|
84
84
|
|---|---|---|
|
|
85
85
|
| Claude Provider | `claude` | `claude/claude` |
|
|
86
|
-
|
|
|
87
|
-
| Slash extractor | `slash` | `
|
|
88
|
-
| At-directive extractor | `at-directive` | `
|
|
86
|
+
| Annotations extractor | `annotations` | `core/annotations` |
|
|
87
|
+
| Slash extractor | `slash` | `core/slash` |
|
|
88
|
+
| At-directive extractor | `at-directive` | `core/at-directive` |
|
|
89
|
+
| Markdown-link extractor | `markdown-link` | `core/markdown-link` |
|
|
89
90
|
| External-URL counter | `external-url-counter` | `core/external-url-counter` |
|
|
90
91
|
| Broken-ref rule | `broken-ref` | `core/broken-ref` |
|
|
91
92
|
| Trigger-collision rule | `trigger-collision` | `core/trigger-collision` |
|
|
92
93
|
| ASCII formatter | `ascii` | `core/ascii` |
|
|
93
94
|
| Validate-all rule | `validate-all` | `core/validate-all` |
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
Built-ins split between two namespaces:
|
|
96
97
|
|
|
97
|
-
- **`core/`** — kernel-internal primitives
|
|
98
|
-
- **`claude/`** — the Claude Code Provider bundle
|
|
98
|
+
- **`core/`** — kernel-internal primitives, platform-agnostic. Owns every built-in rule (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
|
+
- **`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`.
|
|
99
100
|
|
|
100
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.
|
|
101
102
|
|
|
@@ -125,15 +126,15 @@ Every plugin and every built-in bundle declares a **granularity** that controls
|
|
|
125
126
|
|
|
126
127
|
Built-in mapping:
|
|
127
128
|
|
|
128
|
-
- **`claude`** — `granularity: 'bundle'`.
|
|
129
|
-
- **`core`** — `granularity: 'extension'`. `sm plugins disable core/superseded` flips just the supersession rule;
|
|
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 rule; every other core extension (every other rule, the ASCII formatter, the cross-vendor extractors) stays live.
|
|
130
131
|
|
|
131
132
|
Per-verb behaviour:
|
|
132
133
|
|
|
133
134
|
| Command | Bundle granularity | Extension granularity |
|
|
134
135
|
|---|---|---|
|
|
135
136
|
| `sm plugins enable claude` | OK — flips the bundle. | Rejected: `'core' has granularity=extension; use sm plugins enable core/<ext-id>`. |
|
|
136
|
-
| `sm plugins enable claude/
|
|
137
|
+
| `sm plugins enable claude/claude` | Rejected: `'claude' has granularity=bundle; use sm plugins enable claude`. | n/a (no bundle of granularity=bundle accepts qualified ids) |
|
|
137
138
|
| `sm plugins disable core` | n/a | Rejected: same directed message as the bundle row above. |
|
|
138
139
|
| `sm plugins disable core/superseded` | n/a | OK — persists `config_plugins['core/superseded'].enabled = 0`. |
|
|
139
140
|
|
|
@@ -160,7 +161,7 @@ The default (`'bundle'`) is the right answer for almost every plugin — keep th
|
|
|
160
161
|
|
|
161
162
|
### Extractor `applicableKinds` — narrow the pipeline
|
|
162
163
|
|
|
163
|
-
An `Extractor` extension MAY declare an `applicableKinds` array on its manifest. When declared, the kernel runs the extractor **only** against nodes whose `kind` is in the list — the filter is fail-fast (no extractor context, no method call) so
|
|
164
|
+
An `Extractor` extension MAY declare an `applicableKinds` array on its manifest. When declared, the kernel runs the extractor **only** against nodes whose `kind` is in the list — the filter is fail-fast (no extractor context, no method call) so the extractor wastes zero CPU on nodes it cannot meaningfully process.
|
|
164
165
|
|
|
165
166
|
| `applicableKinds` | Behaviour |
|
|
166
167
|
|---|---|
|
|
@@ -171,36 +172,37 @@ An `Extractor` extension MAY declare an `applicableKinds` array on its manifest.
|
|
|
171
172
|
|
|
172
173
|
There is no wildcard syntax (no `'*'`) — omitting the field IS the wildcard. The pattern is intentional: a literal absence is unambiguous, a string sentinel would invite typos that silently disable the extractor.
|
|
173
174
|
|
|
174
|
-
Use case — a
|
|
175
|
+
Use case — a deterministic frontmatter-tag extractor that only makes sense for skills:
|
|
175
176
|
|
|
176
177
|
```javascript
|
|
177
178
|
export default {
|
|
178
|
-
id: 'tag-
|
|
179
|
+
id: 'tag-extractor',
|
|
179
180
|
kind: 'extractor',
|
|
180
|
-
mode: 'probabilistic',
|
|
181
181
|
version: '1.0.0',
|
|
182
|
-
description: '
|
|
182
|
+
description: 'Lifts the `tags:` frontmatter array into `references` links for skill nodes.',
|
|
183
183
|
emitsLinkKinds: ['references'],
|
|
184
|
-
defaultConfidence: '
|
|
185
|
-
scope: '
|
|
184
|
+
defaultConfidence: 'high',
|
|
185
|
+
scope: 'frontmatter',
|
|
186
186
|
applicableKinds: ['skill'],
|
|
187
187
|
async extract(ctx) {
|
|
188
188
|
// Never invoked for agents, commands, hooks, or notes — the kernel
|
|
189
189
|
// skipped this node before reaching us.
|
|
190
|
-
const tags =
|
|
190
|
+
const tags = Array.isArray(ctx.frontmatter.tags) ? ctx.frontmatter.tags : [];
|
|
191
191
|
for (const t of tags) {
|
|
192
192
|
ctx.emitLink({
|
|
193
193
|
source: ctx.node.path,
|
|
194
|
-
target: t
|
|
194
|
+
target: t,
|
|
195
195
|
kind: 'references',
|
|
196
|
-
confidence: '
|
|
197
|
-
sources: ['tag-
|
|
196
|
+
confidence: 'high',
|
|
197
|
+
sources: ['tag-extractor'],
|
|
198
198
|
});
|
|
199
199
|
}
|
|
200
200
|
},
|
|
201
201
|
};
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
+
> **Why no `mode` field?** Extractors are deterministic-only — they sit on `sm scan`'s synchronous loop, and the loop must stay fast and reproducible. If you need an LLM to infer something about a node (tags, summaries, suspicious imports), write an `Action` instead and let the user dispatch it via `sm job submit action:<id>`. The Action's report flows back through the job lifecycle, not through the Extractor pipeline.
|
|
205
|
+
|
|
204
206
|
**Unknown kinds are non-blocking.** An extractor that lists a kind no installed Provider declares (typo, missing Provider plugin) still loads with status `loaded`; `sm plugins doctor` surfaces an informational warning so the author sees the mismatch. The exit code of `doctor` is NOT promoted to 1 by this warning — the corresponding Provider may legitimately arrive later (e.g. when the user installs the matching plugin), and the load contract favours forward compatibility over rigid checks. The full set of "known kinds" is the union of every installed Provider's `defaultRefreshAction` keys.
|
|
205
207
|
|
|
206
208
|
---
|
|
@@ -244,12 +246,12 @@ Authors who explicitly review each minor's changelog **MAY** widen across the ne
|
|
|
244
246
|
|
|
245
247
|
## The six extension kinds
|
|
246
248
|
|
|
247
|
-
The kernel knows six categories.
|
|
249
|
+
The kernel knows six categories. Three are dual-mode (deterministic or probabilistic per [`architecture.md` §Execution modes](./architecture.md)); three are deterministic-only because they sit on the deterministic scan path.
|
|
248
250
|
|
|
249
251
|
| Kind | Method | Receives | Returns | Mode |
|
|
250
252
|
|---|---|---|---|---|
|
|
251
253
|
| `provider` | `walk(roots, opts)` | filesystem roots | `IRawNode[]` | deterministic only |
|
|
252
|
-
| `extractor` | `extract(ctx)` | one node + body + frontmatter + callbacks | `void` (output via `ctx.emitLink` / `ctx.enrichNode` / `ctx.store`) |
|
|
254
|
+
| `extractor` | `extract(ctx)` | one node + body + frontmatter + callbacks | `void` (output via `ctx.emitLink` / `ctx.enrichNode` / `ctx.store`) | deterministic only |
|
|
253
255
|
| `rule` | `evaluate(ctx)` | full graph | `Issue[]` | dual-mode |
|
|
254
256
|
| `action` | `run(ctx)` | one or more nodes | execution record | dual-mode |
|
|
255
257
|
| `formatter` | `format(ctx)` | full graph | `string` | deterministic only |
|
|
@@ -264,10 +266,10 @@ Pure single-node analysis. **Never** read another node, the graph, or the databa
|
|
|
264
266
|
The runtime method is `extract(ctx) → void`. Output flows through three callbacks the kernel binds onto the context:
|
|
265
267
|
|
|
266
268
|
- **`ctx.emitLink(link)`** — append a `Link` to the kernel's `links` table. The kernel validates against the extractor's declared `emitsLinkKinds` before persistence; off-contract kinds are dropped and surface as `extension.error` events. URL-shaped targets are partitioned into `node.externalRefsCount` and never persisted.
|
|
267
|
-
- **`ctx.enrichNode(partial)`** — merge canonical, kernel-curated properties onto the node's enrichment layer (persisted into `node_enrichments` per `db-schema.md`). **Strictly separate from the author-supplied frontmatter** — the latter is IMMUTABLE from any Extractor. Use the enrichment layer for facts the author did not write but the Extractor inferred (computed titles, summaries, signals from
|
|
269
|
+
- **`ctx.enrichNode(partial)`** — merge canonical, kernel-curated properties onto the node's enrichment layer (persisted into `node_enrichments` per `db-schema.md`). **Strictly separate from the author-supplied frontmatter** — the latter is IMMUTABLE from any Extractor. Use the enrichment layer for facts the author did not write but the Extractor inferred (computed titles, summaries, signals derived from the body). Enrichment rows are overwritten via PRIMARY KEY conflict on the next re-extract through the A.9 cache and are never stale-flagged (Extractors are deterministic; re-running is free).
|
|
268
270
|
- **`ctx.store`** — plugin-scoped persistence. Optional, only present when your `plugin.json` declares `storage.mode`. Shape depends on the mode (`KvStore` for mode A, scoped `Database` for mode B). See [`plugin-kv-api.md`](./plugin-kv-api.md).
|
|
269
271
|
|
|
270
|
-
|
|
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).
|
|
271
273
|
|
|
272
274
|
> **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.
|
|
273
275
|
|
|
@@ -604,11 +606,11 @@ The kernel validates the row passed to `ctx.store.write(table, row)` against the
|
|
|
604
606
|
|
|
605
607
|
## Execution modes
|
|
606
608
|
|
|
607
|
-
|
|
609
|
+
Rule / Action / Hook declare `mode` in the manifest. Action's `mode` is required; Rule and Hook default to `deterministic`. Provider / Extractor / Formatter must NOT declare `mode` — they are deterministic-only by spec.
|
|
608
610
|
|
|
609
611
|
```jsonc
|
|
610
|
-
//
|
|
611
|
-
{ "kind": "extractor", "id": "my-extractor",
|
|
612
|
+
// extractor — deterministic by spec, no mode field
|
|
613
|
+
{ "kind": "extractor", "id": "my-extractor", ... }
|
|
612
614
|
```
|
|
613
615
|
|
|
614
616
|
```jsonc
|
|
@@ -721,7 +723,7 @@ By default a contribution lands inside the plugin's `<plugin-id>:` block at the
|
|
|
721
723
|
|
|
722
724
|
```yaml
|
|
723
725
|
# .claude/agents/architect.sm
|
|
724
|
-
|
|
726
|
+
identity:
|
|
725
727
|
path: .claude/agents/architect.md
|
|
726
728
|
bodyHash: ...
|
|
727
729
|
frontmatterHash: ...
|
|
@@ -766,7 +768,7 @@ The resulting sidecar block:
|
|
|
766
768
|
|
|
767
769
|
```yaml
|
|
768
770
|
# .claude/agents/architect.sm
|
|
769
|
-
|
|
771
|
+
identity: { path: ..., bodyHash: ..., frontmatterHash: ... }
|
|
770
772
|
compliance:
|
|
771
773
|
audit: sox-2026
|
|
772
774
|
dueAt: 2026-12-31T23:59:59Z
|
|
@@ -806,6 +808,278 @@ Pure read; no side effects. Built-in catalog fields from `annotations.schema.jso
|
|
|
806
808
|
|
|
807
809
|
---
|
|
808
810
|
|
|
811
|
+
## View contributions
|
|
812
|
+
|
|
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 **contract** by name from a closed kernel catalog, declare per-node emissions in their extension manifest, and emit payloads at scan time via `ctx.emitContribution(id, payload)`. The UI maps contracts to slots and renders. See [`architecture.md`](./architecture.md) §View contribution system for the normative contract.
|
|
814
|
+
|
|
815
|
+
### What it solves
|
|
816
|
+
|
|
817
|
+
Today, the only way a plugin can surface UI is implicit: extractors emit `Link` (rendered by the kernel-built `linked-nodes-panel`), rules 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; the UI decides where and how.
|
|
818
|
+
|
|
819
|
+
### What you NEVER write
|
|
820
|
+
|
|
821
|
+
- HTML, CSS, JavaScript, or Angular components.
|
|
822
|
+
- JSON Schema for your contributions or your settings.
|
|
823
|
+
- The slot id where your contribution appears (slots are UI-only).
|
|
824
|
+
- The renderer component that draws your contribution.
|
|
825
|
+
|
|
826
|
+
You DO write:
|
|
827
|
+
|
|
828
|
+
- The `contract` name (one of 10 closed-catalog values).
|
|
829
|
+
- Optional `label`, `tooltip`, `icon`, `emptyText`, `emitWhenEmpty` per contribution.
|
|
830
|
+
- The per-node payload your `extract(ctx)` emits via `ctx.emitContribution(...)`.
|
|
831
|
+
|
|
832
|
+
### Manifest shape
|
|
833
|
+
|
|
834
|
+
Inside any extension manifest (`IExtractor`, `IRule`, ...), declare a `viewContributions` map next to `annotationContributions`. Each key is your local contribution id; the value picks a contract.
|
|
835
|
+
|
|
836
|
+
```jsonc
|
|
837
|
+
{
|
|
838
|
+
"id": "keyword-finder",
|
|
839
|
+
"kind": "extractor",
|
|
840
|
+
"viewContributions": {
|
|
841
|
+
"breakdown": {
|
|
842
|
+
"contract": "node-breakdown",
|
|
843
|
+
"label": "Keyword hits",
|
|
844
|
+
"emptyText": "No matches."
|
|
845
|
+
},
|
|
846
|
+
"total": {
|
|
847
|
+
"contract": "node-counter",
|
|
848
|
+
"icon": "🔍",
|
|
849
|
+
"label": "kw",
|
|
850
|
+
"emitWhenEmpty": false
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
Field reference (full schema in [`schemas/view-contracts.schema.json`](./schemas/view-contracts.schema.json) at `$defs/IViewContribution`):
|
|
857
|
+
|
|
858
|
+
| Field | Required | Notes |
|
|
859
|
+
|---|---|---|
|
|
860
|
+
| `contract` | yes | One of the 10 catalog names (see below). Unknown name → `invalid-manifest` at load. |
|
|
861
|
+
| `label` | no | Short human-readable label. English-only per [`AGENTS.md`](../AGENTS.md) (`Externalized texts, not internationalized`). |
|
|
862
|
+
| `tooltip` | no | Hover tooltip on the chip / panel header. |
|
|
863
|
+
| `icon` | no | Single string. If matches Unicode `\p{Extended_Pictographic}` → emoji. Otherwise → PrimeIcons name (no `pi-` prefix). |
|
|
864
|
+
| `emptyText` | no | Text shown when payload is empty AND `emitWhenEmpty: true`. |
|
|
865
|
+
| `emitWhenEmpty` | no, default `false` | When `false`, kernel drops empty payloads silently so the slot stays clean. |
|
|
866
|
+
|
|
867
|
+
### Contract catalog (closed)
|
|
868
|
+
|
|
869
|
+
The kernel ships exactly these 10 contracts. Each has a fixed payload shape and a fixed set of UI slots it surfaces in. Adding a contract requires a spec / UI / scaffolder round-trip — discuss in [`ROADMAP.md`](../ROADMAP.md) before opening a PR.
|
|
870
|
+
|
|
871
|
+
| Contract | Payload shape | Surfaces in |
|
|
872
|
+
|---|---|---|
|
|
873
|
+
| `node-counter` | `{ value: integer ≥ 0, severity?, label?, tooltip? }` | card chip + inspector header badge |
|
|
874
|
+
| `node-tag` | `{ label, severity?, tooltip? }` | card chip + inspector header badge |
|
|
875
|
+
| `node-breakdown` | `{ entries: Array<{ label, value, tooltip? }> }` (≤ 20) | inspector body (chart-bar) |
|
|
876
|
+
| `node-records` | `{ columns: ≤6, rows: ≤50 }` | inspector body (table) |
|
|
877
|
+
| `node-tree` | recursive `{ label, marker?, children? }` (depth ≤ 6, total ≤ 200) | inspector body (tree) |
|
|
878
|
+
| `node-key-values` | `{ entries: Array<{ key, value, tooltip? }> }` (≤ 50) | inspector body (key-value list) |
|
|
879
|
+
| `node-link-list` | `{ entries: Array<{ path, label?, kind? }> }` (≤ 100) | inspector body (link list) |
|
|
880
|
+
| `node-markdown` | `{ markdown }` (≤ 4096 chars, sanitized) | inspector body (markdown text) |
|
|
881
|
+
| `node-alert` | `{ icon?, severity?, count?, tooltip? }` | graph node corner badge |
|
|
882
|
+
| `scope-stat` | `{ value, label?, severity?, tooltip? }` | topbar indicator |
|
|
883
|
+
|
|
884
|
+
Per-contract semantics, edge cases, and exact payload schemas live in [`schemas/view-contracts.schema.json`](./schemas/view-contracts.schema.json) at `$defs/payloads/<contract>`. Read that schema before emitting.
|
|
885
|
+
|
|
886
|
+
### Emit path
|
|
887
|
+
|
|
888
|
+
Inside `extract(ctx)`, call:
|
|
889
|
+
|
|
890
|
+
```ts
|
|
891
|
+
ctx.emitContribution('breakdown', {
|
|
892
|
+
entries: Object.entries(perKeyword).map(([label, value]) => ({ label, value })),
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
ctx.emitContribution('total', { value: total });
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
The first argument is the manifest Record key (`'breakdown'` or `'total'` above), NOT the contract name. The kernel composes the qualified id from your plugin id, extension id, and this Record key.
|
|
899
|
+
|
|
900
|
+
The kernel validates the payload against the contract's payload schema in `view-contracts.schema.json#/$defs/payloads/<contract>`. Off-contract payloads emit an `extension.error` event and drop silently — same posture as `emitLink` rejecting links not in your `emitsLinkKinds`.
|
|
901
|
+
|
|
902
|
+
For `scope-stat`, rules use `ctx.emitScopeContribution(id, payload)` (extractors do not see this method — scope-level emission lives in rule context).
|
|
903
|
+
|
|
904
|
+
### Settings
|
|
905
|
+
|
|
906
|
+
User-configurable settings live at the manifest root in `settings: Record<string, ISettingDeclaration>`. Each entry picks an `input-type` from a closed catalog. You NEVER write JSON Schema for settings.
|
|
907
|
+
|
|
908
|
+
```jsonc
|
|
909
|
+
{
|
|
910
|
+
"id": "keyword-finder",
|
|
911
|
+
"version": "1.0.0",
|
|
912
|
+
"specCompat": "^0.20.0",
|
|
913
|
+
"catalogCompat": "^1.0.0",
|
|
914
|
+
"extensions": ["./extension.js"],
|
|
915
|
+
"settings": {
|
|
916
|
+
"keywords": {
|
|
917
|
+
"type": "string-list",
|
|
918
|
+
"label": "Keywords to track",
|
|
919
|
+
"description": "Words counted across each node's body.",
|
|
920
|
+
"default": ["TODO", "FIXME"],
|
|
921
|
+
"min": 1
|
|
922
|
+
},
|
|
923
|
+
"caseSensitive": {
|
|
924
|
+
"type": "boolean-flag",
|
|
925
|
+
"label": "Case-sensitive matching",
|
|
926
|
+
"default": false
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
The 10 input-types:
|
|
933
|
+
|
|
934
|
+
| Type | Value at runtime | Use for |
|
|
935
|
+
|---|---|---|
|
|
936
|
+
| `string-list` | `string[]` | keyword lists, ignore patterns |
|
|
937
|
+
| `single-string` | `string` | URLs, names, identifiers |
|
|
938
|
+
| `boolean-flag` | `boolean` | toggles |
|
|
939
|
+
| `integer` | `number` (always integer) | counts, thresholds |
|
|
940
|
+
| `enum-pick` | `string` | pick one from a closed set |
|
|
941
|
+
| `enum-multipick` | `string[]` | pick zero or more |
|
|
942
|
+
| `path-glob` | `string` or `string[]` | glob patterns |
|
|
943
|
+
| `regex` | `string` | ECMAScript regex (body, no `/` delimiters) |
|
|
944
|
+
| `secret` | `string` | tokens, passwords (encrypted at rest) |
|
|
945
|
+
| `key-value-list` | `Array<{ key, value }>` | custom maps, alias dictionaries |
|
|
946
|
+
|
|
947
|
+
Per-type parameter schema lives in [`schemas/input-types.schema.json`](./schemas/input-types.schema.json) at `$defs/Setting_<TypeName>`.
|
|
948
|
+
|
|
949
|
+
The kernel exposes resolved settings to extractors via `ctx.settings.<settingId>`. Settings are read once at extractor invocation; **changing a setting requires `sm scan` to re-emit** affected contributions. The UI surfaces a "settings changed, rescan needed" indicator.
|
|
950
|
+
|
|
951
|
+
### Catalog version
|
|
952
|
+
|
|
953
|
+
The catalog of contracts and input-types evolves on its own cadence. Declare a semver range in your manifest:
|
|
954
|
+
|
|
955
|
+
```jsonc
|
|
956
|
+
{ "catalogCompat": "^1.0.0" }
|
|
957
|
+
```
|
|
958
|
+
|
|
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 contract you used was removed entirely), the upgrade verb fails loud (CLI exit ≠ 0 + console message) and your manifest needs a manual edit.
|
|
960
|
+
|
|
961
|
+
`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
|
+
|
|
963
|
+
### Worked example — `acme/keyword-finder`
|
|
964
|
+
|
|
965
|
+
Full plugin walkthrough:
|
|
966
|
+
|
|
967
|
+
```
|
|
968
|
+
plugins/acme-keyword-finder/
|
|
969
|
+
├── plugin.json ← manifest with settings + catalogCompat
|
|
970
|
+
└── extensions/
|
|
971
|
+
└── extractor.js ← extract() with ctx.emitContribution
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
`plugin.json`:
|
|
975
|
+
|
|
976
|
+
```jsonc
|
|
977
|
+
{
|
|
978
|
+
"id": "acme-keyword-finder",
|
|
979
|
+
"version": "1.0.0",
|
|
980
|
+
"specCompat": "^0.20.0",
|
|
981
|
+
"catalogCompat": "^1.0.0",
|
|
982
|
+
"extensions": ["./extensions/extractor.js"],
|
|
983
|
+
"settings": {
|
|
984
|
+
"keywords": {
|
|
985
|
+
"type": "string-list",
|
|
986
|
+
"label": "Keywords to track",
|
|
987
|
+
"default": ["TODO", "FIXME"],
|
|
988
|
+
"min": 1
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
`extensions/extractor.js`:
|
|
995
|
+
|
|
996
|
+
```js
|
|
997
|
+
export const extractor = {
|
|
998
|
+
id: 'keyword-finder',
|
|
999
|
+
pluginId: 'acme-keyword-finder',
|
|
1000
|
+
kind: 'extractor',
|
|
1001
|
+
version: '1.0.0',
|
|
1002
|
+
description: 'Counts configured keywords per node.',
|
|
1003
|
+
stability: 'stable',
|
|
1004
|
+
mode: 'deterministic',
|
|
1005
|
+
emitsLinkKinds: [],
|
|
1006
|
+
defaultConfidence: 'high',
|
|
1007
|
+
scope: 'body',
|
|
1008
|
+
|
|
1009
|
+
viewContributions: {
|
|
1010
|
+
breakdown: {
|
|
1011
|
+
contract: 'node-breakdown',
|
|
1012
|
+
label: 'Keyword hits',
|
|
1013
|
+
emptyText: 'No matches.',
|
|
1014
|
+
},
|
|
1015
|
+
total: {
|
|
1016
|
+
contract: 'node-counter',
|
|
1017
|
+
icon: '🔍',
|
|
1018
|
+
label: 'kw',
|
|
1019
|
+
emitWhenEmpty: false,
|
|
1020
|
+
},
|
|
1021
|
+
},
|
|
1022
|
+
|
|
1023
|
+
extract(ctx) {
|
|
1024
|
+
const keywords = ctx.settings.keywords;
|
|
1025
|
+
const perKeyword = Object.create(null);
|
|
1026
|
+
let total = 0;
|
|
1027
|
+
|
|
1028
|
+
for (const kw of keywords) {
|
|
1029
|
+
const re = new RegExp(`\\b${escapeRegex(kw)}\\b`, 'gi');
|
|
1030
|
+
const n = (ctx.body.match(re) ?? []).length;
|
|
1031
|
+
perKeyword[kw] = n;
|
|
1032
|
+
total += n;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
ctx.emitContribution('breakdown', {
|
|
1036
|
+
entries: Object.entries(perKeyword).map(([label, value]) => ({ label, value })),
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
if (total > 0) {
|
|
1040
|
+
ctx.emitContribution('total', { value: total });
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
function escapeRegex(s) {
|
|
1046
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1047
|
+
}
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
After `sm scan`, the UI surfaces:
|
|
1051
|
+
|
|
1052
|
+
- A `🔍 N` chip on every node's card (when `total > 0`).
|
|
1053
|
+
- A "Keyword hits" panel in the inspector body for every node, with a horizontal bar chart per keyword.
|
|
1054
|
+
|
|
1055
|
+
The plugin author wrote zero UI code, zero CSS, zero HTML, zero JSON Schema, and never typed the words "panel", "chip", "renderer", or "slot".
|
|
1056
|
+
|
|
1057
|
+
### Scaffolder
|
|
1058
|
+
|
|
1059
|
+
Hand-writing the manifest is supported but discouraged. Run:
|
|
1060
|
+
|
|
1061
|
+
```sh
|
|
1062
|
+
sm plugins create
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
The scaffolder walks you through the closed catalogs (settings + view contributions) 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 contract picks at author time, while a hand-written manifest only fails at load time.
|
|
1066
|
+
|
|
1067
|
+
Companion verbs:
|
|
1068
|
+
|
|
1069
|
+
- `sm plugins doctor` — surfaces `incompatible-catalog`, `invalid-manifest`, deprecated-contract usage.
|
|
1070
|
+
- `sm plugins upgrade <id>` — applies catalog migrations registered in the kernel.
|
|
1071
|
+
- `sm plugins contracts list` — prints the catalog (contracts + input-types), flags deprecated entries.
|
|
1072
|
+
|
|
1073
|
+
### Watch out for
|
|
1074
|
+
|
|
1075
|
+
- **Don't pick a slot.** The plugin author never types `inspector.body.panel`, `card.footer.left`, etc. Slot mapping is a UI decision; if you find yourself wanting to "place" a contribution, you're working against the model.
|
|
1076
|
+
- **Don't write JSON Schema.** Settings use `type` from the input-type catalog; view contributions use `contract` from the contract catalog.
|
|
1077
|
+
- **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
|
+
- **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
|
+
- **Don't try to read another plugin's contributions.** The BFF rejects cross-plugin reads at the route level.
|
|
1080
|
+
|
|
1081
|
+
---
|
|
1082
|
+
|
|
809
1083
|
## See also
|
|
810
1084
|
|
|
811
1085
|
- [`architecture.md`](./architecture.md) — extension contract, ports, execution modes.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://skill-map.dev/spec/v0/annotations.schema.json",
|
|
4
4
|
"title": "Annotations",
|
|
5
|
-
"description": "Catalog of conventional annotation fields skill-map ships out of the box, written into the `annotations:` block of a sidecar (`<basename>.sm`). Every field is OPTIONAL — a sidecar with an empty `annotations: {}` is valid. Schema is `additionalProperties: true` so users / plugins can add custom keys without coordination; the built-in `unknown-field` rule emits a warning on unrecognized keys (typo guard). The curated catalog is the load-bearing
|
|
5
|
+
"description": "Catalog of conventional annotation fields skill-map ships out of the box, written into the `annotations:` block of a sidecar (`<basename>.sm`). Every field is OPTIONAL — a sidecar with an empty `annotations: {}` is valid. Schema is `additionalProperties: true` so users / plugins can add custom keys without coordination; the built-in `unknown-field` rule emits a warning on unrecognized keys (typo guard). The curated catalog is the load-bearing 13 fields below — versioning + supersession (`version`, `stability`, `supersedes`, `supersededBy`, `requires`, `conflictsWith`, `related`), provenance (`authors`, `license`, `source`, `sourceVersion`), taxonomy (`tags`), docs (`docsUrl`). The activity timestamp lives in the reserved `audit:` block (`audit.lastBumpedAt`), not in `annotations:`. Plugins that want first-class custom keys with their own validation declare `annotationContributions` in their manifest (see Step 9.6.6).",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": true,
|
|
8
8
|
"properties": {
|
|
@@ -64,11 +64,7 @@
|
|
|
64
64
|
"tags": {
|
|
65
65
|
"type": "array",
|
|
66
66
|
"items": { "type": "string", "minLength": 1 },
|
|
67
|
-
"description": "
|
|
68
|
-
},
|
|
69
|
-
"hidden": {
|
|
70
|
-
"type": "boolean",
|
|
71
|
-
"description": "When true, the node is excluded from default listings (still appears under `--all` / `--include-hidden`). Useful for in-progress nodes the author does not want surfaced yet."
|
|
67
|
+
"description": "**User-supplied** taxonomy tags. Skill-map's tag system is dual-source: this field carries the user's post-hoc tags (curator's view of the node from the `.sm` sidecar); `frontmatter.tags` carries the author's tags (intrinsic categories written in the `.md`). Both surfaces are first-class — `sm list --tag <name>` and UI faceted search match the union and the UI distinguishes them visually (`sm list --tag-source author|user` filters one source). Empty array and missing field are equivalent."
|
|
72
68
|
},
|
|
73
69
|
"docsUrl": {
|
|
74
70
|
"type": "string",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"health",
|
|
25
25
|
"scan",
|
|
26
26
|
"sidecar.bumped",
|
|
27
|
-
"annotations.registered"
|
|
27
|
+
"annotations.registered",
|
|
28
|
+
"contributions.registered"
|
|
28
29
|
],
|
|
29
30
|
"description": "Discriminator. List kinds (`nodes`, `links`, `issues`, `plugins`) carry `items` + `filters` + `counts` + `kindRegistry`. The `node` kind carries `item` + `kindRegistry`. The `config` kind carries `value` + `kindRegistry`. The `sidecar.bumped` kind (Step 9.6.5, BFF half) carries `value` + `elapsedMs` (no `filters` / `counts` / `kindRegistry` — it's an action-result projection orthogonal to the kindRegistry surface). The `annotations.registered` kind (Step 9.6.6, BFF half) carries `items` + `counts.total` (no `filters` / `kindRegistry` — pure read-only catalog projection). The `health` / `scan` / `graph` values are reserved for documentation parity with the routes that DON'T use this envelope."
|
|
30
31
|
},
|
|
@@ -103,6 +104,26 @@
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
},
|
|
107
|
+
"contributionsRegistry": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"description": "Catalog of registered view contributions active in the current scope, keyed by qualified contribution id `<pluginId>/<extensionId>/<contributionId>`. Built once per server boot from every enabled extension's `viewContributions` map and embedded into every payload-bearing envelope so the UI can fetch the catalog without a separate request. Sentinel envelopes (`health`, `scan`, `graph`), action-result envelopes (`sidecar.bumped`), and the catalog envelopes themselves (`annotations.registered`, `contributions.registered`) are exempt. Mirror of `kindRegistry`, parallel surface. Each entry references a contract by name from the closed catalog at `view-contracts.schema.json#/$defs/ContractName`; the UI consults its own contract→slot mapping (slots are UI-only) to render. Per-node payloads are delivered separately on `node` envelopes (single via `item.contributions`) or list envelopes (`items[].contributions`, only when `limit ≤ bff.maxBulkContributions`, default 200).",
|
|
110
|
+
"additionalProperties": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"required": ["pluginId", "extensionId", "contributionId", "contract"],
|
|
113
|
+
"additionalProperties": false,
|
|
114
|
+
"properties": {
|
|
115
|
+
"pluginId": { "type": "string", "minLength": 1 },
|
|
116
|
+
"extensionId": { "type": "string", "minLength": 1 },
|
|
117
|
+
"contributionId": { "type": "string", "minLength": 1 },
|
|
118
|
+
"contract": { "$ref": "../view-contracts.schema.json#/$defs/ContractName" },
|
|
119
|
+
"label": { "type": "string", "maxLength": 64 },
|
|
120
|
+
"tooltip": { "type": "string", "maxLength": 256 },
|
|
121
|
+
"icon": { "type": "string", "maxLength": 64 },
|
|
122
|
+
"emptyText": { "type": "string", "maxLength": 128 },
|
|
123
|
+
"emitWhenEmpty": { "type": "boolean", "default": false }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
106
127
|
"counts": {
|
|
107
128
|
"type": "object",
|
|
108
129
|
"required": ["total"],
|
|
@@ -142,8 +163,8 @@
|
|
|
142
163
|
},
|
|
143
164
|
"oneOf": [
|
|
144
165
|
{
|
|
145
|
-
"description": "List envelope — `items` payload + `filters` + `counts` (with `returned`) + `kindRegistry`. Used by `/api/nodes`, `/api/links`, `/api/issues`, `/api/plugins`.",
|
|
146
|
-
"required": ["items", "counts", "filters", "kindRegistry"],
|
|
166
|
+
"description": "List envelope — `items` payload + `filters` + `counts` (with `returned`) + `kindRegistry` + `contributionsRegistry`. Used by `/api/nodes`, `/api/links`, `/api/issues`, `/api/plugins`.",
|
|
167
|
+
"required": ["items", "counts", "filters", "kindRegistry", "contributionsRegistry"],
|
|
147
168
|
"properties": {
|
|
148
169
|
"kind": { "enum": ["nodes", "links", "issues", "plugins"] },
|
|
149
170
|
"counts": { "required": ["total", "returned"] }
|
|
@@ -157,8 +178,8 @@
|
|
|
157
178
|
}
|
|
158
179
|
},
|
|
159
180
|
{
|
|
160
|
-
"description": "Single-resource envelope — `item` payload + `kindRegistry`, no `counts` / `filters`. Used by `/api/nodes/:pathB64`.",
|
|
161
|
-
"required": ["item", "kindRegistry"],
|
|
181
|
+
"description": "Single-resource envelope — `item` payload + `kindRegistry` + `contributionsRegistry`, no `counts` / `filters`. Used by `/api/nodes/:pathB64`.",
|
|
182
|
+
"required": ["item", "kindRegistry", "contributionsRegistry"],
|
|
162
183
|
"properties": {
|
|
163
184
|
"kind": { "const": "node" }
|
|
164
185
|
},
|
|
@@ -171,8 +192,8 @@
|
|
|
171
192
|
}
|
|
172
193
|
},
|
|
173
194
|
{
|
|
174
|
-
"description": "Value envelope — `value` payload + `kindRegistry`, no `counts` / `filters`. Used by `/api/config`.",
|
|
175
|
-
"required": ["value", "kindRegistry"],
|
|
195
|
+
"description": "Value envelope — `value` payload + `kindRegistry` + `contributionsRegistry`, no `counts` / `filters`. Used by `/api/config`.",
|
|
196
|
+
"required": ["value", "kindRegistry", "contributionsRegistry"],
|
|
176
197
|
"properties": {
|
|
177
198
|
"kind": { "const": "config" }
|
|
178
199
|
},
|
|
@@ -185,7 +206,7 @@
|
|
|
185
206
|
}
|
|
186
207
|
},
|
|
187
208
|
{
|
|
188
|
-
"description": "Action-result envelope — `value` + `elapsedMs` siblings, no `filters` / `counts` / `kindRegistry`. Used by `POST /api/sidecar/bump` (Step 9.6.5, BFF half) where the response carries the bump report (`{ nodePath, version, status }`) plus the wall-clock duration. The
|
|
209
|
+
"description": "Action-result envelope — `value` + `elapsedMs` siblings, no `filters` / `counts` / `kindRegistry` / `contributionsRegistry`. Used by `POST /api/sidecar/bump` (Step 9.6.5, BFF half) where the response carries the bump report (`{ nodePath, version, status }`) plus the wall-clock duration. The registries are intentionally absent — the action result is orthogonal to both catalogs and the SPA already has them cached from a prior list call.",
|
|
189
210
|
"required": ["value", "elapsedMs"],
|
|
190
211
|
"properties": {
|
|
191
212
|
"kind": { "const": "sidecar.bumped" }
|
|
@@ -196,12 +217,13 @@
|
|
|
196
217
|
{ "required": ["item"] },
|
|
197
218
|
{ "required": ["filters"] },
|
|
198
219
|
{ "required": ["counts"] },
|
|
199
|
-
{ "required": ["kindRegistry"] }
|
|
220
|
+
{ "required": ["kindRegistry"] },
|
|
221
|
+
{ "required": ["contributionsRegistry"] }
|
|
200
222
|
]
|
|
201
223
|
}
|
|
202
224
|
},
|
|
203
225
|
{
|
|
204
|
-
"description": "
|
|
226
|
+
"description": "Annotation-catalog envelope — `items` + `counts.total` only, no `filters` / `kindRegistry` / `contributionsRegistry` / `returned`. Used by `GET /api/annotations/registered` (Step 9.6.6, BFF half). The catalog is small (typically 0–50 entries), ships in its entirety on every response, and does not paginate; `counts.total` doubles as `items.length`.",
|
|
205
227
|
"required": ["items", "counts"],
|
|
206
228
|
"properties": {
|
|
207
229
|
"kind": { "const": "annotations.registered" },
|
|
@@ -215,12 +237,33 @@
|
|
|
215
237
|
{ "required": ["value"] },
|
|
216
238
|
{ "required": ["filters"] },
|
|
217
239
|
{ "required": ["kindRegistry"] },
|
|
240
|
+
{ "required": ["contributionsRegistry"] },
|
|
241
|
+
{ "required": ["elapsedMs"] }
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"description": "View-contributions-catalog envelope — `items` + `counts.total` only, no `filters` / `kindRegistry` / `contributionsRegistry` / `returned`. Used by `GET /api/contributions/registered`. Mirror of `annotations.registered`. The catalog ships in entirety; `counts.total` doubles as `items.length`. Each item is an `IRegisteredViewContribution` shape: `{ pluginId, extensionId, contributionId, contract, label?, tooltip?, icon?, emptyText?, emitWhenEmpty? }`.",
|
|
247
|
+
"required": ["items", "counts"],
|
|
248
|
+
"properties": {
|
|
249
|
+
"kind": { "const": "contributions.registered" },
|
|
250
|
+
"counts": {
|
|
251
|
+
"not": { "required": ["returned"] }
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
"not": {
|
|
255
|
+
"anyOf": [
|
|
256
|
+
{ "required": ["item"] },
|
|
257
|
+
{ "required": ["value"] },
|
|
258
|
+
{ "required": ["filters"] },
|
|
259
|
+
{ "required": ["kindRegistry"] },
|
|
260
|
+
{ "required": ["contributionsRegistry"] },
|
|
218
261
|
{ "required": ["elapsedMs"] }
|
|
219
262
|
]
|
|
220
263
|
}
|
|
221
264
|
},
|
|
222
265
|
{
|
|
223
|
-
"description": "Sentinel kinds — reserved for routes that do NOT carry an envelope payload at the wire level (`health`, `scan`, `graph`). They do not carry `kindRegistry` either; clients that need
|
|
266
|
+
"description": "Sentinel kinds — reserved for routes that do NOT carry an envelope payload at the wire level (`health`, `scan`, `graph`). They do not carry `kindRegistry` or `contributionsRegistry` either; clients that need either must call any payload-bearing endpoint at boot.",
|
|
224
267
|
"properties": {
|
|
225
268
|
"kind": { "enum": ["health", "scan", "graph"] }
|
|
226
269
|
},
|
|
@@ -230,6 +273,7 @@
|
|
|
230
273
|
{ "required": ["item"] },
|
|
231
274
|
{ "required": ["value"] },
|
|
232
275
|
{ "required": ["kindRegistry"] },
|
|
276
|
+
{ "required": ["contributionsRegistry"] },
|
|
233
277
|
{ "required": ["elapsedMs"] }
|
|
234
278
|
]
|
|
235
279
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"id": {
|
|
10
10
|
"type": "string",
|
|
11
11
|
"pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
|
|
12
|
-
"description": "Kebab-case identifier unique within the extension's kind across the same plugin. The kernel registers every extension under the **qualified** id `<plugin-id>/<id>` (e.g. `core/
|
|
12
|
+
"description": "Kebab-case identifier unique within the extension's kind across the same plugin. The kernel registers every extension under the **qualified** id `<plugin-id>/<id>` (e.g. `core/annotations`, `core/slash`, `hello-world/greet`) — see `architecture.md` §Extension kinds and `plugin-author-guide.md` §Qualified extension ids. Authors declare only the short id here; the qualifier is composed by the loader from the manifest's `id` (or, for built-ins, from the bundle declaration in `built-ins.ts`). The pattern is intentionally constrained to a single kebab-case segment without the `/` separator: the qualifier always lives in the plugin id, never in the extension id."
|
|
13
13
|
},
|
|
14
14
|
"kind": {
|
|
15
15
|
"type": "string",
|
|
@@ -63,6 +63,16 @@
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
"description": "Plugin-contributed annotation keys. Each entry declares an inline JSON Schema for the value the extension writes into a sidecar. Keys default to the plugin's `<plugin-id>:` namespace; opt-in to top-level via `location: 'root'` (requires `ownership: 'exclusive'`). The kernel exposes the runtime catalog via `kernel.getRegisteredAnnotationKeys()` for UI autocomplete; the built-in `core/unknown-field` Rule emits a warning on truly unrecognized keys (typo guard). See `plugin-author-guide.md` §Annotation contributions for the worked examples."
|
|
66
|
+
},
|
|
67
|
+
"viewContributions": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": {
|
|
70
|
+
"$ref": "../view-contracts.schema.json#/$defs/IViewContribution"
|
|
71
|
+
},
|
|
72
|
+
"propertyNames": {
|
|
73
|
+
"pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
|
|
74
|
+
},
|
|
75
|
+
"description": "Plugin-contributed view contributions. Each entry declares one rendering surface in the UI by picking a `contract` name from the closed catalog at `view-contracts.schema.json#/$defs/ContractName`. The kernel validates the manifest at load (`invalid-manifest` on unknown contract); the plugin emits per-node payloads via `ctx.emitContribution(<contributionId>, payload)` during scan; the runtime validates payloads against the contract's payload schema in `view-contracts.schema.json#/$defs/payloads/<contract>`; off-contract payloads emit `extension.error` and drop silently (mirror of `emitLink` off-contract drop). The kernel exposes the runtime catalog via `kernel.getRegisteredViewContributions()`; the BFF surfaces it at `GET /api/contributions/registered`. The plugin author NEVER picks a slot — slot mapping is a UI decision (see `ROADMAP.md` §UI contribution system). See `plugin-author-guide.md` §View contributions for worked examples."
|
|
66
76
|
}
|
|
67
77
|
}
|
|
68
78
|
}
|