@skill-map/spec 0.46.0 → 0.47.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.
Files changed (28) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/architecture.md +15 -8
  3. package/cli-contract.md +6 -11
  4. package/conformance/README.md +1 -0
  5. package/conformance/cases/plugin-missing-ui-rejected.json +1 -1
  6. package/conformance/cases/view-action-button.json +21 -0
  7. package/conformance/cases/view-contribution-payloads.json +19 -0
  8. package/conformance/cases/view-slots-all.json +15 -0
  9. package/conformance/coverage.md +1 -1
  10. package/conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/analyzers/good-badges/index.js +46 -0
  11. package/conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/plugin.json +6 -0
  12. package/conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/analyzers/header-counter/index.js +28 -0
  13. package/conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/plugin.json +6 -0
  14. package/conformance/fixtures/view-action-button/notes/example.md +6 -0
  15. package/conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/analyzers/panels/index.js +37 -0
  16. package/conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/plugin.json +6 -0
  17. package/conformance/fixtures/view-contribution-payloads/notes/example.md +5 -0
  18. package/conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/analyzers/everything/index.js +35 -0
  19. package/conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/plugin.json +6 -0
  20. package/index.json +27 -14
  21. package/package.json +1 -1
  22. package/plugin-author-guide.md +42 -6
  23. package/schemas/api/rest-envelope.schema.json +12 -3
  24. package/schemas/extensions/action.schema.json +32 -0
  25. package/schemas/extensions/base.schema.json +4 -0
  26. package/schemas/plugins-doctor.schema.json +45 -2
  27. package/schemas/plugins-registry.schema.json +4 -0
  28. package/schemas/view-slots.schema.json +112 -23
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Spec changelog
2
2
 
