@skill-map/spec 0.45.1 → 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 (30) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/architecture.md +18 -11
  3. package/cli-contract.md +11 -14
  4. package/conformance/README.md +1 -0
  5. package/conformance/cases/plugin-missing-ui-rejected.json +1 -1
  6. package/conformance/cases/sidecar-end-to-end.json +1 -1
  7. package/conformance/cases/view-action-button.json +21 -0
  8. package/conformance/cases/view-contribution-payloads.json +19 -0
  9. package/conformance/cases/view-slots-all.json +15 -0
  10. package/conformance/coverage.md +1 -1
  11. package/conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/analyzers/good-badges/index.js +46 -0
  12. package/conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/plugin.json +6 -0
  13. package/conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/analyzers/header-counter/index.js +28 -0
  14. package/conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/plugin.json +6 -0
  15. package/conformance/fixtures/view-action-button/notes/example.md +6 -0
  16. package/conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/analyzers/panels/index.js +37 -0
  17. package/conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/plugin.json +6 -0
  18. package/conformance/fixtures/view-contribution-payloads/notes/example.md +5 -0
  19. package/conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/analyzers/everything/index.js +35 -0
  20. package/conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/plugin.json +6 -0
  21. package/index.json +29 -16
  22. package/package.json +1 -1
  23. package/plugin-author-guide.md +42 -6
  24. package/schemas/api/rest-envelope.schema.json +13 -4
  25. package/schemas/extensions/action.schema.json +32 -0
  26. package/schemas/extensions/base.schema.json +4 -0
  27. package/schemas/plugins-doctor.schema.json +45 -2
  28. package/schemas/plugins-registry.schema.json +4 -0
  29. package/schemas/sidecar.schema.json +1 -1
  30. package/schemas/view-slots.schema.json +112 -23
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
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
+
49
+ ## 0.46.0
50
+
51
+ ### Minor Changes
52
+
53
+ - The active-provider lens dropdown in Settings → Project now greys out (and refuses to select) any Provider the operator has disabled. `GET /api/active-provider` gained a `selectable` field listing the Provider ids that are enabled right now; the SPA renders Providers absent from it as disabled instead of offering a lens whose extractors would never run.
54
+
55
+ ## User-facing
56
+
57
+ Disabling a provider plugin now removes it as a choice in **Settings → Project → Active provider**. The provider stays listed but greyed out and labelled `(disabled)`, so you can no longer switch the lens to a provider whose extractors would not run.
58
+
59
+ - `sm bump` and the BFF bump route (`POST /api/sidecar/bump`) now stamp `audit.lastBumpedBy` / `audit.createdBy` with the project's Git author name (`git config user.name`) when the node lives in a Git repository, falling back to the channel literal (`'cli'` / `'ui'`) otherwise. This supersedes Decision A5, which kept the invoker a literal.
60
+
61
+ ## User-facing
62
+
63
+ Bumping a node now records **who** bumped it: the audit `by` fields show your Git author name (`git config user.name`) instead of `cli` / `ui`, when the project is a Git repo. It falls back to `cli` / `ui` outside a Git repo or when no `user.name` is configured.
64
+
65
+ ### Patch Changes
66
+
67
+ - The `core/annotation-stale` analyzer is now neutral instead of warning-tinted: drift is informational, not a warning. Its footer chip (`staleIcon`) carries no severity (the clock renders in the foreground colour instead of the warn tint), and the stale Findings issue is lowered from `warn` to `info`. As `info`, it no longer counts toward the card's warn chip (the issue-counter buckets error/warn only) and never affected `sm check`'s exit code (info and warn are both non-failing).
68
+
3
69
  ## 0.45.1
4
70
 
5
71
  ### Patch Changes
package/architecture.md CHANGED
@@ -629,7 +629,7 @@ Two schemas describe the wire shape:
629
629
 
630
630
  `identity` carries `path` (scope-root-relative, matches the canonical Node identifier in [`schemas/node.schema.json`](./schemas/node.schema.json)) plus `bodyHash` and `frontmatterHash`. Both hashes are sha256 over the kernel's canonical form of the markdown body (post-frontmatter bytes) and frontmatter (YAML re-emitted via `js-yaml dump` with `sortKeys: true`, `lineWidth: -1`, `noRefs: true`, `noCompatMode: true`); each sidecar captures the values the kernel saw at the moment it was last written.
