@skill-map/spec 0.35.0 → 0.37.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 +197 -0
- package/architecture.md +13 -13
- package/cli-contract.md +9 -9
- package/conformance/coverage.md +1 -1
- package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json +1 -2
- package/db-schema.md +2 -2
- package/index.json +15 -15
- package/job-events.md +1 -1
- package/package.json +1 -1
- package/plugin-author-guide.md +59 -38
- package/schemas/bump-report.schema.json +1 -1
- package/schemas/extensions/base.schema.json +2 -2
- package/schemas/link.schema.json +1 -1
- package/schemas/plugins-doctor.schema.json +1 -1
- package/schemas/plugins-registry.schema.json +0 -6
- package/schemas/project-config.schema.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,202 @@
|
|
|
1
1
|
# Spec changelog
|
|
2
2
|
|
|
3
|
+
## 0.37.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d852217: Eliminate the bundle-level toggle entirely. Every plugin extension is now independently toggle-able by its qualified `<bundle>/<ext>` id; the bundle itself is a presentational grouping only.
|
|
8
|
+
|
|
9
|
+
**What changed**
|
|
10
|
+
|
|
11
|
+
- **Manifest schema**: `granularity` is removed from `spec/schemas/plugins-registry.schema.json`. A user plugin manifest that still declares it is rejected as `invalid-manifest` via AJV's `additionalProperties: false`. Built-in `plugin.json` files dropped the field; the generator (`scripts/generate-built-ins.js`) no longer emits it.
|
|
12
|
+
- **Kernel**: `TGranularity` deleted from `src/kernel/types/plugin.ts`; `IDiscoveredPlugin.granularity` and `IPluginManifest.granularity` gone. The runtime resolver (`src/core/runtime/plugin-runtime/resolver.ts`) keys every gate on the qualified extension id.
|
|
13
|
+
- **CLI** (`src/cli/commands/plugins/`): the bare bundle id (`sm plugins disable claude`) is now a **macro** that fans the toggle out across every extension inside the bundle. Single-extension bundles (`openai`, `antigravity`, `agent-skills`) apply without prompting. Multi-extension bundles (`claude`, `core`, multi-extension user plugins) require `--yes` OR an interactive TTY confirm; non-TTY contexts must pass `--yes` or the verb refuses with a directed message and the list of affected extensions. `--all` cascades through every bundle's extensions under the same gate. Qualified-id form (`sm plugins disable claude/at-directive`) toggles exactly that extension with no prompt. The `--yes` / `-y` flag is added to `enable` and `disable`. Granularity-mismatch error messages (`'claude' has granularity=bundle`, `'core' has granularity=extension`) are removed; the new error path is "unknown id" / "macro requires confirmation". `sm plugins doctor` summary reverts to `N enabled extensions · …` and counts every extension independently (built-ins and loaded user plugins alike).
|
|
14
|
+
- **BFF** (`src/server/routes/plugins.ts`): `PATCH /api/plugins/:id` becomes the **cascade endpoint** that persists one `config_plugins` row per child extension. Granularity-mismatch rejections are gone. The qualified-id sibling (`PATCH /api/plugins/:bundleId/extensions/:extensionId`) is unchanged and remains the canonical per-extension surface. `PATCH /api/plugins` (bulk) accepts bare bundle ids and qualified ids in the same batch; bare entries cascade at write time. The `granularity` field is removed from `IPluginListItem` on the wire; the bundle row's `status` aggregates child enablement (`enabled` when ≥1 extension is enabled).
|
|
15
|
+
- **SPA** (`ui/src/app/components/settings-modal/`): the bundle-level `<p-toggleswitch>` is removed (`canToggleBundle()` and `onBundleToggle()` deleted); bundle rows render as labelled headers with their per-extension list underneath. The "kind filter" chip now narrows the extensions array universally (it used to leave bundle-granularity bundles unfiltered, leaking extractors / analyzers when the user clicked "provider"). `IPluginItemApi.granularity` and `TPluginGranularityApi` removed from `ui/src/models/api.ts`.
|
|
16
|
+
- **Spec prose**: `spec/architecture.md` §Plugin loader, `spec/cli-contract.md` §Endpoints + §Error code sources, `spec/plugin-author-guide.md` §Toggle model (replacing the old §Granularity), `spec/db-schema.md` §scan_contributions are all rewritten to reflect the per-extension toggle model and the macro form on bare ids.
|
|
17
|
+
|
|
18
|
+
**Tests + fixtures**
|
|
19
|
+
|
|
20
|
+
- Plugin-loader spec asserts the field is now rejected via `additionalProperties`.
|
|
21
|
+
- CLI plugins spec rewrites the granularity describe block as bundle-macro semantics (`--yes` required for multi-extension bundles, single-extension bundles apply directly, qualified-id form flips exactly that extension).
|
|
22
|
+
- SPA settings-plugins spec updates the helper to call `onExtensionToggle` (the bundle has no toggle method anymore) and asserts the kind filter narrows extensions.
|
|
23
|
+
- Conformance fixture (`spec/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json`) drops the field.
|
|
24
|
+
|
|
25
|
+
**Drive-by**
|
|
26
|
+
|
|
27
|
+
`sm plugins doctor` summary reverts to a plain `N enabled extensions` form (the `4 bundles + 27 extensions` breakdown shipped in `d66bc71` was meaningful when bundles had their own toggle axis; with the unified per-extension model the split is no longer informative). The new `countByStatus` walks every extension uniformly across built-ins and user plugins.
|
|
28
|
+
|
|
29
|
+
## User-facing
|
|
30
|
+
|
|
31
|
+
Plugins no longer have a bundle-level switch; each extension toggles on its own. `sm plugins disable <bundle>` cascades across the bundle's extensions (multi-extension bundles need `--yes`). The kind filter narrows extensions inside matched bundles instead of leaking siblings.
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- f66dbfe: Decouple built-in extensions from per-extension semver. Built-ins ship inside the CLI bundle, so authors no longer declare a `version` literal in each `<plugin>/<kind>s/<name>/index.ts` manifest under `src/plugins/`. The codegen at `scripts/generate-built-ins.js` now reads the CLI version from `src/package.json` and stamps it onto every built-in (alongside the existing `pluginId` stamp) when emitting `src/plugins/built-ins.ts`. The resulting runtime objects still satisfy the full kind interface (`IAnalyzer`, `IExtractor`, ...) and every downstream consumer continues to see `ext.version: string`, so `state_executions.extension_version` keeps recording a meaningful value (= CLI version) for reproducibility.
|
|
36
|
+
|
|
37
|
+
New kernel type `IBuiltInManifest<T extends IExtensionBase> = Omit<T, 'version'>` exported from `kernel/extensions/index.js`. Built-in manifest authors type their export as `IBuiltInManifest<I<Kind>>` and omit `version`; the codegen does the rest. The 34 built-in extensions across the 5 first-party bundles (`core`, `claude`, `antigravity`, `openai`, `agent-skills`) were migrated in-tree, removing 32 cargo-cult `'1.0.0'` literals and the 2 `'0.0.0'` "stub" sentinels.
|
|
38
|
+
|
|
39
|
+
External (user-authored) plugins are unaffected: the AJV check at load time still requires `version` on every extension manifest per `spec/schemas/extensions/base.schema.json#/required`. The schema's `required` list is unchanged; only the `version` field description was updated to document the built-in / external asymmetry.
|
|
40
|
+
|
|
41
|
+
Two kernel API signatures widen from `IProvider` to `IBuiltInManifest<IProvider>` so test files that import raw built-in manifests directly (bypassing the codegen) keep type-checking without a runtime workaround. The widened functions are `resolveProviderWalk` and `buildProviderFrontmatterValidator` (plus the `IProviderFrontmatterValidator.validate` method shape); both only read `id` / `kinds` / `walk` / `read` / `schemas` and never touch `version`, so the widening is structurally safe and production callers passing fully-loaded `IProvider` values continue to type-check (subtype-passes-supertype).
|
|
42
|
+
|
|
43
|
+
The AGENTS.md "stub extensions ship as `version: '0.0.0'`" convention is retired (the chip it surfaced was hidden from the UI / CLI in the previous release). If we later want a visible placeholder signal we'll add a dedicated `stability: 'stub'` field instead of overloading `version`.
|
|
44
|
+
|
|
45
|
+
- 457a60d: Reserve the `graph.node.alert` slot for special-case signals; disconnect every built-in core analyzer from it. Define the **chip-vs-issue policy** for plugin authors and align `reference-broken` to it. The corner badge on the NE tip of each graph card is no longer a generic "this node has a problem" surface. Routine findings (`reference-broken`, `annotation-field-unknown`, `schema-violation`) now ship only as `card.footer.right` chips, the slot's natural home for paired-icon-and-count signals.
|
|
46
|
+
|
|
47
|
+
**What changed**
|
|
48
|
+
|
|
49
|
+
- **Analyzer manifests + emit**: `reference-broken`, `annotation-field-unknown`, and `schema-violation` (under `src/plugins/core/analyzers/`) dropped their `alert` ui declaration and the matching `ctx.emitContribution(nodePath, 'alert', ...)` call. Each analyzer keeps its `card.footer.right` chip with the same tooltip + severity + count semantics. `schema-violation`'s severity-from-worst-finding logic stays (introduced in this same change set), now applied to the chip exclusively (`warn` for missing base fields, `danger` as soon as one schema check returns error).
|
|
50
|
+
- **Icon swaps**: `schema-violation` chip moved from `fa-solid fa-triangle-exclamation` to `fa-solid fa-circle-exclamation`. `reference-broken` chip moved from `fa-regular fa-circle-xmark` to `fa-solid fa-circle-xmark` (the outlined regular variant existed in FA Free for `circle-xmark` but the chip lost its alert sibling that motivated the visual contrast). Both choices documented inline next to the manifest entry: in FA Free `circle-exclamation` ships only in `solid` (`icons.yml`, `styles: [solid]`), so a `fa-regular` declaration would render as a missing-glyph tofu.
|
|
51
|
+
- **Slot kept in the catalog**: `graph.node.alert` stays in `view-slots.schema.json`, `kernel/types/view-catalog.ts`, `ui/src/app/slots/slot-config.ts`, and the renderer map. The mount in `graph-view.html` and the `NodeAlert` renderer are untouched. The slot is now reserved for genuinely independent signals (a future plugin that wants a corner decoration tied to a one-off condition); the slot-config comment documents the bar.
|
|
52
|
+
- **`sm plugins slots list` summary**: the `graph.node.alert` row in `src/cli/commands/plugins/slots-catalog.ts` now reads "Reserved corner badge ... special-case signals only" so plugin authors browsing the catalog see the policy without digging.
|
|
53
|
+
- **Drive-by, `sm init` warning formatting**: `activeProviderNoMarkerWarning` (under `src/core/runtime/i18n/scan-runner.texts.ts`) used to glue itself onto the next stderr line because the catalog string had no trailing newline and the message ran as a single sentence wall. Refactored to the §3.1b "glyph + dim hint" two-line block (mirrors the drift warn next door): yellow `⚠` headline + dim hint indented at column 3. `active-provider-bootstrap.ts` threads `opts.style.warnGlyph` + `opts.style.dim` through `tx(...)` like the drift path already did.
|
|
54
|
+
|
|
55
|
+
**Tests**
|
|
56
|
+
|
|
57
|
+
- Each affected analyzer's spec asserts `chip only, no alert` and lists the surviving slot via `deepStrictEqual(analyzer.ui, { chip: { slot: 'card.footer.right', ... } })`. Locks the new shape and fails fast if a future refactor re-wires the corner.
|
|
58
|
+
- `schema-violation.spec.ts` adds an "escalates severity to danger as soon as one finding is error-level" case that asserts the chip's severity follows the worst underlying finding.
|
|
59
|
+
- The existing `active-provider-bootstrap.spec.ts` test that regex-matched `/no provider markers detected/i` still passes against the new two-line block (the substring is preserved).
|
|
60
|
+
- `e2e/live-bff/` gains a `graph-node-alert.spec.ts` regression: with a fixture node carrying a broken `@mention` (would have triggered the `reference-broken` corner badge under the prior contract), the SPA must render zero `[data-testid="renderer-node-alert"]` elements while still surfacing the footer chip. The fixture (`e2e/live-bff/fixture.ts`) was extended with the broken-ref body line; the bump happy-path spec is unaffected (stale-badge state + version increment do not depend on link findings).
|
|
61
|
+
|
|
62
|
+
**`reference-broken` Issue severity raised from `warn` to `error`**
|
|
63
|
+
|
|
64
|
+
Per the chip-vs-issue policy below, a `danger` chip MUST be backed by an `error` Issue for the same node. `reference-broken` was emitting chip `danger` (red) + Issue `warn`, the only mismatch in the built-in catalog. Bumping the Issue aligns the visual signal with the exit code: any unresolved `@` / `/` link or markdown reference now escalates `sm scan` to exit 1 by default (was exit 0 with a yellow finding). CI pipelines that ran `sm scan` and treated exit 0 as "clean" will now see broken-ref runs fail, the operator was already seeing the red chip on the card; the change makes the exit code match.
|
|
65
|
+
|
|
66
|
+
`scan-readers.spec.ts` gains a `plantWarnOnlyFixture` helper (stale-sidecar based) for the "no error-severity → exit 0" contract tests that previously relied on `reference-broken` being a warn-level finding.
|
|
67
|
+
|
|
68
|
+
**Chip-vs-issue policy (new doc)**
|
|
69
|
+
|
|
70
|
+
Two new sections, one in `context/view-slots.md` ("Chip vs Issue, what counts and what only shows") and a shorter mirror in `spec/plugin-author-guide.md`, articulate the two-channel model:
|
|
71
|
+
|
|
72
|
+
- An `Issue` returned by `evaluate(ctx)` feeds the card's aggregated stats AND the scan / check exit code.
|
|
73
|
+
- A view contribution to `card.footer.right` is purely presentational, its `severity` controls only the chip's own colour.
|
|
74
|
+
|
|
75
|
+
The two channels are independent. The doc lists the 4 combinations (issue × chip) and codifies the colour rule: a chip MAY paint `warn` (yellow) or `danger` (red) only when the same analyzer emits a matching Issue at the same level. Decorative chips use `info`, `success`, or omit the severity field (neutral). Compliance audited across the built-in catalog: every analyzer now follows the rule.
|
|
76
|
+
|
|
77
|
+
**Drive-by, view-slots annex**
|
|
78
|
+
|
|
79
|
+
`context/view-slots.md` table row for `graph.node.alert` now flags the slot as Reserved with a pointer to the policy comment in `slot-config.ts`. The new chip-vs-issue section sits next to it as the cross-channel policy for the rest of the card surface.
|
|
80
|
+
|
|
81
|
+
`spec/index.json` regenerated for the prose addition (no schema changes, just the guide).
|
|
82
|
+
|
|
83
|
+
## User-facing
|
|
84
|
+
|
|
85
|
+
Graph cards drop the corner badge for routine warnings; count + tooltip stay on the footer chip. Broken refs now escalate `sm scan` to exit 1 (were exit 0). `sm init` prints the "no provider markers" advisory as a two-line yellow `⚠` block.
|
|
86
|
+
|
|
87
|
+
- d66bc71: Three findings from a second `sm-tutorial` external-tester session (Adolfo, 2026-05-25).
|
|
88
|
+
|
|
89
|
+
**Finding 1, `sm check --analyzers` silently accepts unknown ids** (`src/cli/commands/check.ts`)
|
|
90
|
+
|
|
91
|
+
`parseAnalyzersFlag` trimmed tokens and dropped empties, then `matchesAnalyzerFilter` compared them against the persisted `analyzerId` set. A typo like `broken-ref` (the real id is `core/reference-broken`, short form `reference-broken`) matched nothing, the verb returned `✓ No issues.` in green with exit 0, identical to a clean run; the planted broken-reference warning was invisible. The tutorial copy itself used `broken-ref` in the example commands, so following the walkthrough verbatim hid the fixture.
|
|
92
|
+
|
|
93
|
+
Fix: load the live Analyzer catalog when `--analyzers` is set, validate every token against both qualified (`core/reference-broken`) and short (`reference-broken`) forms, and on the first unknown id exit `ExitCode.Error` (2) with a stderr message naming the unknown id(s) and listing every valid qualified id. The catalog load is shared with the existing `--include-prob` path so the verb still pays for the runtime exactly once when both flags are present. Tutorial `.claude/skills/sm-tutorial/SKILL.md` updated to use the real ids (`reference-broken`, `core/name-reserved`, `core/link-self-loop`, `core/reference-redundant`).
|
|
94
|
+
|
|
95
|
+
**Finding 2, trigger-style links from universal-provider bodies never resolved** (`src/kernel/orchestrator/lift-resolved-link-confidence.ts`, `spec/architecture.md`)
|
|
96
|
+
|
|
97
|
+
The extractor gate already keys on the **active provider lens** (§Universal extractors and per-provider extractors): `claude/slash-command` under the `claude` lens emits `/handle` links from every node, including `notes/todo.md` classified by `core/markdown`. But the post-walk confidence-lift transform keyed on the **source node's provider id** (`markdown`), which declares no `resolution` map; the lookup short-circuited, the link stayed at `confidence: 0.8`, and `link.resolvedTarget` never got populated. Effect: even after the prior denormalised-`linksInCount` fix (be116dd) read `resolvedTarget ?? target`, markdown-sourced trigger links still incremented `linksInCount` against the authored trigger string (`/demo-command`) instead of the resolved node, and `sm list` IN stayed at 0 for the resolved command / skill node. The UI drew the arrow correctly (it walks `scan_links` directly), so the inconsistency surfaced as "arrow lands but IN=0".
|
|
98
|
+
|
|
99
|
+
Fix: align resolver authority with extractor authority by keying the `resolution` lookup on `ctx.activeProvider` instead of `sourceNode.provider`. `IPostWalkTransformCtx` gains a new `activeProvider: string | null` field; `buildPostWalkTransformCtx` in the orchestrator threads the lens through from `RunScanOptions.activeProvider`. `spec/architecture.md` §Provider · resolution rules updated to match (the prior wording was internally inconsistent with §Universal extractors and per-provider extractors, which already established the lens-driven principle). Existing test that asserted the old behaviour inverted to assert the new contract; a regression test for the exact sm-tutorial fixture (`/demo-command` from `notes/todo.md` under `claude` lens) and a complementary unlensed-project case (`activeProvider === null` short-circuits the name path) added.
|
|
100
|
+
|
|
101
|
+
**Finding 3, `sm plugins doctor` summary count looked off-by-N against `sm plugins list`** (`src/cli/commands/plugins/doctor.ts`)
|
|
102
|
+
|
|
103
|
+
Doctor's `enabled` count adds bundle-granularity bundles (count once) + extension-granularity extensions (count per extension). With a fresh install that totals 4 + 27 = 31. `sm plugins list` lists every individual extension under each bundle, so its surface count is 33 (3 claude + 1 antigravity + 1 openai + 1 agent-skills + 27 core). The two numbers were correct but unexplained; the tester read the doctor header `31 enabled` and the list count `33` and assumed a bug.
|
|
104
|
+
|
|
105
|
+
Fix: extend the doctor summary line to spell out the math: `plugins doctor: 31 enabled (4 bundles + 27 extensions) · 0 issues · 0 warnings`. New `countEnabledByGranularity` helper walks the same shape as `countByStatus` but tracks bundles and extensions separately so the breakdown reflects the project's actual granularity mix.
|
|
106
|
+
|
|
107
|
+
**Drive-by: tutorial wrap-up safer cleanup** (`.claude/skills/sm-tutorial/SKILL.md`)
|
|
108
|
+
|
|
109
|
+
The wrap-up advised `cd ~ && rm -rf <cwd>` with a single "if the cwd was a dedicated dir" caveat. The tester ran the tutorial in their day-to-day work dir; the bulk command would have nuked unrelated files. Wrap-up now branches on whether the cwd looks dedicated and surfaces the explicit per-file list (same shape as the "start over" branch already uses) when it does not.
|
|
110
|
+
|
|
111
|
+
## User-facing
|
|
112
|
+
|
|
113
|
+
`sm check --analyzers <id>` now errors with the valid id list when mistyped, instead of silently saying "No issues." `/invoke` and `@mention` links from any markdown body now contribute to the target's `IN`. `sm plugins doctor` summary spells out its bundle + extension split.
|
|
114
|
+
|
|
115
|
+
## 0.36.0
|
|
116
|
+
|
|
117
|
+
### Minor Changes
|
|
118
|
+
|
|
119
|
+
- 8ab68ed: Rename `core/field-unknown` to `core/annotation-field-unknown` so it
|
|
120
|
+
groups alphabetically with the other sidecar (`.sm`) annotation rules
|
|
121
|
+
(`core/annotation-orphan`, `core/annotation-stale`). The rule's job has
|
|
122
|
+
not changed: it still flags typos / unrecognised keys in sidecars and
|
|
123
|
+
emits a warn issue plus the same `alert` + `chip` view contributions
|
|
124
|
+
on `graph.node.alert` / `card.footer.right`.
|
|
125
|
+
|
|
126
|
+
`contribution-orphan` is intentionally NOT renamed: the `contribution`
|
|
127
|
+
namespace refers to view-slot rows in `scan_contributions` (runtime
|
|
128
|
+
data the analyzers emit for the UI), not to annotation fields in
|
|
129
|
+
sidecars. The two namespaces are distinct.
|
|
130
|
+
|
|
131
|
+
Pre-1.0 minor per `spec/versioning.md`: breaking rename of a public
|
|
132
|
+
qualified id referenced from `settings.json`, `--analyzers <id>` flags,
|
|
133
|
+
and the `analyzerId` filter on `GET /api/issues`. No behavioural
|
|
134
|
+
change, no DB schema change, no event payload shape change. Persisted
|
|
135
|
+
scans created with the old id regenerate cleanly on the next
|
|
136
|
+
`sm scan`.
|
|
137
|
+
|
|
138
|
+
## User-facing
|
|
139
|
+
|
|
140
|
+
Renamed `core/field-unknown` to `core/annotation-field-unknown` so the
|
|
141
|
+
sidecar typo-guard rule groups with the other `core/annotation-*`
|
|
142
|
+
rules. Update references in `settings.json` or
|
|
143
|
+
`sm check --analyzers <id>` to the new name.
|
|
144
|
+
|
|
145
|
+
- 880fe3e: Rename 14 built-in extension ids to a consistent `<domain>-<detail>` pattern. The naming was inconsistent: 10 ids already followed the "area first, attribute after" shape (e.g. `annotation-orphan`, `link-conflict`) while 14 were inverted, redundant, or vague. All built-ins now agree.
|
|
146
|
+
|
|
147
|
+
Full rename map (`old qualified id` → `new qualified id`):
|
|
148
|
+
|
|
149
|
+
| Kind | Old | New |
|
|
150
|
+
| --------- | --------------------------------- | -------------------------- |
|
|
151
|
+
| action | `core/bump` | `core/node-bump` |
|
|
152
|
+
| action | `core/mark-superseded` | `core/node-supersede` |
|
|
153
|
+
| extractor | `core/tools-count` | `core/tools-counter` |
|
|
154
|
+
| extractor | `claude/slash` | `claude/slash-command` |
|
|
155
|
+
| analyzer | `core/broken-ref` | `core/reference-broken` |
|
|
156
|
+
| analyzer | `core/job-orphan-file` | `core/job-file-orphan` |
|
|
157
|
+
| analyzer | `core/link-counts` | `core/link-counter` |
|
|
158
|
+
| analyzer | `core/redundant-target-reference` | `core/reference-redundant` |
|
|
159
|
+
| analyzer | `core/reserved-name` | `core/name-reserved` |
|
|
160
|
+
| analyzer | `core/self-loop` | `core/link-self-loop` |
|
|
161
|
+
| analyzer | `core/stability` | `core/node-stability` |
|
|
162
|
+
| analyzer | `core/superseded` | `core/node-superseded` |
|
|
163
|
+
| analyzer | `core/unknown-field` | `core/field-unknown` |
|
|
164
|
+
| analyzer | `core/validate-all` | `core/schema-violation` |
|
|
165
|
+
|
|
166
|
+
The convention is now documented in `spec/plugin-author-guide.md` §Extension id shape. Counter-style extensions standardise on the `-counter` suffix (`link-counter`, `tools-counter`, `external-url-counter`).
|
|
167
|
+
|
|
168
|
+
CLI verb `sm bump` is **unchanged** (it remains the user-facing verb; the internal action id is what flipped to `core/node-bump`). The `BumpReport` JSON schema title also stays as `BumpReport`, the wire shape is unchanged.
|
|
169
|
+
|
|
170
|
+
Pre-1.0 minor per `spec/versioning.md`: breaking rename of public qualified ids referenced from `settings.json`, `--analyzers <id>` flags, `core/<id>` strings in plugin manifests, and the `analyzerId` filter on `GET /api/issues`. No behavioural change, no DB schema change, no event payload shape change. Persisted scans created with the old ids regenerate cleanly on the next `sm scan`.
|
|
171
|
+
|
|
172
|
+
## User-facing
|
|
173
|
+
|
|
174
|
+
Renamed 14 built-in extension ids to a `<area>-<detail>` shape (e.g. `core/broken-ref` is now `core/reference-broken`). If you reference these by qualified id in `settings.json` or via `sm check --analyzers <id>`, update to the new names.
|
|
175
|
+
|
|
176
|
+
- 1b6e368: Honour per-extension toggles inside bundle-granularity plugins end-to-end. Closes the Phase 4b follow-up (commit `e45d2fd`) gap: BFF + Settings UI started accepting per-extension toggles for any granularity, but three call sites still treated bundle granularity as "one knob, every extension follows", so flipping an individual extension off (e.g. `claude/at-directive`) persisted to `config_plugins` and then did nothing on the next scan.
|
|
177
|
+
|
|
178
|
+
**Runtime (`src/`)**
|
|
179
|
+
|
|
180
|
+
- `core/runtime/plugin-runtime/resolver.ts`: `isBundleEntryEnabled` (built-ins) and `isPluginExtensionEnabled` (drop-ins) now compose the bundle id as a coarse kill-switch with the per-extension override layered on top. When the bundle row resolves to `false` every extension stays disabled regardless of per-extension overrides; when the bundle row resolves to `true` each extension respects its qualified-id override (default `true`). Doc rewritten, the stale "silently ignored, the granularity says this bundle is one knob" wording was the symptom that pointed at the bug.
|
|
181
|
+
- `cli/commands/plugins/shared.ts`: `extensionRowFromBuiltIn` (the row builder behind `sm plugins list / show / doctor`) now reports the same composed effective state instead of mirroring the bundle's `enabled` field verbatim.
|
|
182
|
+
- `core/runtime/__tests__/plugin-runtime-branches.spec.ts`: two new composer cases lock the contract, `(e)` `claude` enabled + `claude/at-directive=false` drops only the extractor, and `(f)` `claude=false` overrides any per-extension `true` override.
|
|
183
|
+
|
|
184
|
+
**UI (`ui/`)**
|
|
185
|
+
|
|
186
|
+
- `app/components/settings-modal/settings-plugins.utils.ts`: `buildStateFromPlugins` now seeds extension keys for both granularities (was: bundle-only seeded the bundle id and skipped the extensions, so the per-extension toggles in the Phase 4b modal defaulted to OFF in the buffer regardless of what the wire shape said about `ext.enabled`, then reverted to OFF on every apply round-trip).
|
|
187
|
+
- `models/api.ts`: `IPluginItemApi.extensions` doc updated, the comment still said "only when granularity === 'extension'" which the BFF stopped honouring in commit `e45d2fd`.
|
|
188
|
+
- `__tests__/settings-plugins.utils.spec.ts`: five new cases cover bundle+extensions, extension-only, bundle-disabled-with-ext-enabled, and failure-row exclusion.
|
|
189
|
+
|
|
190
|
+
**Spec (`spec/`)**
|
|
191
|
+
|
|
192
|
+
- `cli-contract.md`: `GET /api/plugins` row shape doc rewritten, `extensions[]` is emitted for any granularity; the per-extension `enabled` reflects the **preference** axis (DB > settings > default true) and the runtime composition with the bundle row is documented explicitly. `PATCH /api/plugins/:bundleId/extensions/:extensionId` now accepts any granularity and returns 404 (not 400) on an unknown extension id. The 400 `bad-query` enumeration in the error-codes section narrowed to the conditions that still apply.
|
|
193
|
+
- `plugin-author-guide.md` § Resolution order: rewritten to describe bundle-as-kill-switch + per-extension refinement explicitly, including the deliberate asymmetry between the CLI surface (`sm plugins enable/disable <bare-id>` stays coarse) and the UI / direct config-edit surface (qualified ids accepted, refine inside a bundle).
|
|
194
|
+
- `index.json` regenerated.
|
|
195
|
+
|
|
196
|
+
## User-facing
|
|
197
|
+
|
|
198
|
+
In Settings, expanding a bundle plugin (claude, antigravity, openai, agent-skills) now shows the correct per-extension state and the toggles persist, the next scan honours them. `sm plugins list` reflects effective state too.
|
|
199
|
+
|
|
3
200
|
## 0.35.0
|
|
4
201
|
|
|
5
202
|
### Minor Changes
|
package/architecture.md
CHANGED
|
@@ -128,9 +128,9 @@ The loader enforces two id-uniqueness analyzers during discovery (see [`plugin-a
|
|
|
128
128
|
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.
|
|
129
129
|
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.
|
|
130
130
|
|
|
131
|
-
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`, `core/broken
|
|
131
|
+
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 bundles 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.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
Every extension (built-in or drop-in) is independently toggle-able by its qualified id `<bundle>/<ext-id>`. The bundle is a presentational grouping, not a toggle target. The loader's pre-import `resolveEnabled(pluginId)` short-circuit only fires when EVERY extension of the bundle is disabled (the bundle is "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` bundle exercises the per-extension axis explicitly (every kernel built-in is removable, satisfying §Boot invariant: "no extension is privileged"); vendor Provider bundles (`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.
|
|
134
134
|
|
|
135
135
|
### `RunnerPort`
|
|
136
136
|
|
|
@@ -243,7 +243,7 @@ Extensions (Provider / Extractor / Analyzer / Action / Formatter / Hook) are **p
|
|
|
243
243
|
The materialisation of any kernel-managed artefact (the SQLite DB at `.skill-map/skill-map.db`, the `.sm` sidecars next to source files, the job ledger at `.skill-map/jobs/`, the `scan_extractor_runs` cache, the enrichment overlay rows) is the **kernel's** responsibility, gated through the relevant Port:
|
|
244
244
|
|
|
245
245
|
- Extractors persist via `ctx.emitLink` / `ctx.enrichNode` / `ctx.store`, never by writing files. `ctx.store` is plugin-scoped persistence routed through `StoragePort`; it cannot reach the project filesystem.
|
|
246
|
-
- Actions return either a deterministic report (JSON), a rendered prompt (probabilistic), or, for the small subset of actions that legitimately mutate persisted state, an explicit `TActionWrite` discriminated union the kernel interprets. The built-in `core/bump` is the only action that returns `{ kind: 'sidecar' }` today; the kernel routes that write through `SidecarStore.applyPatch`, which is the single gated chokepoint for all `.sm` writes (see §Annotation system · Write consent).
|
|
246
|
+
- Actions return either a deterministic report (JSON), a rendered prompt (probabilistic), or, for the small subset of actions that legitimately mutate persisted state, an explicit `TActionWrite` discriminated union the kernel interprets. The built-in `core/node-bump` is the only action that returns `{ kind: 'sidecar' }` today; the kernel routes that write through `SidecarStore.applyPatch`, which is the single gated chokepoint for all `.sm` writes (see §Annotation system · Write consent).
|
|
247
247
|
- Providers, Analyzers, Formatters, Hooks have no write surface at all.
|
|
248
248
|
|
|
249
249
|
This invariant is what makes the consent gate at the kernel boundary sufficient: no extension can bypass it because no extension has the means to write in the first place. Conformance: a third-party extension that imports `node:fs` write APIs (or equivalent in another language) is non-conforming.
|
|
@@ -280,7 +280,7 @@ The kernel ships every Provider's `ui` block to the BFF at boot; the BFF aggrega
|
|
|
280
280
|
The dispatch contract has two consequences implementations MUST honour:
|
|
281
281
|
|
|
282
282
|
1. **First-claim-wins**. A vendor Provider that classifies a file inside its territory (e.g. claude's `.claude/agents/foo.md` → `agent`) is authoritative; later Providers cannot reclassify it to a different kind. This locks vendor ownership of vendor paths and removes the historical `provider-ambiguous` failure mode for non-overlapping territories.
|
|
283
|
-
2. **`core/markdown` is the universal fallback for unclaimed `.md` files**. The built-in `core/markdown` Provider's `classify` returns `'markdown'` unconditionally (it does NOT inspect the path). Combined with the dedup guarantee above and its terminal position in the iteration order, it picks up exactly the `.md` files no vendor Provider claimed, a `.md` at the project root, under `.claude/hooks/`, `notes/`, `CLAUDE.md`, `GEMINI.md`, or anywhere else outside a known vendor territory. The fallback is **not privileged kernel code**: it ships as a regular built-in Provider under the `core` bundle
|
|
283
|
+
2. **`core/markdown` is the universal fallback for unclaimed `.md` files**. The built-in `core/markdown` Provider's `classify` returns `'markdown'` unconditionally (it does NOT inspect the path). Combined with the dedup guarantee above and its terminal position in the iteration order, it picks up exactly the `.md` files no vendor Provider claimed, a `.md` at the project root, under `.claude/hooks/`, `notes/`, `CLAUDE.md`, `GEMINI.md`, or anywhere else outside a known vendor territory. The fallback is **not privileged kernel code**: it ships as a regular built-in Provider under the `core` bundle, so a user who explicitly does not want it can disable it via `sm plugins disable core/markdown` and the scan reverts to "vendor-only", orphan `.md` files become silently invisible, matching pre-spec-0.9.0 behaviour.
|
|
284
284
|
|
|
285
285
|
The fallback exists because the format-named generic kind `markdown` is provider-agnostic: no vendor owns the universal markdown format. Keeping the fallback as a Provider (rather than a kernel-level special case) preserves the boot invariant that no extension is privileged, when a future vendor Provider (Codex, Cursor, Roo) lands, it slots into the iteration order before `core/markdown` and the fallback semantics stay invariant.
|
|
286
286
|
|
|
@@ -308,11 +308,11 @@ The transform runs after `dedupeLinks` and before the analyzer pipeline. For eac
|
|
|
308
308
|
|
|
309
309
|
1. **Path match (universal)**: if `link.target` equals some node's `path`, confidence is bumped to `1.0`. Applies to every link.kind, ignores the `resolution` map. Drives resolved markdown / at-directive references and `core/mcp-tools` synthetic edges.
|
|
310
310
|
|
|
311
|
-
2. **Name match (links carrying a `trigger.normalizedTrigger`)**: strip the leading `@` / `/` sigil, look up the resulting handle in the cross-kind name index built from every node's declared `identifiers` (see §Provider · kind identifiers). The lookup keys on the
|
|
311
|
+
2. **Name match (links carrying a `trigger.normalizedTrigger`)**: strip the leading `@` / `/` sigil, look up the resulting handle in the cross-kind name index built from every node's declared `identifiers` (see §Provider · kind identifiers). The lookup keys on the ACTIVE PROVIDER LENS: `resolution = providers[activeProvider].resolution`. If `resolution[link.kind]` exists AND any candidate node's kind appears in it, confidence is bumped to `1.0`. Otherwise the link stays at its extractor-emitted confidence.
|
|
312
312
|
|
|
313
|
-
The matrix is **per-link-kind, per-Provider**, strict: a `claude` Provider that declares `resolution: { mentions: ['agent'], invokes: ['command', 'skill'] }` does NOT bump a `/foo` slash matching an agent named `foo` (slash → agent is a kind mismatch surfaced by `link-conflict` / `kind-mismatch` analyzers, not silently treated as a resolution). The strictness is the load-bearing difference from the kind-agnostic `core/broken
|
|
313
|
+
The matrix is **per-link-kind, per-Provider**, strict: a `claude` Provider that declares `resolution: { mentions: ['agent'], invokes: ['command', 'skill'] }` does NOT bump a `/foo` slash matching an agent named `foo` (slash → agent is a kind mismatch surfaced by `link-conflict` / `kind-mismatch` analyzers, not silently treated as a resolution). The strictness is the load-bearing difference from the kind-agnostic `core/reference-broken` analyzer: `broken-ref`'s scope is "the name exists somewhere" (a name-only resolution is enough to clear the broken flag), the post-walk bump is "the name exists AS A VALID resolution for this link.kind". The `not-broken` + `not-bumped` combination is a documented edge case: the trigger resolves to a real node but the link's kind cannot legitimately point there.
|
|
314
314
|
|
|
315
|
-
The lookup uses the
|
|
315
|
+
The lookup uses the ACTIVE PROVIDER LENS deliberately, mirroring the extractor gate (§Universal extractors and per-provider extractors). The lens represents the runtime grammar the operator is authoring under, and that grammar applies across the project's surface, not only to files the matching `classify()` claimed. A `@handle` in `notes/todo.md` (classified by `core/markdown`) under the `claude` lens still parses as a claude mention (the extractor gate authorises it) and resolves against claude's `resolution.mentions` (the resolver gate now mirrors the same authority). The same body under the `openai` lens follows openai's resolution map, or short-circuits if openai declares no entry for that `link.kind`. When `activeProvider === null` (unlensed project: no setting, no filesystem signal), the name path short-circuits uniformly; the path-match rule still applies.
|
|
316
316
|
|
|
317
317
|
**Distinct from the Signal IR `resolverRules` (§Resolver phase).** `resolverRules` rank candidates INSIDE a Signal (Phase 3+, no Provider declares it today); `resolution` runs against the merged Link graph post-walk and is the contract Extractors EMITTING Links rely on. The two surfaces share no mechanism and intentionally do not compose; when a Signal IR materialises into a Link, the `resolution` matrix runs unchanged against the resulting Link.
|
|
318
318
|
|
|
@@ -322,7 +322,7 @@ Each Provider MAY declare an optional `reservedNames: Record<kind, string[]>` ma
|
|
|
322
322
|
|
|
323
323
|
The kernel intersects each Provider's `reservedNames[kind]` catalog with the scanned graph at orchestrator time: for every node, the post-walk pipeline derives its normalised identifiers via the §Provider · kind identifiers contract and asks "does any identifier fall in `reservedNames[node.kind]` for this node's Provider?". Matches land in a per-scan `Set<nodePath>` consumed by two surfaces:
|
|
324
324
|
|
|
325
|
-
1. **The `core/reserved
|
|
325
|
+
1. **The `core/name-reserved` analyzer projects one `warn` issue per reserved-shadow node** (`severity: 'warn'`, message points at the offending file and suggests renaming). The analyzer is a pure projector, detection lives in the orchestrator so the same set drives the next surface.
|
|
326
326
|
|
|
327
327
|
2. **The post-walk confidence-lift transform downgrades any link that resolves to a reserved target** (by path OR by name match) to `RESERVED_TARGET_CONFIDENCE = 0.1` instead of bumping it to `1.0`. The visual weight in the graph drops well below the `0.5` / `0.8` extractor emit floors so the operator sees at a glance that the edge resolves to a file the runtime ignores. When the trigger has multiple candidates (name index collision) and the strict-kind filter accepts more than one, the resolver picks the first allowed candidate, if it is non-reserved, the link bumps to `1.0` normally; only when EVERY accepted candidate is reserved does the downgrade apply.
|
|
328
328
|
|
|
@@ -440,7 +440,7 @@ The two surfaces are deliberately split:
|
|
|
440
440
|
- **`Action.precondition`**, declared on the Action side. Answers "which nodes does this Action apply to?". Evaluated continuously against the node the inspector is focused on, regardless of any issue.
|
|
441
441
|
- **`Analyzer.recommendedActions`**, declared on the Analyzer side. Answers "when this analyzer fires, which Actions are the natural fix?". Surfaces only on nodes the analyzer emitted against.
|
|
442
442
|
|
|
443
|
-
Each `recommendedActions` entry MUST be the qualified id of a registered Action. The kernel logs an `extension.error` event with `kind: 'recommended-action-missing'` when a referenced action is not loaded; the analyzer stays registered and continues emitting issues, only the recommendation hint is dropped. Project-level cleanup verbs (orphan file prune, contribution relink) are CLI commands, not Actions, and are NOT linked through this field. Analyzers whose issues surface deliberate user declarations rather than fixable problems (e.g. `core/superseded`) omit the field.
|
|
443
|
+
Each `recommendedActions` entry MUST be the qualified id of a registered Action. The kernel logs an `extension.error` event with `kind: 'recommended-action-missing'` when a referenced action is not loaded; the analyzer stays registered and continues emitting issues, only the recommendation hint is dropped. Project-level cleanup verbs (orphan file prune, contribution relink) are CLI commands, not Actions, and are NOT linked through this field. Analyzers whose issues surface deliberate user declarations rather than fixable problems (e.g. `core/node-superseded`) omit the field.
|
|
444
444
|
|
|
445
445
|
### Hook · curated trigger set
|
|
446
446
|
|
|
@@ -484,7 +484,7 @@ Hooks introduce no new persisted state and do NOT participate in the determinist
|
|
|
484
484
|
### Locality
|
|
485
485
|
|
|
486
486
|
- **Drop-in**: extensions live inside plugins, discovered at boot from `<cwd>/.skill-map/plugins/<id>/` only. The `--plugin-dir <path>` escape hatch on the `sm plugins …` verb family loads a custom directory per invocation when the user explicitly opts in.
|
|
487
|
-
- **Built-in**: the reference impl bundles a default extension set (one Provider, four extractors, five analyzers, one formatter, one hook). The fifth analyzer, `core/
|
|
487
|
+
- **Built-in**: the reference impl bundles a default extension set (one Provider, four extractors, five analyzers, one formatter, one hook). The fifth analyzer, `core/schema-violation`, replays every scanned node and link through the authoritative spec schemas via AJV, the kernel-side guard against persisting non-conforming graph rows. The first built-in Hook is `core/update-check`, which subscribes to `shutdown` and runs the once-per-day "update available" probe + banner that lived on the CLI entry path before the Hook kind had concrete consumers. These are loaded from `src/extensions/` and are indistinguishable from plugin-supplied extensions from the kernel's point of view.
|
|
488
488
|
|
|
489
489
|
---
|
|
490
490
|
|
|
@@ -603,7 +603,7 @@ At scan time the kernel re-computes the live hashes and compares against the sto
|
|
|
603
603
|
|
|
604
604
|
### Bump model
|
|
605
605
|
|
|
606
|
-
The deterministic built-in `core/bump` Action produces a sidecar patch:
|
|
606
|
+
The deterministic built-in `core/node-bump` Action produces a sidecar patch:
|
|
607
607
|
|
|
608
608
|
- 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").
|
|
609
609
|
- Refreshes `identity.bodyHash` and `identity.frontmatterHash` to the live values.
|
|
@@ -760,7 +760,7 @@ ctx.emitContribution(nodePath, contributionId, payload);
|
|
|
760
760
|
|
|
761
761
|
Parallel to `ctx.emitLink(link)`. The kernel buffers the emission, validates the payload against the slot's payload schema in `$defs/payloads/<slot>` (AJV-compiled at boot), and persists the row to `scan_contributions` during `persistScanResult`. Off-shape payloads emit an `extension.error` event and drop silently, same posture as `emitLink` rejecting off-`emitsLinkKinds` links. Both Extractor and Analyzer emissions land in the same `scan_contributions` rows; the row's `extension_id` records which kind of extension produced it.
|
|
762
762
|
|
|
763
|
-
The Extractor-emit signature binds `nodePath` implicitly (the extractor runs per-node, with `ctx.node.path` available as the only sensible target). The Analyzer-emit signature requires the analyzer to declare the target node explicitly because Analyzers see the full graph at once and may emit for any subset of nodes, the canonical use case is a analyzer that derives per-node values from cross-graph aggregations (`core/link-
|
|
763
|
+
The Extractor-emit signature binds `nodePath` implicitly (the extractor runs per-node, with `ctx.node.path` available as the only sensible target). The Analyzer-emit signature requires the analyzer to declare the target node explicitly because Analyzers see the full graph at once and may emit for any subset of nodes, the canonical use case is a analyzer that derives per-node values from cross-graph aggregations (`core/link-counter` projects `linksOutCount` / `linksInCount` this way).
|
|
764
764
|
|
|
765
765
|
Analyzers MAY also emit scope-level contributions via `IAnalyzerContext.emitScopeContribution(contributionId, payload)` (only slots whose schema permits scope-level emission, today only `topbar.nav.start`). That signature is reserved in the spec; the runtime callback lands when the first scope-level adopter arrives.
|
|
766
766
|
|
|
@@ -831,7 +831,7 @@ Same honest-note posture as [`plugin-kv-api.md`](./plugin-kv-api.md): isolated a
|
|
|
831
831
|
|
|
832
832
|
Two built-ins ship with the system to cover catalog evolution and rename edge cases:
|
|
833
833
|
|
|
834
|
-
- **`core/unknown-slot`**, walks every loaded plugin's `ui[*].slot`; emits an `Issue` of severity `warn` for any slot not in the current kernel catalog. Parallel to `core/
|
|
834
|
+
- **`core/unknown-slot`**, walks every loaded plugin's `ui[*].slot`; emits an `Issue` of severity `warn` for any slot not in the current kernel catalog. Parallel to `core/annotation-field-unknown` for annotations. Note: AJV at manifest load already rejects unknown slots as `invalid-manifest`; this analyzer covers the soft-warning path when a plugin remains loaded across a catalog version bump.
|
|
835
835
|
- **`core/contribution-orphan`**, joins `scan_contributions` against the live `scan_nodes` set; emits an `Issue` of severity `warn` for emissions whose `node_path` no longer exists (post-rename heuristic miss).
|
|
836
836
|
|
|
837
837
|
### Catalog versioning
|
package/cli-contract.md
CHANGED
|
@@ -279,7 +279,7 @@ The three privacy-sensitive keys above PLUS `allowEditSmFiles` are members of `P
|
|
|
279
279
|
- `sm scan [roots...]`: when positional roots are given, they ARE the effective roots (verbatim). When omitted: the effective roots are `[cwd]` plus the appended set below.
|
|
280
280
|
- `scan.extraFolders[]` (project-local config) is appended verbatim (entries starting with `~` resolve against the user home; relative entries resolve against the project root). This is the ONLY way to extend the scan beyond the project: there is no implicit `$HOME` walk, no opt-in global scope, and Providers cannot opt their own directory in. See §Scope is always project-local at the top of this file for the broader principle.
|
|
281
281
|
|
|
282
|
-
**Reference paths** (`scan.referencePaths[]`): walked in parallel by the scan to collect existing absolute paths into a side set. Files there are NOT parsed and NOT indexed as nodes; the kernel passes the set to analyzers via `IAnalyzerContext.referenceablePaths` so `core/broken
|
|
282
|
+
**Reference paths** (`scan.referencePaths[]`): walked in parallel by the scan to collect existing absolute paths into a side set. Files there are NOT parsed and NOT indexed as nodes; the kernel passes the set to analyzers via `IAnalyzerContext.referenceablePaths` so `core/reference-broken` can resolve a link against the filesystem when the in-graph lookup misses.
|
|
283
283
|
|
|
284
284
|
The watcher subscribes to the same roots that `sm scan` walks and respects `.skillmapignore` plus `config.ignore` exactly as the one-shot scan does. Filesystem events are grouped using `scan.watch.debounceMs` (default 300ms) before the watcher re-runs the incremental scan and persists. `SIGINT` / `SIGTERM` close the watcher cleanly. Exit code on clean shutdown is 0.
|
|
285
285
|
|
|
@@ -314,11 +314,11 @@ Actions are not invoked via `sm actions`; invocation is via `sm job submit` (see
|
|
|
314
314
|
|
|
315
315
|
#### Sidecar bump (Step 9.6.4)
|
|
316
316
|
|
|
317
|
-
The built-in deterministic `core/bump` Action is the canonical write channel for `<basename>.sm` annotation sidecars; the verbs below are its CLI surface plus a few sidecar-management helpers. The `bump` verb stays top-level (high frequency, ROADMAP-named); the administrative helpers live under the `sm sidecar` sub-namespace to avoid colliding with the existing `sm refresh` (which targets the enrichment layer, not sidecars). All sidecar-touching verbs are deterministic, they invoke `core/bump` (or the `FilesystemSidecarStore` directly) in-process and never queue jobs.
|
|
317
|
+
The built-in deterministic `core/node-bump` Action is the canonical write channel for `<basename>.sm` annotation sidecars; the verbs below are its CLI surface plus a few sidecar-management helpers. The `bump` verb stays top-level (high frequency, ROADMAP-named); the administrative helpers live under the `sm sidecar` sub-namespace to avoid colliding with the existing `sm refresh` (which targets the enrichment layer, not sidecars). All sidecar-touching verbs are deterministic, they invoke `core/node-bump` (or the `FilesystemSidecarStore` directly) in-process and never queue jobs.
|
|
318
318
|
|
|
319
319
|
| Command | Purpose |
|
|
320
320
|
|---|---|
|
|
321
|
-
| `sm bump <node.path> [--force] [--yes]` | Single-node bump. Wraps `core/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. |
|
|
321
|
+
| `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. |
|
|
322
322
|
| `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. |
|
|
323
323
|
| `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. |
|
|
324
324
|
| `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`. |
|
|
@@ -373,7 +373,7 @@ Tracked as **R6** in the §Step 9.6 review queue: open by design, defer the `js-
|
|
|
373
373
|
|
|
374
374
|
##### BFF endpoint, `POST /api/sidecar/bump` (Step 9.6.5, BFF half)
|
|
375
375
|
|
|
376
|
-
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/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.
|
|
376
|
+
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.
|
|
377
377
|
|
|
378
378
|
| Field | Value |
|
|
379
379
|
|---|---|
|
|
@@ -567,10 +567,10 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
|
|
|
567
567
|
| `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. |
|
|
568
568
|
| `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`. |
|
|
569
569
|
| `GET /api/config` | implemented | `RestEnvelope` (`kind: 'config'`), merged effective config (defaults → user → user-local → project → project-local → override). |
|
|
570
|
-
| `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',
|
|
571
|
-
| `PATCH /api/plugins/:id` | implemented |
|
|
572
|
-
| `PATCH /api/plugins/:bundleId/extensions/:extensionId` | implemented |
|
|
573
|
-
| `PATCH /api/plugins` | implemented | Bulk toggle. Body `{ "changes": Array<{ "id": string, "enabled": boolean }> }` where each `id` is either a bare bundle id or a qualified `<bundle>/<extension>` id
|
|
570
|
+
| `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 bundle itself has no toggle axis; the row's `status` aggregates the children (`'enabled'` when at least one extension is enabled, `'disabled'` otherwise). The `description` field on the bundle item carries the manifest-declared description (built-ins: hardcoded on `IBuiltInBundle`; 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 bundle 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 bundle'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. |
|
|
571
|
+
| `PATCH /api/plugins/:id` | implemented | **Bundle macro endpoint**: fans the toggle out across every extension inside the bundle. `:id` MUST be a top-level bundle id (no slash); qualified-id form is the sibling route below. Body `{ enabled: boolean }` (JSON). Persists one `config_plugins` row per child extension (`<bundle>/<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 bundle 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. |
|
|
572
|
+
| `PATCH /api/plugins/:bundleId/extensions/:extensionId` | implemented | Canonical per-extension toggle. Body `{ enabled: boolean }`. Both segments are URL-path-segment-encoded (no slash inside `:bundleId` or `:extensionId`). 404 `not-found` when the bundle id is unknown or the extension id does not belong to that bundle. **Lock**, rejected with 403 `locked` when either the bundle id or the qualified `bundleId/extensionId` appears in the host lock-list. Same persistence + apply-window semantics as the bundle macro form (including the `startsAsDisabled` exception). The SPA's buffered Settings modal posts here for every per-row flip. |
|
|
573
|
+
| `PATCH /api/plugins` | implemented | Bulk toggle. Body `{ "changes": Array<{ "id": string, "enabled": boolean }> }` where each `id` is either a bare bundle id (cascade macro, same semantics as `PATCH /api/plugins/:id`) or a qualified `<bundle>/<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 bundle 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. |
|
|
574
574
|
| `ALL /api/*` (other) | reserved | structured 404 envelope (see below); future endpoints land in subsequent sub-steps. |
|
|
575
575
|
| `GET /ws` | implemented (v14.4.a) | accepts WebSocket upgrade and registers the client with the BFF broadcaster. Server-push only, the server fans `scan.*` (and forthcoming `issue.*`) events to every connected client. See **WebSocket protocol** below. |
|
|
576
576
|
| `GET *` | implemented | static asset from the resolved UI bundle, falling back to `index.html` for SPA deep links. |
|
|
@@ -603,7 +603,7 @@ Error code sources at v14.2:
|
|
|
603
603
|
- `internal` (500), uncaught error during a request (e.g. config-load failure, DB corruption surfacing through `loadScanResult`).
|
|
604
604
|
- `db-missing` (500), emitted by mutation endpoints (`PATCH /api/plugins/:id`, `PATCH /api/plugins/:bundleId/extensions/:extensionId`, `PATCH /api/plugins`) when the project DB is absent. Read-side routes uniformly degrade to the empty shape (`/api/scan`) or zero items (list endpoints) so they do not emit this code; mutation endpoints cannot persist without a DB so they fail fast instead of silently dropping the write.
|
|
605
605
|
- `not-found` (404) on `PATCH /api/plugins/:id`, unknown plugin id (no built-in bundle, no discovered drop-in matches). The qualified-id form returns the same code when either segment misses. The bulk `PATCH /api/plugins` returns the same code with `error.details.id` set to the first offending id; the batch is rejected before any DB write.
|
|
606
|
-
- `bad-query` (400) on `PATCH /api/plugins/:id`,
|
|
606
|
+
- `bad-query` (400) on `PATCH /api/plugins/:id`, malformed body (missing `enabled`, wrong type), or `:id` contains a slash (the qualified-id sibling is `PATCH /api/plugins/:bundleId/extensions/:extensionId`). The qualified-id sibling returns 404 `not-found` for an unknown bundle or extension id. The bulk `PATCH /api/plugins` returns 400 for malformed `changes` array or missing/typeless `enabled`, with `error.details.id` set to the first offending entry's id.
|
|
607
607
|
- `locked` (403) on `PATCH /api/plugins/:id` and the qualified-id sibling, the target bundle id or qualified extension id is in the host lock-list (`src/server/locked-plugins.ts`). The list is hardcoded, host-only, and not user-editable; `GET /api/plugins` mirrors the same analyzer by stamping `locked: true` on the affected items. The bulk `PATCH /api/plugins` returns the same code with `error.details.id` set to the first locked entry; the batch is rejected before any DB write.
|
|
608
608
|
- `bad-query` (400) on `POST /api/scan`, the server was started with `--no-built-ins` or `--no-plugins` (partial pipeline would persist a misleading DB).
|
|
609
609
|
- `scan-busy` (409) on `POST /api/scan`, another scan (a watcher batch or another POST) is already in flight. Retry once the in-flight scan resolves; the WS `scan.completed` envelope is the unambiguous "now safe" signal.
|
package/conformance/coverage.md
CHANGED
|
@@ -44,7 +44,7 @@ This file is hand-maintained. A CI check before spec release compares the schema
|
|
|
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. |
|
|
45
45
|
| 34 | `conformance-result.schema.json` |, | 🔴 missing | Machine-readable output of `sm conformance run --json`. Self-referential by design (a conformance case would invoke the verb against itself); a direct case is deferred until the runner gains a meta-loopback mode. Implementation tests at `src/test/conformance-cli.test.ts` cover the envelope shape today. |
|
|
46
46
|
| 35 | `user-settings.schema.json` | (indirect via `no-global-scope`) | 🟡 partial | Per-user / per-machine settings file at `~/.skill-map/settings.json` (the narrow `$HOME` exception, see `cli-contract.md` §User-settings file). Direct case is not added because alt-impls MAY choose to not ship an update-check feature, requiring them to produce this file would over-prescribe. The implementation-side AJV round-trip is covered by `src/test/user-settings-store.test.ts` (15 cases: defaults, malformed JSON, schemaVersion mismatch, wrong-type fields, unknown top-level keys, deep-merge writes, off-shape rejection). The behavioral counterpart (no global / user scope) lives at `no-global-scope` in the non-schema table below. |
|
|
47
|
-
| 37 | `signal.schema.json` | `extractor-emits-signal`, `signal-collision-detection` | ✅ covered | Intermediate Representation (IR) emitted by extractors via `ctx.emitSignal()`; the kernel resolver phase consumes Signals and materialises Links. Opt-in: the existing `ctx.emitLink()` path coexists. Phase 2.A wired the resolver end-to-end (filter -> rank -> overlap -> materialise + annotate); Phase 2.B + 2.C migrated all six link-emitter extractors (`claude/at-directive`, `claude/slash`, `core/markdown-link`, `core/annotations`, `core/mcp-tools`, `core/external-url-counter`); Phase 2.D added the `core/signal-collision` analyzer + the two cases. The cases cover (a) `extractor-emits-signal`, a markdown body with one `[text](path)` link materialises one Link via the Signal IR path; (b) `signal-collision-detection`, a body with both `[label](./api.md)` AND `@./api.md` at overlapping byte ranges triggers a cross-extractor collision, the resolver materialises ONE Link (markdown-link wins on confidence) and the loser's `resolution.rejectedBy` reaches the `core/signal-collision` analyzer which emits a `warn` issue naming WHO won, WHO lost, and WHY. |
|
|
47
|
+
| 37 | `signal.schema.json` | `extractor-emits-signal`, `signal-collision-detection` | ✅ covered | Intermediate Representation (IR) emitted by extractors via `ctx.emitSignal()`; the kernel resolver phase consumes Signals and materialises Links. Opt-in: the existing `ctx.emitLink()` path coexists. Phase 2.A wired the resolver end-to-end (filter -> rank -> overlap -> materialise + annotate); Phase 2.B + 2.C migrated all six link-emitter extractors (`claude/at-directive`, `claude/slash-command`, `core/markdown-link`, `core/annotations`, `core/mcp-tools`, `core/external-url-counter`); Phase 2.D added the `core/signal-collision` analyzer + the two cases. The cases cover (a) `extractor-emits-signal`, a markdown body with one `[text](path)` link materialises one Link via the Signal IR path; (b) `signal-collision-detection`, a body with both `[label](./api.md)` AND `@./api.md` at overlapping byte ranges triggers a cross-extractor collision, the resolver materialises ONE Link (markdown-link wins on confidence) and the loser's `resolution.rejectedBy` reaches the `core/signal-collision` analyzer which emits a `warn` issue naming WHO won, WHO lost, and WHY. |
|
|
48
48
|
|
|
49
49
|
> **Note on Provider-owned schemas.** Per-kind frontmatter schemas (`skill`, `agent`, `command`, `note` for the built-in Claude Provider; other Providers MAY declare different kinds) live with the Provider that emits them, for the built-in Claude Provider, under `src/extensions/providers/claude/schemas/`. Those schemas are NOT counted in the spec's coverage matrix above; they belong to the Provider's own conformance suite at `src/extensions/providers/claude/conformance/coverage.md`. The same split applies to the cases that exercise Provider-specific kinds (`basic-scan`, `rename-high`, `orphan-detection`), they live in the Provider's `cases/` directory.
|
|
50
50
|
|
package/db-schema.md
CHANGED
|
@@ -173,7 +173,7 @@ The orchestrator consults this table on `sm scan --changed`: a node-level cache
|
|
|
173
173
|
|
|
174
174
|
Primary key: `(node_path, extractor_id)`. Indexes: `ix_scan_extractor_runs_node`, `ix_scan_extractor_runs_extractor`.
|
|
175
175
|
|
|
176
|
-
**Source-attribution interaction.** `scan_links.sources_json` carries the *short* extractor id the author wrote (e.g. `'slash'`); this table keys on the *qualified* form (`'core/slash'`). When a cached link is reshaped on reuse the orchestrator strips short ids whose owning Extractor is no longer registered (audit trail accuracy: a removed extractor must not stay attributed); links whose sole source is an uninstalled Extractor disappear; links whose sources include a missing-but-still-registered Extractor are dropped so the missing Extractor can re-emit fresh.
|
|
176
|
+
**Source-attribution interaction.** `scan_links.sources_json` carries the *short* extractor id the author wrote (e.g. `'slash'`); this table keys on the *qualified* form (`'core/slash-command'`). When a cached link is reshaped on reuse the orchestrator strips short ids whose owning Extractor is no longer registered (audit trail accuracy: a removed extractor must not stay attributed); links whose sole source is an uninstalled Extractor disappear; links whose sources include a missing-but-still-registered Extractor are dropped so the missing Extractor can re-emit fresh.
|
|
177
177
|
|
|
178
178
|
### `node_enrichments`
|
|
179
179
|
|
|
@@ -242,7 +242,7 @@ Cached nodes' rows survive untouched, they're neither orphaned (still in the liv
|
|
|
242
242
|
|
|
243
243
|
NOT analogous to `state_plugin_kvs` (which is plugin-managed). Belongs to the `scan_*` family, sweep semantics replace pure replace-all but the data is still scan-derived.
|
|
244
244
|
|
|
245
|
-
**Eager purge on disable.** `sm plugins disable <id>` calls `StoragePort.contributions.purgeByPlugin(pluginId, extensionId
|
|
245
|
+
**Eager purge on disable.** `sm plugins disable <id>` calls `StoragePort.contributions.purgeByPlugin(pluginId, extensionId)` immediately after persisting `config_plugins[<id>].enabled = false`. Every persisted toggle key is the qualified `<bundle>/<ext>` shape (the CLI's macro form and the BFF's cascade endpoint expand bare bundle ids before persistence), so the purge always receives both segments. The eager purge avoids the "I disabled the extension but its chips are still rendered in the UI until I re-scan" gap. Re-enabling (`sm plugins enable <id>`) does NOT restore the rows, the next scan re-emits them, same as a cold start. Contributions are scan-derived, so this is cheap; for plugin-managed state (`state_plugin_kvs`, dedicated tables) the opposite policy holds, see `plugin-kv-api.md` § "disable does not drop data".
|
|
246
246
|
|
|
247
247
|
### `scan_node_tags`
|
|
248
248
|
|
package/index.json
CHANGED
|
@@ -174,14 +174,14 @@
|
|
|
174
174
|
}
|
|
175
175
|
]
|
|
176
176
|
},
|
|
177
|
-
"specPackageVersion": "0.
|
|
177
|
+
"specPackageVersion": "0.37.0",
|
|
178
178
|
"integrity": {
|
|
179
179
|
"algorithm": "sha256",
|
|
180
180
|
"files": {
|
|
181
|
-
"CHANGELOG.md": "
|
|
181
|
+
"CHANGELOG.md": "bb9d5ba9168a2f72a1d0fbda347f5e5813a90f63f77a534b48952044858050f3",
|
|
182
182
|
"README.md": "1c4b0ea58c4324f301043e9f5c36976a382d0bd2bc405a2e4e18463b0c50d946",
|
|
183
|
-
"architecture.md": "
|
|
184
|
-
"cli-contract.md": "
|
|
183
|
+
"architecture.md": "b9fba6c60ecd32ad2440c804dc9d47b7211873add6dea73401e13dc26bfee277",
|
|
184
|
+
"cli-contract.md": "a9da7c2c74a6f5400b4e8941099dd1861d53905964bd470c0c39ba74b4495710",
|
|
185
185
|
"conformance/README.md": "6871dde25b5770ed945284c9e0f749e0768ec3f5ba4966bdb215985789e43887",
|
|
186
186
|
"conformance/cases/extractor-emits-signal.json": "34b4808c232d66a0eea0f5db7632a746681432b4f0995b6bf39e8d675538451c",
|
|
187
187
|
"conformance/cases/kernel-empty-boot.json": "2a5be9c93143d07a16d998df09dcc8fa4ea2d2f9a0bff6417573ed5a770352c1",
|
|
@@ -190,12 +190,12 @@
|
|
|
190
190
|
"conformance/cases/plugin-missing-ui-rejected.json": "59a571a2e80c2bac2050eacbe740f4f3f125849dd242954508f011304cc3e036",
|
|
191
191
|
"conformance/cases/sidecar-end-to-end.json": "dbb3640f95769a36b881855a261f918481edadea13a7eb0765c6090f2417a142",
|
|
192
192
|
"conformance/cases/signal-collision-detection.json": "38c6d553c6f82c1b624fb8a8e9b4fc72034fc47bc70f7f011b3b9136817e7388",
|
|
193
|
-
"conformance/coverage.md": "
|
|
193
|
+
"conformance/coverage.md": "106468a6d9a65e5fdefbf75c8de4abecf35d5c08f2b6c44423741c60b723baea",
|
|
194
194
|
"conformance/fixtures/orphan-markdown/.claude/agents/reviewer.md": "7f062731106f2d9811e4fffcf6ab44b8dfff4cfb16536a469514cc0664e832bf",
|
|
195
195
|
"conformance/fixtures/orphan-markdown/ARCHITECTURE.md": "ec903666440bae65da3796b1158c92cfcdce22e0e09c3b20bb690176881a6ac4",
|
|
196
196
|
"conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/kind.json": "6676a89bae5197e23cf50f1c11d596db558ac80f7334a7208fe57d8b92422251",
|
|
197
197
|
"conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/kinds/markdown/schema.json": "42795e7f1759fa25115a426edf5cd1b0c91b091b408aeee3f4f9fbc8f89f32bc",
|
|
198
|
-
"conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json": "
|
|
198
|
+
"conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json": "15164a6bc9e3ad21cefa532af3d4edff1b10cf6140d7f576332dc38800512e35",
|
|
199
199
|
"conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/providers/bad-provider/index.js": "6eac0555de4194c3266c2abef89e39d833159990e1822ae1d4895df67c31d18f",
|
|
200
200
|
"conformance/fixtures/plugin-missing-ui/notes/example.md": "55767f0aa1b6774546a99f28c58e7b732aa9cfa5dfce8d0326470f7f622f577e",
|
|
201
201
|
"conformance/fixtures/preamble-v1.txt": "1e0aeef224b64477bdc13a949c3ad402e68249caf499ecdba1302371677c068b",
|
|
@@ -208,22 +208,22 @@
|
|
|
208
208
|
"conformance/fixtures/signal-ir-collision/.claude/agents/architect.md": "acc46b5b2dff73d98a354e4d53b5041164595deae466a4e2ce41d7c5a72f28fb",
|
|
209
209
|
"conformance/fixtures/signal-ir-single-signal/source.md": "1eda417b4c6eed372b66870e385c8d8cd631372b77cab7e996bb711e22218f89",
|
|
210
210
|
"conformance/fixtures/signal-ir-single-signal/target.md": "527137f2b4f46c0034b0edc8932cf8613d2bf22ffaaf78f01085c82a3baaebe3",
|
|
211
|
-
"db-schema.md": "
|
|
211
|
+
"db-schema.md": "840ed078fa86e8793738748b5f651b068b93162c942db1f341b0d61f00d18893",
|
|
212
212
|
"interfaces/security-scanner.md": "e8049712b9cf7a07c786bf19f8f775f8ef9638f063f7fba5c7a8b1431b92f38e",
|
|
213
|
-
"job-events.md": "
|
|
213
|
+
"job-events.md": "9d5b35d4c451a7f8eef9915d85316d924ac52f1c026a316cdda5f1099d496854",
|
|
214
214
|
"job-lifecycle.md": "9c429121f98a07c8795f8979ed1abc5e5334e3f89db51585a8da55c527ef855b",
|
|
215
|
-
"plugin-author-guide.md": "
|
|
215
|
+
"plugin-author-guide.md": "f0039397cd19d1f0a02c4e0691f213514216188d001cf56f287e69f6631fd39e",
|
|
216
216
|
"plugin-kv-api.md": "1acc69ed82433a74e35ada61d63a6d7379fb61046ff83de1e0facbe884c64704",
|
|
217
217
|
"prompt-preamble.md": "9dd4f6d1bc6a425f8782fcee10cbe75909e8d64e28781fda56c2fae909b02f40",
|
|
218
218
|
"schemas/annotations.schema.json": "e39990d47f53e25a1b3a5587a5714486d0b819b8eeaac10d42783a675296aee1",
|
|
219
219
|
"schemas/api/rest-envelope.schema.json": "926c63af10574599da5583ed067788b4a72be427788f0aa11b61022fb9649461",
|
|
220
|
-
"schemas/bump-report.schema.json": "
|
|
220
|
+
"schemas/bump-report.schema.json": "2e6daff2435ef1a4af6c2c911fd3ce2a30ace40c8910dd6f675dd187fd40a99b",
|
|
221
221
|
"schemas/conformance-case.schema.json": "f6d4c9fb92e79cb516eeeb9d042223572a3bd5ff8e7871a0becce13916f20cf6",
|
|
222
222
|
"schemas/conformance-result.schema.json": "426998e4f5cb079778ca7d0233634667d4fbc5a7e399cc41211fabd768db8ee0",
|
|
223
223
|
"schemas/execution-record.schema.json": "0d61e33f2dc1aaa4cc7337b5eac4ea8b9034022ce24bae9156c3c9f33204c250",
|
|
224
224
|
"schemas/extensions/action.schema.json": "9f6c2427ce3f0d6fa329adf0f13129821116ab79a1d2a53f96464513ff044ebe",
|
|
225
225
|
"schemas/extensions/analyzer.schema.json": "f9bed3ba1305b2b64da277dccfbe760f7c058c4bb62a2d845af9c75787f159f6",
|
|
226
|
-
"schemas/extensions/base.schema.json": "
|
|
226
|
+
"schemas/extensions/base.schema.json": "679d256a317f5be7f80a80dd3ec0c9098ad7d2f7b20c99c70fc70d6a1eb738a6",
|
|
227
227
|
"schemas/extensions/extractor.schema.json": "5994088bf669321d2a7b8262c07cc94e05e5e2f49a235ae5389b7c66ecc1b2e1",
|
|
228
228
|
"schemas/extensions/formatter.schema.json": "d6d417df20260e5ddfe71f104b11a45873869706f86372c3c3c78c583e06e8d5",
|
|
229
229
|
"schemas/extensions/hook.schema.json": "76bf2c07f9e689b3fd1c67cbad4516a4df10604f07103759e82670e5213ddcdf",
|
|
@@ -234,11 +234,11 @@
|
|
|
234
234
|
"schemas/input-types.schema.json": "c713b768d0b0e3d0c764afb401189f7fb624a82b4e988b73aab015cf9c67c01f",
|
|
235
235
|
"schemas/issue.schema.json": "fa3344e75f1c3a5304291ca355bb973046552a68871ad6eb4edafca1cd9e1be8",
|
|
236
236
|
"schemas/job.schema.json": "e43e1761c99920beffe1de12ef8f32fe29f97838bd8686742b637c19c4dbb395",
|
|
237
|
-
"schemas/link.schema.json": "
|
|
237
|
+
"schemas/link.schema.json": "8a01925a9a7c00bbe41cdcbf8eea9269955570d41484da93d4193232a7aa9070",
|
|
238
238
|
"schemas/node.schema.json": "4d7c107ed9cd2f1b7cc4d716c547c06a00ed776bd6092d3979cac634cb5326a5",
|
|
239
|
-
"schemas/plugins-doctor.schema.json": "
|
|
240
|
-
"schemas/plugins-registry.schema.json": "
|
|
241
|
-
"schemas/project-config.schema.json": "
|
|
239
|
+
"schemas/plugins-doctor.schema.json": "bb03eedf5b462b661938a44fe48635705ec13e30a2381ba122c60412416b507d",
|
|
240
|
+
"schemas/plugins-registry.schema.json": "bca763ec61419e001a5c0a4b00a2da77996bfd8113be3fcf8aaa2fa70ff65fef",
|
|
241
|
+
"schemas/project-config.schema.json": "f2a38a82fe47c8a965e6b73208e9be996d3d3545fdcd92f8a136f1d1e5abd3fb",
|
|
242
242
|
"schemas/refresh-report.schema.json": "54519b8caf86ba84c182f9565be9b5084bc1631ae05e9217ee18f34c0039fff3",
|
|
243
243
|
"schemas/report-base-deterministic.schema.json": "9d318d0181d121097c906ef3af1c52d71c782740bd04cf23418d7627ce2c3ed5",
|
|
244
244
|
"schemas/report-base.schema.json": "a1021e9a59b4df9f99cd92454d797e88469766e7d49f52d231c4645ffdfdad8f",
|
package/job-events.md
CHANGED
package/package.json
CHANGED
package/plugin-author-guide.md
CHANGED
|
@@ -52,8 +52,7 @@ plumbing.
|
|
|
52
52
|
{
|
|
53
53
|
"id": "my-plugin",
|
|
54
54
|
"version": "1.0.0",
|
|
55
|
-
"specCompat": "^1.0.0"
|
|
56
|
-
"granularity": "bundle"
|
|
55
|
+
"specCompat": "^1.0.0"
|
|
57
56
|
}
|
|
58
57
|
```
|
|
59
58
|
|
|
@@ -115,19 +114,25 @@ Concrete examples for the reference impl's bundled extensions:
|
|
|
115
114
|
|---|---|---|
|
|
116
115
|
| Claude Provider | `claude` | `claude/claude` |
|
|
117
116
|
| Annotations extractor | `annotations` | `core/annotations` |
|
|
118
|
-
| Slash extractor | `slash` | `
|
|
119
|
-
| At-directive extractor | `at-directive` | `
|
|
117
|
+
| Slash-command extractor | `slash-command` | `claude/slash-command` |
|
|
118
|
+
| At-directive extractor | `at-directive` | `claude/at-directive` |
|
|
120
119
|
| Markdown-link extractor | `markdown-link` | `core/markdown-link` |
|
|
121
120
|
| External-URL counter | `external-url-counter` | `core/external-url-counter` |
|
|
122
|
-
|
|
|
121
|
+
| Reference-broken analyzer | `reference-broken` | `core/reference-broken` |
|
|
123
122
|
| Trigger-collision analyzer | `trigger-collision` | `core/trigger-collision` |
|
|
124
123
|
| ASCII formatter | `ascii` | `core/ascii` |
|
|
125
|
-
|
|
|
124
|
+
| Schema-violation analyzer | `schema-violation` | `core/schema-violation` |
|
|
126
125
|
|
|
127
126
|
Built-ins split between two namespaces:
|
|
128
127
|
|
|
129
|
-
- **`core/`**, kernel-internal primitives, platform-agnostic. Owns every built-in analyzer (including `
|
|
130
|
-
- **`claude/`**, the Claude Code Provider bundle: the Provider that classifies `.claude/{agents,commands,skills}` paths and parses their frontmatter. Other vendor bundles (`antigravity`, `openai`, `agent-skills`) follow the same shape, Provider only, since the syntax their nodes use is shared with Claude
|
|
128
|
+
- **`core/`**, kernel-internal primitives, platform-agnostic. Owns every built-in analyzer (including `schema-violation`), the ASCII formatter, and the cross-vendor extractors (`annotations`, `markdown-link`, `external-url-counter`) any Provider can rely on.
|
|
129
|
+
- **`claude/`**, the Claude Code Provider bundle: the Provider that classifies `.claude/{agents,commands,skills}` paths and parses their frontmatter, plus the Claude-flavoured extractors (`slash-command`, `at-directive`). Other vendor bundles (`antigravity`, `openai`, `agent-skills`) follow the same shape, Provider only, since the syntax their nodes use is shared with Claude.
|
|
130
|
+
|
|
131
|
+
### Extension id shape
|
|
132
|
+
|
|
133
|
+
The naming convention applied to every built-in extension id is **`<domain>-<detail>`** (general to specific). The leftmost segment names the entity the extension reasons about (`node`, `link`, `annotation`, `reference`, `name`, `field`, `tools`, ...), the rest narrows the specific behaviour or signal it produces. Examples: `annotation-orphan`, `link-counter`, `node-stability`, `name-reserved`, `reference-broken`.
|
|
134
|
+
|
|
135
|
+
Authors building their own plugins are not required to follow this pattern, but doing so makes `sm plugins list` self-grouping and the qualified ids predictable. Verb-style ids (e.g. `bump`, `mark-superseded`) are deliberately avoided on built-ins: even Actions live under their entity domain (`node-bump`, `node-supersede`) so the catalog reads as a structured list rather than a mix of nouns and imperatives.
|
|
131
136
|
|
|
132
137
|
For your own plugin, the `id` you declare in `plugin.json` is the namespace for every extension the plugin contains. If your manifest declares `id: "my-plugin"` and your extension file declares `id: "foo-extractor"`, the kernel registers it as `my-plugin/foo-extractor`. You do **not** write the qualifier yourself, the loader injects it.
|
|
133
138
|
|
|
@@ -146,41 +151,40 @@ The kernel guards against two foot-guns:
|
|
|
146
151
|
|
|
147
152
|
For built-ins, the reference impl's `src/plugins/<bundle>/plugin.json` provides the bundle's `id` and the codegen at `scripts/generate-built-ins.js` inlines the `pluginId` injection at build time (the resulting `src/plugins/built-ins.ts` is auto-generated and committed). Authors never hardcode `pluginId` on the extension export.
|
|
148
153
|
|
|
149
|
-
###
|
|
154
|
+
### Toggle model
|
|
150
155
|
|
|
151
|
-
Every
|
|
156
|
+
Every extension is independently toggle-able by its qualified id `<bundle>/<ext-id>` (e.g. `claude/at-directive`, `core/node-superseded`, `my-plugin/orphan-skill`). The **bundle is a presentational grouping**, not a toggle target, the user sees a row per bundle in `sm plugins list` and the Settings UI, with the bundle's extensions listed underneath, each with its own enabled / disabled state.
|
|
152
157
|
|
|
153
|
-
|
|
154
|
-
|---|---|---|
|
|
155
|
-
| `bundle` (default) | the bundle id alone (e.g. `my-plugin`, `claude`) | The plugin's extensions form a coherent product (e.g. a Provider and the extractors that decode its native syntax). The user wants one switch. **95% of plugins.** |
|
|
156
|
-
| `extension` | the qualified extension id (`<bundle>/<ext-id>`, e.g. `core/superseded`, `my-plugin/orphan-skill`) | The plugin ships several orthogonal capabilities a user might reasonably want piecemeal. **Built-in `core` is the canonical example**, the spec promises every kernel built-in is removable, so each one toggles independently. |
|
|
158
|
+
Two id shapes resolve at the toggle surface:
|
|
157
159
|
|
|
158
|
-
|
|
160
|
+
- **Qualified id** (`<bundle>/<ext-id>`): flips exactly that extension. No prompt.
|
|
161
|
+
- **Bare bundle id** (`claude`, `core`, `my-plugin`): the **macro form**. Fans the toggle out across every extension inside the bundle.
|
|
162
|
+
- Bundle with exactly one extension (`openai`, `antigravity`, `agent-skills`): applies the toggle directly. No prompt (1-1 mapping).
|
|
163
|
+
- Bundle with ≥2 extensions (`claude`, `core`, multi-extension user plugins): requires `--yes` OR an interactive TTY confirm. Pipe / CI contexts must pass `--yes`; without it the verb refuses and prints the list of affected extensions plus the re-run hint.
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
- **`core`**, `granularity: 'extension'`. `sm plugins disable core/superseded` flips just the supersession analyzer; every other core extension (every other analyzer, the ASCII formatter, the cross-vendor extractors) stays live.
|
|
165
|
+
`--all` is the cascade variant of the macro: it expands to every extension in every discovered bundle (built-ins + user plugins) and applies the same `--yes` / TTY-confirm gate as a multi-extension bundle id.
|
|
162
166
|
|
|
163
167
|
Per-verb behaviour:
|
|
164
168
|
|
|
165
|
-
| Command |
|
|
166
|
-
|
|
167
|
-
| `sm plugins enable claude` | OK, flips
|
|
168
|
-
| `sm plugins enable
|
|
169
|
-
| `sm plugins disable
|
|
170
|
-
| `sm plugins disable
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
| Command | Result |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `sm plugins enable claude/at-directive` | OK, flips just that extension. |
|
|
172
|
+
| `sm plugins enable openai` | OK, single-child bundle, flips `openai/openai`. No prompt. |
|
|
173
|
+
| `sm plugins disable claude` | Multi-child bundle; TTY: prompts `[y/N]`; non-TTY: refuses without `--yes`. |
|
|
174
|
+
| `sm plugins disable claude --yes` | OK, flips every extension under `claude`. |
|
|
175
|
+
| `sm plugins disable core` | Multi-child bundle; same gate as `claude` above. |
|
|
176
|
+
| `sm plugins disable core/node-superseded` | OK, flips just that analyzer. |
|
|
177
|
+
| `sm plugins disable --all` | Cascades through every bundle; requires `--yes` in non-TTY. |
|
|
173
178
|
|
|
174
|
-
|
|
179
|
+
Resolution order per id is the same as for plugin enabled-state: DB override (`config_plugins`) > settings.json (`#/plugins/<id>/enabled`) > installed default (`true`). `settings.json#/plugins` keys are arbitrary strings (no AJV pattern); persisted toggle keys are always qualified `<bundle>/<ext>` ids (the macro path expands at write time so the DB only ever stores per-extension rows).
|
|
175
180
|
|
|
176
|
-
Set
|
|
181
|
+
Set the manifest fields in your `plugin.json`; the folder layout supplies the extensions and the kernel discovers them automatically. There is no `granularity` field anymore (a manifest that declares it fails AJV with `additionalProperties`):
|
|
177
182
|
|
|
178
183
|
```jsonc
|
|
179
184
|
{
|
|
180
185
|
"id": "my-multi-tool",
|
|
181
186
|
"version": "1.0.0",
|
|
182
|
-
"specCompat": "^1.0.0"
|
|
183
|
-
"granularity": "extension"
|
|
187
|
+
"specCompat": "^1.0.0"
|
|
184
188
|
}
|
|
185
189
|
```
|
|
186
190
|
|
|
@@ -283,7 +287,6 @@ Optional fields:
|
|
|
283
287
|
|
|
284
288
|
| Field | Type | Notes |
|
|
285
289
|
|---|---|---|
|
|
286
|
-
| `granularity` | `'bundle' \| 'extension'` | Default `'extension'` (each extension toggleable by qualified id). Set to `'bundle'` when the plugin's extensions form a coherent unit a user would never want to toggle piecemeal. |
|
|
287
290
|
| `storage` | object | `{ "mode": "kv" }` or `{ "mode": "dedicated", "tables": [...], "migrations": [...] }`. Absent means the plugin does not persist state. |
|
|
288
291
|
| `author` | string | Free-form. |
|
|
289
292
|
| `license` | string | SPDX identifier. |
|
|
@@ -340,7 +343,7 @@ You can read `ctx.node.sidecar.*` freely, the kernel's per-`(node, extractor)` c
|
|
|
340
343
|
> **Pick a syntax that doesn't collide with built-ins.** The built-in `at-directive` and `slash` extractors claim the `@` and `/` prefixes with LLM-aligned semantics:
|
|
341
344
|
>
|
|
342
345
|
> - **`core/at-directive`**: bare handles (`@team-lead`) and namespaced agents (`@my-plugin/foo-extractor`, `@skill-map:explore`) emit `mentions` links; file-flavoured tokens (`@docs/api/v1.md`, `@./readme.md`, `@../parent.md`, `@/abs/path.md`) emit `references` links so the graph treats them as file pointers, not entity mentions, the same way Claude Code / Antigravity CLI / Cursor would resolve them. The kind dispatch keys on (a) an explicit relative / absolute path prefix or (b) a known file extension at the tail.
|
|
343
|
-
> - **`core/slash`**: bare commands (`/scan`, `/skill-map:explore`) emit `invokes`; tokens whose next character is another `/` or any other identifier char are dropped as path segments (`/Volumes/disk`, `/api/v1/items`).
|
|
346
|
+
> - **`core/slash-command`**: bare commands (`/scan`, `/skill-map:explore`) emit `invokes`; tokens whose next character is another `/` or any other identifier char are dropped as path segments (`/Volumes/disk`, `/api/v1/items`).
|
|
344
347
|
> - **Both extractors strip fenced code blocks and inline backticks before matching**, so author-marked literal payload never registers as invocation surface.
|
|
345
348
|
>
|
|
346
349
|
> A new extractor that also matches one of those prefixes will likely fire on the same input, and if the two emit different `target` shapes the kernel raises a `trigger-collision` error. The example below uses a wikilink-style `[[ref:<name>]]` pattern to side-step this; reserve `@` and `/` for the built-ins.
|
|
@@ -404,14 +407,14 @@ export default {
|
|
|
404
407
|
};
|
|
405
408
|
```
|
|
406
409
|
|
|
407
|
-
> **`recommendedActions`, analyzer-side hint, not a precondition.** An Analyzer MAY declare `recommendedActions: string[]` with the qualified ids (`<pluginId>/<id>`) of the per-node Actions that resolve its findings. The built-in `core/annotation-stale` analyzer declares `['core/bump']` because bumping the node refreshes the `for.*` hashes that drove the warning. The UI surfaces matching Actions in the node inspector under "Recommended for issues" alongside the always-applicable list driven by `Action.precondition`.
|
|
410
|
+
> **`recommendedActions`, analyzer-side hint, not a precondition.** An Analyzer MAY declare `recommendedActions: string[]` with the qualified ids (`<pluginId>/<id>`) of the per-node Actions that resolve its findings. The built-in `core/annotation-stale` analyzer declares `['core/node-bump']` because bumping the node refreshes the `for.*` hashes that drove the warning. The UI surfaces matching Actions in the node inspector under "Recommended for issues" alongside the always-applicable list driven by `Action.precondition`.
|
|
408
411
|
>
|
|
409
412
|
> The two surfaces are distinct:
|
|
410
413
|
>
|
|
411
414
|
> - **`Action.precondition`**, declared on the Action side, answers "which nodes does this Action apply to?". Always evaluated against the node the inspector is focused on.
|
|
412
415
|
> - **`Analyzer.recommendedActions`**, declared on the Analyzer side, answers "which Actions are the natural fix when THIS analyzer fires?". Surfaces only when the analyzer emitted an issue against the focused node.
|
|
413
416
|
>
|
|
414
|
-
> Each entry MUST be the qualified id of a registered Action. The kernel logs `recommended-action-missing` (an `extension.error` event) when a referenced action is not loaded, and the analyzer stays registered, only the recommendation hint is dropped. Project-level cleanup verbs (orphan file prune, contribution relink) are CLI commands, not Actions, and are NOT linked through this field. Omit `recommendedActions` when the issue is a deliberate user declaration with no "fix" (e.g. `core/superseded` surfaces user-authored supersession statements).
|
|
417
|
+
> Each entry MUST be the qualified id of a registered Action. The kernel logs `recommended-action-missing` (an `extension.error` event) when a referenced action is not loaded, and the analyzer stays registered, only the recommendation hint is dropped. Project-level cleanup verbs (orphan file prune, contribution relink) are CLI commands, not Actions, and are NOT linked through this field. Omit `recommendedActions` when the issue is a deliberate user declaration with no "fix" (e.g. `core/node-superseded` surfaces user-authored supersession statements).
|
|
415
418
|
|
|
416
419
|
### Formatters
|
|
417
420
|
|
|
@@ -869,15 +872,15 @@ Two plugins claiming the same `(key, location: 'root', ownership: 'exclusive')`
|
|
|
869
872
|
|
|
870
873
|
This is the only fatal path on the plugin-load surface. Every other failure mode (manifest invalid, schema invalid, dynamic-import failure, id collision) is per-plugin and the kernel keeps booting on the survivors.
|
|
871
874
|
|
|
872
|
-
### Tier-1 typo guard (`core/
|
|
875
|
+
### Tier-1 typo guard (`core/annotation-field-unknown`)
|
|
873
876
|
|
|
874
|
-
The built-in `core/
|
|
877
|
+
The built-in `core/annotation-field-unknown` Analyzer walks every parsed `.sm` and emits a `warn` issue per truly-unknown key. Three surfaces are checked:
|
|
875
878
|
|
|
876
879
|
1. Inside `annotations:`, keys not in `annotations.schema.json`'s curated catalog (the 10 conventional fields). Plugins do NOT contribute to `annotations:`; that block is skill-map-curated.
|
|
877
880
|
2. At the sidecar root, keys outside the four reserved blocks (`for`, `annotations`, `settings`, `audit`) that are also NOT a registered plugin namespace `<plugin-id>:` AND NOT a registered `location: 'root'` contribution.
|
|
878
881
|
3. Inside a registered `<plugin-id>:` namespace, values that fail the schema declared by the owning plugin's `annotationContributions[<key>].schema`.
|
|
879
882
|
|
|
880
|
-
The analyzer never blocks a scan; advisories surface through the standard issue channel (CLI, UI, REST). When you ship a contribution, the loader compiles your inline schema, the runtime catalog publishes it, and `core/
|
|
883
|
+
The analyzer never blocks a scan; advisories surface through the standard issue channel (CLI, UI, REST). When you ship a contribution, the loader compiles your inline schema, the runtime catalog publishes it, and `core/annotation-field-unknown` automatically validates user writes against your declaration.
|
|
881
884
|
|
|
882
885
|
### Runtime catalog accessor
|
|
883
886
|
|
|
@@ -986,6 +989,26 @@ The kernel ships exactly these 14 slots. Each slot fixes a renderer + a payload
|
|
|
986
989
|
|
|
987
990
|
Per-slot semantics, edge cases, and exact payload schemas live in [`view-slots.md`](./view-slots.md) (catalog reference) and [`schemas/view-slots.schema.json`](./schemas/view-slots.schema.json) at `$defs/payloads/<slot>`. Read those before emitting.
|
|
988
991
|
|
|
992
|
+
### Chip vs Issue, what counts and what only shows
|
|
993
|
+
|
|
994
|
+
For analyzers, the per-node card surfaces a finding through **two independent channels**:
|
|
995
|
+
|
|
996
|
+
- The `Issue` returned by `evaluate(ctx)` feeds the **aggregated stats** (`errorCount` / `warnCount` on the card) and the **scan / check exit code** (`severity: 'error'` → exit 1; `'warn'` / `'info'` → exit 0). `info` issues never appear in the card's visible issues list, only in `sm show` / `--json`.
|
|
997
|
+
- A view contribution to `card.footer.right` (a chip) is **purely presentational**: its `severity` controls only the chip's own colour, never the aggregated count, never the exit code.
|
|
998
|
+
|
|
999
|
+
The matrix:
|
|
1000
|
+
|
|
1001
|
+
| Goal | Issue? | Chip? | Issue severity | Chip severity |
|
|
1002
|
+
|---|---|---|---|---|
|
|
1003
|
+
| Surface a problem AND count it | yes | yes | `error` / `warn` | `danger` / `warn` (match) |
|
|
1004
|
+
| Show an attribute without counting | no (or `info`) | yes | `info` (or none) | `info` / `success` / none |
|
|
1005
|
+
| Count without a dedicated chip | yes | no | `error` / `warn` | — |
|
|
1006
|
+
| No surface | no | no | — | — |
|
|
1007
|
+
|
|
1008
|
+
**Colour rule.** A chip MAY paint `warn` (yellow) or `danger` (red) **only when** the same analyzer emits a matching Issue at `warn` or `error` severity for the same node. Decorative chips use `severity: 'info'`, `'success'`, or omit the field. The rule is enforced by code review (not by the manifest schema), but breaking it produces visually misleading cards: a red chip on a node that contributes zero to the error stat reads as "missed an exit-code escalation" to the operator.
|
|
1009
|
+
|
|
1010
|
+
The corner slot `graph.node.alert` is **reserved** and is NOT part of this matrix, see `view-slots.md` (and the slot's row in the catalog table) for the policy. No built-in analyzer ships an emission there; the slot is kept for genuinely independent, special-case signals (a future plugin with a one-off corner decoration).
|
|
1011
|
+
|
|
989
1012
|
### Emit path
|
|
990
1013
|
|
|
991
1014
|
Inside `extract(ctx)`, call:
|
|
@@ -1111,7 +1134,6 @@ plugins/acme-keyword-finder/
|
|
|
1111
1134
|
"version": "1.0.0",
|
|
1112
1135
|
"specCompat": "^0.20.0",
|
|
1113
1136
|
"catalogCompat": "^1.0.0",
|
|
1114
|
-
"granularity": "bundle",
|
|
1115
1137
|
"settings": {
|
|
1116
1138
|
"keywords": {
|
|
1117
1139
|
"type": "string-list",
|
|
@@ -1226,7 +1248,6 @@ Companion verbs:
|
|
|
1226
1248
|
- The six plugin statuses (`loaded` / `disabled` / `incompatible-spec` / `invalid-manifest` / `load-error` / `id-collision`) are stable; adding a seventh status is a minor bump.
|
|
1227
1249
|
- The structural analyzer **directory name MUST equal manifest id** is stable; relaxing it (allowing mismatch) is a major bump.
|
|
1228
1250
|
- The cross-root id-collision analyzer (both sides blocked, no precedence) is stable; introducing precedence (e.g. project root wins over global) is a major bump.
|
|
1229
|
-
- The `granularity` field on `PluginManifest` is stable as introduced. The two values (`bundle` / `extension`) are stable. Adding a third value is a minor bump; changing the default away from `bundle` is a major bump (every existing plugin manifest would silently flip toggle semantics).
|
|
1230
1251
|
- The optional `applicableKinds` field on the Extractor manifest is stable as introduced. Adding a wildcard syntax (`'*'`) is a minor bump (additive, the existing "absent = all kinds" semantics keeps holding); changing the default away from "applies to every kind" or making the field required is a major bump. Promoting the unknown-kinds doctor warning to a hard load error is a major bump (today's contract is "load OK, surface as warning").
|
|
1231
1252
|
- The recommended `specCompat` strategy is descriptive prose; revising the recommendation does not require a spec bump as long as the schema stays unchanged.
|
|
1232
1253
|
- The example code blocks track the public TypeScript surface of `@skill-map/cli`; bumping their imports follows the cli's own semver.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://skill-map.dev/spec/v0/bump-report.schema.json",
|
|
4
4
|
"title": "BumpReport",
|
|
5
|
-
"description": "Report shape produced by the built-in deterministic `bump` Action (Step 9.6.3, Decision #125). Extends `report-base-deterministic.schema.json` (the deterministic counterpart to `report-base.schema.json`, which carries the LLM-only `confidence` + `safety` fields). The bump Action returns one of three concrete shapes, distinguished by `ok` / `noop` / `reason`: success-with-write (`{ ok: true, version }`), silent-no-op under `force` (`{ ok: true, noop: true }`), or refusal (`{ ok: false, reason: 'fresh' }`).",
|
|
5
|
+
"description": "Report shape produced by the built-in deterministic `node-bump` Action (Step 9.6.3, Decision #125). Extends `report-base-deterministic.schema.json` (the deterministic counterpart to `report-base.schema.json`, which carries the LLM-only `confidence` + `safety` fields). The `node-bump` Action returns one of three concrete shapes, distinguished by `ok` / `noop` / `reason`: success-with-write (`{ ok: true, version }`), silent-no-op under `force` (`{ ok: true, noop: true }`), or refusal (`{ ok: false, reason: 'fresh' }`).",
|
|
6
6
|
"allOf": [{ "$ref": "report-base-deterministic.schema.json" }],
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": true,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"properties": {
|
|
9
9
|
"version": {
|
|
10
10
|
"type": "string",
|
|
11
|
-
"description": "Extension semver. Bumped independently from the plugin version; frozen into `state_executions.extension_version` on every run for reproducibility."
|
|
11
|
+
"description": "Extension semver. REQUIRED for external (user-authored) plugins, the AJV check at load time rejects manifests missing it. Bumped independently from the plugin's bundle version; frozen into `state_executions.extension_version` on every run for reproducibility. The reference CLI's built-in extensions under `src/plugins/` use a different authoring path (type `IBuiltInManifest<I<Kind>>` = `Omit<I<Kind>, 'version'>`); the codegen at `scripts/generate-built-ins.js` stamps the CLI version onto every built-in at build time so the runtime shape still satisfies the full interface."
|
|
12
12
|
},
|
|
13
13
|
"description": {
|
|
14
14
|
"type": "string",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"type": "object",
|
|
20
20
|
"required": ["schema"],
|
|
21
21
|
"additionalProperties": false,
|
|
22
|
-
"description": "Optional, opt-in declaration of a single sidecar annotation key contributed by this extension. The key is the extension's id (i.e. the leaf folder name). Extensions that need multiple annotation keys split into multiple extensions. The runtime exposes the registered catalog via `kernel.getRegisteredAnnotationKeys()` for UI autocomplete; the built-in `core/
|
|
22
|
+
"description": "Optional, opt-in declaration of a single sidecar annotation key contributed by this extension. The key is the extension's id (i.e. the leaf folder name). Extensions that need multiple annotation keys split into multiple extensions. The runtime exposes the registered catalog via `kernel.getRegisteredAnnotationKeys()` for UI autocomplete; the built-in `core/annotation-field-unknown` Analyzer emits a warning on truly unrecognized keys (typo guard). See `plugin-author-guide.md` §Annotation contribution.",
|
|
23
23
|
"properties": {
|
|
24
24
|
"schema": {
|
|
25
25
|
"type": "object",
|
package/schemas/link.schema.json
CHANGED
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"occurrences": {
|
|
67
67
|
"type": "array",
|
|
68
|
-
"description": "Every syntactic site in the source body that contributed to this edge. One entry per detection. Accumulated by the post-walk dedup when two extractors converge on the same `(source, target, kind, normalizedTrigger)` key. Empty / absent for synthetic links (frontmatter / sidecar-derived). The `core/redundant
|
|
68
|
+
"description": "Every syntactic site in the source body that contributed to this edge. One entry per detection. Accumulated by the post-walk dedup when two extractors converge on the same `(source, target, kind, normalizedTrigger)` key. Empty / absent for synthetic links (frontmatter / sidecar-derived). The `core/reference-redundant` analyzer walks this array to flag multi-form references to the same target.",
|
|
69
69
|
"items": {
|
|
70
70
|
"type": "object",
|
|
71
71
|
"required": ["extractor", "originalTrigger"],
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"id": {
|
|
44
44
|
"type": "string",
|
|
45
45
|
"minLength": 1,
|
|
46
|
-
"description": "
|
|
46
|
+
"description": "Qualified extension id `<bundle>/<ext>`. The bare bundle id form is reserved for the macro path in CLI / BFF requests and never appears in persisted state."
|
|
47
47
|
},
|
|
48
48
|
"status": {
|
|
49
49
|
"type": "string",
|
|
@@ -32,12 +32,6 @@
|
|
|
32
32
|
"minLength": 1,
|
|
33
33
|
"description": "Required short description shown in `sm plugins list` and the UI. English-only per AGENTS.md."
|
|
34
34
|
},
|
|
35
|
-
"granularity": {
|
|
36
|
-
"type": "string",
|
|
37
|
-
"enum": ["bundle", "extension"],
|
|
38
|
-
"default": "extension",
|
|
39
|
-
"description": "Toggle granularity for this plugin. `bundle`, the plugin id is the only enable/disable key; the whole set of extensions follows the toggle. `extension` (default), each extension is independently toggle-able under its qualified id `<plugin-id>/<extension-id>`. Built-in bundles use the same field: the `claude` bundle is `bundle` (the Provider and its kind-aware extractors form a coherent provider); the `core` bundle is `extension`. Plugin authors should pick `bundle` only when the plugin's extensions form a coherent unit a user would never want to toggle piecemeal."
|
|
40
|
-
},
|
|
41
35
|
"storage": {
|
|
42
36
|
"type": "object",
|
|
43
37
|
"description": "Persistence mode for this plugin. Absent = plugin does not persist state.",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"referencePaths": {
|
|
71
71
|
"type": "array",
|
|
72
72
|
"items": { "type": "string" },
|
|
73
|
-
"description": "**Privacy-sensitive, project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`) when entries point outside the project, opens read-only disk access for link validation only. Default `[]`. Directories walked in parallel by the scan to collect existing absolute paths into a side set; the kernel passes the set to analyzers via `IAnalyzerContext.referenceablePaths` so `core/broken
|
|
73
|
+
"description": "**Privacy-sensitive, project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`) when entries point outside the project, opens read-only disk access for link validation only. Default `[]`. Directories walked in parallel by the scan to collect existing absolute paths into a side set; the kernel passes the set to analyzers via `IAnalyzerContext.referenceablePaths` so `core/reference-broken` can resolve a link against the filesystem when the in-graph lookup misses. Files under these paths are NOT parsed and NOT indexed as nodes, the only effect is suppressing `broken-ref` warnings for targets that exist on disk outside the scan. Reference impl gates writes that introduce out-of-project paths behind `--yes` (CLI) and a confirm dialog (UI). **Stripped with a warning when found in the committed `project` layer**, paths are inherently per-machine and must not travel via the shared repo."
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
},
|