3
+ ## 0.47.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Inspector action-button adopters: `core/node-stability`, `core/supersede` and a new `core/tags` analyzer emit Set stability / Supersede / Edit tags buttons, each parametrized via an input-type prompt pre-loaded with the current value, backed by deterministic actions `core/node-set-stability`, `core/node-set-tags`, `core/node-supersede`.
8
+
9
+ ## User-facing
10
+
11
+ The inspector now offers Supersede, Set stability and Edit tags buttons; each opens a small form pre-filled with the node's current value.
12
+
13
+ - Plugins can now contribute action buttons to the inspector: a new `inspector.action.button` slot renders buttons that dispatch a kernel Action via `POST /api/actions/:id`, and the two header badge sub-slots collapse into one `inspector.header.badge` slot. The `.sm` write consent splits into `confirm` (one-shot) and `always` (persists `allowEditSmFiles`). `core/annotation-stale` now emits the Bump button and stale badge as contributions instead of hardcoded UI.
14
+
15
+ ## User-facing
16
+
17
+ The inspector now renders the Bump button and the stale indicator from a plugin instead of hardcoded UI. Writing a `.sm` sidecar now asks for consent every time, with an "always allow" checkbox that persists the permission for the project.
18
+
19
+ - Inspector body view contributions now render one collapsible section per plugin (titled by the trusted `pluginId`, collapsed by default) instead of a shared drawer; the `inspector.body.section` slot is retired. New optional inspector-only `order` fields on `plugin.json` (sorts sections) and the extension manifest (sorts bricks) drive layout, default 100. `inspector.action.button` is now uncapped.
20
+
21
+ ## User-facing
22
+
23
+ Plugin contributions in the inspector now appear as one collapsed section per plugin, ordered by the new `order` fields you can set in `plugin.json` and your extension manifest. The inspector also shows every action button a plugin contributes.
24
+
25
+ - Runtime contribution rejections (an undeclared ref, or a payload that fails the slot's schema) are now persisted per scan to a `scan_contribution_errors` table. `sm plugins doctor` prints a per-plugin "Runtime contribution errors" section and exits non-zero when any exist; `GET /api/plugins` embeds a per-plugin `runtimeContributionErrors[]` field the Settings panel renders as a warning badge plus a collapsible list. The `extension.error` scan event still fires.
26
+
27
+ ## User-facing
28
+
29
+ `sm plugins doctor` now reports view-contribution errors from your last scan (and exits non-zero if any), and the Settings plugin panel shows a per-plugin warning badge with the failed emissions, so a plugin whose chips silently vanished now tells you why.
30
+
31
+ - View contributions are now emitted by object reference, not a string id: declare each as a const in the `ui` map and pass it to `ctx.emitContribution(ref, payload)`. The kernel recovers the id by object identity and rejects an undeclared ref with a loud `extension.error`. The payload is type-checked at author time via generated `SlotPayload<slot>` types (AJV still enforces it at runtime). The three list-payload fields were renamed: breakdown `bars`, key-values `pairs`, link-list `links`.
32
+
33
+ - The `sm tutorial` verb drops its `master` positional variant and now materializes a single `sm-tutorial` skill, restructured into a "book" of ordered parts and chapters with a manifest-driven menu. The advanced walkthrough (plugins, settings, view-slots) and the CLI deep-dive are parts inside that one skill, reached from its menu after the live-UI prologue. `sm tutorial master` exits 2; `.claude/skills/sm-master/` is removed.
34
+
35
+ ## User-facing
36
+
37
+ `sm tutorial master` is gone. Run `sm tutorial`: the advanced parts (plugins, settings, view-slots) and the CLI in depth are now chapters you pick from a menu inside the tutorial, after the live-UI prologue.
38
+
39
+ ### Patch Changes
40
+
41
+ - Plugin load failures read better. A wrong view-slot value collapses AJV's `must be equal to constant` wall into one `<path> is not a valid value` linking to the slot catalog (`spec/view-slots.md`) on GitHub; other manifest errors link to the kind schema. The warning is one non-repetitive line, `plugin <id> (<status>), all extensions skipped: <reason>`. Plugin-load warnings also no longer print twice at `sm serve` boot.
42
+
43
+ ## User-facing
44
+
45
+ Clearer plugin errors: a wrong view-slot name now gives a short message linking to the slot catalog, and the warning spells out that the plugin and all its extensions were skipped. It also no longer appears twice when the server starts.
46
+
47
+ - Harden test and conformance coverage for the emit-by-reference view-contribution refactor: orchestrator rejection-path and renderer unit tests, `sm plugins doctor` runtime-error coverage, two new conformance cases (renamed list payloads with off-shape rejections, and a manifest declaring all 14 slots) plus a fixture-drift fix. The conformance suite now runs in CI via `validate:test`, and the `plugins doctor` docs gain a runtime-error note. No CLI or normative spec change.
48
+
3
49
  ## 0.46.0
4
50
 
5
51
  ### Minor Changes
package/architecture.md CHANGED
@@ -651,18 +651,19 @@ The Action stays pure (no IO). The kernel materializes the patch through the `Si
651
651
 
652
652
  ### Write consent
653
653
 
654
- Every `.sm` write, scaffold (`sm sidecar annotate`), hash-only update (`sm sidecar refresh`), bump (`sm bump`, `POST /api/sidecar/bump`), or any future write surface, passes through `SidecarStore.applyPatch` (or, where the verb writes a fresh sidecar, the equivalent kernel-managed entry point). That single chokepoint MUST consult `allowEditSmFiles` (see §Config layering) before touching disk:
654
+ Every `.sm` write, scaffold (`sm sidecar annotate`), hash-only update (`sm sidecar refresh`), bump (`sm bump`, `POST /api/sidecar/bump`), action dispatch (`POST /api/actions/:id` for any `.sm`-writing Action), or any future write surface, passes through `SidecarStore.applyPatch` (or, where the verb writes a fresh sidecar, the equivalent kernel-managed entry point). That single chokepoint MUST consult `allowEditSmFiles` (see §Config layering) before touching disk. Every write asks unless `allowEditSmFiles === true`; the dispatch / bump body carries two orthogonal consent fields, `confirm` (one-shot grant) and `always` (persist the grant):
655
655
 
656
- - `allowEditSmFiles === true` → write proceeds.
657
- - `allowEditSmFiles === false` AND the caller passes `confirm: true` → the kernel persists `allowEditSmFiles: true` to `<cwd>/.skill-map/settings.local.json` (layer `project-local`) and then performs the write.
658
- - `allowEditSmFiles === false` AND `confirm` is missing / false the kernel raises `EConsentRequiredError`. The driving adapter MUST translate it into a surface-appropriate prompt:
659
- - **CLI on a TTY**: interactive `confirm()` prompt. Accept re-invokes the verb with `confirm: true`; decline aborts without persisting the rejection.
656
+ - `allowEditSmFiles === true` → write proceeds, no prompt (consent already persisted).
657
+ - `allowEditSmFiles === false` AND the caller passes `always: true` → the kernel persists `allowEditSmFiles: true` to `<cwd>/.skill-map/settings.local.json` (layer `project-local`) and then performs the write. `always` **implies** `confirm`: the grant authorises this write too, so a body carrying `always: true` need not also set `confirm`.
658
+ - `allowEditSmFiles === false` AND `confirm: true` (without `always`) a **one-shot** grant. The kernel performs this write but persists **nothing**; the next write re-asks. Use this for "yes, just this once".
659
+ - `allowEditSmFiles === false` AND both `confirm` and `always` missing / false → the kernel raises `EConsentRequiredError`. The driving adapter MUST translate it into a surface-appropriate prompt:
660
+ - **CLI on a TTY**: interactive `confirm()` prompt offering "just this once" (re-invokes with `confirm: true`) vs. "always for this project" (re-invokes with `always: true`). Decline aborts without persisting the rejection.
660
661
  - **CLI without a TTY** (CI, scripts): exit with the standard "user input required" code and a message hinting `--yes`.
661
- - **BFF**: 412 `confirm-required` envelope (`{ ok: false, error: { code: 'confirm-required', message, details: { key: 'allowEditSmFiles' } } }`). The UI catches it, opens a confirm dialog, and on accept retries the original request with `{ confirm: true }`.
662
+ - **BFF**: 412 `confirm-required` envelope (`{ ok: false, error: { code: 'confirm-required', message, details: { key: 'allowEditSmFiles' } } }`). The UI catches it, opens a confirm dialog with the same two choices, and on accept retries the original request with `{ confirm: true }` or `{ always: true }`.
662
663
 
663
- The rejection is **not persisted**. Declining the prompt aborts the current operation but the next attempt re-asks. This is deliberate: a "no" today should not foreclose a "yes" tomorrow without the user having to hand-edit the settings file.
664
+ Declining the prompt persists **nothing**, neither a grant nor a rejection. It aborts the current operation but the next attempt re-asks. This is deliberate: a "no" today should not foreclose a "yes" tomorrow without the user having to hand-edit the settings file, and a one-shot `confirm` never silently enrols the project into unconditional writes.
664
665
 
665
- The flag lives in `project-local` (gitignored) so each collaborator consents independently; a single contributor's "yes" never enrols their teammates without their knowledge.
666
+ The flag lives in `project-local` (gitignored) so each collaborator consents independently; a single contributor's `always` never enrols their teammates without their knowledge.
666
667
 
667
668
  ### Plugin contributions
668
669
 
@@ -758,6 +759,8 @@ Each entry picks a `slot` name from the closed catalog and supplies presentation
758
759
 
759
760
  The plugin author picks ONE slot per contribution; that single decision determines where the data renders, what payload shape `ctx.emitContribution(...)` must produce, and which Angular component draws it. Seven manifest fields per contribution (`slot`, `label?`, `tooltip?`, `icon?`, `emptyText?`, `emitWhenEmpty?`, `priority?`) + the slot catalog page is the entire mental model. See [`plugin-author-guide.md`](./plugin-author-guide.md) §View contributions for worked examples.
760
761
 
762
+ The six `inspector.body.panel.*` slots render grouped **one collapsible section per plugin** in the inspector body (titled by the trusted `pluginId`, collapsed by default); a plugin's bricks never land in another plugin's section. Two optional inspector-only ordering hints drive layout: a plugin-level `order` in `plugin.json` sorts the sections, an extension-level `order` (base extension manifest) sorts the bricks within a section. Both default to 100 and never affect execution order. They are denormalised onto each `contributionsRegistry` entry (`pluginOrder` / `extensionOrder`) so the UI applies them without a second round-trip.
763
+
761
764
  ### Settings
762
765
 
763
766
  Plugin user-configurable settings live **on each extension's manifest** (structure-as-truth) in `settings: Record<string, ISettingDeclaration>` (see [`schemas/extensions/base.schema.json`](./schemas/extensions/base.schema.json) and [`schemas/input-types.schema.json`](./schemas/input-types.schema.json)). Each setting picks an input-type from the closed catalog (`string-list`, `single-string`, `boolean-flag`, `integer`, `enum-pick`, `enum-multipick`, `path-glob`, `regex`, `secret`, `key-value-list`). The kernel exposes resolved settings via `ctx.settings.<settingId>` to the extension's runtime methods (`extract`, `evaluate`, `invoke`, etc.); the UI generates a form per declaration; the CLI's `sm plugins config <plugin>/<extension>` exposes the same surface. Plugin-level settings are no longer supported; the field was moved from `plugin.json` to each extension that consumes it.
@@ -829,6 +832,10 @@ Endpoints under `/api/contributions/*`:
829
832
  - `GET /api/contributions/registered`, runtime catalog. Mirror of `/api/annotations/registered`. Envelope variant `kind: 'contributions.registered'` (see [`schemas/api/rest-envelope.schema.json`](./schemas/api/rest-envelope.schema.json)).
830
833
  - `GET /api/contributions/:pluginId/:extensionId/:contributionId?path=...`, lazy per-node fetch for inspector slots. **Three URL segments** mirror the qualified id `<pluginId>/<extensionId>/<contributionId>`. Filters by qualified id + node path; the BFF enforces `pluginId` ↔ namespace at the route level, no cross-plugin reads via this endpoint.
831
834
 
835
+ The `inspector.action.button` slot dispatches to a generic Action endpoint, sibling of the single-node `POST /api/sidecar/bump`:
836
+
837
+ - `POST /api/actions/:id`, dispatch a kernel Action by qualified id (`:id` is the `<plugin>/<action>` from the button payload's `actionId`). Body carries the target `nodePath`, the optional reserved `input` object (Steps 2+), and the consent fields `confirm` / `always` (see §Annotation system → Write consent) for `.sm`-writing Actions. The kernel resolves the Action in its registry (unknown id → 404), runs it against the node, and answers the action-result envelope `kind: 'action.applied'` (`{ value: { actionId, nodePath, report }, elapsedMs }`, see [`schemas/api/rest-envelope.schema.json`](./schemas/api/rest-envelope.schema.json)). `POST /api/sidecar/bump` remains the dedicated single-purpose route for `core/node-bump` (`kind: 'sidecar.bumped'`); the generic dispatch route shares the same action-result envelope variant.
838
+
832
839
  Plus catalog embedding into every payload-bearing envelope:
833
840
 
834
841
  - `kindRegistry`, `providerRegistry`, and `contributionsRegistry` are siblings on the envelope (see schema). Built once per server boot, embedded into list (`nodes` / `links` / `issues` / `plugins`), single (`node`), and value (`config`) envelopes. Sentinel envelopes (`health` / `scan` / `graph`) and action-result envelopes (`sidecar.bumped`) and the catalog envelopes themselves (`annotations.registered` / `contributions.registered`) carry none of them. `providerRegistry` is the static boot catalog of registered Providers' identity; the dynamic active lens (current value + filesystem-detected candidates + the enabled `selectable` set) is served separately by `GET /api/active-provider`.
package/cli-contract.md CHANGED
@@ -182,32 +182,27 @@ Flags: `--no-scan` (skip the first scan), `--force` (rewrite an existing config)
182
182
 
183
183
  Exit: 0 on success, 2 on failure.
184
184
 
185
- #### `sm tutorial [variant]`
185
+ #### `sm tutorial`
186
186
 
187
- Materialize an interactive tester tutorial as a skill folder under the chosen agent's on-disk territory. Companion to the `sm-tutorial` and `sm-master` skills: a tester drops into an empty directory, runs `sm tutorial` (or `sm tutorial master`) to seed the skill, then opens their agent there and triggers it by speaking one of its trigger phrases (the agent auto-discovers `<skillDir>/<slug>/SKILL.md` on boot).
187
+ Materialize the interactive tester tutorial as a skill folder under the chosen agent's on-disk territory. Companion to the `sm-tutorial` skill: a tester drops into an empty directory, runs `sm tutorial` to seed the skill, then opens their agent there and triggers it by speaking one of its trigger phrases (the agent auto-discovers `<skillDir>/sm-tutorial/SKILL.md` on boot). The skill is a single "book" of parts and chapters: a first-time tester walks the live-UI prologue, then picks further parts (extend skill-map with plugins/settings/view-slots, the CLI in depth) from an in-skill menu. The verb takes **no positional argument**.
188
188
 
189
- The optional positional `variant` argument selects which skill gets materialised. Valid values are:
190
-
191
- - `tutorial` (default, also the behaviour when no argument is passed): the basic onboarding walkthrough, slug `sm-tutorial`.
192
- - `master`: the advanced walkthrough (plugin tour, plugin authoring, settings + view-slots), slug `sm-master`, includes the `references/` sub-folder.
193
-
194
- The destination directory is the selected Provider's `scaffold.skillDir` (e.g. `.claude/skills` for Claude, `.agents/skills` for the open standard adopted by Antigravity); the verb writes `<cwd>/<skillDir>/<slug>/`. Provider selection:
189
+ The destination directory is the selected Provider's `scaffold.skillDir` (e.g. `.claude/skills` for Claude, `.agents/skills` for the open standard adopted by Antigravity); the verb writes `<cwd>/<skillDir>/sm-tutorial/`. Provider selection:
195
190
 
196
191
  - `--for <provider-id>` selects the destination Provider explicitly (e.g. `--for claude`, `--for agent-skills`). The id MUST be a registered Provider that declares `scaffold.skillDir`; any other value is a usage error.
197
192
  - Without `--for`, the default Provider is the first scaffold-capable Provider in catalog order (Claude). The verb requires an empty cwd (see below), so there is no marker to detect: provider auto-detection does not apply.
198
193
  - Without `--for`, on an interactive stdin the verb prompts with a numbered list of the Providers that declare `scaffold.skillDir`, marking the default option (Claude); an empty answer accepts it. Each option shows the Provider label plus any `scaffold.aka` agents in parentheses (e.g. the open standard lists Antigravity and OpenAI Codex). The `aka` strings are display-only and are NOT accepted by `--for`.
199
194
  - Without `--for`, on a non-interactive stdin (pipes, CI) the verb selects the default Provider without prompting, so the verb stays scriptable.
200
195
 
201
- Common behaviour for both variants:
196
+ Behaviour:
202
197
 
203
- - Writes the full skill folder (`SKILL.md` plus any `references/` sub-folder) under the resolved `<skillDir>/<slug>/`.
198
+ - Writes the full skill folder (`SKILL.md` plus its `references/` sub-folder) under the resolved `<skillDir>/sm-tutorial/`.
204
199
  - Content is the canonical skill shipped with the implementation. The `SKILL.md` payload is host-agnostic; only the destination directory varies per Provider. Any conforming implementation MUST embed equivalent tutorial sources (the prose itself is informative; what is normative is that the verb produces a readable skill folder a compatible agent can consume).
205
200
  - Requires the cwd to be empty (a directory listing including dotfiles returns nothing). The tutorial seeds a self-contained scenario and the skill later lays its fixtures and `.skill-map/` directly in the cwd, so the tester can delete the whole directory afterwards without losing prior work; that guarantee only holds when the directory started empty. A non-empty cwd is refused (exit 2) unless `--force` is passed.
206
201
  - Does NOT require an initialized project and never reads or writes `.skill-map/`. It is a pre-bootstrap helper: Provider selection reads the built-in Provider catalog directly, not project config.
207
202
 
208
203
  Flags: `--for <provider-id>` (destination Provider, skips the prompt); `--force` (proceed even when the cwd is not empty, overwriting any existing target folder, without prompting).
209
204
 
210
- Exit: `0` on success; `2` if the cwd is not empty and `--force` was not passed (operational error, refusing to seed the tutorial into a directory that already holds content); `2` if the positional `variant` is set to a value other than `tutorial` or `master`; `2` if `--for` names a Provider that does not exist or declares no `scaffold.skillDir`; `2` on any I/O failure.
205
+ Exit: `0` on success; `2` if the cwd is not empty and `--force` was not passed (operational error, refusing to seed the tutorial into a directory that already holds content); `2` if an unexpected positional argument is passed (the verb takes no positional; e.g. the removed `master` variant, the advanced walkthrough is now a part inside the single skill, reached from its menu); `2` if `--for` names a Provider that does not exist or declares no `scaffold.skillDir`; `2` on any I/O failure.
211
206
 
212
207
  #### `sm version`
213
208
 
@@ -123,6 +123,7 @@ Assertion types beyond this list MAY be proposed via spec-vX.Y.Z minor bumps. Im
123
123
  | `orphan-markdown-fallback` | Multi-Provider corpus where one node lands via the universal `core/markdown` fallback and another via vendor-specific claude classification. Locks the orchestrator's path-dedup contract. |
124
124
  | `plugin-missing-ui-rejected` | Drop-in Provider whose `kinds[*]` entry omits the required `ui` block fails AJV validation with `invalid-manifest` while the rest of the pipeline keeps running. |
125
125
  | `sidecar-end-to-end` | Co-located `.sm` sidecar shape, stale / orphan detection, populated `Node.sidecar` overlay, both `annotation-stale` and `annotation-orphan` issues emitted. |
126
+ | `view-action-button` | An analyzer declaring the unified `inspector.header.badge` + the new `inspector.action.button` slots loads clean, while a sibling declaring the retired `inspector.header.badge.counter` slot fails as `invalid-manifest`; `sm scan` survives. |
126
127
 
127
128
  Cases explicitly referenced elsewhere in the spec (landing before v1.0):
128
129
 
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "assertions": [
11
11
  { "type": "exit-code", "value": 0 },
12
- { "type": "stderr-matches", "pattern": "plugin bad-provider:.*invalid.*must have required property 'ui'" },
12
+ { "type": "stderr-matches", "pattern": "plugin bad-provider \\(invalid-manifest\\), all extensions skipped:.*must have required property 'ui'" },
13
13
  { "type": "json-path", "path": "$.providers.length", "equals": 5 },
14
14
  { "type": "json-path", "path": "$.providers[0]", "equals": "claude" },
15
15
  { "type": "json-path", "path": "$.providers[1]", "equals": "antigravity" },
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
3
+ "id": "view-action-button",
4
+ "description": "Inspector slot catalog change: an analyzer declaring the unified `inspector.header.badge` slot plus the new `inspector.action.button` slot MUST load clean, while an analyzer declaring the retired `inspector.header.badge.counter` slot (folded into `inspector.header.badge`) MUST be rejected as `invalid-manifest` because the id is no longer in the closed `SlotName` enum. `sm scan` MUST exit cleanly with the good plugin and the markdown node intact, locking that the removed sub-slots fail manifest load while the unified badge and the action-button slot are accepted.",
5
+ "fixture": "view-action-button",
6
+ "invoke": {
7
+ "verb": "scan",
8
+ "flags": ["--json"]
9
+ },
10
+ "assertions": [
11
+ { "type": "exit-code", "value": 0 },
12
+ { "type": "json-path", "path": "$.schemaVersion", "equals": 1 },
13
+ { "type": "json-path", "path": "$.stats.nodesCount", "equals": 1 },
14
+ { "type": "json-path", "path": "$.stats.issuesCount", "equals": 0 },
15
+ { "type": "json-path", "path": "$.nodes[0].path", "equals": "notes/example.md" },
16
+ { "type": "json-path", "path": "$.nodes[0].kind", "equals": "markdown" },
17
+ { "type": "json-path", "path": "$.nodes[0].provider", "equals": "markdown" },
18
+ { "type": "stderr-matches", "pattern": "plugin legacy-badge \\(invalid-manifest\\), all extensions skipped:.*/ui/keywords/slot is not a valid value" },
19
+ { "type": "stderr-matches", "pattern": "See https://github.com/crystian/skill-map/blob/main/spec/view-slots.md" }
20
+ ]
21
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
3
+ "id": "view-contribution-payloads",
4
+ "description": "An analyzer emitting well-formed payloads to the renamed list-panel slots (breakdown `bars`, key-values `pairs`, link-list `links`) MUST load and scan clean (exit 0). Two deliberately off-shape emissions MUST each be dropped with a loud `extension.error` on stderr: a payload that fails the key-values AJV schema (a pair missing `value`), and a spread copy that loses the `ui` object identity (the undeclared-contribution-ref guard). Locks the emit-by-reference contract, the renamed payload fields, and the visible off-shape diagnostic.",
5
+ "fixture": "view-contribution-payloads",
6
+ "invoke": {
7
+ "verb": "scan",
8
+ "flags": ["--json"]
9
+ },
10
+ "assertions": [
11
+ { "type": "exit-code", "value": 0 },
12
+ { "type": "json-path", "path": "$.schemaVersion", "equals": 1 },
13
+ { "type": "json-path", "path": "$.stats.nodesCount", "equals": 1 },
14
+ { "type": "json-path", "path": "$.stats.issuesCount", "equals": 0 },
15
+ { "type": "json-path", "path": "$.nodes[0].path", "equals": "notes/example.md" },
16
+ { "type": "stderr-matches", "pattern": "payload failed the .inspector.body.panel.key-values. schema" },
17
+ { "type": "stderr-matches", "pattern": "whose object is not one declared in its .ui. map" }
18
+ ]
19
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
3
+ "id": "view-slots-all",
4
+ "description": "An analyzer whose `ui` map declares a contribution to every one of the 14 view slots (with the required `icon` on the counter and title slots) MUST load clean: `sm plugins doctor` reports `ok` with no `invalid-manifest`. Locks that every slot id in the closed catalog is a valid manifest declaration; removing or renaming a slot would flip a declaration to `invalid-manifest` and fail this case.",
5
+ "fixture": "view-slots-all",
6
+ "invoke": {
7
+ "verb": "plugins",
8
+ "sub": "doctor",
9
+ "flags": ["--json"]
10
+ },
11
+ "assertions": [
12
+ { "type": "exit-code", "value": 0 },
13
+ { "type": "json-path", "path": "$.ok", "equals": true }
14
+ ]
15
+ }
@@ -38,7 +38,7 @@ This file is hand-maintained. A CI check before spec release compares the schema
38
38
  | 27 | `annotations.schema.json` | `sidecar-end-to-end` | 🟢 covered | Curated catalog of 15 conventional skill-map annotation fields (versioning, supersession, provenance, lifecycle, taxonomy, display, docs). `additionalProperties: true` so users / plugins extend without coordination; the `unknown-field` Tier-1 analyzer shipped in Step 9.6.6 emits warnings on truly unrecognized keys. Step 9.6.2 (2026-05-05) wired the kernel reader; Step 9.6.6 (2026-05-06) flips this row 🟢 via `sidecar-end-to-end`, which asserts that an `annotations.version: 7` value round-trips through `state_scan_nodes.annotations_json` and surfaces in the node's `sidecar.annotations` overlay AND in the denormalised `Node.version` column. Structural sample at `fixtures/sidecar-example/agent-example.sm`. Catalog trimmed from 31 to 15 fields on 2026-05-07 after UX review. |
