@skill-map/spec 0.46.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/architecture.md +33 -11
- package/cli-contract.md +7 -12
- package/conformance/README.md +1 -0
- package/conformance/cases/backtick-path-extraction.json +20 -0
- package/conformance/cases/plugin-missing-ui-rejected.json +1 -1
- package/conformance/cases/view-action-button.json +21 -0
- package/conformance/cases/view-contribution-payloads.json +19 -0
- package/conformance/cases/view-slots-all.json +15 -0
- package/conformance/coverage.md +1 -1
- package/conformance/fixtures/backtick-path/docs/target.md +5 -0
- package/conformance/fixtures/backtick-path/source.md +14 -0
- package/conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/analyzers/good-badges/index.js +46 -0
- package/conformance/fixtures/view-action-button/.skill-map/plugins/badge-actions/plugin.json +6 -0
- package/conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/analyzers/header-counter/index.js +28 -0
- package/conformance/fixtures/view-action-button/.skill-map/plugins/legacy-badge/plugin.json +6 -0
- package/conformance/fixtures/view-action-button/notes/example.md +6 -0
- package/conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/analyzers/panels/index.js +37 -0
- package/conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/plugin.json +6 -0
- package/conformance/fixtures/view-contribution-payloads/notes/example.md +5 -0
- package/conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/analyzers/everything/index.js +35 -0
- package/conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/plugin.json +6 -0
- package/db-schema.md +1 -1
- package/index.json +34 -18
- package/package.json +1 -1
- package/plugin-author-guide.md +48 -9
- package/schemas/api/rest-envelope.schema.json +12 -3
- package/schemas/extensions/action.schema.json +32 -0
- package/schemas/extensions/base.schema.json +9 -0
- package/schemas/extensions/provider.schema.json +1 -1
- package/schemas/link.schema.json +2 -2
- package/schemas/plugins-doctor.schema.json +45 -2
- package/schemas/plugins-registry.schema.json +4 -0
- package/schemas/signal.schema.json +1 -1
- package/schemas/view-slots.schema.json +112 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Spec changelog
|
|
2
2
|
|
|
3
|
+
## 0.48.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Adds the `core/backtick-path` extractor: relative `.md` paths written inside inline code spans and fenced blocks become edges, resolved like markdown links. The token grammar is pinned in `spec/architecture.md` (new section "Extractor: code-region file references"), unresolved targets surface via `core/reference-broken`, and the kernel exports `extractCodeRegions`, the exact inverse mask of `stripCodeBlocks`.
|
|
8
|
+
|
|
9
|
+
## User-facing
|
|
10
|
+
|
|
11
|
+
Skills that tell the agent to read a bundled doc with a backtick path (like `references/rules.md`) now show those arrows on the map, and a backtick path pointing at a missing file is flagged as a broken reference.
|
|
12
|
+
|
|
13
|
+
- Extensions can declare an optional `stability` lifecycle label (`experimental`, `beta`, `stable`, `deprecated`) in their manifest. Presentation-only: non-default values render as a badge in `sm plugins list` / `sm plugins show` and the Settings plugins panel; missing means `stable` and the kernel never gates behaviour on it. Declared in the spec's extension base schema and threaded through the loader, the BFF, and the SPA. `core/mcp-tools` is the first built-in flagged `experimental`.
|
|
14
|
+
|
|
15
|
+
## User-facing
|
|
16
|
+
|
|
17
|
+
**Plugin maturity at a glance.** Extensions can now carry an experimental, beta, or deprecated badge next to their name in the Settings plugins panel and in `sm plugins list`, so you can tell which parts of a plugin are still settling before relying on them.
|
|
18
|
+
|
|
19
|
+
- Adds the `points` link kind to the closed enum: `core/backtick-path` now emits `points` instead of `references`, so a backtick path and a markdown link to the same target persist as two coexisting edges instead of merging, and `core/link-conflict` treats `points` as compatible with every other kind (no false conflict warns). `core/reference-broken` labels the kind "pointer".
|
|
20
|
+
|
|
21
|
+
## User-facing
|
|
22
|
+
|
|
23
|
+
Backtick paths get their own "Points" connector kind: a new palette toggle with a backtick glyph, its own edge colour per theme, and arrows separate from markdown-link references on the map.
|
|
24
|
+
|
|
25
|
+
## 0.47.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- 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`.
|
|
30
|
+
|
|
31
|
+
## User-facing
|
|
32
|
+
|
|
33
|
+
The inspector now offers Supersede, Set stability and Edit tags buttons; each opens a small form pre-filled with the node's current value.
|
|
34
|
+
|
|
35
|
+
- 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.
|
|
36
|
+
|
|
37
|
+
## User-facing
|
|
38
|
+
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
- 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.
|
|
42
|
+
|
|
43
|
+
## User-facing
|
|
44
|
+
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
- 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.
|
|
48
|
+
|
|
49
|
+
## User-facing
|
|
50
|
+
|
|
51
|
+
`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.
|
|
52
|
+
|
|
53
|
+
- 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`.
|
|
54
|
+
|
|
55
|
+
- 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.
|
|
56
|
+
|
|
57
|
+
## User-facing
|
|
58
|
+
|
|
59
|
+
`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.
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- 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.
|
|
64
|
+
|
|
65
|
+
## User-facing
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
- 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.
|
|
70
|
+
|
|
3
71
|
## 0.46.0
|
|
4
72
|
|
|
5
73
|
### Minor Changes
|
package/architecture.md
CHANGED
|
@@ -88,7 +88,7 @@ A provider plugin MAY declare it reads source files belonging to ANOTHER provide
|
|
|
88
88
|
|
|
89
89
|
### Universal extractors and per-provider extractors
|
|
90
90
|
|
|
91
|
-
The lens does NOT gate the universal extractors that ship under `core/` (markdown links, external URLs, sidecar annotations). Those run regardless of the active provider because their semantics are provider-agnostic. Provider-specific extractors (Claude's `@`-directive parser, Cursor's picker-derived references, the future Codex AGENTS.md walker, future Antigravity-specific parsers) declare `precondition: { provider: '<id>' }` on their manifest; the orchestrator invokes them on every node visited during the scan as long as the **active lens** is in the declared provider list, regardless of which provider's `classify()` claimed the node.
|
|
91
|
+
The lens does NOT gate the universal extractors that ship under `core/` (markdown links, code-region file paths, external URLs, sidecar annotations). Those run regardless of the active provider because their semantics are provider-agnostic. Provider-specific extractors (Claude's `@`-directive parser, Cursor's picker-derived references, the future Codex AGENTS.md walker, future Antigravity-specific parsers) declare `precondition: { provider: '<id>' }` on their manifest; the orchestrator invokes them on every node visited during the scan as long as the **active lens** is in the declared provider list, regardless of which provider's `classify()` claimed the node.
|
|
92
92
|
|
|
93
93
|
The gate is the active lens, not the node's provider. A `@handle` token in `CLAUDE.md` or `notes/todo.md` (files the `claude` provider disclaims to `core/markdown`) still gets parsed by `claude/at-directive` under the `claude` lens, because the runtime grammar is what the lens represents and the runtime reads markdown across the whole project, not only the files it owns. The earlier double-check ("node's provider matches AND the lens") silently dropped that surface; dropping the node side restores it. Cross-lens isolation is preserved by the lens half alone: under `openai`, claude extractors are silent on every node (including `.claude/*`), because the lens authorisation is missing. When `activeProvider` is `null` (no setting, no filesystem marker), provider-gated extractors are skipped uniformly.
|
|
94
94
|
|
|
@@ -155,7 +155,7 @@ The loader enforces two id-uniqueness analyzers during discovery (see [`plugin-a
|
|
|
155
155
|
1. **Directory name == manifest id.** A plugin lives at `<root>/<id>/plugin.json`. A mismatch surfaces as status `invalid-manifest`. This analyzer eliminates same-root collisions by construction.
|
|
156
156
|
2. **Cross-root id collision blocks both sides.** Two plugins reachable from different roots (e.g. the project default `<cwd>/.skill-map/plugins/` and any `--plugin-dir` combination) that declare the same `id` BOTH receive status `id-collision`. No precedence analyzer applies, coherent with §Boot invariant ("no extension is privileged"). The user resolves by renaming one of them.
|
|
157
157
|
|
|
158
|
-
In addition, the loader **qualifies every extension** with its owning plugin id before registering it. The registry stores extensions under the qualified id `<plugin-id>/<extension-id>` (e.g. `core/slash-command`, `core/reference-broken`, `my-plugin/my-extractor`). Authors continue to declare the short `id` in each extension manifest; the loader composes the qualified form from `manifest.id` at load time. Built-in extensions bundled with the reference impl declare their `pluginId` directly in `built-ins.ts`, `core/` for kernel-internal primitives (every analyzer, the formatter, the cross-vendor extractors `annotations` / `slash` / `at-directive` / `markdown-link` / `external-url-counter` / `stability`) and vendor-specific plugins such as `claude/` (the Claude provider) for Provider integrations whose territory is platform-bound. If a plugin author injects a `pluginId` field on an extension that disagrees with `plugin.json`'s `id`, the loader emits `invalid-manifest` with a directed reason.
|
|
158
|
+
In addition, the loader **qualifies every extension** with its owning plugin id before registering it. The registry stores extensions under the qualified id `<plugin-id>/<extension-id>` (e.g. `core/slash-command`, `core/reference-broken`, `my-plugin/my-extractor`). Authors continue to declare the short `id` in each extension manifest; the loader composes the qualified form from `manifest.id` at load time. Built-in extensions bundled with the reference impl declare their `pluginId` directly in `built-ins.ts`, `core/` for kernel-internal primitives (every analyzer, the formatter, the cross-vendor extractors `annotations` / `slash` / `at-directive` / `markdown-link` / `backtick-path` / `external-url-counter` / `stability`) and vendor-specific plugins such as `claude/` (the Claude provider) for Provider integrations whose territory is platform-bound. If a plugin author injects a `pluginId` field on an extension that disagrees with `plugin.json`'s `id`, the loader emits `invalid-manifest` with a directed reason.
|
|
159
159
|
|
|
160
160
|
Every extension (built-in or drop-in) is independently toggle-able by its qualified id `<plugin>/<ext-id>`. The plugin row is a presentational grouping; the granular toggle target is the extension, while toggling a bare plugin id is the **bundle** (aggregate) macro that fans across every extension. The loader's pre-import `resolveEnabled(pluginId)` short-circuit only fires when EVERY extension of the plugin is disabled (the plugin "starts as disabled"); partial enables let the imports proceed and the runtime composer (`composeScanExtensions` / `composeFormatters` in `src/core/runtime/plugin-runtime/composer.ts`) drops the per-extension disabled rows before they reach the orchestrator. The `core` plugin exercises the per-extension axis explicitly (every kernel built-in is removable, satisfying §Boot invariant: "no extension is privileged"); vendor Provider plugins (`claude`, `antigravity`, `openai`, `agent-skills`) currently have most operators leaving every extension enabled, but the same per-extension toggle surface applies. See [`plugin-author-guide.md` §Toggle model](./plugin-author-guide.md#toggle-model) for the author-facing summary.
|
|
161
161
|
|
|
@@ -370,7 +370,7 @@ Default `undefined` ≡ empty map ≡ no reserved names. Path matches against no
|
|
|
370
370
|
|
|
371
371
|
The `Extractor` runtime contract is `extract(ctx) → void`. The extractor emits its work through three callbacks the kernel binds onto `ctx`:
|
|
372
372
|
|
|
373
|
-
- `ctx.emitLink(link)`, append a `Link` to the kernel's `links` table. The kernel validates `link.kind` against the **global closed enum** of link kinds (`invokes`, `references`, `mentions`, `supersedes`) before persistence; off-enum links are dropped and surface as `extension.error` events (the per-extractor `emitsLinkKinds` allowlist was retired with the structure-as-truth refactor; confidence is declared per emit, default `'medium'`). URL-shaped targets (`http(s)://…`) are partitioned out into `node.externalRefsCount` and never persisted.
|
|
373
|
+
- `ctx.emitLink(link)`, append a `Link` to the kernel's `links` table. The kernel validates `link.kind` against the **global closed enum** of link kinds (`invokes`, `references`, `mentions`, `supersedes`, `points`) before persistence; off-enum links are dropped and surface as `extension.error` events (the per-extractor `emitsLinkKinds` allowlist was retired with the structure-as-truth refactor; confidence is declared per emit, default `'medium'`). URL-shaped targets (`http(s)://…`) are partitioned out into `node.externalRefsCount` and never persisted.
|
|
374
374
|
- `ctx.enrichNode(partial)`, merge canonical, kernel-curated properties onto the current node's enrichment layer (persisted into [`node_enrichments`](./db-schema.md#node_enrichments)). **Strictly separate from the author-supplied frontmatter** (the latter remains immutable across scans). The enrichment layer is the right home for kernel-derived facts (computed titles, summaries, signals an Extractor inferred from the body) without polluting what the user wrote on disk. See §Enrichment layer below for the full lifecycle (per-extractor attribution, refresh verbs).
|
|
375
375
|
- `ctx.store`, plugin-scoped persistence. Optional, present only when the plugin declares `storage.mode` in `plugin.json`. Shape depends on the mode (`KvStore` for mode A, scoped `Database` for mode B). See [`plugin-kv-api.md`](./plugin-kv-api.md). The plugin author MAY opt into shape validation for their own writes by declaring `storage.schema` (Mode A) or `storage.schemas` (Mode B) in the manifest, JSON Schemas the kernel AJV-compiles at load time and runs against every `ctx.store.set(key, value)` / `ctx.store.write(table, row)` call. Absent = permissive (status quo). `emitLink` and `enrichNode` keep their universal validation against `link.schema.json` / `node.schema.json` regardless of this opt-in. See [`plugin-author-guide.md` §`outputSchema`](./plugin-author-guide.md#outputschema--opt-in-correctness-for-custom-storage-writes).
|
|
376
376
|
|
|
@@ -392,6 +392,21 @@ Both materialised and rejected Signals remain on `IAnalyzerContext.signals` post
|
|
|
392
392
|
|
|
393
393
|
The Signal's `range` field (byte offsets in the source) powers two cross-extractor analyses no Link can support today: collision detection (two extractors emitting Signals with overlapping ranges, contract above) and fragmentation detection (an authored intent split across several adjacent Signals, deferred to Phase 5+). Both surface as analyzer issues, not silent merges.
|
|
394
394
|
|
|
395
|
+
### Extractor · code-region file references (`core/backtick-path`)
|
|
396
|
+
|
|
397
|
+
Every body extractor strips fenced code blocks and inline code spans before matching (the code-strip policy): invocation tokens (`@handle`, `/command`, URLs) inside backticks are literal payload the runtime never follows. **Relative file paths are the documented exception.** The Agent Skills open standard mandates that a skill references its bundled resources by relative path and that "agents load these on demand"; prose like ``Read `references/rules.md` `` is an instruction the consuming LLM runtime does follow. The `core/backtick-path` extractor exists to surface exactly that class of references, and ONLY inside code regions, the precise complement of the code-strip policy, so it can never collide with the prose-side extractors.
|
|
398
|
+
|
|
399
|
+
The contract:
|
|
400
|
+
|
|
401
|
+
- **Domain**: the extractor matches exclusively inside fenced code blocks and inline code spans, over the *inverse mask* of the code-strip transform: same-length text where code-region characters survive and everything else is blanked. Same-length masking keeps byte offsets and line numbers valid against the original body.
|
|
402
|
+
- **Token grammar** (pinned; implementations MUST match it exactly): `/(?<![\w/:.-])(?:\.{1,2}\/)?[\w.-]+(?:\/[\w.-]+)+\.md\b(?![\w/])/g`. In words: an optional `./` or `../` prefix, at least one `/` separator, a `.md` suffix at a word boundary. The character classes and guards reject, by construction: URL interiors (`https://example.com/docs/x.md` cannot match at any start position because of the lookbehind), template placeholders and globs (`{PROJECT}`, `*` are outside the segment class), near-miss suffixes (`.mdx`, `.md_var`), slashless filenames (`SKILL.md`), and absolute paths (a leading `/` fails the lookbehind).
|
|
403
|
+
- **Targets**: `.md` only. Markdown files are the one class with a guaranteed node on the scan side (the `core/markdown` fallback), so every resolvable token has a target to land on.
|
|
404
|
+
- **Resolution**: identical to `core/markdown-link`, POSIX-normalised against `dirname(node.path)`. Per-node dedup on the resolved target, first occurrence wins.
|
|
405
|
+
- **Emission**: one single-candidate Signal per distinct resolved target, `kind: 'points'` (the code-region path pointer kind, distinct from `references` so the two surfaces stay visually and semantically separable), confidence `0.85` (same value and rationale as a path-style at-directive: a strong file signal with one degree of inference, the author wrote a path but not an explicit link syntax). The candidate's `normalizedTrigger` is the resolved target so resolution and the confidence lift behave exactly like `markdown-link`. A prose `[x](references/a.md)` (`references`) and a backticked `` `references/a.md` `` (`points`) targeting the same file COEXIST as two Link rows: the post-resolver dedup keys on `kind`, so the rows never merge, and `core/link-conflict` excludes `points` from disagreement detection (two complementary authoring surfaces, not two detectors disputing one meaning).
|
|
406
|
+
- **Unresolved targets are NOT suppressed.** The extractor emits unconditionally, mirroring `markdown-link`; `core/reference-broken` flags targets that resolve to no node. This is deliberate: a backticked path pointing at a deleted or misspelled bundled doc is a real authoring bug (the standard's progressive disclosure breaks at runtime). Out-of-scope paths that the consuming runtime resolves against a different root (a target workspace, a generated tree) are silenced through the existing `scan.referencePaths` escape hatch, not by weakening the extractor.
|
|
407
|
+
|
|
408
|
+
A path written in prose without any wrapping (neither backticks nor markdown-link syntax) stays invisible in this revision; the code-region domain is the verified, bounded surface.
|
|
409
|
+
|
|
395
410
|
### Extractor · enrichment layer
|
|
396
411
|
|
|
397
412
|
`ctx.enrichNode(partial)` is the only writable surface the Extractor pipeline has on a node. The author's frontmatter on `scan_nodes.frontmatter_json` is read-only from any Extractor. Implementations MUST:
|
|
@@ -651,18 +666,19 @@ The Action stays pure (no IO). The kernel materializes the patch through the `Si
|
|
|
651
666
|
|
|
652
667
|
### Write consent
|
|
653
668
|
|
|
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:
|
|
669
|
+
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
670
|
|
|
656
|
-
- `allowEditSmFiles === true` → write proceeds.
|
|
657
|
-
- `allowEditSmFiles === false` AND the caller passes `
|
|
658
|
-
- `allowEditSmFiles === false` AND `confirm`
|
|
659
|
-
|
|
671
|
+
- `allowEditSmFiles === true` → write proceeds, no prompt (consent already persisted).
|
|
672
|
+
- `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`.
|
|
673
|
+
- `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".
|
|
674
|
+
- `allowEditSmFiles === false` AND both `confirm` and `always` missing / false → the kernel raises `EConsentRequiredError`. The driving adapter MUST translate it into a surface-appropriate prompt:
|
|
675
|
+
- **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
676
|
- **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 }`.
|
|
677
|
+
- **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
678
|
|
|
663
|
-
|
|
679
|
+
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
680
|
|
|
665
|
-
The flag lives in `project-local` (gitignored) so each collaborator consents independently; a single contributor's
|
|
681
|
+
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
682
|
|
|
667
683
|
### Plugin contributions
|
|
668
684
|
|
|
@@ -758,6 +774,8 @@ Each entry picks a `slot` name from the closed catalog and supplies presentation
|
|
|
758
774
|
|
|
759
775
|
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
776
|
|
|
777
|
+
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.
|
|
778
|
+
|
|
761
779
|
### Settings
|
|
762
780
|
|
|
763
781
|
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 +847,10 @@ Endpoints under `/api/contributions/*`:
|
|
|
829
847
|
- `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
848
|
- `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
849
|
|
|
850
|
+
The `inspector.action.button` slot dispatches to a generic Action endpoint, sibling of the single-node `POST /api/sidecar/bump`:
|
|
851
|
+
|
|
852
|
+
- `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.
|
|
853
|
+
|
|
832
854
|
Plus catalog embedding into every payload-bearing envelope:
|
|
833
855
|
|
|
834
856
|
- `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
|
|
185
|
+
#### `sm tutorial`
|
|
186
186
|
|
|
187
|
-
Materialize
|
|
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
|
|
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
|
-
|
|
196
|
+
Behaviour:
|
|
202
197
|
|
|
203
|
-
- Writes the full skill folder (`SKILL.md` plus
|
|
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 `
|
|
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
|
|
|
@@ -611,7 +606,7 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
|
|
|
611
606
|
| `GET /api/issues?severity=&analyzerId=&node=` | implemented | `RestEnvelope` (`kind: 'issues'`), list of issues. Filters: `severity` (CSV from `error\|warn\|info`), `analyzerId` (CSV; qualified or short suffix per `sm check --analyzers`), `node` (filter to issues whose `nodeIds` includes the path). No pagination at v14.2. |
|
|
612
607
|
| `GET /api/graph?format=ascii\|json\|md` | implemented | formatter-rendered graph. `Content-Type` per format: `text/plain` (ascii), `application/json` (json), `text/markdown` (md / mermaid). Default `format=ascii`. Unknown format → 400 `bad-query`. |
|
|
613
608
|
| `GET /api/config` | implemented | `RestEnvelope` (`kind: 'config'`), merged effective config (defaults → user → user-local → project → project-local → override). |
|
|
614
|
-
| `GET /api/plugins` | implemented | `RestEnvelope` (`kind: 'plugins'`), list of installed plugins (built-in + drop-in) with status. Item shape: `{ id, version, kinds, status, reason, source: 'built-in'\|'project', description?: string, locked?: boolean, startsAsDisabled?: boolean, extensions?: Array<{ id, kind, version, enabled, description?: string, locked?: boolean }> }`. The plugin row itself has no granular toggle axis; the row's `status` aggregates the children (`'enabled'` when at least one extension is enabled, `'disabled'` otherwise). The `description` field on the plugin item carries the manifest-declared description (built-ins: hardcoded on `IBuiltInPlugin`; drop-ins: `plugin.json#/description`); each `extensions[]` entry carries its extension manifest's `description` per `IExtensionBase` (`extensions/base.schema.json#/properties/description`). The SPA's Settings list renders the descriptions as muted secondary text and includes them in its substring-search index alongside the ids. The `extensions` array is present whenever the plugin declares any extension AND the plugin loaded successfully. Each entry's `enabled` reflects the per-extension override resolution (DB > settings.json > installed default). The optional `locked: true` flag is stamped when the plugin id (or qualified extension id) appears in the host's lock-list (`src/server/locked-plugins.ts`); locked items render the toggle disabled in the SPA and any `PATCH` against them returns `403 locked`. The flag is omitted when false. The optional `startsAsDisabled: true` flag is stamped on drop-in plugins (never built-ins) whose discovery-time `status` was `'disabled'`, that is, every extension of the plugin was disabled in `config_plugins` / `settings.json` at `sm serve` boot, so the handlers were never bucketed into the runtime. The SPA renders a per-row hint when this flag is set AND the user re-enables at least one of the plugin's extensions in the buffered state, since re-enabling them requires `sm serve` restart (the rest of the toggle pipeline applies live). The flag is omitted when false. |
|
|
609
|
+
| `GET /api/plugins` | implemented | `RestEnvelope` (`kind: 'plugins'`), list of installed plugins (built-in + drop-in) with status. Item shape: `{ id, version, kinds, status, reason, source: 'built-in'\|'project', description?: string, locked?: boolean, startsAsDisabled?: boolean, extensions?: Array<{ id, kind, version, enabled, description?: string, stability?: 'experimental'\|'beta'\|'stable'\|'deprecated', locked?: boolean }> }`. The plugin row itself has no granular toggle axis; the row's `status` aggregates the children (`'enabled'` when at least one extension is enabled, `'disabled'` otherwise). The `description` field on the plugin item carries the manifest-declared description (built-ins: hardcoded on `IBuiltInPlugin`; drop-ins: `plugin.json#/description`); each `extensions[]` entry carries its extension manifest's `description` per `IExtensionBase` (`extensions/base.schema.json#/properties/description`), plus the optional `stability` lifecycle label per `extensions/base.schema.json#/properties/stability` (omitted when the manifest does not declare it; missing means `stable`. The SPA badges only the non-default values, `experimental` / `beta` / `deprecated`, next to the extension row; `stable` renders nothing. Presentation-only). The SPA's Settings list renders the descriptions as muted secondary text and includes them in its substring-search index alongside the ids. The `extensions` array is present whenever the plugin declares any extension AND the plugin loaded successfully. Each entry's `enabled` reflects the per-extension override resolution (DB > settings.json > installed default). The optional `locked: true` flag is stamped when the plugin id (or qualified extension id) appears in the host's lock-list (`src/server/locked-plugins.ts`); locked items render the toggle disabled in the SPA and any `PATCH` against them returns `403 locked`. The flag is omitted when false. The optional `startsAsDisabled: true` flag is stamped on drop-in plugins (never built-ins) whose discovery-time `status` was `'disabled'`, that is, every extension of the plugin was disabled in `config_plugins` / `settings.json` at `sm serve` boot, so the handlers were never bucketed into the runtime. The SPA renders a per-row hint when this flag is set AND the user re-enables at least one of the plugin's extensions in the buffered state, since re-enabling them requires `sm serve` restart (the rest of the toggle pipeline applies live). The flag is omitted when false. |
|
|
615
610
|
| `PATCH /api/plugins/:id` | implemented | **Bundle (aggregate) macro endpoint**: fans the toggle out across every extension inside the plugin. `:id` MUST be a top-level plugin id (no slash); qualified-id form is the sibling route below. Body `{ enabled: boolean }` (JSON). Persists one `config_plugins` row per child extension (`<plugin>/<ext>`) inside a single transaction; locked children are silently dropped, mirroring the CLI's bulk-mode lock semantics. Response is the canonical `RestEnvelope` (`kind: 'plugins'`) reflecting the post-write state. **Lock**, rejected with 403 `locked` when the plugin id itself is in the host lock-list (`src/server/locked-plugins.ts`). **Apply window**, the override applies on the next scan; both the BFF and the watcher build a fresh resolver from `config_plugins` before composing extensions, so the toggle is honoured without restarting `sm serve`. The endpoint purges `scan_contributions` rows for each disabled extension immediately so the UI stops rendering its chips before the next scan. **Exception**, drop-in plugins whose discovery-time `status` was `'disabled'` (carried as `startsAsDisabled: true` on the read shape) are NOT in the runtime extension buckets; re-enabling them via PATCH persists the override but requires `sm serve` restart for the handlers to be loaded. The SPA surfaces this per-row when the user re-enables such a plugin. The endpoint does NOT broadcast a WS event today. |
|
|
616
611
|
| `PATCH /api/plugins/:pluginId/extensions/:extensionId` | implemented | Canonical per-extension toggle. Body `{ enabled: boolean }`. Both segments are URL-path-segment-encoded (no slash inside `:pluginId` or `:extensionId`). 404 `not-found` when the plugin id is unknown or the extension id does not belong to that plugin. **Lock**, rejected with 403 `locked` when either the plugin id or the qualified `pluginId/extensionId` appears in the host lock-list. Same persistence + apply-window semantics as the bundle (aggregate) macro form (including the `startsAsDisabled` exception). The SPA's buffered Settings modal posts here for every per-row flip. |
|
|
617
612
|
| `PATCH /api/plugins` | implemented | Bulk toggle. Body `{ "changes": Array<{ "id": string, "enabled": boolean }> }` where each `id` is either a bare plugin id (bundle cascade macro, same semantics as `PATCH /api/plugins/:id`) or a qualified `<plugin>/<extension>` id. Empty `changes` array is accepted as a no-op (returns the current `GET /api/plugins` envelope). The route validates the **entire batch** before writing, any invalid entry (`unknown-plugin` / `locked`) rejects the whole request with the offending id in `error.details.id`, and the DB is not touched. Valid batches are applied in **one SQLite transaction**: bare plugin ids expand to their child qualified ids before persistence; `IConfigPluginsPort.set` is called per resulting key, then one grouped `scan_contributions` purge per disabled extension (enables skip the purge). Response is the same `RestEnvelope` (`kind: 'plugins'`) shape as `GET /api/plugins`, reflecting the post-write state, the SPA replaces its modal state from this envelope. Apply-window and `startsAsDisabled` exception semantics match the per-id routes. The single-id `PATCH /api/plugins/:id` and qualified-id sibling stay available for CLI / external automation; the bulk variant exists so the SPA can stage edits in a buffered modal and ship the final delta atomically. |
|
package/conformance/README.md
CHANGED
|
@@ -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
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://skill-map.ai/spec/v0/conformance-case.schema.json",
|
|
3
|
+
"id": "backtick-path-extraction",
|
|
4
|
+
"description": "Code-region file references, end-to-end. A body whose ONLY references are a backtick-wrapped relative `.md` path in prose and the same path repeated inside a fenced block MUST produce exactly ONE Link via `core/backtick-path` (per-node dedup on the resolved target), with kind `points` and lifted to confidence 1.0 by the path-match rule. The URL-in-backticks bait in the same body MUST NOT produce a link (the pinned token grammar rejects URL interiors).",
|
|
5
|
+
"fixture": "backtick-path",
|
|
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.linksCount", "equals": 1 },
|
|
14
|
+
{ "type": "json-path", "path": "$.links[0].source", "equals": "source.md" },
|
|
15
|
+
{ "type": "json-path", "path": "$.links[0].target", "equals": "docs/target.md" },
|
|
16
|
+
{ "type": "json-path", "path": "$.links[0].kind", "equals": "points" },
|
|
17
|
+
{ "type": "json-path", "path": "$.links[0].confidence", "equals": 1.0 },
|
|
18
|
+
{ "type": "json-path", "path": "$.links[0].sources[0]", "equals": "backtick-path" }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"assertions": [
|
|
11
11
|
{ "type": "exit-code", "value": 0 },
|
|
12
|
-
{ "type": "stderr-matches", "pattern": "plugin bad-provider:.*
|
|
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
|
+
}
|
package/conformance/coverage.md
CHANGED
|
@@ -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`
|
|
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,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: source
|
|
3
|
+
description: Fixture for the `backtick-path-extraction` conformance case. The backtick-wrapped relative path below must reach the graph as ONE Link row via the code-region path extractor, deduped against the fenced repeat, with the URL bait rejected.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Before doing anything else, read `docs/target.md` for the full rules.
|
|
7
|
+
|
|
8
|
+
Validation example (the duplicate path below must dedupe into the same link):
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
report-validator --rules docs/target.md check output.json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
External docs live at `https://example.com/docs/target.md` and must never become a link.
|
|
@@ -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,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
|
+
};
|
package/conformance/fixtures/view-contribution-payloads/.skill-map/plugins/payloads-demo/plugin.json
ADDED
|
@@ -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
|
+
}
|