631
631
 
632
- At scan time the kernel re-computes the live hashes and compares against the stored ones. Mismatch in either is **drift**, surfaced via the built-in `annotation-stale` analyzer (severity `warning`, never blocking, soft mode by design). A `.sm` whose `identity.path` no longer points at an existing `.md` is **orphan**, surfaced via the built-in `annotation-orphan` analyzer (also `warning`). Drift state is **derived**, never stored, pure function over existing data, no flag to drift between flag and reality.
632
+ At scan time the kernel re-computes the live hashes and compares against the stored ones. Mismatch in either is **drift**, surfaced via the built-in `annotation-stale` analyzer (severity `info`, never blocking, soft mode by design: drift is informational, the footer chip is a neutral clock). A `.sm` whose `identity.path` no longer points at an existing `.md` is **orphan**, surfaced via the built-in `annotation-orphan` analyzer (also `warning`). Drift state is **derived**, never stored, pure function over existing data, no flag to drift between flag and reality.
633
633
 
634
634
  ### Bump model
635
635
 
@@ -637,7 +637,7 @@ The deterministic built-in `core/node-bump` Action produces a sidecar patch:
637
637
 
638
638
  - Increments `annotations.version` by 1 (or sets to `1` if missing, single integer monotonic, orthogonal to `stability`; major bumps are not a concept, the convention for breaking changes is "create a new node, supersede the old").
639
639
  - Refreshes `identity.bodyHash` and `identity.frontmatterHash` to the live values.
640
- - Stamps `audit.lastBumpedAt` (ISO 8601 datetime) and `audit.lastBumpedBy` (`'cli'`, `'ui'`, or `'plugin:<id>'`).
640
+ - Stamps `audit.lastBumpedAt` (ISO 8601 datetime) and `audit.lastBumpedBy` (the Git author name from `git config user.name` when the project is a Git repo; otherwise the channel literal `'cli'`, `'ui'`, or `'plugin:<id>'`).
641
641
  - On first-time creation also stamps `audit.createdAt` and `audit.createdBy` (set once, stable thereafter).
642
642
 
643
643
  The Action stays pure (no IO). The kernel materializes the patch through the `SidecarStore` port, a path-keyed read-modify-write critical section that deep-merges the patch into the on-disk file (arrays REPLACE, objects RECURSE, `null` DELETES) and writes atomically via `<path>.tmp` + POSIX rename. Concurrent bumps on the same path serialize through the lock; both patches' effects survive (no lost write).
@@ -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,9 +832,13 @@ 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
- - `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) is served separately by `GET /api/active-provider`.
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`.
835
842
 
836
843
  Plus per-node embedding on node responses:
837
844
 
package/cli-contract.md CHANGED
@@ -114,6 +114,8 @@ CLI surfaces:
114
114
  - **Manual override**: `sm config set activeProvider <id>` switches the lens. The verb drops the `scan_*` zone atomically (see [`db-schema.md`](./db-schema.md#zones)) and triggers an immediate rescan under the new lens. `state_*` and `config_*` zones survive.
115
115
  - **No per-scan flag**: there is no `sm scan --provider=<id>` flag. The lens is a project-level decision, not a per-invocation parameter. The drop+rescan cost makes per-invocation switching the wrong default UX.
116
116
 
117
+ **UI lens-selection surface.** `GET /api/active-provider` returns `{ activeProvider, detected, source, selectable }`. `selectable` is the set of registered-Provider ids that are enabled right now, resolved against the live per-extension enabled resolver (`config_plugins` layered over `settings.json#/plugins`, the same resolution `GET /api/plugins` applies), so it is the subset of `providerRegistry` eligible to become the lens. A Provider the operator disabled is dropped from `selectable` but stays in `providerRegistry` (the static boot catalog keeps it so already-scanned nodes still render their chip / icon). The SPA's active-lens dropdown lists every `providerRegistry` Provider and renders those absent from `selectable` as disabled (greyed, not selectable), so a disabled Provider can never be picked as the lens. This mirrors the scan-time contract that a lens pointing at a disabled Provider runs none of its extractors (the runtime soft-warns on that drift); the dropdown closes the loop on the write side by refusing to create the drift in the first place.
118
+
117
119
  ---
118
120
 
119
121
  ## Targeted fan-out flags
@@ -180,32 +182,27 @@ Flags: `--no-scan` (skip the first scan), `--force` (rewrite an existing config)
180
182
 
181
183
  Exit: 0 on success, 2 on failure.
182
184
 
183
- #### `sm tutorial [variant]`
184
-
185
- 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).
186
-
187
- The optional positional `variant` argument selects which skill gets materialised. Valid values are:
185
+ #### `sm tutorial`
188
186
 
189
- - `tutorial` (default, also the behaviour when no argument is passed): the basic onboarding walkthrough, slug `sm-tutorial`.
190
- - `master`: the advanced walkthrough (plugin tour, plugin authoring, settings + view-slots), slug `sm-master`, includes the `references/` sub-folder.
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**.
191
188
 
192
- 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:
193
190
 
194
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.
195
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.
196
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`.
197
194
  - Without `--for`, on a non-interactive stdin (pipes, CI) the verb selects the default Provider without prompting, so the verb stays scriptable.
198
195
 
199
- Common behaviour for both variants:
196
+ Behaviour:
200
197
 
201
- - 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/`.
202
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).
203
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.
204
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.
205
202
 