39
39
  | 28 | `bump-report.schema.json` |, | 🔴 missing | Report shape produced by the built-in deterministic `bump` Action (Step 9.6.3, Decision #125). Extends `report-base-deterministic.schema.json` (row 29), the deterministic counterpart to `report-base.schema.json` (which is LLM-specific via `confidence` + `safety`). Three concrete shapes: success-with-write, silent-no-op (under `force`), and refusal (`fresh`). Direct conformance case lands together with the `sm bump` CLI verb in Step 9.6.4, it'll exercise all three branches via `sm bump --json` against a primed fixture. Implementation tests at `src/test/bump-action.test.ts` cover the runtime behaviour today. |
40
40
  | 29 | `report-base-deterministic.schema.json` |, (indirect via row 28) | 🟡 partial | Deterministic counterpart to `report-base.schema.json`; every deterministic Action's report extends this base. Direct contract case still pending, landed when first conformance case directly validates a deterministic report against this schema. |
41
- | 30 | `view-slots.schema.json` |, | 🔴 missing | Closed catalog of 14 view slots + the `IViewContribution` manifest declaration shape + per-slot payload schemas. Cases required (3): (a) `plugin-view-contributions-valid`, a plugin manifest declaring contributions of every slot validates; (b) `plugin-view-contributions-invalid-slot`, a manifest referencing a slot not in the catalog rejects with `invalid-manifest`; (c) `plugin-view-contributions-payload-mismatch`, an extractor emitting an off-shape payload triggers `extension.error` and drops silently. Implementation lands with the kernel surface in Phase 2 of the UI contributions plan; conformance fixtures land alongside. |
41
+ | 30 | `view-slots.schema.json` | `view-action-button`, `view-contribution-payloads`, `view-slots-all` | 🟢 covered | Closed catalog of 14 view slots + the `IViewContribution` manifest declaration shape + per-slot payload schemas. `view-action-button` covers the positive load of the unified `inspector.header.badge` slot and the new `inspector.action.button` dispatch slot AND the negative path: an analyzer declaring the retired `inspector.header.badge.counter` slot (folded into `inspector.header.badge`) rejects with `invalid-manifest` while the good plugin and the markdown node survive (exit 0). `view-contribution-payloads` covers the renamed list-payload fields (breakdown `bars`, key-values `pairs`, link-list `links`) loading + scanning clean, plus two visible off-shape rejections, an AJV payload failure and an undeclared-ref spread copy, each surfacing an `extension.error` on stderr. `view-slots-all` declares a contribution to every one of the 14 catalog slots and asserts `sm plugins doctor` loads it clean (`ok`), locking that every slot id is a valid manifest declaration. |
42
42
  | 31 | `input-types.schema.json` |, | 🔴 missing | Closed catalog of 10 input-types for plugin settings + the `ISettingDeclaration` discriminated-union manifest shape. Cases required (2): (a) `plugin-settings-valid`, a plugin manifest declaring at least one setting of each input-type validates; (b) `plugin-settings-invalid-type`, a manifest referencing a `type` not in the catalog rejects with `invalid-manifest`. Lands together with the spec/CLI surface for `sm plugins config <id>`. |
43
43
  | 32 | `refresh-report.schema.json` |, | 🔴 missing | Machine-readable output of `sm refresh <node.path> --json` and `sm refresh --stale --json`. Reports the count of enrichment rows persisted across targeted nodes (universal enrichment layer per `architecture.md` §A.8). Direct conformance case pending: seed a fixture with one Provider-classified node, run `sm refresh <node> --json`, assert the envelope validates and `refreshed >= 0`. Implementation tests at `src/test/node-enrichments.test.ts` cover the runtime behaviour today. |
44
44
  | 33 | `plugins-doctor.schema.json` |, | 🔴 missing | Machine-readable output of `sm plugins doctor --json`. Aggregates per-status counts plus structured issue / warning lists. Direct conformance case pending: prime a scope with one healthy + one invalid-manifest drop-in plugin, run `sm plugins doctor --json`, assert the envelope validates and the invalid plugin appears under `issues[]`. Implementation tests at `src/test/plugins-cli.test.ts` cover the runtime behaviour. |
@@ -0,0 +1,46 @@
1
+ // Conformance fixture: an analyzer whose `ui` map declares the two
2
+ // new inspector slots, the unified `inspector.header.badge` and the
3
+ // `inspector.action.button` dispatch slot. Both manifest declarations
4
+ // are well-formed against `view-slots.schema.json#/$defs/IViewContribution`,
5
+ // so the loader MUST accept the plugin and register it.
6
+ //
7
+ // At evaluate time the analyzer emits well-formed payloads for each
8
+ // slot: a header badge ({ count, icon }) the kernel validates against
9
+ // `$defs/payloads/inspector.header.badge`, and an action button
10
+ // ({ actionId, label, enabled }) it validates against
11
+ // `$defs/payloads/inspector.action.button`. The companion case
12
+ // `view-action-button.json` asserts the plugin loads (no stderr
13
+ // rejection) and `sm scan` exits cleanly.
14
+ // Contributions are declared as consts and emitted BY REFERENCE (the kernel
15
+ // recovers the id from the `ui` map by object identity); `ui` lists them by
16
+ // shorthand so each const and its `ui` entry are the same object.
17
+ const keywords = {
18
+ slot: 'inspector.header.badge',
19
+ label: 'keywords',
20
+ };
21
+ const bump = {
22
+ slot: 'inspector.action.button',
23
+ };
24
+
25
+ export default {
26
+ version: '0.1.0',
27
+ description: 'analyzer declaring inspector.header.badge + inspector.action.button',
28
+ mode: 'deterministic',
29
+
30
+ ui: { keywords, bump },
31
+
32
+ evaluate(ctx) {
33
+ for (const node of ctx.nodes) {
34
+ // Well-formed `inspector.header.badge` payload (count + icon).
35
+ ctx.emitContribution(node.path, keywords, { count: 3, icon: 'pi-search' });
36
+ // Well-formed `inspector.action.button` payload.
37
+ ctx.emitContribution(node.path, bump, {
38
+ actionId: 'core/node-bump',
39
+ label: 'Bump version',
40
+ icon: 'pi-arrow-up',
41
+ enabled: true,
42
+ });
43
+ }
44
+ return [];
45
+ },
46
+ };
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "specCompat": "*",
4
+ "catalogCompat": "*",
5
+ "description": "Conformance fixture: one analyzer declaring the unified `inspector.header.badge` plus the `inspector.action.button` slots (loads clean), and a sibling analyzer declaring the retired `inspector.header.badge.counter` slot (rejected as invalid-manifest)."
6
+ }
@@ -0,0 +1,28 @@
1
+ // Conformance fixture: an analyzer whose `ui` map declares the RETIRED
2
+ // `inspector.header.badge.counter` slot. That sub-slot (alongside
3
+ // `inspector.header.badge.tag`) was folded into the unified
4
+ // `inspector.header.badge` slot, so the id is no longer a member of
5
+ // `view-slots.schema.json#/$defs/SlotName`. The loader MUST reject this
6
+ // extension as invalid-manifest (AJV rejects the unknown slot name) and
7
+ // degrade the plugin, leaving the rest of the scan pipeline running.
8
+ //
9
+ // The companion case `view-action-button.json` asserts the stderr
10
+ // rejection text and that `sm scan` survives with the good plugin and
11
+ // the markdown node intact.
12
+ export default {
13
+ version: '0.1.0',
14
+ description: 'analyzer declaring the retired inspector.header.badge.counter slot',
15
+ mode: 'deterministic',
16
+
17
+ ui: {
18
+ keywords: {
19
+ slot: 'inspector.header.badge.counter',
20
+ icon: 'pi-search',
21
+ label: 'keywords',
22
+ },
23
+ },
24
+
25
+ evaluate() {
26
+ return [];
27
+ },
28
+ };
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "specCompat": "*",
4
+ "catalogCompat": "*",
5
+ "description": "Conformance fixture: an analyzer declaring the retired `inspector.header.badge.counter` slot, which the unified `inspector.header.badge` replaced. The loader MUST reject it as invalid-manifest while the rest of the pipeline keeps running."
6
+ }
@@ -0,0 +1,6 @@
1
+ # Example note
2
+
3
+ A single markdown node so the scan produces one node the good
4
+ analyzer can decorate with `inspector.header.badge` and
5
+ `inspector.action.button` contributions. The universal `core/markdown`
6
+ provider classifies it as kind `markdown`.
@@ -0,0 +1,37 @@
1
+ // Conformance fixture: an analyzer that emits well-formed payloads to the
2
+ // three list-panel slots using the renamed payload fields (breakdown `bars`,
3
+ // key-values `pairs`, link-list `links`), plus two deliberately rejected
4
+ // emissions. Contributions are declared as consts and emitted BY REFERENCE;
5
+ // the kernel recovers the id from the `ui` map by object identity.
6
+ //
7
+ // The companion case `view-contribution-payloads.json` asserts the good
8
+ // emissions scan clean (exit 0) and that each rejection surfaces a loud
9
+ // `extension.error` on stderr:
10
+ // - a payload that fails the key-values AJV schema (a pair missing `value`);
11
+ // - a spread copy `{ ...dist }` that loses the `ui` object identity
12
+ // (`undeclared-contribution-ref`).
13
+ const summary = { slot: 'inspector.body.panel.key-values', label: 'Summary' };
14
+ const dist = { slot: 'inspector.body.panel.breakdown', label: 'Distribution' };
15
+ const related = { slot: 'inspector.body.panel.link-list', label: 'Related' };
16
+
17
+ export default {
18
+ version: '0.1.0',
19
+ description: 'analyzer emitting bars/pairs/links payloads plus two rejected emissions',
20
+ mode: 'deterministic',
21
+
22
+ ui: { summary, dist, related },
23
+
24
+ evaluate(ctx) {
25
+ for (const node of ctx.nodes) {
26
+ // Well-formed emissions to the renamed list-payload fields.
27
+ ctx.emitContribution(node.path, summary, { pairs: [{ key: 'kind', value: node.kind }] });
28
+ ctx.emitContribution(node.path, dist, { bars: [{ label: 'len', value: 1 }] });
29
+ ctx.emitContribution(node.path, related, { links: [{ path: node.path }] });
30
+ // Rejection 1: payload fails the key-values schema (pair missing `value`).
31
+ ctx.emitContribution(node.path, summary, { pairs: [{ key: 'broken' }] });
32
+ // Rejection 2: a spread copy loses object identity vs the `ui` map.
33
+ ctx.emitContribution(node.path, { ...dist }, { bars: [{ label: 'x', value: 1 }] });
34
+ }
35
+ return [];
36
+ },
37
+ };
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "specCompat": "*",
4
+ "catalogCompat": "*",
5
+ "description": "Conformance fixture: an analyzer emitting well-formed bars/pairs/links payloads plus two deliberately rejected emissions (off-shape payload + spread copy that loses ui object identity)."
6
+ }
@@ -0,0 +1,5 @@
1
+ # Example note
2
+
3
+ A single markdown node so the scan produces one node the analyzer can
4
+ decorate with `inspector.body.panel.*` contributions. The universal
5
+ `core/markdown` provider classifies it as kind `markdown`.
@@ -0,0 +1,35 @@
1
+ // Conformance fixture: an analyzer whose `ui` map declares a contribution to
2
+ // EVERY one of the 14 view slots in the closed catalog. The counter slots
3
+ // (`card.subtitle.left`, `card.footer.left`, `card.footer.right`) and the
4
+ // standalone icon slot (`card.title.right`) require `icon` in the manifest;
5
+ // the rest only need `slot`. The `ui` keys are kebab-case (the manifest schema
6
+ // constrains contribution ids). The companion case `view-slots-all.json`
7
+ // asserts the plugin loads clean (`sm plugins doctor` reports ok), locking
8
+ // that every catalog slot id is a valid manifest declaration. `evaluate`
9
+ // emits nothing: this case exercises manifest validation, not emission.
10
+ export default {
11
+ version: '0.1.0',
12
+ description: 'analyzer declaring a contribution to every one of the 14 view slots',
13
+ mode: 'deterministic',
14
+
15
+ ui: {
16
+ 'card-title': { slot: 'card.title.right', icon: 'pi-flag' },
17
+ 'card-subtitle': { slot: 'card.subtitle.left', icon: 'pi-hashtag' },
18
+ 'card-footer-left': { slot: 'card.footer.left', icon: 'pi-download' },
19
+ 'card-footer-right': { slot: 'card.footer.right', icon: 'pi-upload' },
20
+ 'graph-alert': { slot: 'graph.node.alert' },
21
+ 'header-badge': { slot: 'inspector.header.badge' },
22
+ 'action-button': { slot: 'inspector.action.button' },
23
+ 'breakdown': { slot: 'inspector.body.panel.breakdown' },
24
+ 'records': { slot: 'inspector.body.panel.records' },
25
+ 'tree': { slot: 'inspector.body.panel.tree' },
26
+ 'key-values': { slot: 'inspector.body.panel.key-values' },
27
+ 'link-list': { slot: 'inspector.body.panel.link-list' },
28
+ 'markdown': { slot: 'inspector.body.panel.markdown' },
29
+ 'scope-stat': { slot: 'topbar.nav.start' },
30
+ },
31
+
32
+ evaluate() {
33
+ return [];
34
+ },
35
+ };
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "specCompat": "*",
4
+ "catalogCompat": "*",
5
+ "description": "Conformance fixture: one analyzer whose ui map declares a contribution to every one of the 14 view slots, so the loader must accept every slot id in the closed catalog."
6
+ }
package/index.json CHANGED
@@ -174,23 +174,26 @@
174
174
  }
