@skill-map/spec 0.12.0 → 0.13.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 +624 -3
- package/cli-contract.md +49 -7
- package/conformance/cases/plugin-missing-ui-rejected.json +17 -0
- package/conformance/coverage.md +2 -1
- package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json +6 -0
- package/conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/provider.js +31 -0
- package/conformance/fixtures/plugin-missing-ui/notes/example.md +8 -0
- package/index.json +10 -5
- package/package.json +1 -1
- package/schemas/api/rest-envelope.schema.json +170 -0
- package/schemas/extensions/provider.schema.json +61 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,496 @@
|
|
|
1
1
|
# Spec changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e0fb57e: Step 14.2 — REST read-side endpoints + DataSource contract
|
|
8
|
+
|
|
9
|
+
Fills the `### Server` subsection's endpoint catalogue from the v14.1 stub
|
|
10
|
+
(`/api/health` real, `/api/*` 404) to the eight read-side endpoints the
|
|
11
|
+
Angular SPA at 14.3 will consume. New `spec/schemas/api/rest-envelope.schema.json`
|
|
12
|
+
formalises the list-envelope shape. Test totals 764 → 832 (+68).
|
|
13
|
+
|
|
14
|
+
**Files added (server)**
|
|
15
|
+
|
|
16
|
+
- `src/server/path-codec.ts` — `encodeNodePath` / `decodeNodePath`. Base64url (RFC 4648 §5, no padding). Mirrored at `ui/src/services/data-source/path-codec.ts` in 14.3.
|
|
17
|
+
- `src/server/envelope.ts` — list / single / value envelope builders. `REST_ENVELOPE_SCHEMA_VERSION = '1'`. Hardcoded to track `spec/schemas/api/rest-envelope.schema.json#/properties/schemaVersion/const`.
|
|
18
|
+
- `src/server/query-adapter.ts` — `urlParamsToExportQuery(params)` lifts URL search params into the kernel's `IExportQuery` via `parseExportQuery` (one grammar, two transports). `filterNodesWithoutIssues` post-filter handles `hasIssues=false` (the one filter the kernel grammar can't express).
|
|
19
|
+
- `src/server/routes/deps.ts` — shared `IRouteDeps` bag (`options`, `runtimeContext`).
|
|
20
|
+
- `src/server/routes/health.ts` — extracted from `app.ts` for symmetry with the other routes (no behavior change).
|
|
21
|
+
- `src/server/routes/scan.ts` — `/api/scan` + `/api/scan?fresh=1`. DB absent → returns the empty `ScanResult` shape (matches the `loadScanResult` synthetic fallback). `?fresh=1` rejects when the server was started with `--no-built-ins` or `--no-plugins`.
|
|
22
|
+
- `src/server/routes/nodes.ts` — `/api/nodes/:pathB64` (single) registered BEFORE `/api/nodes` (list) so the param doesn't shadow the literal prefix. Pagination defaults `offset=0`, `limit=100`; max `limit=1000`.
|
|
23
|
+
- `src/server/routes/links.ts` — `/api/links?kind=&from=&to=`.
|
|
24
|
+
- `src/server/routes/issues.ts` — `/api/issues?severity=&ruleId=&node=`. `ruleId` filter mirrors `sm check`'s qualified-or-suffix match.
|
|
25
|
+
- `src/server/routes/graph.ts` — `/api/graph?format=ascii|json|md`. Per-format content-type. Unknown format → `bad-query` 400.
|
|
26
|
+
- `src/server/routes/config.ts` — `/api/config`. Wraps `loadConfig` from the kernel. Layered-loader warnings forwarded to `process.stderr`.
|
|
27
|
+
- `src/server/routes/plugins.ts` — `/api/plugins`. Built-ins (gated by `noBuiltIns`) + drop-ins (gated by `noPlugins`). `source: 'built-in' | 'project' | 'global'` derived from the plugin's filesystem path against `defaultProjectPluginsDir`.
|
|
28
|
+
|
|
29
|
+
**Files edited (server)**
|
|
30
|
+
|
|
31
|
+
- `src/server/app.ts` — `IAppDeps` gains `runtimeContext` (mandatory). Routes registered via the new `routes/*` registrars BEFORE the `/api/*` 404 catch-all. `app.onError` extended to map `ExportQueryError` → 400 `bad-query` (alongside the existing HTTPException + uncaught-Error branches).
|
|
32
|
+
- `src/server/index.ts` — `createServer(options, extra?)` accepts an optional `extra.runtimeContext` so tests can drive against a tempdir scope; production callers (the `sm serve` verb) leave it undefined and the composition root falls back to `defaultRuntimeContext()`.
|
|
33
|
+
- `src/server/i18n/server.texts.ts` — adds error message templates: `dbMissingHint`, `freshScanRequiresPipeline`, `graphUnknownFormat`, `paginationLimitTooLarge`, `paginationInvalidInteger`, `nodeNotFound`, `pathB64Malformed`.
|
|
34
|
+
|
|
35
|
+
**Tests added (68)**
|
|
36
|
+
|
|
37
|
+
- `src/test/server-endpoints.test.ts` (24) — happy + error path per endpoint. Uses real `runScan` + `persistScanResult` against a `mkdtempSync` fixture (no `:memory:` per `feedback_sqlite_in_memory_workaround.md`).
|
|
38
|
+
- `src/test/server-pagination.test.ts` (10) — default page caps at 100, `?limit=1000` accepted, `?limit=1001` rejected, offset/limit boundaries, `?offset=-1` and `?offset=foo` rejected, offset past total returns empty + preserves total.
|
|
39
|
+
- `src/test/server-errors.test.ts` (8) — every `code` value maps to the documented HTTP status; canonical envelope shape on every error response.
|
|
40
|
+
- `src/test/server-query-adapter.test.ts` (16) — URL-param → IExportQuery matrix; `filterNodesWithoutIssues` post-filter behaviour.
|
|
41
|
+
- `src/test/server-path-codec.test.ts` (10) — round-trip on POSIX / unicode / spaces / very long paths; rejection of empty, non-alphabet, single-char inputs; uniqueness for distinct inputs.
|
|
42
|
+
|
|
43
|
+
**Spec**
|
|
44
|
+
|
|
45
|
+
- `spec/schemas/api/rest-envelope.schema.json` — new schema. `$id: https://skill-map.dev/spec/v0/api/rest-envelope.schema.json`. `oneOf` enforces that an envelope carries exactly one of `items` / `item` / `value` per kind (with sentinel kinds `health` / `scan` / `graph` reserved for routes that don't use the envelope).
|
|
46
|
+
- `spec/cli-contract.md` `### Server` — endpoint table expanded from 4 rows (v14.1 surface) to 12 rows (v14.2 surface) with full filters / status / shape per row. Error code source enumeration added (`not-found` / `bad-query` / `internal` / reserved `db-missing`). Stability stays `experimental — locks at v0.6.0`.
|
|
47
|
+
- `spec/CHANGELOG.md` `[Unreleased]` `### Minor` — entry for BFF endpoints + envelope schema.
|
|
48
|
+
- `spec/conformance/coverage.md` — row 25 added for `api/rest-envelope.schema.json` (status: 🔴 missing — implementation-side coverage exists in `src/test/server-endpoints.test.ts`; a kernel-agnostic conformance case is still required before v1.0.0 ships).
|
|
49
|
+
- `spec/index.json` — regenerated (40 → 41 files hashed).
|
|
50
|
+
|
|
51
|
+
**Decisions during implementation (flag for orchestrator)**
|
|
52
|
+
|
|
53
|
+
- The `db-missing` error code is kept in the documented enum but no v14.2 route currently emits it — `/api/scan` returns the empty `ScanResult` when the DB is absent, list routes return zero items, and `/api/health` already advertises `db: 'missing'`. Documented in the spec as "reserved for future endpoints (post-v0.6.0 mutations) where degradation is not safe". Removing the code would be a breaking change to the envelope contract; keeping it costs nothing.
|
|
54
|
+
- `ExportQueryError` from `parseExportQuery` is funneled to `bad-query` 400 via a new branch in `app.onError`. The brief listed it as a route-level concern; centralising in the global handler means future routes that go through the kernel grammar (e.g. a future `/api/export?q=...`) inherit the same envelope mapping for free.
|
|
55
|
+
- `urlParamsToExportQuery` builds a canonical raw query string and re-parses it through `parseExportQuery` instead of constructing `IExportQuery` directly. The extra parse is microseconds and guarantees the BFF and `sm export` can never drift on what counts as a valid filter token. When the grammar grows (e.g. `has=findings` post-Step 11), only `parseExportQuery` changes.
|
|
56
|
+
- `/api/scan?fresh=1` rejection on `--no-built-ins` / `--no-plugins` matches Decision §14.1's intent: the BFF surface should not silently produce empty results that look indistinguishable from "your project has no nodes". The `bad-query` envelope tells the operator they're holding a knife by the blade.
|
|
57
|
+
- Tests use `noPlugins: true` by default to keep them deterministic against `process.cwd()` — `loadPluginRuntime` walks the live cwd's plugins dir, which would surface ambient plugins from the test runner's host (none in CI today, but a developer running tests locally with their own plugins installed would see flake).
|
|
58
|
+
- The route registration order in `app.ts` is documented in the file's header comment. `/api/nodes/:pathB64` MUST register before `/api/nodes` (Hono matches in declaration order; the literal prefix wins otherwise).
|
|
59
|
+
|
|
60
|
+
- d5488bf: Step 14.4.a — BFF WS broadcaster + chokidar wiring + scan event emission
|
|
61
|
+
|
|
62
|
+
First half of Step 14.4 lands. The BFF's `/ws` endpoint flips from
|
|
63
|
+
"upgrade-only stub" to a real broadcaster fed by a chokidar
|
|
64
|
+
filesystem watcher: every debounced batch runs the same
|
|
65
|
+
`runScanWithRenames` + persistence pipeline `sm watch` uses, and the
|
|
66
|
+
kernel's `ProgressEmitterPort` is bridged directly to the broadcaster
|
|
67
|
+
so `scan.*` / `extractor.completed` / `rule.completed` / `extension.error`
|
|
68
|
+
events reach every connected client verbatim — no envelope
|
|
69
|
+
construction in the BFF for the routine cases. Tests 832 → 854 (+22).
|
|
70
|
+
|
|
71
|
+
The UI-side consumer (`WsEventStreamService`) ships separately as
|
|
72
|
+
14.4.b.
|
|
73
|
+
|
|
74
|
+
**Files added (server)**
|
|
75
|
+
|
|
76
|
+
- `src/server/broadcaster.ts` — `WsBroadcaster` class. Owns the
|
|
77
|
+
connected-clients Set, fans `JSON.stringify(envelope)` once across
|
|
78
|
+
every open socket, evicts on backpressure (`bufferedAmount > 4 MiB`
|
|
79
|
+
→ close 1009 + unregister), drains every client with code 1001 +
|
|
80
|
+
reason `'server shutdown'` on `shutdown()`. `IBroadcasterClient`
|
|
81
|
+
interface is structural so unit tests inject fakes without a real
|
|
82
|
+
`WebSocket`.
|
|
83
|
+
- `src/server/watcher.ts` — `createWatcherService(deps)` factory.
|
|
84
|
+
Wraps `createChokidarWatcher` with `scan.watch.debounceMs` from
|
|
85
|
+
config (override via `--watcher-debounce-ms`), runs the kernel scan
|
|
86
|
+
pipeline per debounced batch, persists via `withSqlite(...).scans.persist(...)`.
|
|
87
|
+
The per-batch `ProgressEmitterPort` bridges every event the kernel
|
|
88
|
+
orchestrator emits during the scan to `broadcaster.broadcast(envelope)`.
|
|
89
|
+
Per-batch failures log + continue (transient FS errors must not
|
|
90
|
+
kill the broadcaster); chokidar instance errors broadcast a
|
|
91
|
+
`watcher.error` advisory.
|
|
92
|
+
- `src/server/events.ts` — envelope helpers (`IWsEventEnvelope` shape,
|
|
93
|
+
`buildWatcherStartedEvent`, `buildWatcherErrorEvent`). The
|
|
94
|
+
`watcher.*` events are BFF-internal advisories — non-normative,
|
|
95
|
+
prefixed with `watcher.` to flag their non-spec status. Spec-mandated
|
|
96
|
+
shapes (`scan.*`, `extractor.completed`, `rule.completed`) are
|
|
97
|
+
forwarded verbatim from the kernel emitter, so this file does not
|
|
98
|
+
build them.
|
|
99
|
+
|
|
100
|
+
**Files added (tests)**
|
|
101
|
+
|
|
102
|
+
- `src/test/server-ws-broadcaster.test.ts` (15 tests) — broadcaster
|
|
103
|
+
unit tests against fake `IBroadcasterClient` instances. Coverage:
|
|
104
|
+
register/unregister/clientCount accounting, broadcast fan-out + JSON
|
|
105
|
+
stringify, readyState filter (skip closing/closed), per-client
|
|
106
|
+
`send()` failure isolation, backpressure eviction at the documented
|
|
107
|
+
threshold (`WS_BACKPRESSURE_BYTES = 4 MiB`), shutdown idempotency
|
|
108
|
+
- close-code/reason assertions, post-shutdown register immediate
|
|
109
|
+
close, post-shutdown broadcast no-op, circular-envelope serialization
|
|
110
|
+
failure handling.
|
|
111
|
+
- `src/test/server-ws-integration.test.ts` (7 tests) — end-to-end
|
|
112
|
+
against a real server. Boots `createServer({...})` with
|
|
113
|
+
`noWatcher: false`, watches a `mkdtempSync` cwd via the
|
|
114
|
+
`runtimeContext` override (production callers' cwd would point at the
|
|
115
|
+
test runner's repo root). Exercises: initial-batch `scan.completed`
|
|
116
|
+
observed by a connected client; multi-client fan-out (one batch fires
|
|
117
|
+
to two open clients); `clientCount` decrement on disconnect;
|
|
118
|
+
`handle.close()` shuts the watcher cleanly under 2s;
|
|
119
|
+
`validateServerOptions` rejects `--no-built-ins + watcher on`;
|
|
120
|
+
`--no-watcher` confirms no `scan.*` events fire.
|
|
121
|
+
|
|
122
|
+
**Files edited (server)**
|
|
123
|
+
|
|
124
|
+
- `src/server/ws.ts` — `noopWebSocketRoute(app)` deleted, replaced
|
|
125
|
+
with `attachBroadcasterRoute(app, broadcaster)`. Pulls the underlying
|
|
126
|
+
`ws` library `WebSocket` off `WSContext.raw` and registers it on
|
|
127
|
+
`onOpen`; unregisters on `onClose` / `onError`. Server-push only —
|
|
128
|
+
`onMessage` intentionally not registered at v14.4.a.
|
|
129
|
+
- `src/server/index.ts` — `createServer` composition root grows the
|
|
130
|
+
broadcaster + watcher lifecycle: instantiate `WsBroadcaster` →
|
|
131
|
+
build app (broadcaster threaded into `IAppDeps`) → bind listener →
|
|
132
|
+
start watcher (unless `--no-watcher`); `handle.close()` shuts in
|
|
133
|
+
order: `watcherService.stop()` → `broadcaster.shutdown()` → http
|
|
134
|
+
close → `wss.close()`. `ServerHandle` exposes the `broadcaster`
|
|
135
|
+
field for tests asserting `clientCount`.
|
|
136
|
+
- `src/server/app.ts` — `IAppDeps.attachWs: TWsRegistrar` removed;
|
|
137
|
+
replaced with `IAppDeps.broadcaster: WsBroadcaster`. The BFF wires
|
|
138
|
+
`attachBroadcasterRoute` directly inside `createApp` now (route
|
|
139
|
+
registrar pattern was the v14.1 scaffolding to allow swap-in at
|
|
140
|
+
v14.4 — that work is done, no need for the indirection).
|
|
141
|
+
- `src/server/options.ts` — adds `noWatcher: boolean` (default `false`
|
|
142
|
+
per Decision #121: a server with stale DB is a footgun) and
|
|
143
|
+
`watcherDebounceMs?: number` (override the config value).
|
|
144
|
+
Validator gains `watcher-requires-pipeline` (rejects
|
|
145
|
+
`--no-built-ins + watcher on` — would persist empty scans on every
|
|
146
|
+
batch) and `watcher-debounce-invalid` (non-integer / negative).
|
|
147
|
+
- `src/server/i18n/server.texts.ts` — eight new keys for watcher /
|
|
148
|
+
broadcaster lifecycle log lines.
|
|
149
|
+
|
|
150
|
+
**Files edited (CLI)**
|
|
151
|
+
|
|
152
|
+
- `src/cli/commands/serve.ts` — plumbs `--no-watcher` (documented) +
|
|
153
|
+
hidden `--watcher-debounce-ms` flag through to `IServerOptionsInput`.
|
|
154
|
+
- `src/cli/i18n/serve.texts.ts` — two new keys
|
|
155
|
+
(`watcherRequiresPipeline`, `watcherDebounceInvalid`).
|
|
156
|
+
|
|
157
|
+
**Files edited (tests)**
|
|
158
|
+
|
|
159
|
+
- `src/test/server-boot.test.ts` — the no-broadcaster-yet
|
|
160
|
+
close-1000-on-`onOpen` assertion is replaced with a "connection
|
|
161
|
+
stays open + registers" assertion. Default options grow
|
|
162
|
+
`noWatcher: true` (the watcher is exercised in the dedicated
|
|
163
|
+
integration file).
|
|
164
|
+
- `src/test/server-{db-missing,endpoints,errors,pagination}.test.ts`
|
|
165
|
+
— default options grow `noWatcher: true` so chokidar doesn't
|
|
166
|
+
subscribe to the test runner's cwd. No behavior change for these
|
|
167
|
+
tests; they exercise the REST surface, not the watcher.
|
|
168
|
+
|
|
169
|
+
**Spec**
|
|
170
|
+
|
|
171
|
+
- `spec/cli-contract.md` `### Server` — new **WebSocket protocol**
|
|
172
|
+
subsection. Documents the wire envelope (delegated to
|
|
173
|
+
`job-events.md` §Common envelope), the v14.4.a event catalog
|
|
174
|
+
(`scan.started` / `scan.progress` / `scan.completed` plus the
|
|
175
|
+
side-effect events `extractor.completed` / `rule.completed` /
|
|
176
|
+
`extension.error`, plus the BFF-internal advisories
|
|
177
|
+
`watcher.started` / `watcher.error`), the connection lifecycle
|
|
178
|
+
(no state push on connect; client polls `/api/scan` to seed; close
|
|
179
|
+
codes 1000 / 1001 / 1009), the backpressure rule, and the
|
|
180
|
+
loopback-only assumption (no per-connection auth through v0.6.0
|
|
181
|
+
per Decision #119). The endpoint table flips `GET /ws` from
|
|
182
|
+
`upgrade-only` to `implemented (v14.4.a)`. The `sm serve` flag
|
|
183
|
+
table grows `--no-watcher`. The verb-catalog row for `sm serve`
|
|
184
|
+
mirrors the new flag.
|
|
185
|
+
- `spec/CHANGELOG.md` `[Unreleased]` `### Minor` entry.
|
|
186
|
+
- `spec/index.json` — regenerated (41 files hashed; no schema added).
|
|
187
|
+
|
|
188
|
+
**ROADMAP.md** — bumped `Last updated`, marked Step 14.4.a landed
|
|
189
|
+
(14.4 carries an explicit (a/b) split now), 14.4.b still owes the
|
|
190
|
+
UI-side consumer. Earlier 14.3 prose pushed to "Earlier prose".
|
|
191
|
+
|
|
192
|
+
**Decisions taken inline (flag for orchestrator)**
|
|
193
|
+
|
|
194
|
+
- `issue.added` / `issue.resolved` (per `spec/job-events.md` §Issue
|
|
195
|
+
events line 446) **deferred to a follow-up**. The diff requires
|
|
196
|
+
comparing the new `ScanResult.issues` set against the prior
|
|
197
|
+
persisted snapshot; the watcher already loads the prior for the
|
|
198
|
+
rename heuristic, so the data is at hand, but the diff plumbing
|
|
199
|
+
(key derivation, set comparison, two emit calls per delta) is
|
|
200
|
+
enough material that it deserves its own brief. The 14.4.a surface
|
|
201
|
+
fans out exactly what the kernel emitter already produces.
|
|
202
|
+
- `scan.failed` **deferred to a follow-up**. The shape is not yet
|
|
203
|
+
locked in `spec/job-events.md` and would need a normative
|
|
204
|
+
addition. For 14.4.a, per-batch failures log via the kernel logger
|
|
205
|
+
and the watcher loop continues — same behavior as `sm watch`'s
|
|
206
|
+
`WATCH_TEXTS.batchFailed`.
|
|
207
|
+
- `scan.progress` **emitted, not throttled**. The kernel
|
|
208
|
+
orchestrator emits one event per node walked; on a small workspace
|
|
209
|
+
this is a handful of events per batch, on a large workspace it's
|
|
210
|
+
hundreds. The brief flagged throttling as optional at 14.4.a; the
|
|
211
|
+
bridge forwards verbatim today. The integration test observed 13
|
|
212
|
+
`scan.progress` events for a 4-file fixture, which is fine. A
|
|
213
|
+
throttle (250ms aggregation) is the obvious 14.6 polish if the
|
|
214
|
+
bundle / perf pass shows the fan-out swamping the channel.
|
|
215
|
+
- `watcher.started` / `watcher.error` BFF-internal advisories
|
|
216
|
+
**emitted** rather than silent. They give the SPA event-log a
|
|
217
|
+
clear "armed" signal and a surface for chokidar errors that don't
|
|
218
|
+
fit the spec's `scan.*` shape. Prefix marks them as non-normative;
|
|
219
|
+
consumers that follow the spec's "ignore unknown event types"
|
|
220
|
+
rule will not break.
|
|
221
|
+
- `IHealthResponse.watcher: 'on' | 'off'` **NOT added**. Keeping
|
|
222
|
+
the v14.2 health response shape stable was preferable to adding
|
|
223
|
+
one field for what tests / `--no-watcher` already cover. The
|
|
224
|
+
broadcaster's `clientCount` is exposed on `ServerHandle.broadcaster`
|
|
225
|
+
for test introspection without polluting the public health surface.
|
|
226
|
+
- The validator rejects `--no-built-ins + watcher on` because the
|
|
227
|
+
watcher would persist empty scans on every batch, silently wiping
|
|
228
|
+
the DB. `--no-plugins + watcher on` is OK (the built-in pipeline
|
|
229
|
+
is still complete on its own).
|
|
230
|
+
- `attachBroadcasterRoute` does NOT register `onMessage`. v14.4.a
|
|
231
|
+
is server-push only. A future client-initiated heartbeat / filter
|
|
232
|
+
request lands at 14.4.b or later.
|
|
233
|
+
- `WsBroadcaster` is a class (not a factory) per AGENTS.md
|
|
234
|
+
§Adapter wiring rule 5: factories scope to "adapters consumed via
|
|
235
|
+
ports", and the broadcaster is a plain BFF helper with no kernel
|
|
236
|
+
port to satisfy. The class is grandfathered no-`I*`-prefix per
|
|
237
|
+
§Type naming convention category 4.
|
|
238
|
+
|
|
239
|
+
**Smoke (live BFF, one-shot per AGENTS.md)**
|
|
240
|
+
|
|
241
|
+
The integration tests cover the live boot + WS upgrade + chokidar
|
|
242
|
+
batch + broadcast end-to-end against a `mkdtempSync` scope. The
|
|
243
|
+
diagnostic line `ws events received: scan.started, scan.progress
|
|
244
|
+
× 13, extractor.completed × 4, rule.completed × 5, scan.completed`
|
|
245
|
+
confirms the full event sequence reaches a connected client during
|
|
246
|
+
a real scan against a 4-file fixture.
|
|
247
|
+
|
|
248
|
+
- 4ff3f38: Step 14.5.d — Provider-driven kind presentation + envelope kindRegistry
|
|
249
|
+
|
|
250
|
+
Pre-1.0 minor breaking per `versioning.md` § Pre-1.0.
|
|
251
|
+
|
|
252
|
+
The Provider extension surface gains the required `kinds[*].ui` field
|
|
253
|
+
so each kind a Provider declares carries the presentation metadata the
|
|
254
|
+
UI needs to render it (label, base color, optional dark-theme color,
|
|
255
|
+
optional emoji, optional icon). The icon is a discriminated union —
|
|
256
|
+
`{ kind: 'pi'; id: 'pi-…' }` for PrimeIcons or `{ kind: 'svg'; path:
|
|
257
|
+
'…' }` for raw SVG path data. The UI derives `bg` / `fg` tints from
|
|
258
|
+
`color` per theme via a deterministic helper, so the Provider declares
|
|
259
|
+
one base color per theme rather than four hex values.
|
|
260
|
+
|
|
261
|
+
The REST envelope shape (`spec/schemas/api/rest-envelope.schema.json`)
|
|
262
|
+
gains a new required `kindRegistry` field on every payload-bearing
|
|
263
|
+
variant (`nodes` / `links` / `issues` / `plugins` / `node` / `config`);
|
|
264
|
+
sentinel envelopes (`health` / `scan` / `graph`) stay exempt. The
|
|
265
|
+
registry is keyed by kind name and carries `{ providerId, label,
|
|
266
|
+
color, colorDark?, emoji?, icon? }` — the BFF assembles it once at
|
|
267
|
+
boot from every enabled Provider and attaches it to every applicable
|
|
268
|
+
response so the UI can render Provider-declared kinds (built-in and
|
|
269
|
+
user-plugin alike) without hardcoding a closed kind enum. The change
|
|
270
|
+
keeps `schemaVersion` at `'1'` (greenfield — no released consumers
|
|
271
|
+
depend on the prior shape).
|
|
272
|
+
|
|
273
|
+
**Files edited (spec)**
|
|
274
|
+
|
|
275
|
+
- `spec/schemas/extensions/provider.schema.json` — adds `ui` to the
|
|
276
|
+
required field set on each `kinds[*]` entry, with discriminated
|
|
277
|
+
`oneOf` for `icon`.
|
|
278
|
+
- `spec/schemas/api/rest-envelope.schema.json` — new `kindRegistry`
|
|
279
|
+
definition; required on every payload-bearing variant; sentinel
|
|
280
|
+
variants explicitly forbid the field via `not.anyOf`. Version stays
|
|
281
|
+
at `'1'` (greenfield).
|
|
282
|
+
- `spec/CHANGELOG.md` — `[Unreleased]` `### Minor` entry.
|
|
283
|
+
|
|
284
|
+
**Files edited (kernel + built-in)**
|
|
285
|
+
|
|
286
|
+
- `src/kernel/extensions/provider.ts` — adds `IProviderKindUi` and
|
|
287
|
+
`IProviderKindIcon`; `ui` becomes required on `IProviderKind`.
|
|
288
|
+
- `src/built-in-plugins/providers/claude/index.ts` — every kind
|
|
289
|
+
(skill / agent / command / hook / note) declares its `ui` block
|
|
290
|
+
reusing the colors / labels / icons previously hardcoded in
|
|
291
|
+
`ui/src/styles.css`, `ui/src/i18n/kinds.texts.ts`, and
|
|
292
|
+
`ui/src/app/components/kind-icon/kind-icon.html`.
|
|
293
|
+
- `src/built-in-plugins/providers/claude/claude.test.ts` — new test
|
|
294
|
+
asserts every kind declares a well-formed `ui` block.
|
|
295
|
+
- `src/test/external-provider-kind.test.ts` — three mock providers
|
|
296
|
+
updated to declare `ui` on their `cursorRule` kinds.
|
|
297
|
+
- `src/test/plugins-cli.test.ts` — `dropMockProvider` helper template
|
|
298
|
+
declares `ui` on the inline mock `note` kind.
|
|
299
|
+
|
|
300
|
+
**Files added (conformance)**
|
|
301
|
+
|
|
302
|
+
- `spec/conformance/fixtures/plugin-missing-ui/` — drop-in Provider
|
|
303
|
+
fixture whose `kinds[*]` omits `ui` (plus a trivial `notes/example.md`
|
|
304
|
+
for the built-in Claude scan to grab).
|
|
305
|
+
- `spec/conformance/cases/plugin-missing-ui-rejected.json` — locks the
|
|
306
|
+
loader contract: `sm scan --json` exits 0, stderr matches
|
|
307
|
+
`plugin bad-provider:.*invalid.*must have required property 'ui'`,
|
|
308
|
+
the envelope still contains the built-in Claude provider, and the
|
|
309
|
+
one fixture node still gets scanned (one bad plugin does not take
|
|
310
|
+
down the scan).
|
|
311
|
+
|
|
312
|
+
**Decisions taken inline (flag for orchestrator)**
|
|
313
|
+
|
|
314
|
+
- `ui` is required, not optional — making it optional reintroduces the
|
|
315
|
+
pre-14.5.d trap of silently collapsing unknown kinds to `'note'`.
|
|
316
|
+
The cost (one object per kind in the manifest) is small.
|
|
317
|
+
- Icon is a discriminated union (`oneOf` with `kind` discriminator)
|
|
318
|
+
rather than two optional fields. Keeps the UI dispatch exhaustive
|
|
319
|
+
and AJV validates each variant cleanly.
|
|
320
|
+
- `schemaVersion` stays at `'1'` despite the required-field add.
|
|
321
|
+
Greenfield — no released consumers; a versioned migration buys
|
|
322
|
+
nothing today. Bumps the day a third-party consumer ships against
|
|
323
|
+
the wire.
|
|
324
|
+
- Severity (PrimeNG `<p-tag>` `severity` enum) is NOT declared by the
|
|
325
|
+
Provider. The UI tints kind tags with the registry's `color`
|
|
326
|
+
directly, avoiding a Provider-side dependency on a UI-framework
|
|
327
|
+
enum.
|
|
328
|
+
- BFF + UI sub-steps land in follow-up commits (14.5.d.iii / .iv /
|
|
329
|
+
.v) — the spec + kernel + built-in surface ship first so the
|
|
330
|
+
contract is visible before consumers wire up.
|
|
331
|
+
|
|
332
|
+
- de20bc2: Step 14.5 (a + b) — Inspector polish: markdown body opt-in + linked-nodes panel + dead-link verify hybrid
|
|
333
|
+
|
|
334
|
+
Two sub-steps land together as a single feature unit. The Inspector
|
|
335
|
+
view (UI workspace) gains a real markdown body card, a dedicated
|
|
336
|
+
linked-nodes panel fed by the BFF's `/api/links` endpoint, and a
|
|
337
|
+
hybrid dead-link checker that combines the in-memory heuristic with
|
|
338
|
+
on-demand BFF verification. The spec + server side ships the minimal
|
|
339
|
+
contract the new UI surface depends on: an opt-in `?include=body`
|
|
340
|
+
parameter on `GET /api/nodes/:pathB64`, plus a corrected single-node
|
|
341
|
+
response shape. Tests 854 → 868 (+14 server) and UI 113 → 138 (+25
|
|
342
|
+
inspector / linked-nodes specs).
|
|
343
|
+
|
|
344
|
+
**Why on-demand body reads instead of persisting bodies in the DB**:
|
|
345
|
+
the kernel persists `body_hash` only (per `db-schema.md` §scan_nodes)
|
|
346
|
+
— the body itself is human content, not machine state, and
|
|
347
|
+
duplicating it in SQLite would inflate the DB without serving any
|
|
348
|
+
read-side query the kernel cares about. Inspector cards that DO want
|
|
349
|
+
to render the body (markdown preview at Step 14.5) opt into the
|
|
350
|
+
filesystem re-read; the list / graph / kind-palette views never need
|
|
351
|
+
it.
|
|
352
|
+
|
|
353
|
+
**Files added (server)**
|
|
354
|
+
|
|
355
|
+
- `src/server/node-body.ts` — on-demand body reader. Exports
|
|
356
|
+
`readNodeBody(cwd, relPath)` (returns `string | null`; `null` on
|
|
357
|
+
ENOENT / EACCES / EISDIR / ENOTDIR) and `stripFrontmatter(body)`
|
|
358
|
+
(drops the leading `---\n…\n---\n` block when present, leaves
|
|
359
|
+
fences in mid-document untouched). Path-traversal hardened: refuses
|
|
360
|
+
absolute paths and any relative path that resolves outside `cwd`.
|
|
361
|
+
- `src/test/server-node-body.test.ts` (11 unit cases) — covers
|
|
362
|
+
`stripFrontmatter` edge cases (empty, no frontmatter, missing
|
|
363
|
+
closing fence, fence in mid-document) and `readNodeBody` traversal
|
|
364
|
+
rejection + the four `null`-returning errno branches.
|
|
365
|
+
|
|
366
|
+
**Files edited (server)**
|
|
367
|
+
|
|
368
|
+
- `src/server/routes/nodes.ts` — `GET /api/nodes/:pathB64` extends
|
|
369
|
+
with `?include=body` opt-in (CSV-tolerant via the new
|
|
370
|
+
`parseIncludes` helper, so `?include=body,future-extension` reads
|
|
371
|
+
cleanly the day a second include lands). Same handler also FIXES a
|
|
372
|
+
long-standing shape bug: was emitting `{ item: { node, linksOut,
|
|
373
|
+
linksIn, issues } }` (raw `INodeBundle` pass-through), now emits
|
|
374
|
+
the documented `{ item: Node, links: { incoming, outgoing },
|
|
375
|
+
issues }` that the UI's `INodeDetailApi` and `StaticDataSource`
|
|
376
|
+
already expected. No prod consumer ran against the legacy shape
|
|
377
|
+
(the UI was internally branching on the legacy shape before the
|
|
378
|
+
REST adapter landed at 14.3.a), so the corrected shape ships as a
|
|
379
|
+
minor.
|
|
380
|
+
- `src/test/server-endpoints.test.ts` — assertions corrected to the
|
|
381
|
+
documented shape; 2 new cases for `?include=body` (returns body
|
|
382
|
+
on present file, returns `null` when the file is missing).
|
|
383
|
+
|
|
384
|
+
**Files added (UI)**
|
|
385
|
+
|
|
386
|
+
- `ui/src/app/components/linked-nodes-panel/{ts,html,css,spec.ts}`
|
|
387
|
+
— standalone Angular component. Inputs: `path`. Outputs:
|
|
388
|
+
`openPath`. Internally fires `dataSource.listLinks({from})` +
|
|
389
|
+
`listLinks({to})` in parallel; state machine
|
|
390
|
+
`idle/loading/ready/error`. Subscribes to `events()` filtered on
|
|
391
|
+
`scan.completed` for reactive refresh, plus a manual refresh
|
|
392
|
+
button in the card header. Token guard handles rapid path
|
|
393
|
+
changes. Renders rows with kind tag + clickable path +
|
|
394
|
+
confidence chip + sources. 10 spec cases.
|
|
395
|
+
- `ui/src/i18n/linked-nodes-panel.texts.ts` — i18n catalog.
|
|
396
|
+
- `ui/src/app/views/inspector-view/inspector-view.spec.ts` (15
|
|
397
|
+
cases) — first inspector-view spec. Covers empty / loading /
|
|
398
|
+
body-card states, stale-fetch token guard, kind-card smoke,
|
|
399
|
+
dead-link verify icon flow (heuristic-dead renders icon,
|
|
400
|
+
click → 404 confirms, click → 200 flips to live).
|
|
401
|
+
|
|
402
|
+
**Files edited (UI)**
|
|
403
|
+
|
|
404
|
+
- `ui/src/models/api.ts` — `INodeApi.body?: string | null` added.
|
|
405
|
+
- `ui/src/services/data-source/data-source.port.ts` —
|
|
406
|
+
`IDataSourcePort.getNode(path, opts?: {includeBody?: boolean})`.
|
|
407
|
+
- `ui/src/services/data-source/rest-data-source.ts` — propagates
|
|
408
|
+
`includeBody` to `?include=body`.
|
|
409
|
+
- `ui/src/services/data-source/static-data-source.ts` — ignores
|
|
410
|
+
the flag (demo bundle ships bodies inline; see
|
|
411
|
+
`scripts/build-demo-dataset.js` below).
|
|
412
|
+
- `ui/src/services/collection-loader.ts` — minor signature touch
|
|
413
|
+
for the `getNode` opts pass-through.
|
|
414
|
+
- `ui/src/models/node.ts` — `INodeView` loses three fields:
|
|
415
|
+
`body`, `raw`, `mockSummary`. The "Summary" mock card is
|
|
416
|
+
retired (description already lives in `inspector__desc`).
|
|
417
|
+
- `ui/src/app/views/inspector-view/inspector-view.ts` — body card
|
|
418
|
+
switches from `<pre>{{ n.body }}</pre>` to a `@switch` over a
|
|
419
|
+
`bodyState` signal (idle / loading / empty / unavailable /
|
|
420
|
+
error / ready) with token-guarded fetch via `effect()` keyed on
|
|
421
|
+
`path()`; markdown rendered via `MarkdownRenderer` and
|
|
422
|
+
`[innerHTML]`. Mounts `<sm-linked-nodes-panel>` as a separate
|
|
423
|
+
card between Relations and Body. Dead-link verify hybrid: the
|
|
424
|
+
Relations card chips (`supersededBy` / `supersedes` / `requires`
|
|
425
|
+
/ `related`) keep the in-memory heuristic but now carry a verify
|
|
426
|
+
icon (`pi-question-circle`) that fires `getNode(path)` against
|
|
427
|
+
the BFF; three visual states `live` / `dead-confirmed` (404 → red
|
|
428
|
+
dashed border + `pi-times-circle`) / `dead-heuristic` (not in
|
|
429
|
+
scope, not yet verified). Per-node signals
|
|
430
|
+
`verifiedAlive` / `verifiedDead` / `verifyInFlight` reset on
|
|
431
|
+
`path()` change. Template refactor consolidates 4 inline
|
|
432
|
+
duplicated chip blocks into a single `<ng-template #pathChip>`
|
|
433
|
+
shared via `*ngTemplateOutlet`.
|
|
434
|
+
- `ui/src/app/views/inspector-view/inspector-view.{html,css}` —
|
|
435
|
+
templates + styles for the new body / verify states.
|
|
436
|
+
- `ui/src/i18n/inspector-view.texts.ts` — drops `summary*`, adds
|
|
437
|
+
`body.*` (loading / empty / unavailable / renderError),
|
|
438
|
+
`relations.verifyHint`, `relations.deadConfirmed`. `body: 'Body'`
|
|
439
|
+
(was `'Body (raw markdown)'`).
|
|
440
|
+
|
|
441
|
+
**Files edited (build pipeline)**
|
|
442
|
+
|
|
443
|
+
- `scripts/build-demo-dataset.js` — new `embedBodies(scan,
|
|
444
|
+
fixtureDir)` post-processor reads each fixture's body from disk,
|
|
445
|
+
strips frontmatter, attaches to the demo `data.json` so the
|
|
446
|
+
demo experience matches the live BFF (~40 KB extra for 21
|
|
447
|
+
fixtures; bodies-on-bundle is the explicit demo-mode tradeoff).
|
|
448
|
+
|
|
449
|
+
**Spec**
|
|
450
|
+
|
|
451
|
+
- `spec/cli-contract.md` `### Server` — `/api/nodes/:pathB64` row
|
|
452
|
+
flips its shape column from the legacy bundle to the documented
|
|
453
|
+
`{ item, links: { incoming, outgoing }, issues }` and gains the
|
|
454
|
+
`?include=body` filter column.
|
|
455
|
+
- `spec/CHANGELOG.md` `[Unreleased]` `### Minor` — entry covering
|
|
456
|
+
the `?include=body` opt-in, the corrected response shape, and
|
|
457
|
+
the path-traversal defense.
|
|
458
|
+
- `spec/index.json` — regenerated (41 files hashed; no schema
|
|
459
|
+
added).
|
|
460
|
+
|
|
461
|
+
**ROADMAP** — `Last updated` bumped, "YOU ARE HERE" updated,
|
|
462
|
+
completeness marker now lists 14.5.a + 14.5.b as complete; "Next"
|
|
463
|
+
points at 14.5.c.
|
|
464
|
+
|
|
465
|
+
**Decisions taken inline (flag for orchestrator)**
|
|
466
|
+
|
|
467
|
+
- The corrected single-node shape ships as a minor (additive on
|
|
468
|
+
the contract surface) rather than a major. Rationale: no public
|
|
469
|
+
consumer ran against the legacy shape; the UI was decoding the
|
|
470
|
+
legacy shape internally before the REST adapter at 14.3.a
|
|
471
|
+
introduced the documented shape; and the spec table already
|
|
472
|
+
documented the new shape (the bug was in the implementation,
|
|
473
|
+
not the spec). Keeping the bump minor avoids burning a major
|
|
474
|
+
on a never-shipped wire format.
|
|
475
|
+
- `parseIncludes` is CSV-tolerant from day one (`?include=body`
|
|
476
|
+
and `?include=body,foo` both parse) so the second include can
|
|
477
|
+
land without a parser refactor. Unknown include values are
|
|
478
|
+
silently ignored — the BFF surface mirrors the spec's
|
|
479
|
+
"ignore unknown event types" rule for forward compatibility.
|
|
480
|
+
- Bodies are fetched per-node on inspector open, not pre-fetched
|
|
481
|
+
in the list endpoint. Keeps the list `/api/nodes` response
|
|
482
|
+
small (the list view never renders bodies) and matches the
|
|
483
|
+
read-side hot path: most nodes are listed but few are inspected.
|
|
484
|
+
- The dead-link verify is opt-in per chip click, not auto-fired
|
|
485
|
+
on inspector open. Heuristic-dead nodes are common in scoped
|
|
486
|
+
scans (a workspace that scans `docs/` but references `src/`);
|
|
487
|
+
auto-firing would burn one BFF round-trip per such reference.
|
|
488
|
+
- Per-node verification signals reset on `path()` change to avoid
|
|
489
|
+
stale state bleeding between inspector navigations. The signals
|
|
490
|
+
are scoped to the component instance; no global cache (the
|
|
491
|
+
cost is one BFF call per re-verify on revisit, which the user
|
|
492
|
+
triggers intentionally by clicking the icon).
|
|
493
|
+
|
|
3
494
|
## 0.12.0
|
|
4
495
|
|
|
5
496
|
### Minor Changes
|
|
@@ -225,6 +716,136 @@ list`, `sm plugins doctor`, `sm db prune` plugin filter, runtime
|
|
|
225
716
|
|
|
226
717
|
### Minor
|
|
227
718
|
|
|
719
|
+
- **Provider-driven kind presentation + envelope `kindRegistry`** —
|
|
720
|
+
Step 14.5.d. The Provider extension surface gains the required
|
|
721
|
+
`kinds[*].ui` field (label, color, optional dark-theme color, optional
|
|
722
|
+
emoji, optional icon) so each kind a Provider declares carries the
|
|
723
|
+
presentation metadata the UI needs to render it. The icon is a
|
|
724
|
+
discriminated union — `{ kind: 'pi'; id: 'pi-…' }` for PrimeIcons or
|
|
725
|
+
`{ kind: 'svg'; path: '…' }` for raw SVG path data wrapped in a
|
|
726
|
+
`viewBox="0 0 24 24"` tinted with `currentColor`. The UI derives bg /
|
|
727
|
+
fg tints from `color` per theme via a deterministic helper, so the
|
|
728
|
+
Provider declares one base color per theme rather than four hex
|
|
729
|
+
values.
|
|
730
|
+
|
|
731
|
+
The REST envelope shape (`schemas/api/rest-envelope.schema.json`)
|
|
732
|
+
gains a new required `kindRegistry` field on every payload-bearing
|
|
733
|
+
variant (`nodes` / `links` / `issues` / `plugins` lists, the `node`
|
|
734
|
+
single, and the `config` value envelope); sentinel envelopes
|
|
735
|
+
(`health` / `scan` / `graph`) stay exempt because they don't carry a
|
|
736
|
+
payload at the wire level either. The registry is keyed by kind
|
|
737
|
+
name and carries `{ providerId, label, color, colorDark?, emoji?,
|
|
738
|
+
icon? }` — the BFF assembles it once at boot from every enabled
|
|
739
|
+
Provider and attaches it to every applicable response so the UI can
|
|
740
|
+
render Provider-declared kinds (built-in and user-plugin alike)
|
|
741
|
+
without hardcoding a closed kind enum.
|
|
742
|
+
|
|
743
|
+
**Why required, not optional**: making `ui` optional reintroduces the
|
|
744
|
+
trap the UI had pre-14.5.d (silently collapsing unknown kinds to
|
|
745
|
+
`'note'`). Forcing every Provider to declare presentation up-front
|
|
746
|
+
means the UI never has to invent visuals; the cost is one small
|
|
747
|
+
object per kind in the manifest.
|
|
748
|
+
|
|
749
|
+
**Why discriminated icon instead of two optional fields**: the
|
|
750
|
+
`oneOf` shape (with `kind: 'pi' | 'svg'` discriminator) keeps the UI
|
|
751
|
+
dispatch exhaustive without string-sniffing the payload, and AJV
|
|
752
|
+
validates each variant cleanly — a manifest cannot ship both `id` and
|
|
753
|
+
`path` simultaneously.
|
|
754
|
+
|
|
755
|
+
**Why `schemaVersion` stays at `'1'`**: the BFF is greenfield — no
|
|
756
|
+
released consumers depend on the previous (kindRegistry-less)
|
|
757
|
+
shape, so a versioned migration would only add ceremony. The
|
|
758
|
+
shape change is documented under this changelog entry; the version
|
|
759
|
+
bumps the day a third-party consumer ships against the wire.
|
|
760
|
+
|
|
761
|
+
Pre-1.0 minor breaking per `versioning.md` § Pre-1.0. The built-in
|
|
762
|
+
Claude Provider migrates in the same step (every kind declares its
|
|
763
|
+
`ui` block reusing the visuals previously hardcoded in the UI).
|
|
764
|
+
|
|
765
|
+
Conformance: new case `plugin-missing-ui-rejected` (with fixture
|
|
766
|
+
`plugin-missing-ui/`) locks the loader's behaviour against a drop-in
|
|
767
|
+
Provider that omits `ui` — `sm scan --json` exits 0, stderr matches
|
|
768
|
+
the canonical `must have required property 'ui'` diagnostic, and the
|
|
769
|
+
rest of the pipeline (built-in Claude) keeps running. Suite total:
|
|
770
|
+
5/5 passing across 2 scopes.
|
|
771
|
+
|
|
772
|
+
- **BFF `/api/nodes/:pathB64` body opt-in** — Step 14.5.a extends the
|
|
773
|
+
single-node detail endpoint with the optional `?include=body` query
|
|
774
|
+
parameter. When set, the response's `item` carries `body: string |
|
|
775
|
+
null` (the post-frontmatter file content read from disk on demand);
|
|
776
|
+
`null` indicates the source file was missing or unreadable when the
|
|
777
|
+
request landed. Without the flag, `item.body` stays `undefined` and
|
|
778
|
+
the handler does not touch the filesystem. The single-node response
|
|
779
|
+
shape is also corrected to the documented `{ schemaVersion, kind:
|
|
780
|
+
'node', item: Node, links: { incoming: Link[], outgoing: Link[] },
|
|
781
|
+
issues: Issue[] }` (the `### Server` table previously declared the
|
|
782
|
+
shape as the legacy `{ node, linksOut, linksIn, issues }` bundle —
|
|
783
|
+
see prose for the bug-fix rationale). No prod consumer ran against
|
|
784
|
+
the legacy shape, so the corrected shape ships as a minor.
|
|
785
|
+
|
|
786
|
+
**Why on-demand instead of persisting bodies in `scan_nodes`**: the
|
|
787
|
+
kernel persists `body_hash` only (per `db-schema.md` §scan_nodes) —
|
|
788
|
+
the body itself is human content, not machine state, and duplicating
|
|
789
|
+
it in SQLite would inflate the DB without serving any read-side
|
|
790
|
+
query the kernel cares about. Inspector cards that DO want to render
|
|
791
|
+
the body (markdown preview at Step 14.5) opt into the filesystem
|
|
792
|
+
re-read; the list / graph / kind-palette views never need it.
|
|
793
|
+
|
|
794
|
+
**Path-traversal defense**: the body reader (`src/server/node-body.ts`)
|
|
795
|
+
refuses absolute paths and any relative path that resolves outside
|
|
796
|
+
the scope root. A corrupted DB row or a future Provider that
|
|
797
|
+
forgets to sanitise its node paths cannot use this endpoint to leak
|
|
798
|
+
arbitrary files.
|
|
799
|
+
|
|
800
|
+
- **BFF `/ws` protocol + watcher contract** — Step 14.4.a documents
|
|
801
|
+
the WebSocket surface. The `### Server` subsection grows a
|
|
802
|
+
**WebSocket protocol** block enumerating the wire envelope (delegated
|
|
803
|
+
to `job-events.md` §Common envelope), the v14.4.a event catalog
|
|
804
|
+
(`scan.started` / `scan.progress` / `scan.completed` plus the
|
|
805
|
+
side-effect events `extractor.completed` / `rule.completed` /
|
|
806
|
+
`extension.error`, plus the BFF-internal advisories
|
|
807
|
+
`watcher.started` / `watcher.error`), the connection lifecycle (no
|
|
808
|
+
state push on connect; client polls `/api/scan` to seed; close codes
|
|
809
|
+
used: 1000 / 1001 / 1009), the backpressure rule (4 MiB `bufferedAmount`
|
|
810
|
+
→ close 1009 + unregister), and the loopback-only assumption (no
|
|
811
|
+
per-connection auth through v0.6.0 per Decision #119). The endpoint
|
|
812
|
+
table flips `GET /ws` from `upgrade-only` to `implemented (v14.4.a)`.
|
|
813
|
+
The `sm serve` flag table grows `--no-watcher` (default off — watcher
|
|
814
|
+
on per Decision #121); combining `--no-watcher` is documented;
|
|
815
|
+
combining `--no-built-ins` with the watcher is rejected (would
|
|
816
|
+
persist empty scans on every batch). The verb-catalog row for
|
|
817
|
+
`sm serve` mirrors the new flag.
|
|
818
|
+
|
|
819
|
+
`issue.added` / `issue.resolved` (the diff-based events from
|
|
820
|
+
`job-events.md` §Issue events) and `scan.failed` are explicitly
|
|
821
|
+
flagged as deferred — the 14.4.a surface fans out only what the
|
|
822
|
+
kernel emitter already produces; per-batch failure events and the
|
|
823
|
+
diff-based issue stream require additional plumbing in the BFF
|
|
824
|
+
watcher loop and land in a follow-up.
|
|
825
|
+
|
|
826
|
+
Additive minor per `versioning.md` § Pre-1.0; no breaking change to
|
|
827
|
+
any prior `/ws` behavior (the v14.1 / v14.2 / v14.3 surfaces all had
|
|
828
|
+
`/ws` documented as "upgrade-only — broadcaster lands at v14.4").
|
|
829
|
+
|
|
830
|
+
- **BFF read-side endpoints** — Step 14.2 fills the `### Server`
|
|
831
|
+
subsection's endpoint catalogue from the v14.1 stub
|
|
832
|
+
(`GET /api/health` real, `ALL /api/*` 404) to the eight read-side
|
|
833
|
+
endpoints the Web UI consumes:
|
|
834
|
+
`GET /api/scan` (latest persisted `ScanResult`; `?fresh=1` for an
|
|
835
|
+
in-memory scan), `GET /api/nodes` (paginated, filtered list — same
|
|
836
|
+
query grammar as `sm export`), `GET /api/nodes/:pathB64` (single-node
|
|
837
|
+
bundle; `pathB64` is base64url of `node.path`), `GET /api/links`,
|
|
838
|
+
`GET /api/issues`, `GET /api/graph?format=...` (formatter rendering),
|
|
839
|
+
`GET /api/config`, `GET /api/plugins`. New
|
|
840
|
+
[`schemas/api/rest-envelope.schema.json`](schemas/api/rest-envelope.schema.json)
|
|
841
|
+
formalises the list-envelope shape (`{ schemaVersion, kind, items \|
|
|
842
|
+
item \| value, filters, counts }`). The error envelope subsection
|
|
843
|
+
enumerates the v14.2 sources for each `code` value (`not-found` /
|
|
844
|
+
`bad-query` / `internal` / reserved `db-missing`). Stability stays
|
|
845
|
+
`experimental — locks at v0.6.0` (no change). Additive minor per
|
|
846
|
+
`versioning.md` § Pre-1.0; no breaking change — the v14.1 endpoint
|
|
847
|
+
catalogue was a stub explicitly slated to fill at v14.2.
|
|
848
|
+
|
|
228
849
|
- **`sm serve` row + `### Server` subsection** in `cli-contract.md` —
|
|
229
850
|
Step 14.1 promotes `sm serve` from an implementation-defined stub to a
|
|
230
851
|
documented surface. The verb row at `§Verb catalog` › `### Server`
|
|
@@ -1229,9 +1850,9 @@ kind, normalizedTrigger)` and prints one row per group with the
|
|
|
1229
1850
|
(`Links out (12, 9 unique)`). When N > 1 detector emits the same
|
|
1230
1851
|
logical link, the row also gets a `(×N)` suffix.
|
|
1231
1852
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1853
|
+
`--json` output is byte-identical to before — raw rows, no merge.
|
|
1854
|
+
Storage is byte-identical to before. The grouping is purely a
|
|
1855
|
+
read-time presentation choice for human eyes.
|
|
1235
1856
|
|
|
1236
1857
|
**Spec changes (patch)**:
|
|
1237
1858
|
|
package/cli-contract.md
CHANGED
|
@@ -316,7 +316,7 @@ Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interacti
|
|
|
316
316
|
|
|
317
317
|
| Command | Purpose |
|
|
318
318
|
|---|---|
|
|
319
|
-
| `sm serve [--port N] [--host ...] [--scope project\|global] [--db <path>] [--no-built-ins] [--no-plugins] [--open\|--no-open] [--dev-cors] [--ui-dist <path>]` | Start Hono + WebSocket for the Web UI. Single-port mandate: SPA + REST + WS under one listener. Default port 4242, default host 127.0.0.1 (loopback-only through v0.6.0; multi-host deferred — see §Server). |
|
|
319
|
+
| `sm serve [--port N] [--host ...] [--scope project\|global] [--db <path>] [--no-built-ins] [--no-plugins] [--open\|--no-open] [--dev-cors] [--ui-dist <path>] [--no-watcher]` | Start Hono + WebSocket for the Web UI. Single-port mandate: SPA + REST + WS under one listener. Default port 4242, default host 127.0.0.1 (loopback-only through v0.6.0; multi-host deferred — see §Server). The watcher is on by default (Decision #121: a server with stale DB is a footgun); pass `--no-watcher` for CI / read-only deployments. |
|
|
320
320
|
|
|
321
321
|
#### Server
|
|
322
322
|
|
|
@@ -326,14 +326,25 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
|
|
|
326
326
|
|
|
327
327
|
**Boot resilience**: `sm serve` boots even when the project DB is missing. `/api/health` reports `db: 'missing'` so the SPA can render an empty-state CTA instead of failing the connection. Explicit `--db <path>` that doesn't exist is the exception — that exits 5 (NotFound) per `§Exit codes`.
|
|
328
328
|
|
|
329
|
-
**Endpoints (v14.
|
|
329
|
+
**Endpoints (v14.2 surface)**:
|
|
330
330
|
|
|
331
331
|
| Path | Status | Shape |
|
|
332
332
|
|---|---|---|
|
|
333
333
|
| `GET /api/health` | implemented | `{ ok: true, schemaVersion, specVersion, implVersion, scope: 'project'\|'global', db: 'present'\|'missing' }` |
|
|
334
|
-
| `
|
|
335
|
-
| `GET /
|
|
336
|
-
| `GET
|
|
334
|
+
| `GET /api/scan` | implemented | latest persisted `ScanResult` (1:1 with `scan-result.schema.json`; byte-equal to `sm scan --json` modulo whitespace). DB absent → empty `ScanResult` shape (zero `nodes` / `links` / `issues`). |
|
|
335
|
+
| `GET /api/scan?fresh=1` | implemented | runs an in-memory scan and returns the produced `ScanResult` without persistence. Rejects with `bad-query` (400) when the server was started with `--no-built-ins` or `--no-plugins` (would yield empty / partial results). |
|
|
336
|
+
| `GET /api/nodes?kind=&hasIssues=&path=&limit=&offset=` | implemented | `RestEnvelope` (`kind: 'nodes'`) — paginated, filtered list. Filters share the `kind=` / `has=issues` / `path=<glob>` grammar with `sm export`. `hasIssues=false` is a server-side post-filter (not representable in the kernel grammar). Pagination defaults `offset=0`, `limit=100`; max `limit=1000`. |
|
|
337
|
+
| `GET /api/nodes/:pathB64[?include=body]` | implemented | Single-node detail envelope: `{ schemaVersion, kind: 'node', item: Node, links: { incoming: Link[], outgoing: Link[] }, issues: Issue[] }`. `:pathB64` is base64url (RFC 4648 §5, no padding) of `node.path`. Missing node or malformed `pathB64` → 404 `not-found`. **`?include=body`** (Step 14.5.a) — opt-in flag that adds `item.body: string \| null` to the response. The body is read from disk on demand at request time (the kernel persists `bodyHash` only). `null` indicates the source file was missing / unreadable when the request landed (the watcher will re-emit `scan.completed` when it catches up). Without the flag, `item.body` is `undefined` and the handler does not touch the filesystem. |
|
|
338
|
+
| `GET /api/links?kind=&from=&to=` | implemented | `RestEnvelope` (`kind: 'links'`) — list of links. Filters: `kind` (CSV whitelist of `link.kind`), `from` (exact match on `link.source`), `to` (exact match on `link.target`). No pagination at v14.2. |
|
|
339
|
+
| `GET /api/issues?severity=&ruleId=&node=` | implemented | `RestEnvelope` (`kind: 'issues'`) — list of issues. Filters: `severity` (CSV from `error\|warn\|info`), `ruleId` (CSV; qualified or short suffix per `sm check --rules`), `node` (filter to issues whose `nodeIds` includes the path). No pagination at v14.2. |
|
|
340
|
+
| `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`. |
|
|
341
|
+
| `GET /api/config` | implemented | `RestEnvelope` (`kind: 'config'`) — merged effective config (defaults → user → user-local → project → project-local → override). |
|
|
342
|
+
| `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'\|'global' }`. |
|
|
343
|
+
| `ALL /api/*` (other) | reserved | structured 404 envelope (see below); future endpoints land in subsequent sub-steps. |
|
|
344
|
+
| `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. |
|
|
345
|
+
| `GET *` | implemented | static asset from the resolved UI bundle, falling back to `index.html` for SPA deep links. |
|
|
346
|
+
|
|
347
|
+
List endpoints conform to [`schemas/api/rest-envelope.schema.json`](schemas/api/rest-envelope.schema.json). The `/api/scan` and `/api/health` responses carry their underlying `ScanResult` / `IHealthResponse` shapes directly (no envelope wrap). The `/api/graph` response carries the formatter's native textual output.
|
|
337
348
|
|
|
338
349
|
**Error envelope** (mirrors `§Machine-readable output rules`):
|
|
339
350
|
|
|
@@ -350,6 +361,13 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
|
|
|
350
361
|
|
|
351
362
|
HTTP status mapping: `400` → `bad-query`, `404` → `not-found`, `500` → `internal` / `db-missing`.
|
|
352
363
|
|
|
364
|
+
Error code sources at v14.2:
|
|
365
|
+
|
|
366
|
+
- `not-found` (404) — unknown `/api/*` path; missing node on `/api/nodes/:pathB64`; malformed `pathB64` (treated as "no such node" so the client UX is uniform).
|
|
367
|
+
- `bad-query` (400) — `ExportQueryError` from `parseExportQuery`; pagination beyond `limit ≤ 1000`; non-integer / negative `limit` / `offset`; unknown formatter on `/api/graph`; `?fresh=1` when the server started with `--no-built-ins` or `--no-plugins`.
|
|
368
|
+
- `internal` (500) — uncaught error during a request (e.g. config-load failure, DB corruption surfacing through `loadScanResult`).
|
|
369
|
+
- `db-missing` (500) — reserved for endpoints that cannot degrade to an empty result. The v14.2 routes uniformly degrade (`/api/scan` returns the empty shape; list endpoints return zero items) so this code is not currently emitted by any handler — it is documented for future endpoints (post-v0.6.0 mutations) where degradation is not safe.
|
|
370
|
+
|
|
353
371
|
**Flag surface**:
|
|
354
372
|
|
|
355
373
|
| Flag | Default | Purpose |
|
|
@@ -363,8 +381,32 @@ HTTP status mapping: `400` → `bad-query`, `404` → `not-found`, `500` → `in
|
|
|
363
381
|
| `--open` / `--no-open` | `--open` | Auto-open the SPA in the user's default browser after listen. |
|
|
364
382
|
| `--dev-cors` | off | Enable permissive CORS for the Angular dev-server proxy workflow. Loopback-only when set. |
|
|
365
383
|
| `--ui-dist <path>` | auto | Override the UI bundle directory. Hidden flag — used by the demo build pipeline + tests; everyday users never need it. |
|
|
366
|
-
|
|
367
|
-
|
|
384
|
+
| `--no-watcher` | off | Disable the chokidar-fed scan-and-broadcast loop. Use only for CI / read-only deployments — without the watcher, `/ws` stays open but no `scan.*` events ever fire. Combining with `--no-built-ins` is rejected (the watcher cannot run with an empty pipeline; would persist empty scans on every batch). |
|
|
385
|
+
|
|
386
|
+
**WebSocket protocol** *(Stability: experimental — locks at v0.6.0)*:
|
|
387
|
+
|
|
388
|
+
The `/ws` endpoint is the live-events channel for the SPA. Clients connect once at bootstrap, the server pushes events as they happen, and the SPA reconciles its in-memory store against the deltas. The wire envelope and `scan.*` payload shapes are normative in [`job-events.md`](./job-events.md) — the BFF emits them verbatim.
|
|
389
|
+
|
|
390
|
+
- **Wire format**: each event is a single WebSocket text frame carrying one JSON object that conforms to `job-events.md` §Common envelope (`type`, `timestamp`, `runId?`, `jobId? | null`, `data`).
|
|
391
|
+
- **Event catalog at v14.4.a**:
|
|
392
|
+
- `scan.started` (per `job-events.md` §Scan events line 325).
|
|
393
|
+
- `scan.progress` (per `job-events.md` line 345 — emitted by the kernel orchestrator at every node; throttling deferred to a follow-up).
|
|
394
|
+
- `scan.completed` (per `job-events.md` line 363).
|
|
395
|
+
- `extractor.completed` (per `job-events.md` line 384) and `rule.completed` (per `job-events.md` line 404) ride along as side effects of the same emitter bridge.
|
|
396
|
+
- `extension.error` (kernel-internal — emitted when an extension violates its declared contract; the BFF forwards verbatim).
|
|
397
|
+
- `watcher.started` and `watcher.error` — BFF-internal advisories. Non-normative; consumers MUST ignore unknown event types per the forward-compatibility rule.
|
|
398
|
+
- **Deferred to a follow-up**: `issue.added` / `issue.resolved` (per `job-events.md` §Issue events line 446) and `scan.failed`. The 14.4.a surface fans out only the events the kernel emitter already produces; the diff-based issue events and a dedicated batch-failure event require additional plumbing inside the BFF watcher loop.
|
|
399
|
+
- **Connection lifecycle**:
|
|
400
|
+
1. Client opens `ws://<host>:<port>/ws`. The server completes the WS handshake and registers the underlying socket with the broadcaster.
|
|
401
|
+
2. Server pushes events. The client sends nothing at v14.4.a — `onMessage` is intentionally not registered. A future heartbeat / subscribe / filter request lands in a follow-up.
|
|
402
|
+
3. Server has NO state push on connect (no replay of last events). The client SHOULD poll `/api/scan` once on connect to seed initial state, then rely on `/ws` for deltas.
|
|
403
|
+
4. On normal disconnect: client closes with code 1000 ('normal closure') or 1001 ('going away'). The broadcaster unregisters silently.
|
|
404
|
+
5. On server shutdown (SIGINT / SIGTERM): the broadcaster sends close code 1001 + reason `'server shutdown'` to every client, then closes the http listener.
|
|
405
|
+
6. **Backpressure**: if a client's outbound buffer (`bufferedAmount`) exceeds an implementation-defined threshold (the reference impl uses 4 MiB), the broadcaster closes that client with code 1009 ('message too big') and unregisters it. Clients SHOULD reconnect after backpressure eviction with a fresh `/api/scan` poll.
|
|
406
|
+
7. **Reconnect responsibility**: the server does NOT reconnect on the client's behalf and does NOT replay missed events on reconnect. The client SHOULD treat `/ws` as a best-effort delta channel and re-seed via `/api/scan` whenever the connection drops.
|
|
407
|
+
- **Loopback-only assumption (Decision #119)**: no per-connection authentication on `/ws` through v0.6.0. The transport security boundary is the `--host` flag (defaults to `127.0.0.1`); the server rejects `--dev-cors` combined with a non-loopback `--host` precisely because that combination would expose `/ws` over the network without auth. Multi-host serve and an auth model re-open post-v0.6.0.
|
|
408
|
+
|
|
409
|
+
**Graceful shutdown**: SIGINT / SIGTERM trigger a graceful close; the verb returns exit 0 on clean shutdown. Bind failure (port in use, EACCES) returns exit 2. The shutdown sequence drains the in-flight watcher batch (if any), closes every WS client with code 1001, then closes the http listener.
|
|
368
410
|
|
|
369
411
|
---
|
|
370
412
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://skill-map.dev/spec/v0/conformance-case.schema.json",
|
|
3
|
+
"id": "plugin-missing-ui-rejected",
|
|
4
|
+
"description": "A drop-in Provider plugin whose `kinds[*]` entry omits the required `ui` block (Step 14.5.d) MUST be rejected by the loader with a clear `missing required property 'ui'` diagnostic on stderr, AND `sm scan` MUST exit cleanly with the rest of the pipeline (built-in Claude Provider) still running. Locks the contract that one bad plugin does not take down the scan.",
|
|
5
|
+
"fixture": "plugin-missing-ui",
|
|
6
|
+
"invoke": {
|
|
7
|
+
"verb": "scan",
|
|
8
|
+
"flags": ["--json"]
|
|
9
|
+
},
|
|
10
|
+
"assertions": [
|
|
11
|
+
{ "type": "exit-code", "value": 0 },
|
|
12
|
+
{ "type": "stderr-matches", "pattern": "plugin bad-provider:.*invalid.*must have required property 'ui'" },
|
|
13
|
+
{ "type": "json-path", "path": "$.providers.length", "equals": 1 },
|
|
14
|
+
{ "type": "json-path", "path": "$.providers[0]", "equals": "claude" },
|
|
15
|
+
{ "type": "json-path", "path": "$.nodes.length", "equals": 1 }
|
|
16
|
+
]
|
|
17
|
+
}
|
package/conformance/coverage.md
CHANGED
|
@@ -32,8 +32,9 @@ This file is hand-maintained. A CI check before spec release compares the schema
|
|
|
32
32
|
| 22 | `extensions/formatter.schema.json` | — | 🔴 missing | Case: `ascii` formatter manifest validates. |
|
|
33
33
|
| 23 | `history-stats.schema.json` | — | 🔴 missing | Blocked by Step 5 (history). Case: seed `state_executions` with a deterministic fixture, run `sm history stats --json --since <T0> --until <T1> --period month --top 5`, assert the document validates and that `totals.executionsCount == sum(perAction.executionsCount)` and `errorRates.global == totals.failedCount / totals.executionsCount`. Percentiles (`p95`/`p99`) intentionally omitted in v1 — add later as a minor bump without breaking consumers. |
|
|
34
34
|
| 24 | `extensions/hook.schema.json` | — | 🔴 missing | Case: a `deterministic` hook manifest with `triggers: ['scan.completed']` validates; a hook declaring an unknown trigger (e.g. `scan.progress`) fails with `invalid-manifest` at load time. |
|
|
35
|
+
| 25 | `api/rest-envelope.schema.json` | — | 🔴 missing | Step 14.2 BFF list-envelope shape (`{ schemaVersion, kind, items \| item \| value, filters, counts }`). Case: hit `GET /api/nodes` against a primed scope, validate the response against the schema; assert the `oneOf` rejects an envelope that carries both `items` and `item`. Implementation-side coverage exists today (`src/test/server-endpoints.test.ts`) but a kernel-agnostic conformance case is required before v1.0.0 ships. |
|
|
35
36
|
|
|
36
|
-
> **Note on Provider-owned schemas.** Per spec 0.8.0 Phase 3, the per-kind frontmatter schemas (`skill`, `agent`, `command`, `hook`, `note`) live with the Provider that emits them — for the built-in Claude Provider, that is `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 (Phase 5 / A.13 — `src/extensions/providers/claude/conformance/coverage.md`). Phase 5 / A.13 also relocated the cases that exercised them (`basic-scan`, `rename-high`, `orphan-detection`) to the Provider's own `cases/` directory. The matrix shrinks from 28 to 23 rows accordingly. The Hook kind (A.11) brings it back up to 24.
|
|
37
|
+
> **Note on Provider-owned schemas.** Per spec 0.8.0 Phase 3, the per-kind frontmatter schemas (`skill`, `agent`, `command`, `hook`, `note`) live with the Provider that emits them — for the built-in Claude Provider, that is `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 (Phase 5 / A.13 — `src/extensions/providers/claude/conformance/coverage.md`). Phase 5 / A.13 also relocated the cases that exercised them (`basic-scan`, `rename-high`, `orphan-detection`) to the Provider's own `cases/` directory. The matrix shrinks from 28 to 23 rows accordingly. The Hook kind (A.11) brings it back up to 24, and Step 14.2's BFF envelope brings it to 25.
|
|
37
38
|
|
|
38
39
|
Status legend: 🟢 covered (at least one case asserts the schema end-to-end) · 🟡 partial (covered only indirectly or via a sub-shape) · 🔴 missing.
|
|
39
40
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Conformance fixture: provider whose `kinds[*]` entry deliberately
|
|
2
|
+
// omits the required `ui` block (Step 14.5.d). The plugin loader MUST
|
|
3
|
+
// reject this manifest with a clear "missing required property 'ui'"
|
|
4
|
+
// diagnostic and the plugin MUST end up in `invalid-manifest` status.
|
|
5
|
+
// The companion case `plugin-missing-ui-rejected.json` asserts the
|
|
6
|
+
// stderr text and that `sm scan` survives (the loader degrades the
|
|
7
|
+
// bad plugin and lets the rest of the pipeline continue).
|
|
8
|
+
export default {
|
|
9
|
+
kind: 'provider',
|
|
10
|
+
id: 'bad-provider-provider',
|
|
11
|
+
version: '0.1.0',
|
|
12
|
+
description: 'provider whose note kind is missing the ui block',
|
|
13
|
+
stability: 'experimental',
|
|
14
|
+
explorationDir: '~/.bad',
|
|
15
|
+
kinds: {
|
|
16
|
+
note: {
|
|
17
|
+
schema: './schemas/note.schema.json',
|
|
18
|
+
schemaJson: {
|
|
19
|
+
$id: 'urn:test:bad-provider/note',
|
|
20
|
+
type: 'object',
|
|
21
|
+
additionalProperties: true,
|
|
22
|
+
},
|
|
23
|
+
defaultRefreshAction: 'bad-provider/summarize-note',
|
|
24
|
+
// NOTE: deliberately no `ui` — this is what the case asserts.
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
async *walk() {},
|
|
28
|
+
classify() {
|
|
29
|
+
return 'note';
|
|
30
|
+
},
|
|
31
|
+
};
|
package/index.json
CHANGED
|
@@ -166,17 +166,21 @@
|
|
|
166
166
|
}
|
|
167
167
|
]
|
|
168
168
|
},
|
|
169
|
-
"specPackageVersion": "0.
|
|
169
|
+
"specPackageVersion": "0.13.0",
|
|
170
170
|
"integrity": {
|
|
171
171
|
"algorithm": "sha256",
|
|
172
172
|
"files": {
|
|
173
|
-
"CHANGELOG.md": "
|
|
173
|
+
"CHANGELOG.md": "3e4ba3c64fdd33110465741fb697234a5cec0b5b604caa6721ee872aaf6df177",
|
|
174
174
|
"README.md": "bd30780525e75379eaeb5f8a903bdc601daf3862f3ec69dffc96c437e8d476fc",
|
|
175
175
|
"architecture.md": "9a6d96d150af60ed8d476af572d07dcce605f116fde720bebb2662b11250bf4b",
|
|
176
|
-
"cli-contract.md": "
|
|
176
|
+
"cli-contract.md": "45c2c72a8ff9266fb67593ad5f4e86e806d58cec48a1844da02ad1aaa36b8085",
|
|
177
177
|
"conformance/README.md": "838b1247e1ffb402d96bd8a0fe9c1c0f4a99ed0fbc4bf8156f7a58330cac27f5",
|
|
178
178
|
"conformance/cases/kernel-empty-boot.json": "ad4bbe9d637537625025c8bdb61285b1433568a2544b1ce0248f304ccff8c350",
|
|
179
|
-
"conformance/
|
|
179
|
+
"conformance/cases/plugin-missing-ui-rejected.json": "c6ce8f62a430d662aea33dec8ebf6493be6455037be3114e0d93d0eb57777287",
|
|
180
|
+
"conformance/coverage.md": "bddece19aa42297e75b2b1f6595bc9bb72fc849332a421b099fbdd1e8789e77d",
|
|
181
|
+
"conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/plugin.json": "4d78af6f12faa9d131e2a19f1dbb8f250baacc525978f3a8c858932b95da4ff6",
|
|
182
|
+
"conformance/fixtures/plugin-missing-ui/.skill-map/plugins/bad-provider/provider.js": "da40b134d70f8bc8175cfa9c380ecb55d26b2240c8b467f22f3fcfab750c8747",
|
|
183
|
+
"conformance/fixtures/plugin-missing-ui/notes/example.md": "55767f0aa1b6774546a99f28c58e7b732aa9cfa5dfce8d0326470f7f622f577e",
|
|
180
184
|
"conformance/fixtures/preamble-v1.txt": "1e0aeef224b64477bdc13a949c3ad402e68249caf499ecdba1302371677c068b",
|
|
181
185
|
"db-schema.md": "cbd2d3395ca4f01065d6f15405c7442d59a468b15448377d6f9373fa6aeff334",
|
|
182
186
|
"interfaces/security-scanner.md": "4a982667008f233656f44c61ce9948e062432d3debdcbf7a134da03bd4139d7d",
|
|
@@ -185,6 +189,7 @@
|
|
|
185
189
|
"plugin-author-guide.md": "2dcdf8c570342d94c2c8f8d47594715254e3956d7939443023f1d6420e2b30d0",
|
|
186
190
|
"plugin-kv-api.md": "04b2178f46fb88adeae9240df9c9e1761b660396072001dac32cd402e11a2d7d",
|
|
187
191
|
"prompt-preamble.md": "fb40ab510234383326f198dec82cd6d744f28b7432eebac6cbfbb7ca1c483b7d",
|
|
192
|
+
"schemas/api/rest-envelope.schema.json": "7d9d74bcb2158019cb6e30306d40b9c7ffc67e9d202fb8210fe4e4a9e8fa4dab",
|
|
188
193
|
"schemas/conformance-case.schema.json": "7cd0f3aae5124f24be57cddb213d002d0466f79d06fd3da896075c8b28650410",
|
|
189
194
|
"schemas/execution-record.schema.json": "9628fa557cb856402f3a5f1d1167c609e46a197c850fe8171abfddd46c1028a8",
|
|
190
195
|
"schemas/extensions/action.schema.json": "262272175c06a2e33c08f819a45c3ef8260276c91a9d0542fdffc932aeb32db7",
|
|
@@ -192,7 +197,7 @@
|
|
|
192
197
|
"schemas/extensions/extractor.schema.json": "122d3f81ef91edcde9798e7dc8fcbf442a2996deea65aa4b03c9d5cb01ba2519",
|
|
193
198
|
"schemas/extensions/formatter.schema.json": "2ab092aa37ae349c69b93071ed4f0e131affb7bb5799516ca82c721262631b36",
|
|
194
199
|
"schemas/extensions/hook.schema.json": "7465c38e0765edf23e49d4f96c539d04323f1cf564af1c60ee637c79a6d39239",
|
|
195
|
-
"schemas/extensions/provider.schema.json": "
|
|
200
|
+
"schemas/extensions/provider.schema.json": "518d7666841cfef8eb28aab788a6e6dfabf9e12f3f06de1c81be915cbe6c1088",
|
|
196
201
|
"schemas/extensions/rule.schema.json": "8ff420bde498f50db114c352305d487c71aef2dd746fd0c24976ff6a09865c22",
|
|
197
202
|
"schemas/frontmatter/base.schema.json": "dfee192458765b8cb872ef9e7145ec31b9e07ceb19ee44be48af2172329e7a38",
|
|
198
203
|
"schemas/history-stats.schema.json": "23f472d1de06d23fc775aabba821f8375f347af4dc8d89ba567980d61a11f9de",
|
package/package.json
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://skill-map.dev/spec/v0/api/rest-envelope.schema.json",
|
|
4
|
+
"title": "RestEnvelope",
|
|
5
|
+
"description": "Wrapper shape for REST responses under `/api/*` (Step 14.2). Three variants distinguished by the `kind` discriminator and which payload field is present (`items` for list kinds, `item` for single-resource kinds, `value` for `kind: 'config'`). The `/api/scan` and `/api/health` responses are exempt — they carry the underlying `ScanResult` / `IHealthResponse` shape directly. The `/api/graph` response is also exempt — it returns the formatter's native textual output (text/plain or text/markdown). Step 14.5.d adds the required `kindRegistry` field on every payload-bearing variant so the UI can render Provider-declared kinds (label, color, icon) without hardcoding visuals; sentinel kinds (`health`, `scan`, `graph`) stay exempt because they don't carry an envelope payload. The change keeps `schemaVersion` at `'1'` — the BFF is greenfield (no released consumers run against `'1'` without `kindRegistry`), so a versioned migration buys nothing.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schemaVersion", "kind"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"const": "1",
|
|
12
|
+
"description": "Envelope shape version. Bumped only on breaking changes. The Step 14.5.d addition of `kindRegistry` keeps the version at `'1'` because there are no released consumers in the wild."
|
|
13
|
+
},
|
|
14
|
+
"kind": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["nodes", "links", "issues", "plugins", "config", "graph", "node", "health", "scan"],
|
|
17
|
+
"description": "Discriminator. List kinds (`nodes`, `links`, `issues`, `plugins`) carry `items`. The `node` kind carries `item`. The `config` kind carries `value`. The `health` / `scan` / `graph` values are reserved for documentation parity with the routes that DON'T use this envelope."
|
|
18
|
+
},
|
|
19
|
+
"items": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"description": "Present when `kind` is one of the list kinds (`nodes`, `links`, `issues`, `plugins`). Empty array is valid and means the filter matched zero rows."
|
|
22
|
+
},
|
|
23
|
+
"item": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"description": "Present when `kind` is `'node'`. Single-resource envelope payload."
|
|
26
|
+
},
|
|
27
|
+
"value": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"description": "Present when `kind` is `'config'`. Carries the merged effective config object."
|
|
30
|
+
},
|
|
31
|
+
"filters": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"description": "Echo of the URL filters the server applied, normalized into a JSON-friendly shape (arrays for multi-value filters, `null` for absent ones). Helps the client correlate the response with the request."
|
|
34
|
+
},
|
|
35
|
+
"kindRegistry": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"description": "Catalog of node kinds active in the current scope, keyed by kind name. Built once per server boot from every enabled Provider's `kinds` map and embedded into every payload-bearing envelope so the UI can render kind tags / palette swatches / graph nodes against Provider-declared visuals (label, color, icon) without ever hardcoding a closed kind enum. Sentinel envelopes (`health`, `scan`, `graph`) are exempt.",
|
|
38
|
+
"additionalProperties": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"required": ["providerId", "label", "color"],
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"properties": {
|
|
43
|
+
"providerId": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"minLength": 1,
|
|
46
|
+
"description": "Id of the Provider that contributed this kind. Lets the UI scope kind ownership and disambiguate when two Providers happen to declare the same kind name (kernel surfaces this as `provider-ambiguous` but the UI may still receive the merged registry during the conflict window)."
|
|
47
|
+
},
|
|
48
|
+
"label": { "type": "string", "minLength": 1 },
|
|
49
|
+
"color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
|
|
50
|
+
"colorDark": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
|
|
51
|
+
"emoji": { "type": "string", "minLength": 1, "maxLength": 8 },
|
|
52
|
+
"icon": {
|
|
53
|
+
"oneOf": [
|
|
54
|
+
{
|
|
55
|
+
"type": "object",
|
|
56
|
+
"required": ["kind", "id"],
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"properties": {
|
|
59
|
+
"kind": { "const": "pi" },
|
|
60
|
+
"id": { "type": "string", "pattern": "^pi-[a-z0-9]+(-[a-z0-9]+)*$" }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "object",
|
|
65
|
+
"required": ["kind", "path"],
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"properties": {
|
|
68
|
+
"kind": { "const": "svg" },
|
|
69
|
+
"path": { "type": "string", "minLength": 1 }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"counts": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["total", "returned"],
|
|
80
|
+
"properties": {
|
|
81
|
+
"total": {
|
|
82
|
+
"type": "integer",
|
|
83
|
+
"minimum": 0,
|
|
84
|
+
"description": "Total rows after filtering, before pagination is applied."
|
|
85
|
+
},
|
|
86
|
+
"returned": {
|
|
87
|
+
"type": "integer",
|
|
88
|
+
"minimum": 0,
|
|
89
|
+
"description": "Rows actually carried in `items` (≤ `limit`)."
|
|
90
|
+
},
|
|
91
|
+
"page": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"required": ["offset", "limit"],
|
|
94
|
+
"properties": {
|
|
95
|
+
"offset": {
|
|
96
|
+
"type": "integer",
|
|
97
|
+
"minimum": 0,
|
|
98
|
+
"description": "Pagination window offset (zero-based)."
|
|
99
|
+
},
|
|
100
|
+
"limit": {
|
|
101
|
+
"type": "integer",
|
|
102
|
+
"minimum": 0,
|
|
103
|
+
"description": "Maximum items the page may carry."
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"additionalProperties": false,
|
|
107
|
+
"description": "Pagination window. Present when the endpoint paginates (today: `/api/nodes` only)."
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"additionalProperties": false,
|
|
111
|
+
"description": "Tally + paging info. Present on every list / single envelope; absent on `health` / `scan` / `graph` responses (which don't use this envelope)."
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"oneOf": [
|
|
115
|
+
{
|
|
116
|
+
"description": "List envelope — `items` payload + `counts` + `kindRegistry`.",
|
|
117
|
+
"required": ["items", "counts", "filters", "kindRegistry"],
|
|
118
|
+
"properties": {
|
|
119
|
+
"kind": { "enum": ["nodes", "links", "issues", "plugins"] }
|
|
120
|
+
},
|
|
121
|
+
"not": {
|
|
122
|
+
"anyOf": [
|
|
123
|
+
{ "required": ["item"] },
|
|
124
|
+
{ "required": ["value"] }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"description": "Single-resource envelope — `item` payload + `kindRegistry`, no `counts` / `filters`.",
|
|
130
|
+
"required": ["item", "kindRegistry"],
|
|
131
|
+
"properties": {
|
|
132
|
+
"kind": { "const": "node" }
|
|
133
|
+
},
|
|
134
|
+
"not": {
|
|
135
|
+
"anyOf": [
|
|
136
|
+
{ "required": ["items"] },
|
|
137
|
+
{ "required": ["value"] }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"description": "Value envelope — `value` payload + `kindRegistry`, no `counts` / `filters`.",
|
|
143
|
+
"required": ["value", "kindRegistry"],
|
|
144
|
+
"properties": {
|
|
145
|
+
"kind": { "const": "config" }
|
|
146
|
+
},
|
|
147
|
+
"not": {
|
|
148
|
+
"anyOf": [
|
|
149
|
+
{ "required": ["items"] },
|
|
150
|
+
{ "required": ["item"] }
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"description": "Sentinel kinds — reserved for routes that do NOT carry an envelope payload at the wire level (`health`, `scan`, `graph`). They do not carry `kindRegistry` either; clients that need it must call any payload-bearing endpoint at boot.",
|
|
156
|
+
"properties": {
|
|
157
|
+
"kind": { "enum": ["health", "scan", "graph"] }
|
|
158
|
+
},
|
|
159
|
+
"not": {
|
|
160
|
+
"anyOf": [
|
|
161
|
+
{ "required": ["items"] },
|
|
162
|
+
{ "required": ["item"] },
|
|
163
|
+
{ "required": ["value"] },
|
|
164
|
+
{ "required": ["kindRegistry"] }
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
"additionalProperties": false
|
|
170
|
+
}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"additionalProperties": {
|
|
33
33
|
"type": "object",
|
|
34
|
-
"required": ["schema", "defaultRefreshAction"],
|
|
34
|
+
"required": ["schema", "defaultRefreshAction", "ui"],
|
|
35
35
|
"additionalProperties": false,
|
|
36
36
|
"properties": {
|
|
37
37
|
"schema": {
|
|
@@ -43,6 +43,66 @@
|
|
|
43
43
|
"type": "string",
|
|
44
44
|
"pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*/[a-z][a-z0-9]*(-[a-z0-9]+)*$",
|
|
45
45
|
"description": "Qualified action id (`<plugin-id>/<action-id>`) the UI's probabilistic-refresh surface (`🧠 prob`) dispatches for nodes of this kind. The action MUST exist in the registry by the time the graph is queried; a dangling reference disables the Provider with status `invalid-manifest`."
|
|
46
|
+
},
|
|
47
|
+
"ui": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"required": ["label", "color"],
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"description": "Presentation metadata the UI uses to render nodes of this kind (palette swatches, list tags, graph nodes, filter chips). Required so the UI never has to invent visuals for a kind a Provider declares. The Provider declares intent (label + base color, optional dark variant + emoji + icon); the UI derives bg/fg tints from `color` per theme via a deterministic helper. Reaches the UI via the `kindRegistry` field embedded in REST envelopes (`api/rest-envelope.schema.json`).",
|
|
52
|
+
"properties": {
|
|
53
|
+
"label": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"minLength": 1,
|
|
56
|
+
"description": "Plural human-readable label for groups of this kind (e.g. `'Skills'`, `'Agents'`, `'Cursor Rules'`). Used in filter dropdowns, palette tooltips, and any list grouping."
|
|
57
|
+
},
|
|
58
|
+
"color": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"pattern": "^#[0-9a-fA-F]{6}$",
|
|
61
|
+
"description": "Base hex color (light theme). The UI derives `bg` and `fg` tints from this value at runtime; declaring a single base value (instead of three) keeps the manifest small and lets the UI control accessibility-driven contrast."
|
|
62
|
+
},
|
|
63
|
+
"colorDark": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"pattern": "^#[0-9a-fA-F]{6}$",
|
|
66
|
+
"description": "Optional dark-theme variant of `color`. When absent, the UI falls back to `color`. Declared explicitly because a luminosity flip rarely matches the brand intent for kinds that should stand out in dark mode."
|
|
67
|
+
},
|
|
68
|
+
"emoji": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"minLength": 1,
|
|
71
|
+
"maxLength": 8,
|
|
72
|
+
"description": "Optional decorative emoji used as a fallback when `icon` is absent or fails to render. Bound to a small length so the UI can lay it out predictably alongside text."
|
|
73
|
+
},
|
|
74
|
+
"icon": {
|
|
75
|
+
"description": "Optional discriminated icon descriptor. The UI prefers `icon` over `emoji`; when both are absent, the UI falls back to the first letter of `label` colored with `color`.",
|
|
76
|
+
"oneOf": [
|
|
77
|
+
{
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["kind", "id"],
|
|
80
|
+
"additionalProperties": false,
|
|
81
|
+
"properties": {
|
|
82
|
+
"kind": { "const": "pi" },
|
|
83
|
+
"id": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"pattern": "^pi-[a-z0-9]+(-[a-z0-9]+)*$",
|
|
86
|
+
"description": "PrimeIcons identifier (e.g. `pi-cog`, `pi-bolt`). Matched verbatim against the `pi pi-<id>` class the UI emits."
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"type": "object",
|
|
92
|
+
"required": ["kind", "path"],
|
|
93
|
+
"additionalProperties": false,
|
|
94
|
+
"properties": {
|
|
95
|
+
"kind": { "const": "svg" },
|
|
96
|
+
"path": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"minLength": 1,
|
|
99
|
+
"description": "Raw SVG path data (the `d` attribute of one or more `<path>` elements, joined). The UI wraps it in `<svg viewBox=\"0 0 24 24\"><path d=\"...\"/></svg>` and tints it with `currentColor`."
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
46
106
|
}
|
|
47
107
|
}
|
|
48
108
|
}
|