@skill-map/spec 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +105 -0
  3. package/architecture.md +218 -0
  4. package/cli-contract.md +336 -0
  5. package/conformance/README.md +140 -0
  6. package/conformance/cases/basic-scan.json +17 -0
  7. package/conformance/cases/kernel-empty-boot.json +24 -0
  8. package/conformance/fixtures/minimal-claude/agents/reviewer.md +16 -0
  9. package/conformance/fixtures/minimal-claude/commands/status.md +17 -0
  10. package/conformance/fixtures/minimal-claude/hooks/pre-commit.md +13 -0
  11. package/conformance/fixtures/minimal-claude/notes/architecture.md +11 -0
  12. package/conformance/fixtures/minimal-claude/skills/hello.md +22 -0
  13. package/conformance/fixtures/preamble-v1.txt +54 -0
  14. package/db-schema.md +359 -0
  15. package/dispatch-lifecycle.md +213 -0
  16. package/index.json +205 -0
  17. package/interfaces/security-scanner.md +233 -0
  18. package/job-events.md +322 -0
  19. package/package.json +49 -0
  20. package/plugin-kv-api.md +208 -0
  21. package/prompt-preamble.md +152 -0
  22. package/schemas/conformance-case.schema.json +185 -0
  23. package/schemas/execution-record.schema.json +88 -0
  24. package/schemas/frontmatter/agent.schema.json +22 -0
  25. package/schemas/frontmatter/base.schema.json +136 -0
  26. package/schemas/frontmatter/command.schema.json +39 -0
  27. package/schemas/frontmatter/hook.schema.json +29 -0
  28. package/schemas/frontmatter/note.schema.json +11 -0
  29. package/schemas/frontmatter/skill.schema.json +37 -0
  30. package/schemas/issue.schema.json +54 -0
  31. package/schemas/job.schema.json +75 -0
  32. package/schemas/link.schema.json +66 -0
  33. package/schemas/node.schema.json +95 -0
  34. package/schemas/plugins-registry.schema.json +99 -0
  35. package/schemas/project-config.schema.json +87 -0
  36. package/schemas/report-base.schema.json +41 -0
  37. package/schemas/scan-result.schema.json +71 -0
  38. package/schemas/summaries/agent.schema.json +46 -0
  39. package/schemas/summaries/command.schema.json +50 -0
  40. package/schemas/summaries/hook.schema.json +43 -0
  41. package/schemas/summaries/note.schema.json +37 -0
  42. package/schemas/summaries/skill.schema.json +57 -0
  43. package/versioning.md +94 -0