175
175
  ]
176
176
  },
177
- "specPackageVersion": "0.46.0",
177
+ "specPackageVersion": "0.47.0",
178
178
  "integrity": {
179
179
  "algorithm": "sha256",
180
180
  "files": {
181
- "CHANGELOG.md": "f144432840afc98bb65fb0982865d1f7ed233cae2ec0b0eaea94c84d2299f771",
181
+ "CHANGELOG.md": "456edc2e8eaab28524945c04f3678354b06f4cd4dfa793ff9b612e2b25aa7fc9",
182
182
  "README.md": "a7505a7b0672c39a8b011e3c5e7d41826306476ee63768249bba4bdb3c03d4d1",
183
- "architecture.md": "b45089d52095177228c443c037ea699061f59ba4fb2b70231ebf8218ab49561d",
184
- "cli-contract.md": "92259a45962f4c0374f707b33b814bce6430eca7936d2ff52e7095484c634279",
185
- "conformance/README.md": "0c69bd9becf511ada9175b1e428ba183e31d1c8a49ff09eedf4c950bb831ec4d",
183
+ "architecture.md": "159163aa0e2a225f5d701e4edd18ef45ce7ca22a75f6ce8e76e2a4d528561c3b",
184
+ "cli-contract.md": "71067c7ff7a845afaa3537f1dd0be5b34b20034d06b42fc29261486ff3f6021b",
185
+ "conformance/README.md": "4ec22ca3cc8e4282fe0bfd111f22b121e0781e2b525867cd092258b8f58ae1e1",
186
186
  "conformance/cases/extractor-emits-signal.json": "0115c7bb62a7a705f72e9d8048b3f0396e5caaeb3d04dea204415e279e58479d",
187
187
  "conformance/cases/kernel-empty-boot.json": "9b51b85ff62479cd0eee37cad260245208d94f6d79644f7ee40945a934960913",
188
188
  "conformance/cases/no-global-scope.json": "1c83343422144be2ad9e3d27d2062e61af87c7c1c1f3b051b6b9f687d845ac7b",
189
189
  "conformance/cases/orphan-markdown-fallback.json": "506119323ddde85c1fb4c986c7f6f40a345d44adb06de8d84002591df0e479ee",
190
- "conformance/cases/plugin-missing-ui-rejected.json": "7c910b74e6f718ab5c1a590cd3544602f056559251d18995a26bca0a0648a2fa",
190
+ "conformance/cases/plugin-missing-ui-rejected.json": "2074fd71937feae136c999f76da81f334f2caf8b65bfe8dc9d7fb800699fb85c",
191
191
  "conformance/cases/sidecar-end-to-end.json": "0a0d941ab50bd7619e1021a6c6d6dc92918429c2efcf25236b42b5fac9eab901",
192
192
  "conformance/cases/signal-collision-detection.json": "c5e39a406ded6928a14c1a22b84f7b3cd49805bec56bd65de83130d9e419c09e",
193
- "conformance/coverage.md": "70cfa9a5736f1e12845da46c4c217b8a6061148f54548b67a30f1c74e3381bc5",
193
+ "conformance/cases/view-action-button.json": "51331f725be1c3655351f8fca6fc9d3d301ae68ea1741ff6c79998332ba2dfeb",
194
+ "conformance/cases/view-contribution-payloads.json": "e8f54ed62e64a2a0f86729866e507abb1f4246683f0e60d538280536f7cd3ecc",
195
+ "conformance/cases/view-slots-all.json": "05284e0324dd2da72b6b21d397c11b355802229a68053e9dddc323f69b3a1eba",
196
+ "conformance/coverage.md": "f93676ede774003c4d15ccf8d3bf2f65b5d032d75ae572df01dff892aeb1a8cf",
194
197
  "conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md": "7f062731106f2d9811e4fffcf6ab44b8dfff4cfb16536a469514cc0664e832bf",
195
198
  "conformance/fixtures/orphan-markdown/ARCHITECTURE.md": "ec903666440bae65da3796b1158c92cfcdce22e0e09c3b20bb690176881a6ac4",
196
199
  "conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/kind.json": "6676a89bae5197e23cf50f1c11d596db558ac80f7334a7208fe57d8b92422251",
@@ -208,22 +211,32 @@
208
211
  "conformance/fixtures/signal-ir-collision/.claude/agents/architect.md": "acc46b5b2dff73d98a354e4d53b5041164595deae466a4e2ce41d7c5a72f28fb",
209
212
  "conformance/fixtures/signal-ir-single-signal/source.md": "1eda417b4c6eed372b66870e385c8d8cd631372b77cab7e996bb711e22218f89",
210
213
  "conformance/fixtures/signal-ir-single-signal/target.md": "527137f2b4f46c0034b0edc8932cf8613d2bf22ffaaf78f01085c82a3baaebe3",
214
+ "conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/analyzers/good-badges/index.js": "943fc3f11c328d2ccc4f8474106f4ae92077d353d02bd0207153efd1d0a1cf42",
215
+ "conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/plugin.json": "ed7b048c140d3d5acdf4456678acba8d9d55fd63511013c8621122b7062f40be",
216
+ "conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/analyzers/header-counter/index.js": "7e097f3f2efcf1175dd02c926a8872f9d2de584c1e6a09fcceb56d603a4386ce",
217
+ "conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/plugin.json": "1ed0decf76d195da2caa5949d6b11fc8fea097416e263d77ed294e5d158304dc",
218
+ "conformance/fixtures/view-action-button/notes/example.md": "c8fad69ee251b25080869c36d84c1f6b697773526a1cd8bda5a577e2027ebb55",
219
+ "conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/analyzers/panels/index.js": "b313ec830ab48bd72e9f347e9d161469a56b8a805cf0861061d0012a452a2706",
220
+ "conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/plugin.json": "18b7d246f004829ed3e86fa40654b34ac2e1ab416aa083fa17ae2e4f13ac3c0d",
221
+ "conformance/fixtures/view-contribution-payloads/notes/example.md": "312b1919cd7fd0f233648b053acfb2975662ede3c65dd391cc508204b67ad6fb",
222
+ "conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/analyzers/everything/index.js": "ea0022fec7f0fd5a26ba12db1310335f434f2f820682206a3a9542d98db0d346",
223
+ "conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/plugin.json": "c48e8a0574947ade0b4eb189d6bc27a48e24f92f616aacdc177f2d22d472a599",
211
224
  "db-schema.md": "f74ce6766bf7f2dcda187a49f82e1768bc1c091d9492846e718903a379610e2e",
212
225
  "interfaces/security-scanner.md": "e8049712b9cf7a07c786bf19f8f775f8ef9638f063f7fba5c7a8b1431b92f38e",
213
226
  "job-events.md": "9d5b35d4c451a7f8eef9915d85316d924ac52f1c026a316cdda5f1099d496854",
214
227
  "job-lifecycle.md": "9c429121f98a07c8795f8979ed1abc5e5334e3f89db51585a8da55c527ef855b",
215
- "plugin-author-guide.md": "b0c510529eb660c753cedfa6397fbf0520135f83f6c8e3cb3816fbfa1c9537a9",
228
+ "plugin-author-guide.md": "8ba0906346300f0d10f0d4a1646b8b27f82cc867c9bfe4d495ebf0f146d6ad0f",
216
229
  "plugin-kv-api.md": "1acc69ed82433a74e35ada61d63a6d7379fb61046ff83de1e0facbe884c64704",
217
230
  "prompt-preamble.md": "9dd4f6d1bc6a425f8782fcee10cbe75909e8d64e28781fda56c2fae909b02f40",
218
231
  "schemas/annotations.schema.json": "8c639b149cad675fdd4e7d6be2b47e920cfdd24087b41361d6e1b8280f646322",
219
- "schemas/api/rest-envelope.schema.json": "cbab82381a49a0a7db796d61a11977abd51d62be423cc19af988a5301a55354f",
232
+ "schemas/api/rest-envelope.schema.json": "8eeb1c2d79fb69eaef23737a2231d48d67e59b8b19aad816239ab4680e2c4752",
220
233
  "schemas/bump-report.schema.json": "c763e1f89f2665c479d6a4985c1d324c65e5278331ebab82220287a07e4c4429",
221
234
  "schemas/conformance-case.schema.json": "958b316d646d0c64a715a7a28cee66d2c2d2498a60dbfc5ae8970687c2a96954",
222
235
  "schemas/conformance-result.schema.json": "14f983a8f4e62cd4ff964688c9b2b026a3bee3a0b762b64091c8c34db5b75777",
223
236
  "schemas/execution-record.schema.json": "db0eb16153493ad9f13ea0ecede44191e4a8536979adffd17ca278ddf8786c77",
224
- "schemas/extensions/action.schema.json": "dc4f52d23c163c6239a487fa1c1ad9c09685cf38833d3962c604d5872716cff9",
237
+ "schemas/extensions/action.schema.json": "8b300532c0217c0f65c454edd6df86d1fe4245590fb5e0974944ce9e593f7f28",
225
238
  "schemas/extensions/analyzer.schema.json": "8def4a5ca4934197c34abde97da70704b2751041a443c859eddd4b783e2fe1db",
226
- "schemas/extensions/base.schema.json": "49baa06a4ce8a6ce75fec52b650d9bf3566e5de0b1053b06f73a71ce103e4fdf",
239
+ "schemas/extensions/base.schema.json": "525226313fd8886a934f944218d34178a3a4e234ee3ca3ddb189ec4f60caec85",
227
240
  "schemas/extensions/extractor.schema.json": "ee44bf562b19318c93116c574a811857cdef1f4119326a9a604fa408889dd230",
228
241
  "schemas/extensions/formatter.schema.json": "880dc379ad545a62404403533a01eda5171edba0390561fc46ec6e986e0b9bd3",
229
242
  "schemas/extensions/hook.schema.json": "f56aef59e9986ffdf7d86aa2e048dccccf217000a358b8c64737cbd911c48dad",
@@ -236,8 +249,8 @@
236
249
  "schemas/job.schema.json": "dbcedf137de03fde38f74686f594e600c627bf808f2aad23511a26617a663a02",
237
250
  "schemas/link.schema.json": "10f700feb3e23d89453d4d11cbe559bcc0b31f6edf08fffbe9e6773e58120512",
238
251
  "schemas/node.schema.json": "14ed2e4c44d01e3f662e240219819895cca06dead374a5cadccfd423c520ed69",
239
- "schemas/plugins-doctor.schema.json": "2238266f31402a446b313af16f933e395a02eca70128e39ab99a11de90a4735f",
240
- "schemas/plugins-registry.schema.json": "6d850d06cdf70e233f20d0d7968bb0c34306f11f30ce2505cec173cd9fa784e5",
252
+ "schemas/plugins-doctor.schema.json": "03e2dc51c052a09bf0198c80e2c26e6129734ada4a748e483245de3dd8576c42",
253
+ "schemas/plugins-registry.schema.json": "211d081691fc83526e1593c79ed9741ad8a5dbd4db1a756f72141b0cced2ea15",
241
254
  "schemas/project-config.schema.json": "0a4a12a3409f900bd19b47c34588c77ac894b944d21a9beebb91ae1e9c0f3d01",
242
255
  "schemas/refresh-report.schema.json": "47184d4f6b15e9b7671dc178b3b3886a64422da198898508ecdb2cb27876db04",
243
256
  "schemas/report-base-deterministic.schema.json": "59785fe6f3ceb34814bbbd03d10fa7336a32835ce598946f2923d469b32aa32a",
@@ -251,7 +264,7 @@
251
264
  "schemas/summaries/markdown.schema.json": "369d75a18710eb6c7ee4220b2899767ddbc07dada24f81b611827fe2e3a2977a",
252
265
  "schemas/summaries/skill.schema.json": "85d68056054bade62391948cc038fcfa70cdcf465e2b295f69cd520bbdba0134",
253
266
  "schemas/user-settings.schema.json": "d155552ffca9c7dd4c6e31398aff4950dd9721d2a1f4b308cf0fe33000ca31b5",
254
- "schemas/view-slots.schema.json": "4623cf8d774f44435f960b1dd3ad4c8b241e37b234ce8eab5b390b9ae8a2acb1",
267
+ "schemas/view-slots.schema.json": "886487a1f38fd7e4270fa6213653664c0cf906043e8aa9e832017149932bf6a2",
255
268
  "telemetry.md": "fa659b47c59e692f50c7a091470888d5e7c98dcf858978fa549af25b2562803f",
256
269
  "versioning.md": "28a13f165f837921fe5066f4bfce61012cd9f1b7c451f88eeb67252e39a0981a"
257
270
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-map/spec",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "description": "JSON Schemas, prose contracts, and conformance suite for the skill-map specification.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -557,6 +557,27 @@ Inside an extractor or analyzer manifest, declare a `ui` map (sibling of `annota
557
557
  }
558
558
  ```
559
559
 
560
+ In TypeScript, declare each contribution as a module-level const with `satisfies IViewContribution` and build `ui` by shorthand. You then emit by passing the SAME object by reference (see [Emit path](#emit-path)) and get a typed payload for free:
561
+
562
+ ```ts
563
+ import type { IViewContribution } from '@skill-map/cli';
564
+
565
+ const breakdown = {
566
+ slot: 'inspector.body.panel.breakdown', label: 'Keyword hits', emptyText: 'No matches.',
567
+ } satisfies IViewContribution;
568
+ const total = {
569
+ slot: 'card.footer.left', icon: '🔍', label: 'kw', emitWhenEmpty: false,
570
+ } satisfies IViewContribution;
571
+
572
+ export default {
573
+ // ...
574
+ ui: { breakdown, total },
575
+ // ...
576
+ };
577
+ ```
578
+
579
+ The `ui` **key** (kebab-case per the manifest schema) is the contribution id; the const's variable name is incidental, because the kernel matches an emission to its declaration by object identity, not by name. Plain `.js` plugins use the same shape without `satisfies` (they get the runtime check, not the compile-time one).
580
+
560
581
  Field reference (full schema in [`schemas/view-slots.schema.json`](./schemas/view-slots.schema.json) at `$defs/IViewContribution`):
561
582
 
562
583
  | Field | Required | Notes |
@@ -596,8 +617,8 @@ The kernel ships exactly these 14 slots. Each fixes a renderer + a payload shape
596
617
  | `card.footer.left` | counter chip (icon required) |
597
618
  | `card.footer.right` | counter chip (icon required) |
598
619
  | `graph.node.alert` | graph corner badge (reserved, see `view-slots.md`) |
599
- | `inspector.header.badge.counter` | counter chip (icon required) |
600
- | `inspector.header.badge.tag` | tag chip |
620
+ | `inspector.header.badge` | unified header badge (icon and/or label and/or count) |
621
+ | `inspector.action.button` | action button (dispatches an Action, see `view-slots.md`) |
601
622
  | `inspector.body.panel.breakdown` | bar chart panel |
602
623
  | `inspector.body.panel.records` | table panel |
603
624
  | `inspector.body.panel.tree` | tree panel |
@@ -606,6 +627,19 @@ The kernel ships exactly these 14 slots. Each fixes a renderer + a payload shape
606
627
  | `inspector.body.panel.markdown` | sanitized markdown panel |
607
628
  | `topbar.nav.start` | scope chip |
608
629
 
630
+ ### Inspector grouping and `order`
631
+
632
+ 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 ever land in its own section: a plugin cannot contribute into another plugin's space.
633
+
634
+ Two optional, inspector-only `order` hints (both `number`, default `100`) control layout:
635
+
636
+ | Field | Where | Effect |
637
+ |---|---|---|
638
+ | `order` | `plugin.json` (plugin level) | Sorts the plugin sections, ASC, tie-break by plugin id. |
639
+ | `order` | extension manifest (extension level) | Sorts the bricks inside a plugin's section, ASC, tie-break by the contribution `priority` then qualified id. |
640
+
641
+ `order` is purely presentational and never affects execution order (analyzer `phase` + registration order govern that). It only applies to the inspector body sections; `priority` still governs ordering within the card / header / action slots.
642
+
609
643
  ### Chip vs Issue
610
644
 
611
645
  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 the colour rule produces visually misleading cards and is caught in code review, not by the schema.
@@ -614,14 +648,14 @@ For analyzers, a per-node card surfaces a finding through two independent channe
614
648
 
615
649
  ```ts
616
650
  // Extractor (per-node walk): nodePath is implicit (ctx.node.path)
617
- ctx.emitContribution('breakdown', { entries: [...] });
618
- ctx.emitContribution('total', { value: total });
651
+ ctx.emitContribution(breakdown, { bars: [...] });
652
+ ctx.emitContribution(total, { value });
619
653
 
620
654
  // Analyzer (post-merge graph): explicit nodePath, the analyzer sees every node at once
621
- ctx.emitContribution(nodePath, 'breakdown', { ... });
655
+ ctx.emitContribution(nodePath, breakdown, { bars: [...] });
622
656
  ```
623
657
 
624
- The first id argument is the **manifest `ui` key**, NOT the slot name; the kernel composes the qualified id from your plugin id, extension id, and the key, and looks up the declared slot to validate the payload against `view-slots.schema.json#/$defs/payloads/<slot>`. Off-shape payloads emit an `extension.error` and drop silently, same posture as `emitLink`. For `topbar.nav.start`, analyzers use `ctx.emitScopeContribution(id, payload)` (reserved in the spec; the runtime callback lands when the first scope-level adopter arrives).
658
+ 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 that is not one of 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).
625
659
 
626
660
  To surface the same data in two surfaces, declare two contributions (one per slot) and emit twice, there is no broadcast.
627
661
 
@@ -705,6 +739,8 @@ Analyzers take a `ctx` with `nodes`, `links`, and (if you assert on view contrib
705
739
 
706
740
  `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.
707
741
 
742
+ 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 also 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 actually 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 them.
743
+
708
744
  ---
709
745
 
710
746
  ## Scaffolder
@@ -24,6 +24,7 @@
24
24
  "health",
25
25
  "scan",
26
26
  "sidecar.bumped",
27
+ "action.applied",
27
28
  "annotations.registered",
28
29
  "contributions.registered"
29
30
  ],
@@ -39,7 +40,7 @@
39
40
  },
40
41
  "value": {
41
42
  "type": "object",
42
- "description": "Present when `kind` is `'config'` or `'sidecar.bumped'`. For `'config'`, carries the merged effective config object. For `'sidecar.bumped'`, carries `{ nodePath, version, status }` (the Action-result payload from `POST /api/sidecar/bump`)."
43
+ "description": "Present when `kind` is `'config'`, `'sidecar.bumped'`, or `'action.applied'`. For `'config'`, carries the merged effective config object. For `'sidecar.bumped'`, carries `{ nodePath, version, status }` (the bump report from `POST /api/sidecar/bump`). For `'action.applied'`, carries `{ actionId, nodePath, report }` (the generic Action-result payload from `POST /api/actions/:id`)."
43
44
  },
44
45
  "elapsedMs": {
45
46
  "type": "integer",
@@ -124,6 +125,14 @@
124
125
  "priority": {
125
126
  "type": "number",
126
127
  "description": "Optional ordering hint (default 100 when omitted). Slots whose `order` is `'priority'` sort contributions ASC by this value with alphabetical tie-break by qualified id. Mirror of `IViewContribution.priority` in `view-slots.schema.json#/$defs/ViewContribution`; propagated so the UI can apply the manifest-declared order without a second round-trip."
128
+ },
129
+ "pluginOrder": {
130
+ "type": "number",
131
+ "description": "Optional inspector-only ordering hint, denormalised from the owning plugin's `plugin.json` `order` field (default 100). The inspector groups `inspector.body.panel.*` contributions into one collapsible section per plugin and sorts those sections ASC by this value, tie-break by plugin id. Every contribution of a given plugin carries the same value."
132
+ },
133
+ "extensionOrder": {
134
+ "type": "number",
135
+ "description": "Optional inspector-only ordering hint, denormalised from the owning extension's `order` manifest field (default 100). Inside a plugin's inspector section, bricks are sorted ASC by this value, tie-break by `priority` then qualified id. Every contribution of a given extension carries the same value."
127
136
  }
128
137
  }
129
138
  }
@@ -251,10 +260,10 @@
251
260
  }
252
261
  },