206
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).
207
204
 
208
- 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.
209
206
 
210
207
  #### `sm version`
211
208
 
@@ -360,7 +357,7 @@ The built-in deterministic `core/node-bump` Action is the canonical write channe
360
357
 
361
358
  | Command | Purpose |
362
359
  |---|---|
363
- | `sm bump <node.path> [--force] [--yes]` | Single-node bump. Wraps `core/node-bump`. Refuses on a fresh node (`{ ok: false, reason: 'fresh' }`, exit `2`) unless `--force` is passed; with `--force` on a fresh node the verb is a silent no-op (exit `0`, no stdout). On a stale node (or first-time creation) increments `annotations.version`, refreshes `for.{bodyHash, frontmatterHash}`, and stamps the audit block (`audit.lastBumpedAt` + `audit.lastBumpedBy: 'cli'`; on first creation also `audit.createdAt` + `audit.createdBy: 'cli'`). Exit `5` if the node is not in the persisted scan. `--json` emits the report shape declared by `bump-report.schema.json`. `--yes` confirms consent for `.sm` writes in this project, see §`.sm` write consent below. |
360
+ | `sm bump <node.path> [--force] [--yes]` | Single-node bump. Wraps `core/node-bump`. Refuses on a fresh node (`{ ok: false, reason: 'fresh' }`, exit `2`) unless `--force` is passed; with `--force` on a fresh node the verb is a silent no-op (exit `0`, no stdout). On a stale node (or first-time creation) increments `annotations.version`, refreshes `for.{bodyHash, frontmatterHash}`, and stamps the audit block (`audit.lastBumpedAt` + `audit.lastBumpedBy`; on first creation also `audit.createdAt` + `audit.createdBy`). The `by` fields carry the Git author name (`git config user.name`) when the project is a Git repository, otherwise the channel literal `'cli'`. Exit `5` if the node is not in the persisted scan. `--json` emits the report shape declared by `bump-report.schema.json`. `--yes` confirms consent for `.sm` writes in this project, see §`.sm` write consent below. |
364
361
  | `sm bump --pending [--staged] [--force] [--yes]` | Batch bump. Walks every node whose sidecar overlay reports drift in `node.path` ASC order and bumps each. `--staged` runs `git add <sidecar-path>` after each successful bump so the new content lands in the same commit; `git add` failure degrades to a stderr warning, the batch keeps running. Empty stale set → exit `0` with a "nothing to do" advisory. `--json` envelope: `{ bumped, refused, skipped, errors[], elapsedMs }`. Exit `0` on a clean run; `1` when at least one per-node error landed in `errors[]`. **Git error matrix for `--staged`**: not inside a git repo (no `.git/` parent of `cwd`) → exit `5`; `git` binary not on PATH (spawn ENOENT) → exit `2`. Both checks run BEFORE any sidecar write so a misconfigured environment never produces partial state. `--yes` confirms consent for `.sm` writes, see §`.sm` write consent below. |