package/job-events.md ADDED
@@ -0,0 +1,322 @@
1
+ # Job events
2
+
3
+ Canonical event stream emitted during job execution. Every implementation MUST emit these events in the order described, with the shapes defined below. Consumers include the CLI pretty printer, the `--json` ndjson output, the Server's WebSocket broadcaster, and any third-party integration.
4
+
5
+ This document is **normative**. The set of event types, their payload shapes, and their ordering rules are stable contracts.
6
+
7
+ ---
8
+
9
+ ## Transport
10
+
11
+ Events are records produced by the kernel through `ProgressEmitterPort` (see `architecture.md`). An implementation MUST provide three output adapters:
12
+
13
+ | Adapter | Purpose | Format |
14
+ |---|---|---|
15
+ | `pretty` | Default TTY output. Human-readable, colored, line-based progress. | Free-form; not normative. |
16
+ | `stream-output` | Pretty + model tokens inline. Debugging mode. | Free-form; not normative. |
17
+ | `json` | Machine-readable ndjson. One event per line; each line is a complete JSON object. | **Normative.** Matches the shapes below. |
18
+
19
+ The Server exposes the same events over WebSocket (`/ws`) using the same JSON shapes; each event is a single WebSocket text frame.
20
+
21
+ ---
22
+
23
+ ## Common envelope
24
+
25
+ Every event is a JSON object with this envelope:
26
+
27
+ ```json
28
+ {
29
+ "type": "<event-type>",
30
+ "timestamp": <unix-ms>,
31
+ "runId": "<run-id>",
32
+ "jobId": "<job-id> | null",
33
+ "data": { ... }
34
+ }
35
+ ```
36
+
37
+ | Field | Required | Meaning |
38
+ |---|---|---|
39
+ | `type` | always | One of the canonical event types below. |
40
+ | `timestamp` | always | Unix milliseconds when the event was emitted. |
41
+ | `runId` | always | Identifier of the `sm job run` invocation. One run emits many events. Format: `r-YYYYMMDD-HHMMSS-XXXX`. |
42
+ | `jobId` | when job-scoped | The job the event refers to. Null for run-level events (`run.*`). |
43
+ | `data` | per-event | Event-specific payload, shape defined below. |
44
+
45
+ Implementations MUST include every envelope field in every event, even if `jobId` is null. This simplifies consumers.
46
+
47
+ Unknown fields in `data` MUST be ignored by consumers (forward compatibility).
48
+
49
+ ---
50
+
51
+ ## Event catalog
52
+
53
+ Emitted in roughly this order during a `sm job run --all` invocation. The exact sequence may interleave for parallel runs (post-MVP).
54
+
55
+ ### `run.started`
56
+
57
+ Emitted once at the start of every `sm job run` invocation.
58
+
59
+ ```json
60
+ {
61
+ "type": "run.started",
62
+ "timestamp": 1745159455123,
63
+ "runId": "r-20260420-143055-a3f2",
64
+ "jobId": null,
65
+ "data": {
66
+ "mode": "single | all | max",
67
+ "maxJobs": 10,
68
+ "filter": { "action": "skill-summarizer" }
69
+ }
70
+ }
71
+ ```
72
+
73
+ - `mode`: what the runner was asked to do.
74
+ - `maxJobs`: cap on concurrent drain (`--max N` or null).
75
+ - `filter`: resolved filter predicate, free-form object.
76
+
77
+ ### `run.reap.started`
78
+
79
+ Emitted before auto-reap scans for expired jobs.
80
+
81
+ ```json
82
+ {
83
+ "type": "run.reap.started",
84
+ "timestamp": 1745159455200,
85
+ "runId": "...",
86
+ "jobId": null,
87
+ "data": {}
88
+ }
89
+ ```
90
+
91
+ ### `run.reap.completed`
92
+
93
+ Emitted after auto-reap finishes.
94
+
95
+ ```json
96
+ {
97
+ "type": "run.reap.completed",
98
+ "timestamp": 1745159455201,
99
+ "runId": "...",
100
+ "jobId": null,
101
+ "data": {
102
+ "reapedCount": 0,
103
+ "reapedIds": []
104
+ }
105
+ }
106
+ ```
107
+
108
+ - `reapedIds` lists the jobs transitioned from `running` to `failed`. May be empty.
109
+
110
+ ### `job.claimed`
111
+
112
+ Emitted when the runner successfully claims a job.
113
+
114
+ ```json
115
+ {
116
+ "type": "job.claimed",
117
+ "timestamp": 1745159455300,
118
+ "runId": "...",
119
+ "jobId": "d-20260420-143055-b001",
120
+ "data": {
121
+ "actionId": "skill-summarizer",
122
+ "actionVersion": "1.2.0",
123
+ "nodeId": "skills/my-skill.md",
124
+ "ttlSeconds": 180,
125
+ "priority": 0
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### `job.skipped`
131
+
132
+ Emitted when a drain attempts to claim but finds no eligible job.
133
+
134
+ ```json
135
+ {
136
+ "type": "job.skipped",
137
+ "timestamp": 1745159455400,
138
+ "runId": "...",
139
+ "jobId": null,
140
+ "data": {
141
+ "reason": "queue-empty | filter-excluded-all"
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### `job.spawning`
147
+
148
+ Emitted when the runner is about to execute the job file.
149
+
150
+ ```json
151
+ {
152
+ "type": "job.spawning",
153
+ "timestamp": 1745159455500,
154
+ "runId": "...",
155
+ "jobId": "...",
156
+ "data": {
157
+ "runner": "cli | skill | in-process",
158
+ "command": "claude -p",
159
+ "jobFilePath": ".skill-map/jobs/d-20260420-143055-b001.md"
160
+ }
161
+ }
162
+ ```
163
+
164
+ `command` is implementation-defined free-form; it is descriptive, not invokable.
165
+
166
+ ### `model.delta`
167
+
168
+ Emitted in `stream-output` mode only. Carries incremental model output.
169
+
170
+ ```json
171
+ {
172
+ "type": "model.delta",
173
+ "timestamp": 1745159456000,
174
+ "runId": "...",
175
+ "jobId": "...",
176
+ "data": {
177
+ "text": "Analyzing the skill...",
178
+ "channel": "assistant | thinking | tool-use"
179
+ }
180
+ }
181
+ ```
182
+
183
+ Consumers of the canonical `json` output MAY receive these events if the runner chose to emit them. `pretty` and `json` adapters MAY drop `model.delta` events for brevity.
184
+
185
+ ### `job.callback.received`
186
+
187
+ Emitted inside `sm record` when the callback arrives and passes nonce validation.
188
+
189
+ ```json
190
+ {
191
+ "type": "job.callback.received",
192
+ "timestamp": 1745159465000,
193
+ "runId": "...",
194
+ "jobId": "...",
195
+ "data": {
196
+ "status": "completed | failed",
197
+ "model": "claude-opus-4-7",
198
+ "reportPath": ".skill-map/reports/d-20260420-143055-b001.json"
199
+ }
200
+ }
201
+ ```
202
+
203
+ `runId` on this event is the run that originally claimed the job. If the record is called from outside a run (interactive skill), `runId` is a synthetic id prefixed `r-ext-`.
204
+
205
+ ### `job.completed`
206
+
207
+ Emitted when a job transitions to `completed`.
208
+
209
+ ```json
210
+ {
211
+ "type": "job.completed",
212
+ "timestamp": 1745159465100,
213
+ "runId": "...",
214
+ "jobId": "...",
215
+ "data": {
216
+ "durationMs": 9700,
217
+ "tokensIn": 2431,
218
+ "tokensOut": 1072,
219
+ "model": "claude-opus-4-7",
220
+ "reportPath": ".skill-map/reports/d-20260420-143055-b001.json"
221
+ }
222
+ }
223
+ ```
224
+
225
+ ### `job.failed`
226
+
227
+ Emitted when a job transitions to `failed` by any path.
228
+
229
+ ```json
230
+ {
231
+ "type": "job.failed",
232
+ "timestamp": 1745159465200,
233
+ "runId": "...",
234
+ "jobId": "...",
235
+ "data": {
236
+ "reason": "runner-error | report-invalid | timeout | abandoned | job-file-missing | user-cancelled",
237
+ "message": "Subprocess exited with code 127",
238
+ "exitCode": 127,
239
+ "durationMs": 180000
240
+ }
241
+ }
242
+ ```
243
+
244
+ `reason` enum matches `execution-record.failureReason`. `message` is human-readable free-form; MAY be truncated for display.
245
+
246
+ ### `run.summary`
247
+
248
+ Emitted once at the end of `sm job run`, after the last job event.
249
+
250
+ ```json
251
+ {
252
+ "type": "run.summary",
253
+ "timestamp": 1745159475000,
254
+ "runId": "...",
255
+ "jobId": null,
256
+ "data": {
257
+ "jobsAttempted": 5,
258
+ "jobsCompleted": 4,
259
+ "jobsFailed": 1,
260
+ "totalDurationMs": 20000,
261
+ "totalTokensIn": 12500,
262
+ "totalTokensOut": 5300
263
+ }
264
+ }
265
+ ```
266
+
267
+ `jobsAttempted = jobsCompleted + jobsFailed` always.
268
+
269
+ ---
270
+
271
+ ## Ordering rules
272
+
273
+ For each job, the normative order is:
274
+
275
+ ```
276
+ job.claimed → job.spawning → (model.delta)* → job.callback.received → (job.completed | job.failed)
277
+ ```
278
+
279
+ For a run:
280
+
281
+ ```
282
+ run.started
283
+ → run.reap.started → run.reap.completed
284
+ → (per-job sequence above)*
285
+ → run.summary
286
+ ```
287
+
288
+ A parallel implementation MAY interleave per-job sequences across different `jobId` values, but MUST preserve ordering within a single `jobId`.
289
+
290
+ `job.failed` with reason `abandoned` MAY appear without a matching `job.claimed` in the current run — it refers to a job claimed in a previous run that expired before the next reap.
291
+
292
+ ---
293
+
294
+ ## Error handling
295
+
296
+ If an event payload cannot be serialized (internal bug), the implementation MUST emit a synthetic event:
297
+
298
+ ```json
299
+ {
300
+ "type": "emitter.error",
301
+ "timestamp": <now>,
302
+ "runId": "<runId>",
303
+ "jobId": null,
304
+ "data": {
305
+ "message": "failed to emit event of type '<type>': <reason>"
306
+ }
307
+ }
308
+ ```
309
+
310
+ Consumers MAY treat `emitter.error` as a soft failure (log and continue). Implementations MUST NOT crash the run because of a serialization failure.
311
+
312
+ ---
313
+
314
+ ## Stability
315
+
316
+ The **event type list** above is stable as of spec v1.0.0. Adding a new event type is a minor bump. Removing or renaming one is a major bump.
317
+
318
+ **Adding** fields to `data` is a minor bump. Changing a field's type or removing a field is a major bump.
319
+
320
+ Consumers MUST ignore unknown fields (forward compatibility).
321
+
322
+ The envelope (`type`, `timestamp`, `runId`, `jobId`, `data`) is stable. Adding an envelope field is a major bump because every consumer would need to handle it.
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@skill-map/spec",
3
+ "version": "0.1.0",
4
+ "description": "JSON Schemas, prose contracts, and conformance suite for the skill-map specification.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://skill-map.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/crystian/skill-map.git",
11
+ "directory": "spec"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/crystian/skill-map/issues"
15
+ },
16
+ "keywords": [
17
+ "skill-map",
18
+ "spec",
19
+ "specification",
20
+ "json-schema",
21
+ "ai-agents",
22
+ "markdown",
23
+ "claude-code"
24
+ ],
25
+ "exports": {
26
+ ".": "./index.json",
27
+ "./index.json": "./index.json",
28
+ "./schemas/*.json": "./schemas/*.json"
29
+ },
30
+ "files": [
31
+ "README.md",
32
+ "CHANGELOG.md",
33
+ "versioning.md",
34
+ "architecture.md",
35
+ "cli-contract.md",
36
+ "dispatch-lifecycle.md",
37
+ "job-events.md",
38
+ "prompt-preamble.md",
39
+ "db-schema.md",
40
+ "plugin-kv-api.md",
41
+ "interfaces/",
42
+ "schemas/",
43
+ "conformance/",
44
+ "index.json"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
@@ -0,0 +1,208 @@
1
+ # Plugin KV API
2
+
3
+ Normative contract for plugin-accessible persistence. Two modes exist (see `db-schema.md` for the catalog entries):
4
+
5
+ - **Mode A — KV**: plugin uses the kernel-provided `ctx.store.*` accessor. Backed by the shared `state_plugin_kv` table.
6
+ - **Mode B — Dedicated**: plugin owns its own tables with the `plugin_<normalizedId>_` prefix, migrated by the kernel.
7
+
8
+ This document defines mode A in full and clarifies the boundary with mode B. Implementations MUST expose this API to every plugin that declares `"storage": { "mode": "kv" }` in its manifest.
9
+
10
+ ---
11
+
12
+ ## Overview
13
+
14
+ A plugin extension receives a `ctx` object at construction time. `ctx.store` is present if and only if the plugin declared storage. Its shape depends on the mode:
15
+
16
+ | Mode | `ctx.store` shape |
17
+ |---|---|
18
+ | No storage declared | `undefined`. |
19
+ | `mode: "kv"` | `KvStore` (this document). |
20
+ | `mode: "dedicated"` | `DedicatedStore` (scoped Database wrapper). See mode B below. |
21
+
22
+ Plugins SHOULD pick the minimum mode they need. Mode A is simpler, deployed across every scope from day zero, and requires no migrations. Mode B is for plugins that need relational shape, indexes, or cross-row queries.
23
+
24
+ ---
25
+
26
+ ## Mode A: `ctx.store` KV accessor
27
+
28
+ ### Interface
29
+
30
+ ```typescript
31
+ interface KvStore {
32
+ get<T = unknown>(key: string, options?: { nodePath?: string }): Promise<T | null>;
33
+ set<T = unknown>(key: string, value: T, options?: { nodePath?: string }): Promise<void>;
34
+ delete(key: string, options?: { nodePath?: string }): Promise<boolean>;
35
+ list(options?: { nodePath?: string; prefix?: string }): Promise<KvEntry[]>;
36
+ }
37
+
38
+ interface KvEntry {
39
+ key: string;
40
+ value: unknown;
41
+ nodePath: string | null;
42
+ updatedAt: number;
43
+ }
44
+ ```
45
+
46
+ Implementations in other languages MUST expose the same semantic surface.
47
+
48
+ ### Scoping
49
+
50
+ Every operation is scoped by the caller's `pluginId`. The plugin cannot specify, override, or observe another plugin's `pluginId`. This is enforced by the kernel when constructing the `ctx.store` — the `pluginId` is captured at registration time and is not an argument.
51
+
52
+ Operations MAY be additionally scoped by `nodePath`:
53
+
54
+ - **Global KV (no `nodePath`)**: `{pluginId, nodePath: null, key}`. One row per plugin + key.
55
+ - **Node-scoped KV (with `nodePath`)**: `{pluginId, nodePath: "<path>", key}`. One row per plugin + node + key.
56
+
57
+ Both scopes share the same underlying `state_plugin_kv` table (see `db-schema.md`). The `nodePath` column is nullable; implementations MUST use a sentinel empty string internally when the backing engine rejects NULL in composite primary keys.
58
+
59
+ ### Semantics
60
+
61
+ | Operation | Behaviour |
62
+ |---|---|
63
+ | `get(key, { nodePath })` | Returns the stored value (JSON-decoded) or `null` if no row exists. Never throws for "missing". |
64
+ | `set(key, value, { nodePath })` | Upsert. Replaces any existing value. Updates `updatedAt`. The value is JSON-encoded by the kernel; it MUST be JSON-serializable. Cyclic or non-serializable values MUST be rejected with a typed error. |
65
+ | `delete(key, { nodePath })` | Deletes the row if present. Returns `true` if a row was deleted, `false` otherwise. Idempotent. |
66
+ | `list({ nodePath, prefix })` | Returns all entries matching the scope. `nodePath` omitted: returns global entries (`nodePath IS NULL`). `nodePath: null` (explicit): same as omitted. `nodePath: "<path>"`: returns entries for that node. `prefix`: filters keys starting with the given string. |
67
+
68
+ Return order of `list` is NOT specified by this spec; consumers MUST NOT rely on ordering. Implementations SHOULD order by `key ASC` for developer ergonomics.
69
+
70
+ ### Key constraints
71
+
72
+ - `key` MUST be a non-empty string, length ≤ 256 bytes (UTF-8).
73
+ - `key` SHOULD be dot-separated namespaces (`foo.bar.baz`) for discoverability, but this is not enforced.
74
+ - The kernel MAY log a warning when `key` exceeds a reasonable length (e.g. 128), but MUST NOT reject below 256.
75
+
76
+ ### Value constraints
77
+
78
+ - Value MUST be JSON-serializable (plain objects, arrays, strings, numbers, booleans, null).
79
+ - Values containing `undefined` or functions MUST be rejected with a typed error before writing.
80
+ - The kernel MAY impose a per-value size limit (reference impl: 1 MiB). Exceeding it is a typed error, not a silent truncation.
81
+
82
+ ### Transactions
83
+
84
+ The `KvStore` operations are individually atomic. There is NO multi-operation transaction in mode A — plugins that need transactional semantics across several rows MUST use mode B.
85
+
86
+ Implementations MUST NOT expose a `transaction()` method on `KvStore` in mode A. The shape is intentionally minimal to keep the backing table simple.
87
+
88
+ ### Errors
89
+
90
+ All errors are typed. An implementation MUST expose these error classes (or language equivalents):
91
+
92
+ | Error | Cause |
93
+ |---|---|
94
+ | `KvKeyInvalidError` | Key is empty, non-string, or too long. |
95
+ | `KvValueNotSerializableError` | Value cannot be JSON-encoded. |
96
+ | `KvValueTooLargeError` | Encoded value exceeds the size limit. |
97
+ | `KvOperationFailedError` | Unexpected backend failure (e.g., DB full, IO error). Wraps the underlying cause. |
98
+
99
+ Errors MUST NOT leak backend-specific details (SQL strings, file paths) to plugin code unless wrapped in `KvOperationFailedError.cause`.
100
+
101
+ ---
102
+
103
+ ## Mode B: dedicated tables
104
+
105
+ Mode B is governed by `db-schema.md` (catalog rules + triple protection). This section restates the API surface.
106
+
107
+ ### Declaration
108
+
109
+ ```json
110
+ {
111
+ "storage": {
112
+ "mode": "dedicated",
113
+ "tables": ["rule_exceptions", "cache_entries"],
114
+ "migrations": ["migrations/001_initial.sql"]
115
+ }
116
+ }
117
+ ```
118
+
119
+ The `tables` array lists logical table names **without** the `plugin_<id>_` prefix. The kernel prepends the prefix when applying migrations and when routing queries.
120
+
121
+ ### Accessor
122
+
123
+ ```typescript
124
+ interface DedicatedStore {
125
+ db: Database; // scoped wrapper, see below
126
+ }
127
+ ```
128
+
129
+ `DedicatedStore.db` is a wrapper — NOT a raw handle. Every query passes through a validator that rejects:
130
+
131
+ - References to tables whose name doesn't start with this plugin's prefix.
132
+ - DDL statements (`CREATE`, `ALTER`, `DROP`, `TRUNCATE`). Mode B DDL is runtime-immutable after migrations; plugins change shape via a new migration, not at runtime.
133
+ - `ATTACH DATABASE` statements.
134
+ - `PRAGMA` statements that aren't scoped to the plugin's own tables.
135
+
136
+ A query that fails validation raises `ScopedDbViolationError`. The plugin continues to run; only the offending query is rejected.
137
+
138
+ ### Transaction support
139
+
140
+ Mode B plugins MAY call `db.transaction(async (tx) => { ... })`. The kernel provides transaction isolation consistent with the backing engine. Nested transactions are NOT supported; the kernel MUST reject a nested `transaction()` call with a typed error.
141
+
142
+ ### Migrations
143
+
144
+ - Location: `<plugin-dir>/migrations/NNN_snake_case.sql`.
145
+ - Applied in order after kernel migrations on boot.
146
+ - Prefix injection: the kernel rewrites `CREATE TABLE <name>` into `CREATE TABLE plugin_<id>_<name>` if the prefix is missing.
147
+ - Index and constraint prefixes are similarly injected.
148
+ - A failing plugin migration disables only that plugin (`status: load-error`); other plugins and the kernel continue.
149
+
150
+ See `db-schema.md` for the normative migration rules.
151
+
152
+ ---
153
+
154
+ ## Mode selection guidance
155
+
156
+ Non-normative; descriptive guidance for plugin authors.
157
+
158
+ **Prefer mode A when**:
159
+
160
+ - Each value is a small JSON blob (preferences, per-node flags, hash pins).
161
+ - Queries are "get by key" or "list under a prefix".
162
+ - You need to ship without asking the user to run a migration.
163
+
164
+ **Prefer mode B when**:
165
+
166
+ - You need indexes beyond `(pluginId, nodePath, key)`.
167
+ - You need to `JOIN` rows, aggregate, or do relational queries.
168
+ - Your data model is actually tabular (cache with TTL, observation log, provider registry).
169
+ - You are willing to own migrations forever.
170
+
171
+ A plugin MUST declare **exactly one** storage mode. Mixing modes in the same plugin is forbidden. The `plugins-registry.schema.json` enforces this at the manifest level (`storage` is a `oneOf` between `kv` and `dedicated`), and at runtime `ctx.store` exposes either the `KvStore` or the `DedicatedStore` shape — never both. A plugin that needs both KV-like and relational access MUST use mode B and implement KV-style rows as a dedicated table.
172
+
173
+ ---
174
+
175
+ ## Visibility rules
176
+
177
+ - A plugin MUST NOT read or write rows outside its scope. Mode A: the accessor is scoped. Mode B: the validator enforces the prefix.
178
+ - The kernel MAY expose read-only introspection for diagnostics (e.g., `sm plugins show <id> --storage` lists key counts). This is authoritative, not a plugin-level API.
179
+ - `sm db shell` can read any table. This is an operator-level escape hatch; plugins MUST NOT rely on it.
180
+
181
+ ---
182
+
183
+ ## Backup and retention
184
+
185
+ - Mode A rows are stored in `state_plugin_kv` and are backed up with `sm db backup`.
186
+ - Mode B rows live in the plugin's dedicated tables, prefixed `plugin_<id>_`, and are likewise backed up.
187
+ - `sm plugins disable <id>` does NOT drop the plugin's data — disabled plugins keep their KV rows and dedicated tables. `sm plugins forget <id>` (post-MVP) is the verb that wipes.
188
+ - `sm db reset` drops `scan_*` + `state_*`. This includes `state_plugin_kv` (mode A) AND every `plugin_<id>_*` table (mode B). Users MUST be warned by the CLI before `reset` proceeds.
189
+
190
+ ---
191
+
192
+ ## Honest note on isolation
193
+
194
+ Mode A is perfectly isolated at the row level: the accessor physically cannot see another plugin's rows.
195
+
196
+ Mode B is **isolated against accidents, not hostile code**. The scoped `Database` wrapper rejects cross-namespace queries at runtime. But a malicious plugin running in the same JavaScript process can bypass the wrapper by importing raw engine bindings directly. Plugins are user-placed code; the kernel trusts the user's judgement at install time.
197
+
198
+ Post-v1.0 work: signed manifest, sandboxed worker-thread isolation, per-plugin DB file. None of these land before cut 1.
199
+
200
+ ---
201
+
202
+ ## Stability
203
+
204
+ - The `KvStore` interface (method names, options, return shapes) is **stable** as of spec v1.0.0.
205
+ - Adding a method to `KvStore` is a minor bump; removing or changing signature is a major bump.
206
+ - Mode names (`kv`, `dedicated`) are **stable**. Adding a third mode is a minor bump.
207
+ - Key and value size limits are implementation-defined and MAY change without a spec bump; implementations MUST document their limits in their own changelog.
208
+ - Error class names are **stable**; adding a new error class is a minor bump.