253
262
  {
254
- "description": "Action-result envelope, `value` + `elapsedMs` siblings, no `filters` / `counts` / `kindRegistry` / `providerRegistry` / `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 every catalog and the SPA already has them cached from a prior list call.",
263
+ "description": "Action-result envelope, `value` + `elapsedMs` siblings, no `filters` / `counts` / `kindRegistry` / `providerRegistry` / `contributionsRegistry`. Used by `POST /api/actions/:id` (`action.applied`, carries `{ actionId, nodePath, report }`) and the legacy `POST /api/sidecar/bump` (`sidecar.bumped`, Step 9.6.5, carries `{ nodePath, version, status }`), in both cases plus the wall-clock duration. The registries are intentionally absent, the action result is orthogonal to every catalog and the SPA already has them cached from a prior list call.",
255
264
  "required": ["value", "elapsedMs"],
256
265
  "properties": {
257
- "kind": { "const": "sidecar.bumped" }
266
+ "kind": { "enum": ["sidecar.bumped", "action.applied"] }
258
267
  },
259
268
  "not": {
260
269
  "anyOf": [
@@ -51,6 +51,38 @@
51
51
  "description": "Qualified analyzer ids whose findings this action is intended to resolve (Modelo B, replaces the deprecated `Analyzer.recommendedActions`). The UI surfaces matching actions in the node inspector under 'Resolve this issue' when the analyzer's id matches an entry here. Format `<plugin>/<analyzer>` or `<plugin>/<analyzer>:<sub-id>` when the analyzer emits sub-typed issues. Dangling references warn via `recommended-action-missing` in `sm plugins doctor` but do NOT block load."
52
52
  }
53
53
  }
54
+ },
55
+ "prompt": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "required": ["inputType", "paramKey", "label"],
59
+ "properties": {
60
+ "inputType": {
61
+ "$ref": "../input-types.schema.json#/$defs/InputTypeName",
62
+ "description": "Input-type id from the closed catalog. The UI renders the matching control before dispatch."
63
+ },
64
+ "paramKey": {
65
+ "type": "string",
66
+ "minLength": 1,
67
+ "maxLength": 48,
68
+ "description": "Key under which the UI-collected value is placed in the dispatch `input` body."
69
+ },
70
+ "label": { "type": "string", "minLength": 1, "maxLength": 64 },
71
+ "options": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "required": ["value", "label"],
77
+ "properties": {
78
+ "value": { "type": "string" },
79
+ "label": { "type": "string" }
80
+ }
81
+ },
82
+ "description": "Choice list for `enum-pick` / `enum-multipick` input types."
83
+ }
84
+ },
85
+ "description": "Reserved (Steps 3+). When set, a parametrized Action declares the single user input it needs; the UI renders the matching input-type control before dispatch and places the value under `paramKey` in the dispatch body. Deterministic no-prompt actions (e.g. `node-bump`) omit it. Mirrors `view-slots.schema.json#/$defs/payloads/_ActionPrompt`."
54
86
  }