365
362
  | `sm sidecar refresh <node.path> [--yes]` | Hash-only update on the sidecar. Refreshes `for.{bodyHash, frontmatterHash}` to match the live node WITHOUT bumping `annotations.version` and WITHOUT touching the audit block. Useful when the user knows a body change is editorial-only and doesn't want to spend a version increment. Distinct from the top-level `sm refresh` (which targets the enrichment layer at Step A.8), different storage, different concept; the sub-namespace prefix prevents the collision. Exit `5` if the node has no sidecar or is not in the persisted scan. No-op on a fresh node (informational stderr, exit `0`). `--yes` confirms consent for `.sm` writes, see §`.sm` write consent below. |
366
363
  | `sm sidecar prune [--dry-run] [--yes]` | Delete orphan `.sm` files (sidecars whose accompanying `<basename>.md` does not exist on disk). Destructive, without `--dry-run` prompts for interactive confirmation listing every file to be deleted (per the §Dry-run analyzer for destructive verbs). `--yes` (alias `--force`) bypasses the destructive-confirmation prompt for non-interactive callers (CI, the pre-commit hook, scripts), NOT to be confused with the `.sm` write-consent gate, which does not apply to prune (delete is not a write). With `--dry-run` reports what would be deleted without touching disk and never prompts. Different domain from `sm orphans`, that verb operates on the node graph (rename heuristic); this one operates on the filesystem layer. `--json` envelope: `{ deleted, wouldDelete, errors, items[], elapsedMs }`. Exit `1` when delete failures landed in `errors`. |
