@skill-map/spec 0.12.0 → 0.13.1
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 +48 -1923
- package/architecture.md +24 -7
- package/cli-contract.md +51 -7
- package/conformance/README.md +2 -2
- package/conformance/cases/plugin-missing-ui-rejected.json +17 -0
- package/conformance/coverage.md +13 -4
- 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/db-schema.md +1 -1
- package/index.json +14 -9
- package/package.json +1 -1
- package/plugin-author-guide.md +21 -11
- package/schemas/api/rest-envelope.schema.json +170 -0
- package/schemas/extensions/provider.schema.json +61 -1
package/architecture.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
Normative description of skill-map's internal boundaries: the **kernel**, the **ports** it exposes, the **adapters** that drive and serve it, and the
|
|
3
|
+
Normative description of skill-map's internal boundaries: the **kernel**, the **ports** it exposes, the **adapters** that drive and serve it, and the six **extension kinds** that live outside the kernel.
|
|
4
4
|
|
|
5
5
|
Any conforming implementation — reference or third-party — MUST respect these boundaries. The conformance suite under [`conformance/`](./conformance/README.md) enforces the kernel-agnostic invariants; per-Provider suites (e.g. `src/extensions/providers/claude/conformance/` for the reference impl's Claude Provider) enforce the kind-catalog cases. Both are driven via `sm conformance run`.
|
|
6
6
|
|
|
@@ -150,7 +150,7 @@ Mode is a property of the extension as a whole, not of an individual call. **An
|
|
|
150
150
|
|
|
151
151
|
Provider and Formatter are locked to deterministic because they sit at the **boundaries** of the system. A Provider resolves `path → kind` during boot; probabilistic classification would make the boot phase slow, costly, and non-reproducible. A formatter must produce diffable output (`sm scan` snapshots round-trip in CI). Probabilistic narrators of the graph are a valid product but they live in jobs and emit Findings, not in formatters.
|
|
152
152
|
|
|
153
|
-
> **Naming note — `Provider` vs hexagonal `adapter`.**
|
|
153
|
+
> **Naming note — `Provider` vs hexagonal `adapter`.** A `Provider` is an **extension** authored by plugins (it recognises a platform and declares its kind catalog). The hexagonal-architecture term `adapter` refers to **port implementations** internal to the kernel package — `RunnerPort.adapter`, `StoragePort.adapter`, `FilesystemPort.adapter`, `PluginLoaderPort.adapter` — and lives under `kernel/adapters/`. The two concepts share an architectural lineage (both bridge two worlds) but live in deliberately disjoint namespaces so plugin authors and impl maintainers never confuse them.
|
|
154
154
|
|
|
155
155
|
### When each mode runs
|
|
156
156
|
|
|
@@ -182,12 +182,27 @@ Six kinds, all first-class, all loaded through the same registry. Each kind has
|
|
|
182
182
|
|
|
183
183
|
### Provider · `kinds` catalog
|
|
184
184
|
|
|
185
|
-
Every `Provider`
|
|
185
|
+
Every `Provider` MUST declare a non-empty map `kinds: { <kind>: { schema, defaultRefreshAction, ui } }` covering every `kind` it classifies into. Each entry carries three required fields:
|
|
186
186
|
|
|
187
|
-
- **`schema`** — path to the
|
|
188
|
-
- **`defaultRefreshAction`** — qualified action id (`<plugin-id>/<action-id>`) the UI's probabilistic-refresh surface (`🧠 prob`) dispatches for nodes of this kind. The
|
|
187
|
+
- **`schema`** — path (relative to the Provider package) to the kind's frontmatter JSON Schema. The schema MUST extend [`frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) via `allOf` + `$ref` to base's `$id`. The kernel registers it with AJV at boot and validates every node's frontmatter against the entry matching its classified kind.
|
|
188
|
+
- **`defaultRefreshAction`** — 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; a dangling reference disables the Provider with status `invalid-manifest`. Plugins MAY override per-node via `metadata.refreshAction`; the Provider default is normative.
|
|
189
|
+
- **`ui`** — presentation block: `{ label, color, colorDark?, emoji?, icon? }`. See §Provider · `ui` presentation below.
|
|
189
190
|
|
|
190
|
-
The catalog is the single source of truth for "which kinds does this Provider emit" — the `IProvider` runtime contract derives the kind set from `Object.keys(kinds)`.
|
|
191
|
+
The catalog is the single source of truth for "which kinds does this Provider emit" — the `IProvider` runtime contract derives the kind set from `Object.keys(kinds)`.
|
|
192
|
+
|
|
193
|
+
### Provider · `ui` presentation
|
|
194
|
+
|
|
195
|
+
Each `kinds[*].ui` entry declares how the UI renders nodes of that kind:
|
|
196
|
+
|
|
197
|
+
- **`label`** — short human name (e.g. `'Skill'`, `'Agent'`). Used in palette chips, list view, inspector header.
|
|
198
|
+
- **`color`** — base color (any CSS color string) for the kind. The UI derives bg / fg tints per theme via a deterministic helper, so the Provider declares one base color per theme rather than four hex values.
|
|
199
|
+
- **`colorDark?`** — optional dark-theme override. Defaults to `color` when omitted.
|
|
200
|
+
- **`emoji?`** — optional single-glyph emoji rendered alongside the label.
|
|
201
|
+
- **`icon?`** — optional discriminated union: either `{ kind: 'pi'; id: 'pi-…' }` (a PrimeIcons class id) or `{ kind: 'svg'; path: '…' }` (raw SVG path data wrapped by the UI in `viewBox="0 0 24 24"` and tinted with `currentColor`). The discriminator keeps UI dispatch exhaustive without string-sniffing; AJV validates each variant cleanly.
|
|
202
|
+
|
|
203
|
+
The `ui` block is required (not optional) by design: making it optional would force the UI to invent visuals for missing entries, silently collapsing unknown kinds to a default rendering and hiding manifest gaps. Forcing the Provider to declare presentation up-front means the UI never guesses.
|
|
204
|
+
|
|
205
|
+
The kernel ships every Provider's `ui` block to the BFF at boot; the BFF aggregates them into a `kindRegistry` map and embeds it in every payload-bearing REST envelope (see [`cli-contract.md` §Server](./cli-contract.md#server)). The UI consumes `kindRegistry` directly — built-in and user-plugin kinds render identically.
|
|
191
206
|
|
|
192
207
|
### Provider · `explorationDir`
|
|
193
208
|
|
|
@@ -400,7 +415,9 @@ This is what makes "CLI-first" a coherent rule: every CLI verb is a kernel funct
|
|
|
400
415
|
|
|
401
416
|
The **port list** is stable as of spec v1.0.0. Adding a sixth port is a major bump.
|
|
402
417
|
|
|
403
|
-
The **extension kind list** (
|
|
418
|
+
The **extension kind list** (6 kinds: Provider, Extractor, Rule, Action, Formatter, Hook) is stable as of spec v1.0.0. Adding a seventh kind is a major bump. Removing or renaming a kind is a major bump.
|
|
419
|
+
|
|
420
|
+
The **Hook curated trigger set** (eight events: `scan.started`, `scan.completed`, `extractor.completed`, `rule.completed`, `action.completed`, `job.spawning`, `job.completed`, `job.failed`) is stable as of spec v1.0.0. Adding a ninth trigger is a minor bump; removing or renaming any of the eight is a major bump.
|
|
404
421
|
|
|
405
422
|
The **execution modes** (`deterministic` / `probabilistic`) and the per-kind mode capability matrix above are stable as of spec v1.0.0. Adding a third mode or changing which kinds are dual-mode is a major bump. Renaming or repurposing the mode enum values is a major bump.
|
|
406
423
|
|
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,27 @@ 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.
|
|
348
|
+
|
|
349
|
+
**`kindRegistry` envelope field.** Every payload-bearing variant of the REST envelope (`nodes` / `links` / `issues` / `plugins` lists, the `node` single, the `config` value envelope) embeds a required `kindRegistry: { [kindName]: { providerId, label, color, colorDark?, emoji?, icon? } }` field. Sentinel envelopes (`health`, `scan`, `graph`) are exempt — they carry no payload at the wire level. The BFF assembles the registry once at boot from every enabled Provider's `kinds[*].ui` block (see [`architecture.md` §Provider · `ui` presentation](architecture.md#provider--ui-presentation)) and attaches the same map to every applicable response. The UI consumes `kindRegistry` directly to render kind palettes, list rows, and inspector headers — built-in and user-plugin kinds render identically. A kind appearing in a response payload (e.g. `node.kind`) without a matching `kindRegistry` entry is a contract violation; the kernel rejects Providers without a `ui` block at load time so the registry is always complete for whatever kinds appear in the response.
|
|
337
350
|
|
|
338
351
|
**Error envelope** (mirrors `§Machine-readable output rules`):
|
|
339
352
|
|
|
@@ -350,6 +363,13 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
|
|
|
350
363
|
|
|
351
364
|
HTTP status mapping: `400` → `bad-query`, `404` → `not-found`, `500` → `internal` / `db-missing`.
|
|
352
365
|
|
|
366
|
+
Error code sources at v14.2:
|
|
367
|
+
|
|
368
|
+
- `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).
|
|
369
|
+
- `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`.
|
|
370
|
+
- `internal` (500) — uncaught error during a request (e.g. config-load failure, DB corruption surfacing through `loadScanResult`).
|
|
371
|
+
- `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.
|
|
372
|
+
|
|
353
373
|
**Flag surface**:
|
|
354
374
|
|
|
355
375
|
| Flag | Default | Purpose |
|
|
@@ -363,8 +383,32 @@ HTTP status mapping: `400` → `bad-query`, `404` → `not-found`, `500` → `in
|
|
|
363
383
|
| `--open` / `--no-open` | `--open` | Auto-open the SPA in the user's default browser after listen. |
|
|
364
384
|
| `--dev-cors` | off | Enable permissive CORS for the Angular dev-server proxy workflow. Loopback-only when set. |
|
|
365
385
|
| `--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
|
-
|
|
386
|
+
| `--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). |
|
|
387
|
+
|
|
388
|
+
**WebSocket protocol** *(Stability: experimental — locks at v0.6.0)*:
|
|
389
|
+
|
|
390
|
+
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.
|
|
391
|
+
|
|
392
|
+
- **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`).
|
|
393
|
+
- **Event catalog at v14.4.a**:
|
|
394
|
+
- `scan.started` (per `job-events.md` §Scan events line 325).
|
|
395
|
+
- `scan.progress` (per `job-events.md` line 345 — emitted by the kernel orchestrator at every node; throttling deferred to a follow-up).
|
|
396
|
+
- `scan.completed` (per `job-events.md` line 363).
|
|
397
|
+
- `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.
|
|
398
|
+
- `extension.error` (kernel-internal — emitted when an extension violates its declared contract; the BFF forwards verbatim).
|
|
399
|
+
- `watcher.started` and `watcher.error` — BFF-internal advisories. Non-normative; consumers MUST ignore unknown event types per the forward-compatibility rule.
|
|
400
|
+
- **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.
|
|
401
|
+
- **Connection lifecycle**:
|
|
402
|
+
1. Client opens `ws://<host>:<port>/ws`. The server completes the WS handshake and registers the underlying socket with the broadcaster.
|
|
403
|
+
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.
|
|
404
|
+
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.
|
|
405
|
+
4. On normal disconnect: client closes with code 1000 ('normal closure') or 1001 ('going away'). The broadcaster unregisters silently.
|
|
406
|
+
5. On server shutdown (SIGINT / SIGTERM): the broadcaster sends close code 1001 + reason `'server shutdown'` to every client, then closes the http listener.
|
|
407
|
+
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.
|
|
408
|
+
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.
|
|
409
|
+
- **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.
|
|
410
|
+
|
|
411
|
+
**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
412
|
|
|
369
413
|
---
|
|
370
414
|
|
package/conformance/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Language-neutral test suite the specification demands. A conforming implementation passes every case; failing any case is a conformance bug.
|
|
4
4
|
|
|
5
|
-
The suite splits across two ownership boundaries
|
|
5
|
+
The suite splits across two ownership boundaries:
|
|
6
6
|
|
|
7
7
|
- **Spec-owned cases** — kernel-agnostic. They live in this directory and ship with `@skill-map/spec`. Today: `kernel-empty-boot` (boot invariant) and the `preamble-bitwise-match` deferred case. The universal preamble fixture (`preamble-v1.txt`) lives here too.
|
|
8
8
|
- **Provider-owned cases** — exercise a Provider's own `kinds` catalog. They live next to the Provider's manifest, under `<plugin-dir>/conformance/`. The reference impl ships one such suite at [`src/extensions/providers/claude/conformance/`](../../src/extensions/providers/claude/conformance/) covering Claude's five kinds (`skill` / `agent` / `command` / `hook` / `note`) via cases `basic-scan`, `rename-high`, `orphan-detection`.
|
|
@@ -154,7 +154,7 @@ for (const caseFile of await readdir('spec/conformance/cases')) {
|
|
|
154
154
|
}
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
A Provider-owned runner mirrors the loop with a different cases / fixtures root — `<plugin-dir>/conformance/cases/` and `<plugin-dir>/conformance/fixtures/`. The reference CLI ships both as `sm conformance run
|
|
157
|
+
A Provider-owned runner mirrors the loop with a different cases / fixtures root — `<plugin-dir>/conformance/cases/` and `<plugin-dir>/conformance/fixtures/`. The reference CLI ships both as `sm conformance run`; the verb resolves the spec scope via `@skill-map/spec` and discovers Provider scopes by walking each built-in plugin's `conformance/` directory.
|
|
158
158
|
|
|
159
159
|
The reference implementation's runner ships under `src/conformance/index.ts`; the verb lives at `src/cli/commands/conformance.ts` and uses the runner one case at a time.
|
|
160
160
|
|
|
@@ -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
|
@@ -18,22 +18,23 @@ This file is hand-maintained. A CI check before spec release compares the schema
|
|
|
18
18
|
| 8 | `job.schema.json` | — | 🔴 missing | Blocked by Step 10 (job system). Needs a case that submits a local action (no LLM), inspects `sm job show --json`. |
|
|
19
19
|
| 9 | `report-base.schema.json` | — | 🔴 missing | Indirect coverage once any summarizer case lands. Direct contract case: validate a handcrafted minimal report ({confidence, safety}) against the base schema. |
|
|
20
20
|
| 10 | `conformance-case.schema.json` | — | 🔴 missing | Self-referential: every `*.json` under `cases/` MUST validate against this schema. Add a meta-case that enumerates + validates all cases. |
|
|
21
|
-
| 11 | `frontmatter/base.schema.json` | — | 🔴 missing | Universal frontmatter shape. Per-kind schemas (skill / agent / command / hook / note)
|
|
21
|
+
| 11 | `frontmatter/base.schema.json` | — | 🔴 missing | Universal frontmatter shape. Per-kind schemas (`skill` / `agent` / `command` / `hook` / `note`) live with the Provider that emits them (the Claude Provider ships them under `src/extensions/providers/claude/schemas/`) and extend this base via `$ref`-by-`$id`. Direct spec-level case still pending: fixture with min-required frontmatter only, no Provider needed (Provider-disabled mode + a single `notes/<file>.md` with `name: ...` + `description: ...`). |
|
|
22
22
|
| 12 | `summaries/skill.schema.json` | — | 🔴 missing | Blocked by Step 10 (`skill-summarizer`). Case: submit summarizer, validate report. |
|
|
23
23
|
| 13 | `summaries/agent.schema.json` | — | 🔴 missing | Blocked by Step 11. |
|
|
24
24
|
| 14 | `summaries/command.schema.json` | — | 🔴 missing | Blocked by Step 11. |
|
|
25
25
|
| 15 | `summaries/hook.schema.json` | — | 🔴 missing | Blocked by Step 11. |
|
|
26
26
|
| 16 | `summaries/note.schema.json` | — | 🔴 missing | Blocked by Step 11. |
|
|
27
27
|
| 17 | `extensions/base.schema.json` | — | 🔴 missing | Meta-case: every manifest under `src/extensions/` validates against the appropriate kind schema (which extends base via `allOf`). |
|
|
28
|
-
| 18 | `extensions/provider.schema.json` |
|
|
28
|
+
| 18 | `extensions/provider.schema.json` | `plugin-missing-ui-rejected` | 🟡 partial | A drop-in Provider whose `kinds[*]` entry omits the required `ui` block fails AJV validation with `invalid-manifest` while the rest of the pipeline keeps running (built-in Claude Provider, exit 0). The complementary positive case (canonical Claude Provider manifest validates) lives in `provider:claude` conformance. Direct cases for missing `kinds` / `explorationDir` rejection still pending. |
|
|
29
29
|
| 19 | `extensions/extractor.schema.json` | — | 🔴 missing | Case: `frontmatter` + `slash` + `at-directive` extractor manifests validate; an extractor emitting a disallowed `emitsLinkKinds` value fails. |
|
|
30
30
|
| 20 | `extensions/rule.schema.json` | — | 🔴 missing | Case: `trigger-collision`, `broken-ref`, `superseded` manifests validate. |
|
|
31
31
|
| 21 | `extensions/action.schema.json` | — | 🔴 missing | Case: a `deterministic` action manifest validates; a `probabilistic` action WITHOUT `promptTemplateRef` fails. |
|
|
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
|
|
37
|
+
> **Note on Provider-owned schemas.** Per-kind frontmatter schemas (`skill`, `agent`, `command`, `hook`, `note`) 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.
|
|
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
|
|
|
@@ -51,7 +52,7 @@ These have their own conformance cases even though they are not JSON Schemas.
|
|
|
51
52
|
| F | Nonce mismatch | — | 🔴 missing | Blocked by Step 10. `sm record` with wrong nonce → exit 4. |
|
|
52
53
|
| G | Reap | — | 🔴 missing | Blocked by Step 10. Set TTL to 1s; claim; wait; next `sm job run` reaps with reason `abandoned`. |
|
|
53
54
|
| H | `run.*` event envelope for Skill agent | — | 🔴 missing | Blocked by Step 10. Skill-agent flow emits synthetic `r-ext-*` run envelope around one job. |
|
|
54
|
-
| I | Rename heuristic | `rename-high`, `orphan-detection` (Provider-owned) | 🟢 covered | High-confidence rename emits no issue and the new path is the sole node. Orphan branch emits exactly one `orphan` issue (severity `info`) when a deleted node has no replacement. Cases
|
|
55
|
+
| I | Rename heuristic | `rename-high`, `orphan-detection` (Provider-owned) | 🟢 covered | High-confidence rename emits no issue and the new path is the sole node. Orphan branch emits exactly one `orphan` issue (severity `info`) when a deleted node has no replacement. Cases live with the Claude Provider (they reach a Provider's `kinds` catalog by construction); see [`src/extensions/providers/claude/conformance/`](../../src/extensions/providers/claude/conformance/). Medium / ambiguous branches are exercised by `src/test/rename-heuristic.test.ts` until the conformance schema grows richer assertions. |
|
|
55
56
|
| J | Plugin DDL rejection | — | 🔴 missing | Blocked by Step 9. Plugin migration referencing `state_jobs` → disabled with `invalid-manifest`. |
|
|
56
57
|
| K | Plugin prefix injection | — | 🔴 missing | Blocked by Step 9. Plugin declares `CREATE TABLE foo` → kernel applies as `plugin_<id>_foo`. |
|
|
57
58
|
| L | Elapsed-time reporting | — | 🔴 missing | Blocked by Step 4 (first real verb work). Run any in-scope verb; stderr last line MUST match `/^done in (\d+ms\|\d+\.\d+s\|\d+m \d+s)$/`. In-scope verb with `--json` returning an object MUST carry `elapsedMs`. Exempt verb (`sm version`) MUST NOT emit the line. |
|
|
@@ -61,3 +62,11 @@ These have their own conformance cases even though they are not JSON Schemas.
|
|
|
61
62
|
- **spec v0.x**: partial coverage acceptable. Every case added as the reference impl lands the verb that makes it runnable.
|
|
62
63
|
- **spec v1.0.0 release**: all rows above MUST be 🟢 covered or explicitly 🟠 deferred to v1.1 with a linked issue.
|
|
63
64
|
- **CI check**: [`scripts/check-coverage.js`](../../scripts/check-coverage.js) compares `spec/schemas/**/*.schema.json` against the matrix above on every PR. A schema without a row here, or a row pointing at a missing schema, fails CI (exit 1 with a `::error::` annotation). Wired into `ci.yml` §validate and into `npm run spec:check`.
|
|
65
|
+
|
|
66
|
+
## Stability
|
|
67
|
+
|
|
68
|
+
The **coverage matrix gating policy** (every shipped schema MUST have a row; every row MUST be 🟢 covered or 🟠 deferred-to-v1.1 before the spec v1.0.0 tag) is stable as of spec v1.0.0. Relaxing the v1.0 release gate (e.g. allowing 🔴 missing rows to ship) is a major bump. Tightening the gate further (e.g. forbidding 🟠 deferrals) is a minor bump.
|
|
69
|
+
|
|
70
|
+
The **assertion-type vocabulary** (`exit-code`, `json-path`, `file-exists`, `file-contains-verbatim`, `file-matches-schema`, `stderr-matches`) is stable as of spec v1.0.0. Adding a new assertion type is a minor bump; renaming or removing one is a major bump.
|
|
71
|
+
|
|
72
|
+
The **per-row schema and case set** above is mutable through every spec release: rows are added when a new schema or normative artifact lands, marked deferred when the runtime that exercises them ships in a later step, and flipped to 🟢 covered when the case file is committed. Adding rows is non-breaking; deleting rows MUST coincide with deleting the corresponding schema (which is itself a major bump).
|
|
@@ -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/db-schema.md
CHANGED
|
@@ -177,7 +177,7 @@ Primary key: `(node_path, extractor_id)`. Indexes: `ix_scan_extractor_runs_node`
|
|
|
177
177
|
|
|
178
178
|
Universal enrichment layer (A.8). Stores `ctx.enrichNode(partial)` outputs separately from the author-supplied frontmatter on `scan_nodes.frontmatter_json`, which the Extractor pipeline NEVER mutates.
|
|
179
179
|
|
|
180
|
-
One row per `(node_path, extractor_id)` pair an Extractor enriched. Both deterministic and probabilistic Extractors write here; only probabilistic rows participate in stale tracking — when a body changes between scans, the kernel flags the surviving probabilistic row `stale = 1` (NOT deleted, preserving the LLM cost paid to produce it). Deterministic rows simply
|
|
180
|
+
One row per `(node_path, extractor_id)` pair an Extractor enriched. Both deterministic and probabilistic Extractors write here; only probabilistic rows participate in stale tracking — when a body changes between scans, the kernel flags the surviving probabilistic row `stale = 1` (NOT deleted, preserving the LLM cost paid to produce it). Deterministic rows are simply overwritten via PRIMARY KEY conflict on the next re-extract through the A.9 cache.
|
|
181
181
|
|
|
182
182
|
| Column | Type | Constraint |
|
|
183
183
|
|---|---|---|
|
package/index.json
CHANGED
|
@@ -166,25 +166,30 @@
|
|
|
166
166
|
}
|
|
167
167
|
]
|
|
168
168
|
},
|
|
169
|
-
"specPackageVersion": "0.
|
|
169
|
+
"specPackageVersion": "0.13.1",
|
|
170
170
|
"integrity": {
|
|
171
171
|
"algorithm": "sha256",
|
|
172
172
|
"files": {
|
|
173
|
-
"CHANGELOG.md": "
|
|
173
|
+
"CHANGELOG.md": "e99ca15631aa9ce6392612d055379d5cb2f2757df2008c8c184acc059a4178c3",
|
|
174
174
|
"README.md": "bd30780525e75379eaeb5f8a903bdc601daf3862f3ec69dffc96c437e8d476fc",
|
|
175
|
-
"architecture.md": "
|
|
176
|
-
"cli-contract.md": "
|
|
177
|
-
"conformance/README.md": "
|
|
175
|
+
"architecture.md": "c14e69faa7ce7f657d6a2790762daaea8a5ff350375de8c254cd870b5494b896",
|
|
176
|
+
"cli-contract.md": "1aa9349ef1a8db94236a8057ed77dd2802b561eec382451be9a2afdbbbdc6f91",
|
|
177
|
+
"conformance/README.md": "5f94a6ac637b7c992fcd7e53d32eed1b8887eeef05eb6ca3b5ec8a0b5045cd21",
|
|
178
178
|
"conformance/cases/kernel-empty-boot.json": "ad4bbe9d637537625025c8bdb61285b1433568a2544b1ce0248f304ccff8c350",
|
|
179
|
-
"conformance/
|
|
179
|
+
"conformance/cases/plugin-missing-ui-rejected.json": "c6ce8f62a430d662aea33dec8ebf6493be6455037be3114e0d93d0eb57777287",
|
|
180
|
+
"conformance/coverage.md": "a06b650c5b5684aec9b508181b3e091bf1aaa0cb45a0f8e76fdfb8f2a9c7b9a7",
|
|
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
|
-
"db-schema.md": "
|
|
185
|
+
"db-schema.md": "730b423d844d7b10aecaec59a78f254b600f0e16b9300305c49aa8c01d03d68f",
|
|
182
186
|
"interfaces/security-scanner.md": "4a982667008f233656f44c61ce9948e062432d3debdcbf7a134da03bd4139d7d",
|
|
183
187
|
"job-events.md": "8f371e0991816eca2e1a55cbd8a50733546ca5e7c861588048c18be1d22dbd57",
|
|
184
188
|
"job-lifecycle.md": "12bfc27690c92cf93682a3b6fbfeb7e2d252d33f704fd2d7de9a13db713e6281",
|
|
185
|
-
"plugin-author-guide.md": "
|
|
189
|
+
"plugin-author-guide.md": "ada6af03cc26439fd890efc9fa6939d1757f4625ea88f4be6f5e07497244f42e",
|
|
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
package/plugin-author-guide.md
CHANGED
|
@@ -264,7 +264,7 @@ Pure single-node analysis. **Never** read another node, the graph, or the databa
|
|
|
264
264
|
The runtime method is `extract(ctx) → void`. Output flows through three callbacks the kernel binds onto the context:
|
|
265
265
|
|
|
266
266
|
- **`ctx.emitLink(link)`** — append a `Link` to the kernel's `links` table. The kernel validates against the extractor's declared `emitsLinkKinds` before persistence; off-contract kinds are dropped and surface as `extension.error` events. URL-shaped targets are partitioned into `node.externalRefsCount` and never persisted.
|
|
267
|
-
- **`ctx.enrichNode(partial)`** — merge canonical, kernel-curated properties onto the node's enrichment layer (persisted into `node_enrichments` per `db-schema.md`). **Strictly separate from the author-supplied frontmatter** — the latter is IMMUTABLE from any Extractor. Use the enrichment layer for facts the author did not write but the
|
|
267
|
+
- **`ctx.enrichNode(partial)`** — merge canonical, kernel-curated properties onto the node's enrichment layer (persisted into `node_enrichments` per `db-schema.md`). **Strictly separate from the author-supplied frontmatter** — the latter is IMMUTABLE from any Extractor. Use the enrichment layer for facts the author did not write but the Extractor inferred (computed titles, summaries, signals from probabilistic Extractors). Probabilistic enrichments track `body_hash_at_enrichment`; when the scan loop sees a body change, those rows are flagged `stale = 1` (NOT deleted, preserving the LLM cost paid to produce them) and surface for refresh via `sm refresh <node>` or `sm refresh --stale`. Deterministic enrichments are simply overwritten via PRIMARY KEY conflict on the next re-extract through the A.9 cache and are never stale-flagged.
|
|
268
268
|
- **`ctx.store`** — plugin-scoped persistence. Optional, only present when your `plugin.json` declares `storage.mode`. Shape depends on the mode (`KvStore` for mode A, scoped `Database` for mode B). See [`plugin-kv-api.md`](./plugin-kv-api.md).
|
|
269
269
|
|
|
270
270
|
A probabilistic extractor additionally receives `ctx.runner` (the `RunnerPort`) for LLM dispatch.
|
|
@@ -300,7 +300,6 @@ export default {
|
|
|
300
300
|
};
|
|
301
301
|
```
|
|
302
302
|
|
|
303
|
-
> **Migration note (spec 0.8.x).** This kind was previously named `Detector` with a `detect(ctx) → Link[]` signature. The rename to `Extractor` and the move to callback-based output landed as a single breaking minor in the pre-1.0 line. The mechanical migration: rename `kind: 'detector'` → `kind: 'extractor'`, rename `detect` → `extract`, replace `return links` with `for (const l of links) ctx.emitLink(l)`. The `applicableKinds`, `emitsLinkKinds`, `defaultConfidence`, and `scope` fields are unchanged.
|
|
304
303
|
|
|
305
304
|
### Rules
|
|
306
305
|
|
|
@@ -407,11 +406,15 @@ These ship later in the v1.x line as bundled built-ins; the spec already pins th
|
|
|
407
406
|
|
|
408
407
|
#### Provider — `kinds` catalog and `explorationDir`
|
|
409
408
|
|
|
410
|
-
Every Provider declares two required fields beyond the manifest base
|
|
409
|
+
Every Provider declares two required top-level fields beyond the manifest base: `kinds` and `explorationDir`.
|
|
411
410
|
|
|
412
|
-
**`kinds` catalog.** Maps each kind the Provider emits to its frontmatter schema
|
|
411
|
+
**`kinds` catalog.** Maps each kind the Provider emits to its frontmatter schema, its qualified `defaultRefreshAction`, and its `ui` presentation block. The kernel derives the supported kind set from `Object.keys(kinds)`. Each entry has three required fields:
|
|
413
412
|
|
|
414
|
-
**`
|
|
413
|
+
- **`schema`** — path (relative to the Provider package) to the kind's frontmatter JSON Schema. MUST extend [`schemas/frontmatter/base.schema.json`](./schemas/frontmatter/base.schema.json) via `allOf` + `$ref` to base's `$id`.
|
|
414
|
+
- **`defaultRefreshAction`** — qualified action id (`<plugin-id>/<action-id>`) the UI's `🧠 prob` button dispatches. The action MUST exist in the registry; a dangling reference disables the Provider with `invalid-manifest`.
|
|
415
|
+
- **`ui`** — presentation block: `{ label, color, colorDark?, emoji?, icon? }`. The UI ships every `ui` block to the front-end via the `kindRegistry` envelope so built-in and user-plugin kinds render identically. `icon` is a discriminated union (`{ kind: 'pi'; id }` for PrimeIcons, `{ kind: 'svg'; path }` for raw SVG). The `ui` block is required (not optional) so the UI never has to invent visuals for unknown kinds. See [`architecture.md` §Provider · `ui` presentation](./architecture.md#provider--ui-presentation) for the field-by-field contract.
|
|
416
|
+
|
|
417
|
+
**`explorationDir`.** Filesystem directory the kernel walks at boot/scan time to discover candidate files. `sm doctor` checks the resolved path exists and emits a non-blocking warning when it does not — the user may legitimately install the matching platform later. Bare `~` and `~/...` resolve against the current user's home (shell convention); relative paths fall back to the cwd.
|
|
415
418
|
|
|
416
419
|
```jsonc
|
|
417
420
|
{
|
|
@@ -422,20 +425,27 @@ Every Provider declares two required fields beyond the manifest base.
|
|
|
422
425
|
"kinds": {
|
|
423
426
|
"skill": {
|
|
424
427
|
"schema": "./schemas/skill.schema.json",
|
|
425
|
-
"defaultRefreshAction": "cursor/summarize-skill"
|
|
428
|
+
"defaultRefreshAction": "cursor/summarize-skill",
|
|
429
|
+
"ui": {
|
|
430
|
+
"label": "Skill",
|
|
431
|
+
"color": "#7c3aed",
|
|
432
|
+
"colorDark": "#a78bfa",
|
|
433
|
+
"icon": { "kind": "pi", "id": "pi-bolt" }
|
|
434
|
+
}
|
|
426
435
|
},
|
|
427
436
|
"command": {
|
|
428
437
|
"schema": "./schemas/command.schema.json",
|
|
429
|
-
"defaultRefreshAction": "cursor/summarize-command"
|
|
438
|
+
"defaultRefreshAction": "cursor/summarize-command",
|
|
439
|
+
"ui": {
|
|
440
|
+
"label": "Command",
|
|
441
|
+
"color": "#0ea5e9",
|
|
442
|
+
"icon": { "kind": "svg", "path": "M3 6h18M3 12h18M3 18h18" }
|
|
443
|
+
}
|
|
430
444
|
}
|
|
431
445
|
}
|
|
432
446
|
}
|
|
433
447
|
```
|
|
434
448
|
|
|
435
|
-
Bare `~` and `~/...` prefixes in `explorationDir` resolve against the current user's home (the same convention the shell applies); relative paths fall back to the cwd. Keep `explorationDir` short and platform-canonical; the doctor warning is the only place the user sees the field, so misleading values create confusion later.
|
|
436
|
-
|
|
437
|
-
> **Migration note (spec 0.8.x).** Pre-0.8 Providers declared two separate fields, `emits: string[]` and a flat `defaultRefreshAction: { <kind>: actionId }`. Both collapsed into the `kinds` map in 0.8.0 (Phase 3 of plug-in model overhaul); the per-kind frontmatter schema (which previously lived under `spec/schemas/frontmatter/<kind>.schema.json`) joined the same map entry. Migration: drop `emits` (replaced by `Object.keys(kinds)`); move each `defaultRefreshAction[<kind>]` value into `kinds[<kind>].defaultRefreshAction`; ship your per-kind schemas inside the plugin package and reference them via `kinds[<kind>].schema`.
|
|
438
|
-
|
|
439
449
|
---
|
|
440
450
|
|
|
441
451
|
## Frontmatter validation — three-tier model
|