55
87
  },
56
88
  "allOf": [
@@ -15,6 +15,10 @@
15
15
  "minLength": 1,
16
16
  "description": "Required short description (1-3 sentences) shown by `sm <kind>s list` and the UI inspector. English-only per AGENTS.md."
17
17
  },
18
+ "order": {
19
+ "type": "number",
20
+ "description": "Optional visual ordering hint, inspector-only. Inside a plugin's inspector section (which groups the plugin's `inspector.body.panel.*` contributions), the bricks contributed by each extension are sorted ASC by this value (default 100), tie-break by the contribution's `priority` then qualified id. Does NOT affect execution order, which is governed by `phase` (analyzers) and registration order."
21
+ },
18
22
  "annotation": {
19
23
  "type": "object",
20
24
  "required": ["schema"],
@@ -2,9 +2,9 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://skill-map.ai/spec/v0/plugins-doctor.schema.json",
4
4
  "title": "PluginsDoctorReport",
5
- "description": "Machine-readable output of `sm plugins doctor --json`. Aggregates per-status counts across built-in and drop-in plugins plus the structured issue / warning lists the human renderer produces. The `elapsedMs` top-level field is the command's own wall-clock (see `cli-contract.md` §Elapsed time).",
5
+ "description": "Machine-readable output of `sm plugins doctor --json`. Aggregates per-status counts across built-in and drop-in plugins plus the structured issue / warning lists the human renderer produces, and the runtime contribution rejections persisted by the last scan. The `elapsedMs` top-level field is the command's own wall-clock (see `cli-contract.md` §Elapsed time).",
6
6
  "type": "object",
7
- "required": ["ok", "kind", "counts", "issues", "warnings", "elapsedMs"],
7
+ "required": ["ok", "kind", "counts", "issues", "warnings", "contributionErrors", "elapsedMs"],
8
8
  "additionalProperties": false,
9
9
  "properties": {
10
10
  "ok": {
@@ -88,6 +88,49 @@
88
88
  }
89
89
  }
90
90
  },