@@ -415,7 +412,7 @@ Tracked as **R6** in the §Step 9.6 review queue: open by design, defer the `js-
415
412
 
416
413
  ##### BFF endpoint, `POST /api/sidecar/bump` (Step 9.6.5, BFF half)
417
414
 
418
- The Hono BFF exposes the single-node bump flow over REST so the UI can drive the same Action / Store the CLI uses. Behaviour mirrors `sm bump <node.path> [--force]` 1:1: same `core/node-bump` Action, same `FilesystemSidecarStore`, same fresh-vs-stale refusal semantics. The only differences from the CLI verb are the invoker label (`'ui'` vs `'cli'`, Decision A5 of 9.6.4 left this as a literal) and the wire shape. Batch (`--pending`) is intentionally CLI-only at 9.6.5, surfacing it over REST needs a job-style progress channel and lands later.
415
+ The Hono BFF exposes the single-node bump flow over REST so the UI can drive the same Action / Store the CLI uses. Behaviour mirrors `sm bump <node.path> [--force]` 1:1: same `core/node-bump` Action, same `FilesystemSidecarStore`, same fresh-vs-stale refusal semantics. The only differences from the CLI verb are the invoker channel fallback (`'ui'` vs `'cli'`, used only when no Git author resolves) and the wire shape. This supersedes Decision A5 of 9.6.4 (which left the invoker a literal): both routes now stamp the Git `user.name` when the project is a Git repository, falling back to the channel literal otherwise. Batch (`--pending`) is intentionally CLI-only at 9.6.5, surfacing it over REST needs a job-style progress channel and lands later.
419
416
 
420
417
  | Field | Value |
421
418
  |---|---|
@@ -621,7 +618,7 @@ List endpoints conform to [`schemas/api/rest-envelope.schema.json`](schemas/api/
621
618
 
622
619
  **`kindRegistry` envelope field.** Every payload-bearing variant of the REST envelope (`nodes` / `links` / `issues` / `plugins` lists, the `node` single, the `config` value envelope) embeds a required `kindRegistry: { [kindName]: { providerId, label, color, colorDark?, emoji?, icon? } }` field. Sentinel envelopes (`health`, `scan`, `graph`) are exempt, they carry no payload at the wire level. The BFF assembles the registry once at boot from EVERY built-in Provider's `kinds[*].ui` block (regardless of the boot-time enabled verdict, their module code is statically imported by `built-ins.ts` and always in memory) PLUS every drop-in user Provider that loaded successfully at boot (see [`architecture.md` §Provider · `ui` presentation](architecture.md#provider--ui-presentation)). The registry is then attached to every applicable response. Built-ins are listed unconditionally because a user re-enabling one mid-session expects its kinds to render on the next scan; the runtime enabled/disabled axis is enforced at SCAN-TIME by `composeScanExtensions` reading the fresh resolver, not by hiding kinds from the registry. Drop-ins that loaded as `disabled` carry `startsAsDisabled: true` on `GET /api/plugins` and need `sm serve` restart to register, their module code was never imported. The UI consumes `kindRegistry` directly to render kind palettes, list rows, and inspector headers, built-in and user-plugin kinds render identically. A kind appearing in a response payload (e.g. `node.kind`) without a matching `kindRegistry` entry is a contract violation; the kernel rejects Providers without a `ui` block at load time so the registry is always complete for whatever kinds appear in the response.
623
620
 
624
- **`providerRegistry` envelope field.** The same payload-bearing envelopes also embed a required `providerRegistry: { [providerId]: { label, color, colorDark?, emoji?, icon?, hideChip? } }` field (sibling of `kindRegistry`). Sentinel envelopes (`health`, `scan`, `graph`), action-result envelopes (`sidecar.bumped`), and the catalog envelopes (`annotations.registered`, `contributions.registered`) are exempt. Same boot-time assembly discipline as `kindRegistry`: assembled once from EVERY built-in Provider's top-level `presentation` block (regardless of enabled verdict) PLUS every drop-in user Provider that loaded at boot. The UI consumes `providerRegistry` to render the active-lens dropdown, the topbar lens chip, and the per-node provider chip from the real registered-Provider set, never a hardcoded list; `hideChip: true` (the universal `markdown` fallback) suppresses only the per-card chip. This is the static boot catalog of Provider identity; the dynamic active lens (current value + filesystem-detected candidates) is served separately by `GET /api/active-provider`.
621
+ **`providerRegistry` envelope field.** The same payload-bearing envelopes also embed a required `providerRegistry: { [providerId]: { label, color, colorDark?, emoji?, icon?, hideChip? } }` field (sibling of `kindRegistry`). Sentinel envelopes (`health`, `scan`, `graph`), action-result envelopes (`sidecar.bumped`), and the catalog envelopes (`annotations.registered`, `contributions.registered`) are exempt. Same boot-time assembly discipline as `kindRegistry`: assembled once from EVERY built-in Provider's top-level `presentation` block (regardless of enabled verdict) PLUS every drop-in user Provider that loaded at boot. The UI consumes `providerRegistry` to render the active-lens dropdown, the topbar lens chip, and the per-node provider chip from the real registered-Provider set, never a hardcoded list; `hideChip: true` (the universal `markdown` fallback) suppresses only the per-card chip. This is the static boot catalog of Provider identity; the dynamic active lens (current value + filesystem-detected candidates + the enabled `selectable` set) is served separately by `GET /api/active-provider`.
625
622
 
626
623
  **`contributionsRegistry` envelope field.** Same payload-bearing envelopes also embed `contributionsRegistry: { "<pluginId>/<extensionId>/<contributionId>": { pluginId, extensionId, contributionId, slot, label?, tooltip?, icon?, emptyText?, emitWhenEmpty } }`. Same boot-time assembly discipline as `kindRegistry`: ALL built-in declarations are listed regardless of enabled state (so re-enabling a built-in mid-session renders correctly on the next scan), plus drop-in user plugins that loaded at boot. The `slot` value comes from the closed catalog in `spec/schemas/view-slots.schema.json`. A view contribution emitted by an extension whose qualified id is missing from the registry is dropped by the UI's slot host (mirrors the kindRegistry contract, `startsAsDisabled` drop-ins illustrate the absence path).
627
624
 
@@ -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" },
@@ -20,6 +20,6 @@
20
20
  { "type": "json-path", "path": "$.issues[0].severity", "equals": "warn" },
21
21
  { "type": "json-path", "path": "$.issues[0].data.expectedMdPath", "equals": ".claude/agents/orphan.md" },
22
22
  { "type": "json-path", "path": "$.issues[1].analyzerId", "equals": "annotation-stale" },
23
- { "type": "json-path", "path": "$.issues[1].severity", "equals": "warn" }
23
+ { "type": "json-path", "path": "$.issues[1].severity", "equals": "info" }
24
24
  ]
25
25
  }
@@ -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
+ }