91
+ "contributionErrors": {
92
+ "type": "array",
93
+ "description": "View contributions the last persisted scan REJECTED at emit time (the \"off-shape visible\" follow-up): an `ctx.emitContribution(...)` call whose ref was not a declared contribution, or whose payload failed the target slot's payload schema. Read from `scan_contribution_errors`; empty when the last scan had no rejected emissions, or when no scan has run yet (fresh project / missing DB). Each entry gates the exit code (any contribution error → exit 1). Iteration order matches the human renderer (`pluginId`, `extensionId`, `nodePath`, `emittedAt` ASC).",
94
+ "items": {
95
+ "type": "object",
96
+ "required": ["pluginId", "extensionId", "nodePath", "reason", "message"],
97
+ "additionalProperties": false,
98
+ "properties": {
99
+ "pluginId": {
100
+ "type": "string",
101
+ "minLength": 1,
102
+ "description": "Plugin id of the extension whose emission was rejected."
103
+ },
104
+ "extensionId": {
105
+ "type": "string",
106
+ "minLength": 1,
107
+ "description": "Extension id (within the plugin) that emitted the rejected contribution."
108
+ },
109
+ "nodePath": {
110
+ "type": "string",
111
+ "minLength": 1,
112
+ "description": "Target node path the contribution was emitted against."
113
+ },
114
+ "reason": {
115
+ "type": "string",
116
+ "minLength": 1,
117
+ "description": "Rejection reason: the literal `undeclared-contribution-ref`, or the AJV error string when the payload failed the slot's payload schema."
118
+ },
119
+ "message": {
120
+ "type": "string",
121
+ "description": "Sanitised human-readable diagnostic (mirrors the ephemeral `extension.error` event of kind `contribution-rejected`)."
122
+ },
123
+ "contributionId": {
124
+ "type": "string",
125
+ "description": "Resolved contribution id. Absent for the `undeclared-contribution-ref` shape (no contribution was resolved)."
126
+ },
127
+ "slot": {
128
+ "type": "string",
129
+ "description": "Resolved target slot. Absent for the `undeclared-contribution-ref` shape."
130
+ }
131
+ }
132
+ }
133
+ },
91
134
  "elapsedMs": {
92
135
  "type": "integer",
93
136
  "minimum": 0,
@@ -32,6 +32,10 @@
32
32
  "minLength": 1,
33
33
  "description": "Required short description shown in `sm plugins list` and the UI. English-only per AGENTS.md."
34
34
  },
35
+ "order": {
36
+ "type": "number",
37
+ "description": "Optional visual ordering hint, inspector-only. The inspector renders one collapsible section per plugin (grouping the plugin's `inspector.body.panel.*` contributions); sections are sorted ASC by this value (default 100), tie-break by plugin id. Does NOT affect extension execution order, which is governed by `phase` (analyzers) and registration order."
38
+ },
35
39
  "storage": {
36
40
  "type": "object",
37
41
  "description": "Persistence mode for this plugin. Absent = plugin does not persist state.",
@@ -13,8 +13,8 @@
13
13
  { "const": "card.footer.left", "description": "Counter chip in the left footer of the card." },
14
14
  { "const": "card.footer.right", "description": "Counter chip in the right footer of the card." },
15
15
  { "const": "graph.node.alert", "description": "Reserved corner badge on the graph node, special-case signals only. No core analyzer emits here; routine \"this node has a problem\" findings belong in `card.footer.right`." },
16
- { "const": "inspector.header.badge.counter", "description": "Counter chip in the inspector header badge cluster." },
17
- { "const": "inspector.header.badge.tag", "description": "Qualitative tag chip in the inspector header badge cluster." },
16
+ { "const": "inspector.header.badge", "description": "Unified badge in the inspector header cluster: icon and/or label and/or count, optional severity. Multi-cardinality, priority order, modeled on card.footer.left. Replaces the retired inspector.header.badge.counter and inspector.header.badge.tag sub-slots." },
17
+ { "const": "inspector.action.button", "description": "Action button in the inspector. Click dispatches a kernel Action by qualified id via POST /api/actions/:id. Always emitted; the payload `enabled` flag carries the dynamic condition (e.g. isStale for the bump button)." },
18
18
  { "const": "inspector.body.panel.breakdown", "description": "Top-N labeled values rendered as a bar chart in the inspector body." },
19
19
  { "const": "inspector.body.panel.records", "description": "Tabular data (rows × columns ≤ 50 × 6) in the inspector body." },
20
20
  { "const": "inspector.body.panel.tree", "description": "Recursive label/children hierarchy (depth ≤ 6, total ≤ 200) in the inspector body." },
@@ -78,8 +78,7 @@
78
78
  "enum": [
79
79
  "card.subtitle.left",
80
80
  "card.footer.left",
81
- "card.footer.right",
82
- "inspector.header.badge.counter"
81
+ "card.footer.right"
83
82
  ]
84
83
  }
85
84
  }
@@ -96,6 +95,7 @@
96
95
  "payloads": {
97
96
  "description": "Per-slot payload schemas. The kernel validates `ctx.emitContribution(id, payload)` calls against the entry corresponding to the declared slot before persisting to `scan_contributions`. Off-shape payloads emit an `extension.error` event and drop silently (mirror of `emitLink` off-contract drop pattern). Slots that share a renderer share a payload shape; the schema is defined inline at each slot to keep lookups direct (no internal indirection).",
98
97
  "card.title.right": {
98
+ "title": "IconMarkerPayload",
99
99
  "type": "object",
100
100
  "additionalProperties": false,
101
101
  "properties": {
@@ -111,8 +111,8 @@
111
111
  "card.subtitle.left": { "$ref": "#/$defs/payloads/_counter" },
112
112
  "card.footer.left": { "$ref": "#/$defs/payloads/_counter" },
113
113
  "card.footer.right": { "$ref": "#/$defs/payloads/_counter" },
114
- "inspector.header.badge.counter": { "$ref": "#/$defs/payloads/_counter" },
115
114
  "_counter": {
115
+ "title": "CounterPayload",
116
116
  "type": "object",
117
117
  "additionalProperties": false,
118
118
  "required": ["value"],
@@ -130,23 +130,105 @@
130
130
  },
131
131
  "description": "Single icon + integer pair, modelled after the `.sm-gnode__stat` rows in the card footer. Manifest requires `icon` (enforced by `IViewContribution.allOf` for every counter slot); payload carries `value`, optionally `severity` and `tooltip`. The manifest `label` is metadata (docs / plugin-doctor / aria-label) and is NOT rendered inline."
132
132
  },
133
- "inspector.header.badge.tag": { "$ref": "#/$defs/payloads/_tag" },
134
- "_tag": {
133
+ "inspector.header.badge": {
134
+ "title": "BadgePayload",
135
135
  "type": "object",
136
136
  "additionalProperties": false,
137
- "required": ["label"],
138
137
  "properties": {
139
- "label": {
138
+ "icon": { "$ref": "#/$defs/IconString" },
139
+ "label": { "type": "string", "minLength": 1, "maxLength": 32 },
140
+ "count": { "type": "integer", "minimum": 0 },
141
+ "severity": { "$ref": "#/$defs/Severity" },
142
+ "tooltip": { "type": "string", "maxLength": 256 }
143
+ },
144
+ "anyOf": [
145
+ { "required": ["icon"] },
146
+ { "required": ["label"] },
147
+ { "required": ["count"] }
148
+ ],
149
+ "description": "Unified inspector header badge. At least one of `icon`, `label`, `count` is required; optional `severity` tint and `tooltip`. Multi-cardinality slot (priority order, modeled on card.footer.left); a plugin extension may emit several. 'Empty' for `emitWhenEmpty` is the absence of `icon` AND `label` AND `count`. Replaces the retired `_counter`/`_tag` header sub-slots: a counter-style badge sets `count` (+`icon`), a tag-style badge sets `label` (+`severity`), the stale clock sets `icon` + `tooltip`."
150
+ },
151
+ "inspector.action.button": {
152
+ "title": "ActionButtonPayload",
153
+ "type": "object",
154
+ "additionalProperties": false,
155
+ "required": ["actionId", "label", "enabled"],
156
+ "properties": {
157
+ "actionId": {
140
158
  "type": "string",
141
- "minLength": 1,
142
- "maxLength": 32,
143
- "description": "Tag text (e.g. 'fresh', 'stale', '7d ago'). 'Empty' for `emitWhenEmpty` is `label === ''`."
159
+ "pattern": "^[a-z][a-z0-9-]*/[a-z][a-z0-9-]*$",
160
+ "description": "Qualified Action id `<plugin>/<action>` the click dispatches via POST /api/actions/:id. Resolved by the kernel registry; an unknown id makes the BFF answer 404."
144
161
  },
162
+ "label": { "type": "string", "minLength": 1, "maxLength": 48 },
163
+ "icon": { "$ref": "#/$defs/IconString" },
145
164
  "severity": { "$ref": "#/$defs/Severity" },
146
- "tooltip": { "type": "string", "maxLength": 256 }
147
- }
165
+ "enabled": {
166
+ "type": "boolean",
167
+ "description": "Dynamic gate. The button is ALWAYS emitted (the persistence upsert refreshes the row each scan); `false` renders it disabled. e.g. `isStale` for the bump button."
168
+ },
169
+ "disabledReason": {
170
+ "type": "string",
171
+ "maxLength": 128,
172
+ "description": "Tooltip shown when `enabled` is false."
173
+ },
174
+ "input": {
175
+ "type": "object",
176
+ "description": "Reserved (Step 2+). Static input merged into the dispatch body for parametrized actions that need no user prompt."
177
+ },
178
+ "prompt": {
179
+ "$ref": "#/$defs/payloads/_ActionPrompt",
180
+ "description": "Reserved (Step 3+). Declares an input-type prompt the UI collects before dispatching (enum-pick for stability, single-string for tags)."
181
+ },
182
+ "confirm": {
183
+ "type": "boolean",
184
+ "default": false,
185
+ "description": "Reserved. Require an extra confirm step before dispatch (destructive actions)."
186
+ }
187
+ },
188
+ "description": "An action button rendered in the inspector. The manifest declares only `{ slot: 'inspector.action.button' }`; the per-node payload carries the action id, label, and the dynamic `enabled` flag. Click dispatches the Action via POST /api/actions/:id. `emitWhenEmpty` does not apply (a button is always meaningful)."
189
+ },
190
+ "_ActionPrompt": {
191
+ "title": "ActionPrompt",
192
+ "type": "object",
193
+ "additionalProperties": false,
194
+ "required": ["inputType", "paramKey", "label"],
195
+ "properties": {
196
+ "inputType": {
197
+ "$ref": "input-types.schema.json#/$defs/InputTypeName",
198
+ "description": "Input-type id from the closed catalog. The UI renders the matching control before dispatch (`single-string`, `enum-pick` and `string-list` today; other types degrade to a graceful 'unsupported' notice)."
199
+ },
200
+ "paramKey": {
201
+ "type": "string",
202
+ "minLength": 1,
203
+ "maxLength": 48,
204
+ "description": "Key under which the collected value is placed in the dispatch `input` body."
205
+ },
206
+ "label": { "type": "string", "minLength": 1, "maxLength": 64 },
207
+ "defaultValue": {
208
+ "oneOf": [
209
+ { "type": "string" },
210
+ { "type": "array", "items": { "type": "string" } }
211
+ ],
212
+ "description": "Optional pre-filled value the UI seeds the control with before the user edits (e.g. a node's current tags for a `string-list` edit). String for scalar input-types, string array for list input-types."
213
+ },
214
+ "options": {
215
+ "type": "array",
216
+ "items": {
217
+ "type": "object",
218
+ "additionalProperties": false,
219
+ "required": ["value", "label"],
220
+ "properties": {
221
+ "value": { "type": "string" },
222
+ "label": { "type": "string" }
223
+ }
224
+ },
225
+ "description": "Choice list for `enum-pick` / `enum-multipick` input types."
226
+ }
227
+ },
228
+ "description": "Reserved (Steps 3+). Declares a parameter the UI prompts for before dispatching the Action, reusing the input-type catalog. Step 1 actions (bump) omit it."
148
229
  },
149
230
  "graph.node.alert": {
231
+ "title": "AlertPayload",
150
232
  "type": "object",
151
233
  "additionalProperties": false,
152
234
  "properties": {
@@ -163,11 +245,12 @@
163
245
  "description": "Decoration on the graph node (corner badge / pin). At least one of `icon`, `severity`, `count` is required. 'Empty' for `emitWhenEmpty` is the absence of `icon` AND `count`. Hard cap 1 marker per node per plugin extension (slot config enforces)."
164
246
  },
165
247
  "inspector.body.panel.breakdown": {
248
+ "title": "BreakdownPayload",
166
249
  "type": "object",
167
250
  "additionalProperties": false,
168
- "required": ["entries"],
251
+ "required": ["bars"],
169
252
  "properties": {
170
- "entries": {
253
+ "bars": {
171
254
  "type": "array",
172
255
  "maxItems": 20,
173
256
  "items": {
@@ -180,11 +263,12 @@
180
263
  "tooltip": { "type": "string", "maxLength": 256 }
181
264
  }
182
265
  },
183
- "description": "Top-N labeled values rendered as a horizontal bar chart. Hard cap 20 entries (overflow rejected at validation, plugin should pre-truncate). 'Empty' for `emitWhenEmpty` is `entries.length === 0`."
266
+ "description": "Top-N labeled values rendered as a horizontal bar chart. Hard cap 20 bars (overflow rejected at validation, plugin should pre-truncate). 'Empty' for `emitWhenEmpty` is `bars.length === 0`."
184
267
  }
185
268
  }
186
269
  },
187
270
  "inspector.body.panel.records": {
271
+ "title": "RecordsPayload",
188
272
  "type": "object",
189
273
  "additionalProperties": false,
190
274
  "required": ["columns", "rows"],
@@ -227,6 +311,7 @@
227
311
  "description": "Recursive tree rendered as an indented hierarchy. Hard caps: max depth 6, max 200 total nodes per tree (validator enforces). 'Empty' for `emitWhenEmpty` is the root having no `children`."
228
312
  },
229
313
  "_TreeNode": {
314
+ "title": "TreeNode",
230
315
  "type": "object",
231
316
  "additionalProperties": false,
232
317
  "required": ["label"],
@@ -241,11 +326,12 @@
241
326
  }
242
327
  },
243
328
  "inspector.body.panel.key-values": {
329
+ "title": "KeyValuesPayload",
244
330
  "type": "object",
245
331
  "additionalProperties": false,
246
- "required": ["entries"],
332
+ "required": ["pairs"],
247
333
  "properties": {
248
- "entries": {
334
+ "pairs": {
249
335
  "type": "array",
250
336
  "maxItems": 50,
251
337
  "items": {
@@ -265,16 +351,17 @@
265
351
  "tooltip": { "type": "string", "maxLength": 256 }
266
352
  }
267
353
  },
268
- "description": "Flat key/value pairs (max 50). Renders as a definition list. 'Empty' for `emitWhenEmpty` is `entries.length === 0`."
354
+ "description": "Flat key/value pairs (max 50). Renders as a definition list. 'Empty' for `emitWhenEmpty` is `pairs.length === 0`."
269
355
  }
270
356
  }
271
357
  },
272
358
  "inspector.body.panel.link-list": {
359
+ "title": "LinkListPayload",
273
360
  "type": "object",
274
361
  "additionalProperties": false,
275
- "required": ["entries"],
362
+ "required": ["links"],
276
363
  "properties": {
277
- "entries": {
364
+ "links": {
278
365
  "type": "array",
279
366
  "maxItems": 100,
280
367
  "items": {
@@ -297,11 +384,12 @@
297
384
  }
298
385
  }
299
386
  },
300
- "description": "List of in-scope node paths (max 100). 'Empty' for `emitWhenEmpty` is `entries.length === 0`."
387
+ "description": "List of in-scope node paths (max 100). 'Empty' for `emitWhenEmpty` is `links.length === 0`."
301
388
  }
302
389
  }
303
390
  },
304
391
  "inspector.body.panel.markdown": {
392
+ "title": "MarkdownPayload",
305
393
  "type": "object",
306
394
  "additionalProperties": false,
307
395
  "required": ["markdown"],
@@ -314,6 +402,7 @@
314
402
  }
315
403
  },
316
404
  "topbar.nav.start": {
405
+ "title": "ScopeStatPayload",
317
406
  "type": "object",
318
407
  "additionalProperties": false,
319
408
  "required": ["value"],