@henols/vice-mcp 0.2.2 → 0.2.3

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 (77) hide show
  1. package/README.md +2 -2
  2. package/THIRD-PARTY-NOTICES.md +422 -1
  3. package/anno-bank.ts +171 -0
  4. package/anno-cli.ts +1674 -99
  5. package/anno-enum-gen.ts +416 -30
  6. package/anno-export-asm.ts +1175 -89
  7. package/anno-graphics.ts +338 -0
  8. package/anno-hazard-report.ts +1367 -0
  9. package/anno-import.ts +495 -0
  10. package/anno-join.ts +480 -0
  11. package/anno-provenance-ledger.ts +472 -0
  12. package/anno-register.ts +159 -0
  13. package/anno-store-export.ts +661 -0
  14. package/anno-store.ts +518 -2
  15. package/anno-tools.ts +1169 -16
  16. package/anno-types.ts +275 -2
  17. package/backend-detect.mts +124 -312
  18. package/build.ts +3 -1
  19. package/capture-predicate.ts +597 -0
  20. package/channel-lock.ts +349 -0
  21. package/evid-ingest.ts +217 -0
  22. package/evid-reconcile.ts +316 -0
  23. package/host-tool-client.ts +430 -0
  24. package/incident-record.ts +23 -12
  25. package/install-resources.ts +29 -13
  26. package/memmap-lookup.ts +285 -0
  27. package/package.json +27 -8
  28. package/prg-image.ts +1 -2
  29. package/repo-root.ts +87 -3
  30. package/resources/backend-detect.mjs +98 -236
  31. package/resources/broker-control.mjs +189 -16
  32. package/resources/broker-epoch.mjs +1 -1
  33. package/resources/broker-kill.mjs +8 -2
  34. package/resources/broker-launch.mjs +365 -210
  35. package/resources/broker-state.mjs +64 -18
  36. package/resources/container-guard.mjs +1 -1
  37. package/resources/ghidra-project.mjs +790 -0
  38. package/resources/host-tool.mjs +2561 -0
  39. package/resources/vice-broker.mjs +330 -184
  40. package/resources/vice-launcher.sh +127 -9
  41. package/stock-address.ts +1 -1
  42. package/stock-condition.ts +1 -1
  43. package/stock-connect.ts +9 -5
  44. package/stock-derived.ts +29 -37
  45. package/stock-diagnose.ts +200 -36
  46. package/stock-dispatch.ts +179 -77
  47. package/stock-handler.ts +1 -1
  48. package/stock-paths.ts +18 -14
  49. package/stock-petscii.ts +1 -1
  50. package/stock-protocol.ts +1 -1
  51. package/stock-recycle.ts +83 -2
  52. package/stock-reproducible-run.ts +811 -0
  53. package/stock-run-until.ts +100 -1
  54. package/stock-symbols.ts +4 -4
  55. package/stock-timing.ts +1 -1
  56. package/stop-oracle.ts +167 -0
  57. package/text-capability-probe.ts +660 -0
  58. package/text-connect.ts +157 -0
  59. package/text-protocol.ts +810 -0
  60. package/text-tools.ts +778 -0
  61. package/textmon-backtrace.ts +385 -0
  62. package/textmon-cpuhistory.ts +335 -0
  63. package/textmon-memmap.ts +494 -0
  64. package/textmon-profile.ts +458 -0
  65. package/textmon-registers.ts +748 -0
  66. package/tools-manifest.stock.json +864 -3
  67. package/vice-broker-client.ts +189 -42
  68. package/vice-errors.ts +268 -0
  69. package/vice-proxy.ts +339 -2144
  70. package/vsf-slice.ts +640 -0
  71. package/anno-d64.ts +0 -310
  72. package/capability-registry.ts +0 -390
  73. package/refresh-manifest.ts +0 -124
  74. package/tools-manifest.json +0 -1223
  75. package/vice-probe.ts +0 -278
  76. package/vice-sync.ts +0 -336
  77. package/vice.ts +0 -772
@@ -0,0 +1,661 @@
1
+ #!/usr/bin/env node
2
+ // anno-store-export.ts
3
+ //
4
+ // Phase 45, plan 45-02 (D-02): the general JSON export/import module for a
5
+ // per-fixture `.annostore` -- NOT Ghidra-shaped (that is `anno-import.ts`'s
6
+ // job, and it writes cross-references only). No general store-JSON round
7
+ // trip exists anywhere else in this tree today (RESEARCH.md Section 5,
8
+ // VERIFIED by a full census of `anno_*` tool names): this module is it.
9
+ //
10
+ // THIS MODULE RECEIVES AN ALREADY-OPEN STORE HANDLE for export, and an
11
+ // already-open handle for import -- it never opens or closes a store itself,
12
+ // never names `node:sqlite` directly for a connection, and never resolves a
13
+ // workspace path. Confinement is the CALLER's job, through the existing
14
+ // `storePathWithinWorkspace()` seam every other `anno-cli.ts` verb already
15
+ // uses -- never a second path validator here.
16
+ //
17
+ // WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR:
18
+ // - The JSON schema for a full store export: typed ranges, labels,
19
+ // comments (tagged `provenance: "derived" | "authored"` per D-03),
20
+ // project enums and their usage bindings, cross-references, and
21
+ // runtime-execution-observation rows.
22
+ // - The three decline/provenance comment-text conventions
23
+ // (`DECLINE_COMMENT_PREFIX`, `DISAGREEMENT_ACCEPTED_COMMENT_PREFIX`,
24
+ // `AUTHORED_PROVENANCE_COMMENT_PREFIX`), all riding the ONE existing
25
+ // `anno_set_comment` API -- never a second mechanism, never a schema
26
+ // column.
27
+ // - The import round trip: `importStoreDocument()` parses and validates
28
+ // the WHOLE document into an in-memory write plan before the first
29
+ // mutating call.
30
+ //
31
+ // WHAT NOT TO DO:
32
+ // - Never write a row before the WHOLE document has parsed and validated
33
+ // successfully (`anno-import.ts`'s own rule, copied deliberately: a
34
+ // partial import must never leave the store in a state that is neither
35
+ // the old nor the new content).
36
+ // - Never collapse the derived/authored provenance tag (D-03) -- the
37
+ // export schema exists specifically so a diff shows an authored
38
+ // purpose-comment change, which a binary `.annostore` cannot.
39
+ // - Never resolve a workspace path here -- the caller does, through
40
+ // `storePathWithinWorkspace()`, the one seam.
41
+ // - Never persist `provenance` as a real store column. It is NOT one:
42
+ // `RangeRow`/`CommentRow` (`anno-types.ts`) carry no such field, and
43
+ // `setDataType()`/`setComment()` accept no such argument. Ranges are
44
+ // ALWAYS the derived half (D-03: "block types ... regenerate
45
+ // deterministically from the bytes") -- there is no per-row fact in the
46
+ // schema that could ever make one authored instead, so `"derived"` is a
47
+ // constant for every exported range, not a per-row classification.
48
+ // Comments are ALWAYS the authored half (D-03, confirmed again by
49
+ // RESEARCH.md Section 5: "labels/comments/(and ... decline-comments) are
50
+ // the authored half") -- `provenanceForComment()` is written as a
51
+ // function of the comment's own text (rather than a bare constant) only
52
+ // so a future comment-writing DERIVATION path has exactly one place to
53
+ // add a rule; today it always answers `"authored"`, including for a
54
+ // a decline comment. This is DELIBERATE and is guarded by this
55
+ // module's own test suite (`anno-store-export.test.ts`'s Test 6): a
56
+ // decline comment is machine-written by `runMemmapJoin()`'s bank-state
57
+ // resolution, but the TEXT it produces is exactly as authored/frozen as
58
+ // a hand-written purpose comment once persisted -- neither regenerates
59
+ // byte-for-byte from a re-run in the way a typed range does, because a
60
+ // decline's reason strings can change across `anno-join.ts` code
61
+ // revisions even though the underlying bytes have not. Because
62
+ // `provenance` is never a real column, the round trip reproduces it for
63
+ // free: re-exporting a freshly imported document recomputes the same
64
+ // answer from the same rules, with nothing to persist or drift.
65
+ // - Never re-implement a validator this project already owns.
66
+ // `assertDataType()`, `assertRangeShape()`, `parseStoreAddress()`,
67
+ // `assertCommentType()`, `assertCommentText()`, `assertLabelKind()`,
68
+ // `assertLegalLabel()`, `assertAccessKind()`, `assertEvidSourceBank()`,
69
+ // `assertRunIdentityDigest()`, `assertRunIdentitySeed()` and
70
+ // `assertEnumName()` (all `anno-types.ts`) are pure functions of their
71
+ // arguments -- none touches SQL -- so every one of them is reused here,
72
+ // during validation, before any store write happens. The one exception
73
+ // is project-enum `variants`/`description` shape: `anno-store.ts`'s own
74
+ // `validatedVariants()`/`validatedDescription()` are private to that
75
+ // module, so this module's `assertExportVariants()` is a NEW, narrower
76
+ // shape check (plain object, non-empty string values) that catches gross
77
+ // malformation before any write; `createProjectEnum()` still applies its
78
+ // own full validation at write time, and a document that passes this
79
+ // module's own pre-check but fails that stricter one is a disclosed,
80
+ // narrow residual gap -- see this module's test file for what IS
81
+ // covered by the pre-write pass.
82
+
83
+ import { basename } from "node:path";
84
+
85
+ import {
86
+ setDataType,
87
+ setLabel,
88
+ setComment,
89
+ createProjectEnum,
90
+ applyEnumUsage,
91
+ putXref,
92
+ insertExecObservations,
93
+ addScope,
94
+ listRanges,
95
+ listLabels,
96
+ listComments,
97
+ listProjectEnums,
98
+ listEnumUsage,
99
+ listXrefs,
100
+ listExecObservations,
101
+ listScopes,
102
+ } from "./anno-store.ts";
103
+ import type { AnnoStoreHandle } from "./anno-store.ts";
104
+ import {
105
+ assertDataType,
106
+ assertRangeShape,
107
+ parseStoreAddress,
108
+ assertCommentType,
109
+ assertCommentText,
110
+ assertLabelKind,
111
+ assertLegalLabel,
112
+ assertAccessKind,
113
+ assertEvidSourceBank,
114
+ assertRunIdentityDigest,
115
+ assertRunIdentitySeed,
116
+ assertEnumName,
117
+ } from "./anno-types.ts";
118
+ import type { DataType, CommentType, LabelKind, XrefAccessKind, EvidSourceBank } from "./anno-types.ts";
119
+
120
+ /** The one version number a document carries. Bumped only when this file's
121
+ * own export shape changes in a way an older importer could not read
122
+ * safely. An unrecognised version is REFUSED BY NAME (this project's
123
+ * standing decline-by-name pattern), never best-effort imported. */
124
+ export const STORE_EXPORT_SCHEMA_VERSION = 1;
125
+
126
+ /** The two provenance classes D-03 splits the store into. See this file's
127
+ * own header for why this is never a real store column. */
128
+ export type RowProvenance = "derived" | "authored";
129
+
130
+ /** Criterion 4's "what is unknown and why" convention: a bank-state or
131
+ * disagreement resolution that could not be determined. Matches
132
+ * `anno-join.ts`'s own decline reasons in spirit (RESEARCH.md Section 3). */
133
+ export const DECLINE_COMMENT_PREFIX = "DECLINED:";
134
+
135
+ /** Criterion 2's "reviewed and accepted" convention -- a DIFFERENT fact from
136
+ * "could not be determined": a runtime disagreement a human/agent explicitly
137
+ * reviewed and chose to accept rather than reclassify. */
138
+ export const DISAGREEMENT_ACCEPTED_COMMENT_PREFIX = "DISAGREEMENT-ACCEPTED:";
139
+
140
+ /** D-10's per-range authored marker: an explicit statement that a range's
141
+ * typing rests on authored judgement rather than derivation or runtime
142
+ * observation. */
143
+ export const AUTHORED_PROVENANCE_COMMENT_PREFIX = "PROVENANCE: authored";
144
+
145
+ /** True iff `text` is a recorded decline (criterion 4's own convention).
146
+ * The ONE predicate the completeness report and the closure passes both
147
+ * need, so neither re-derives a prefix match locally. Distinct from
148
+ * `provenanceForComment()`: a decline comment is machine-written but still
149
+ * classified `"authored"` -- see this file's own header. */
150
+ export function isDeclineComment(text: string): boolean {
151
+ return text.startsWith(DECLINE_COMMENT_PREFIX);
152
+ }
153
+
154
+ /** The provenance of ONE comment, as a function of its own text. Always
155
+ * `"authored"` today -- see this file's header for why, and
156
+ * `anno-store-export.test.ts`'s Test 6 for the regression guard against the
157
+ * opposite (and wrong) assumption that a decline comment is derived. */
158
+ export function provenanceForComment(text: string): RowProvenance {
159
+ void text;
160
+ return "authored";
161
+ }
162
+
163
+ /** One typed range as the export document holds it. `id` is deliberately
164
+ * OMITTED -- a store-internal rowid, never portable across a re-derivation
165
+ * or a fresh import target, and carrying it would make the round-trip test
166
+ * compare numbers that were never meant to agree. `provenance` is always
167
+ * `"derived"` here -- see this file's header. */
168
+ export interface StoreExportRangeRow {
169
+ start: number;
170
+ endInclusive: number;
171
+ dataType: DataType;
172
+ bank: number | null;
173
+ provenance: RowProvenance;
174
+ }
175
+
176
+ /** One label as the export document holds it. `id` omitted -- see
177
+ * `StoreExportRangeRow`'s own doc comment for why. */
178
+ export interface StoreExportLabelRow {
179
+ address: number;
180
+ name: string;
181
+ kind: LabelKind;
182
+ bank: number | null;
183
+ }
184
+
185
+ /** One comment as the export document holds it. `id` omitted. `provenance`
186
+ * is computed per row via `provenanceForComment()`. */
187
+ export interface StoreExportCommentRow {
188
+ address: number;
189
+ commentType: CommentType;
190
+ text: string;
191
+ bank: number | null;
192
+ provenance: RowProvenance;
193
+ }
194
+
195
+ /** One project-local enum as the export document holds it. `id` omitted. */
196
+ export interface StoreExportProjectEnumRow {
197
+ name: string;
198
+ variants: Readonly<Record<string, string>>;
199
+ description: string | null;
200
+ }
201
+
202
+ /** One enum usage as the export document holds it. `enumId` is deliberately
203
+ * REPLACED by `enumName` -- `enumId` is a store-internal foreign key with no
204
+ * meaning across a fresh import target (the target's own `anno_enum` rows
205
+ * get their own fresh ids), while `enumName` is exactly what
206
+ * `applyEnumUsage()` already accepts as its own portable identifier
207
+ * (`anno-store.ts`'s own doc comment: "The association is by `anno_enum.id`,
208
+ * never by name" is the STORE's internal invariant, not this export's --
209
+ * this export's OWN cross-row reference is by name, resolved back to an id
210
+ * at import time exactly the way a caller already does). */
211
+ export interface StoreExportEnumUsageRow {
212
+ address: number;
213
+ enumName: string;
214
+ bank: number | null;
215
+ }
216
+
217
+ /** One cross-reference as the export document holds it. `id` omitted. */
218
+ export interface StoreExportXrefRow {
219
+ fromAddress: number;
220
+ toAddress: number;
221
+ accessKind: XrefAccessKind;
222
+ bank: number | null;
223
+ }
224
+
225
+ /** One lexical scope as the export document holds it. `id` omitted -- see
226
+ * `StoreExportRangeRow`'s own doc comment for why. A scope carries no `bank`
227
+ * and no `provenance`: `ScopeRow` (`anno-types.ts`) is a bare `{id, start,
228
+ * endInclusive}` -- a lexical region, not a memory view, and not itself
229
+ * derived from or authored over bytes the way a range or a comment is. Added
230
+ * for the multi-file export path (Phase 47): `exportAsmTree()` reads scopes
231
+ * straight off an open store handle via `listScopes()`, never from this
232
+ * document, so a scope this generic export omitted would silently vanish the
233
+ * moment a committed export got re-imported into a fresh store ahead of that
234
+ * export -- exactly the gap a purpose-built fixture whose whole point is
235
+ * exercising scoped, multi-file export would otherwise hit. Additive only:
236
+ * `STORE_EXPORT_SCHEMA_VERSION` does not change, and `importStoreDocument()`
237
+ * treats an absent `scopes` array (every export produced before this field
238
+ * existed) as empty, never as a schema violation. */
239
+ export interface StoreExportScopeRow {
240
+ start: number;
241
+ endInclusive: number;
242
+ }
243
+
244
+ /** One runtime-execution observation as the export document holds it. `id`
245
+ * omitted. */
246
+ export interface StoreExportExecObservationRow {
247
+ imageSha256: string;
248
+ argvDigest: string;
249
+ seed: string;
250
+ address: number;
251
+ sourceBank: EvidSourceBank;
252
+ }
253
+
254
+ /** The full document `exportStoreDocument()` returns and
255
+ * `importStoreDocument()` accepts. Every array `exportStoreDocument()` itself
256
+ * produces is present even when empty (never omitted), and every array is
257
+ * sorted by its own stated stable key so two exports of the same store are
258
+ * byte-identical (Test 2). `store` is the BASENAME only -- an absolute host
259
+ * path in a committed artifact is a portability defect (T-45-08). `scopes`
260
+ * is the one field a hand-written or older document may omit -- see its own
261
+ * doc comment. */
262
+ export interface StoreExportDocument {
263
+ schemaVersion: number;
264
+ store: string;
265
+ ranges: StoreExportRangeRow[];
266
+ labels: StoreExportLabelRow[];
267
+ comments: StoreExportCommentRow[];
268
+ projectEnums: StoreExportProjectEnumRow[];
269
+ enumUsage: StoreExportEnumUsageRow[];
270
+ xrefs: StoreExportXrefRow[];
271
+ execObservations: StoreExportExecObservationRow[];
272
+ /** Optional for backward compatibility with a document exported before this
273
+ * field existed -- `importStoreDocument()` treats an absent array as
274
+ * empty. `exportStoreDocument()` always populates it. */
275
+ scopes?: StoreExportScopeRow[];
276
+ }
277
+
278
+ /** Per-row-class counts of what `importStoreDocument()` wrote. */
279
+ export interface ImportSummary {
280
+ ranges: number;
281
+ labels: number;
282
+ comments: number;
283
+ projectEnums: number;
284
+ enumUsage: number;
285
+ xrefs: number;
286
+ execObservations: number;
287
+ scopes: number;
288
+ }
289
+
290
+ /** This module's own refusal class for document-shape violations that are
291
+ * not already covered by one of `anno-types.ts`'s own validators (an
292
+ * unrecognised `schemaVersion`, a `provenance` value that is neither
293
+ * `"derived"` nor `"authored"`, an enum usage naming an enum the document
294
+ * itself never defines, or a malformed `projectEnums` entry). A bare `Error`
295
+ * subclass, mirroring `AnnoImportError`'s own reasoning: this module's
296
+ * VALIDATION phase never touches the store's persistence, so it has no
297
+ * reason to join the `AnnoStoreError`/`ViceError` family for that phase. */
298
+ export class AnnoStoreExportError extends Error {
299
+ constructor(message: string) {
300
+ super(message);
301
+ this.name = "AnnoStoreExportError";
302
+ }
303
+ }
304
+
305
+ function isRowProvenance(value: unknown): value is RowProvenance {
306
+ return value === "derived" || value === "authored";
307
+ }
308
+
309
+ function assertRowProvenance(value: unknown, what: string): RowProvenance {
310
+ if (!isRowProvenance(value)) {
311
+ throw new AnnoStoreExportError(`${what}: provenance must be "derived" or "authored"; got ${JSON.stringify(value)}`);
312
+ }
313
+ return value;
314
+ }
315
+
316
+ /**
317
+ * 45-REVIEW WR-01's fix: `bank` is exported faithfully by `exportStoreDocument()`
318
+ * for every row kind (`StoreExportRangeRow.bank`, `StoreExportLabelRow.bank`,
319
+ * `StoreExportCommentRow.bank`, `StoreExportEnumUsageRow.bank`,
320
+ * `StoreExportXrefRow.bank`), but no write call on `anno-store.ts`'s current
321
+ * surface (`setDataType`, `setLabel`, `setComment`, `applyEnumUsage`, `putXref`)
322
+ * accepts a `bank` argument -- every fresh insert hard-codes `bank: null`
323
+ * (`anno-store.ts:1890`). Threading a real value through five write calls with
324
+ * no bank-carrying writer anywhere in the codebase to prove it against would be
325
+ * exactly the kind of speculative widening this project's other modules refuse
326
+ * (D-10's "narrower than the review's own sketch" pattern). Refusing a non-null
327
+ * `bank` BY NAME instead -- this project's standing "refuse by name, never
328
+ * silently drop" convention (`enumUsage[i]` naming an undefined enum, above, is
329
+ * the same shape) -- means the moment a real writer starts producing a
330
+ * non-null `bank`, importing that document fails LOUDLY, naming the row and
331
+ * the value, rather than silently losing it while `importStoreDocument()`
332
+ * reports success. Every one of this phase's own nine committed fixtures
333
+ * carries `bank: null` throughout (confirmed by `anno-store-export.test.ts`
334
+ * and this module's own round-trip proof), so this refusal is unreachable on
335
+ * every fixture that exists today -- it exists for the writer that does not
336
+ * exist yet.
337
+ */
338
+ function assertExportBankIsNull(bank: unknown, what: string): null {
339
+ if (bank !== null) {
340
+ throw new AnnoStoreExportError(
341
+ `${what}: bank must be null -- no write call on anno-store.ts's current surface accepts a bank argument (every fresh insert ` +
342
+ `hard-codes bank: null), so a non-null bank in an imported document would be silently discarded rather than round-tripped. ` +
343
+ `Refusing by name (got ${JSON.stringify(bank)}) rather than importing it and reporting success.`,
344
+ );
345
+ }
346
+ return bank;
347
+ }
348
+
349
+ /** A narrow, NEW shape check for a project enum's `variants` mapping --
350
+ * `anno-store.ts`'s own `validatedVariants()` is private to that module (see
351
+ * this file's header). Catches gross malformation (not a plain object, or a
352
+ * non-string/empty variant name) before any write; `createProjectEnum()`
353
+ * still applies its own full validation (numeric-string key format) at
354
+ * write time. */
355
+ function assertExportVariants(variants: unknown, what: string): Record<string, string> {
356
+ if (typeof variants !== "object" || variants === null || Array.isArray(variants)) {
357
+ throw new AnnoStoreExportError(`${what}: variants must be a plain object mapping value strings to variant names; got ${JSON.stringify(variants)}`);
358
+ }
359
+ for (const [key, value] of Object.entries(variants as Record<string, unknown>)) {
360
+ if (typeof value !== "string" || value === "") {
361
+ throw new AnnoStoreExportError(`${what}: variant ${JSON.stringify(key)} must map to a non-empty string name; got ${JSON.stringify(value)}`);
362
+ }
363
+ }
364
+ return variants as Record<string, string>;
365
+ }
366
+
367
+ function assertExportDescription(description: unknown, what: string): string | null {
368
+ if (description !== null && typeof description !== "string") {
369
+ throw new AnnoStoreExportError(`${what}: description must be a string or null; got ${JSON.stringify(description)}`);
370
+ }
371
+ return description;
372
+ }
373
+
374
+ /**
375
+ * Exports the WHOLE state of an already-open store as a versioned,
376
+ * stably-sorted JSON document. Every array is sorted by its own key so two
377
+ * exports of the same store are byte-identical (Test 2) -- never left to
378
+ * `node:sqlite`'s own row order, which is insertion order and can drift
379
+ * across a rewritten table.
380
+ */
381
+ export function exportStoreDocument(handle: AnnoStoreHandle, opts: { storeName?: string } = {}): StoreExportDocument {
382
+ const storeName = opts.storeName ?? basename(handle.path);
383
+
384
+ const ranges: StoreExportRangeRow[] = [...listRanges(handle)]
385
+ .sort((a, b) => a.start - b.start || a.endInclusive - b.endInclusive)
386
+ .map((row) => ({
387
+ start: row.start,
388
+ endInclusive: row.endInclusive,
389
+ dataType: row.dataType,
390
+ bank: row.bank,
391
+ provenance: "derived" as const,
392
+ }));
393
+
394
+ const labels: StoreExportLabelRow[] = [...listLabels(handle)]
395
+ .sort((a, b) => a.address - b.address || a.name.localeCompare(b.name))
396
+ .map((row) => ({ address: row.address, name: row.name, kind: row.kind, bank: row.bank }));
397
+
398
+ const comments: StoreExportCommentRow[] = [...listComments(handle)]
399
+ .sort((a, b) => a.address - b.address || a.commentType.localeCompare(b.commentType))
400
+ .map((row) => ({
401
+ address: row.address,
402
+ commentType: row.commentType,
403
+ text: row.text,
404
+ bank: row.bank,
405
+ provenance: provenanceForComment(row.text),
406
+ }));
407
+
408
+ const projectEnums: StoreExportProjectEnumRow[] = [...listProjectEnums(handle)]
409
+ .sort((a, b) => a.name.localeCompare(b.name))
410
+ .map((row) => ({ name: row.name, variants: row.variants, description: row.description }));
411
+
412
+ const enumUsage: StoreExportEnumUsageRow[] = [...listEnumUsage(handle)]
413
+ .sort((a, b) => a.address - b.address || a.enumName.localeCompare(b.enumName))
414
+ .map((row) => ({ address: row.address, enumName: row.enumName, bank: row.bank }));
415
+
416
+ const xrefs: StoreExportXrefRow[] = [...listXrefs(handle)]
417
+ .sort((a, b) => a.fromAddress - b.fromAddress || a.toAddress - b.toAddress || a.accessKind.localeCompare(b.accessKind))
418
+ .map((row) => ({ fromAddress: row.fromAddress, toAddress: row.toAddress, accessKind: row.accessKind, bank: row.bank }));
419
+
420
+ const execObservations: StoreExportExecObservationRow[] = [...listExecObservations(handle)]
421
+ .sort(
422
+ (a, b) =>
423
+ a.address - b.address ||
424
+ a.sourceBank.localeCompare(b.sourceBank) ||
425
+ a.imageSha256.localeCompare(b.imageSha256) ||
426
+ a.argvDigest.localeCompare(b.argvDigest) ||
427
+ a.seed.localeCompare(b.seed),
428
+ )
429
+ .map((row) => ({
430
+ imageSha256: row.imageSha256,
431
+ argvDigest: row.argvDigest,
432
+ seed: row.seed,
433
+ address: row.address,
434
+ sourceBank: row.sourceBank,
435
+ }));
436
+
437
+ const scopes: StoreExportScopeRow[] = [...listScopes(handle)]
438
+ .sort((a, b) => a.start - b.start)
439
+ .map((row) => ({ start: row.start, endInclusive: row.endInclusive }));
440
+
441
+ return {
442
+ schemaVersion: STORE_EXPORT_SCHEMA_VERSION,
443
+ store: storeName,
444
+ ranges,
445
+ labels,
446
+ comments,
447
+ projectEnums,
448
+ enumUsage,
449
+ xrefs,
450
+ execObservations,
451
+ scopes,
452
+ };
453
+ }
454
+
455
+ /** One fully-validated write, ready to apply with no further checking. A
456
+ * discriminated union so `importStoreDocument()`'s apply loop is a single
457
+ * `switch`, never a re-inspection of the original document shape. */
458
+ type PlannedWrite =
459
+ | { kind: "range"; start: number; endInclusive: number; dataType: DataType }
460
+ | { kind: "label"; address: number; name: string; kind_: LabelKind }
461
+ | { kind: "comment"; address: number; commentType: CommentType; text: string }
462
+ | { kind: "projectEnum"; name: string; variants: Record<string, string>; description: string | null }
463
+ | { kind: "enumUsage"; address: number; name: string }
464
+ | { kind: "xref"; fromAddress: number; toAddress: number; accessKind: XrefAccessKind }
465
+ | { kind: "execObservationGroup"; imageSha256: string; argvDigest: string; seed: string; observations: { address: number; sourceBank: EvidSourceBank }[] }
466
+ | { kind: "scope"; start: number; endInclusive: number };
467
+
468
+ /**
469
+ * Imports a `StoreExportDocument` into an already-open store handle.
470
+ *
471
+ * THE WHOLE DOCUMENT IS PARSED AND VALIDATED INTO AN IN-MEMORY WRITE PLAN
472
+ * BEFORE THE FIRST MUTATING CALL (`anno-import.ts`'s own rule, copied
473
+ * deliberately). A validation failure ANYWHERE in the document -- including
474
+ * its very last row -- throws before any `set*`/`put*`/`insert*` function on
475
+ * `handle` has been called, so the target store is left byte-identical to
476
+ * how it started (Test 4).
477
+ *
478
+ * `provenance` is validated for SHAPE (must be `"derived"` or `"authored"`)
479
+ * but is never itself written anywhere: it is not a real store column (see
480
+ * this file's header), and re-exporting the freshly imported store
481
+ * recomputes the same answer from the same rules.
482
+ */
483
+ export function importStoreDocument(handle: AnnoStoreHandle, doc: StoreExportDocument): ImportSummary {
484
+ if (doc.schemaVersion !== STORE_EXPORT_SCHEMA_VERSION) {
485
+ throw new AnnoStoreExportError(
486
+ `anno-store-export refused: document schemaVersion ${JSON.stringify(doc.schemaVersion)} is not the version this build reads ` +
487
+ `(expected ${STORE_EXPORT_SCHEMA_VERSION}) -- an unrecognised version is refused by name, never best-effort imported.`,
488
+ );
489
+ }
490
+
491
+ const plan: PlannedWrite[] = [];
492
+
493
+ for (const [i, row] of doc.ranges.entries()) {
494
+ const dataType = assertDataType(row.dataType);
495
+ assertRangeShape(row.start, row.endInclusive, dataType);
496
+ assertRowProvenance(row.provenance, `ranges[${i}]`);
497
+ assertExportBankIsNull(row.bank, `ranges[${i}]`);
498
+ plan.push({ kind: "range", start: row.start, endInclusive: row.endInclusive, dataType });
499
+ }
500
+
501
+ for (const [i, row] of doc.labels.entries()) {
502
+ const address = parseStoreAddress(row.address, { what: `labels[${i}].address` });
503
+ const name = assertLegalLabel(row.name);
504
+ const kind = assertLabelKind(row.kind);
505
+ assertExportBankIsNull(row.bank, `labels[${i}]`);
506
+ plan.push({ kind: "label", address, name, kind_: kind });
507
+ }
508
+
509
+ for (const [i, row] of doc.comments.entries()) {
510
+ const address = parseStoreAddress(row.address, { what: `comments[${i}].address` });
511
+ const commentType = assertCommentType(row.commentType);
512
+ const text = assertCommentText(row.text, { what: `comments[${i}].text` });
513
+ assertRowProvenance(row.provenance, `comments[${i}]`);
514
+ assertExportBankIsNull(row.bank, `comments[${i}]`);
515
+ plan.push({ kind: "comment", address, commentType, text });
516
+ }
517
+
518
+ const definedEnumNames = new Set<string>();
519
+ for (const [i, row] of doc.projectEnums.entries()) {
520
+ const name = assertEnumName(row.name);
521
+ const variants = assertExportVariants(row.variants, `projectEnums[${i}]`);
522
+ const description = assertExportDescription(row.description, `projectEnums[${i}]`);
523
+ definedEnumNames.add(name);
524
+ plan.push({ kind: "projectEnum", name, variants, description });
525
+ }
526
+
527
+ for (const [i, row] of doc.enumUsage.entries()) {
528
+ const address = parseStoreAddress(row.address, { what: `enumUsage[${i}].address` });
529
+ const name = assertEnumName(row.enumName);
530
+ if (!definedEnumNames.has(name)) {
531
+ throw new AnnoStoreExportError(
532
+ `anno-store-export refused: enumUsage[${i}] names project enum ${JSON.stringify(name)}, which this document's own projectEnums array does not define -- an enum usage naming an undefined enum is refused, never imported against a guess.`,
533
+ );
534
+ }
535
+ assertExportBankIsNull(row.bank, `enumUsage[${i}]`);
536
+ plan.push({ kind: "enumUsage", address, name });
537
+ }
538
+
539
+ for (const [i, row] of doc.xrefs.entries()) {
540
+ const fromAddress = parseStoreAddress(row.fromAddress, { what: `xrefs[${i}].fromAddress` });
541
+ const toAddress = parseStoreAddress(row.toAddress, { what: `xrefs[${i}].toAddress` });
542
+ const accessKind = assertAccessKind(row.accessKind);
543
+ assertExportBankIsNull(row.bank, `xrefs[${i}]`);
544
+ plan.push({ kind: "xref", fromAddress, toAddress, accessKind });
545
+ }
546
+
547
+ const execGroups = new Map<string, { imageSha256: string; argvDigest: string; seed: string; observations: { address: number; sourceBank: EvidSourceBank }[] }>();
548
+ for (const [i, row] of doc.execObservations.entries()) {
549
+ const imageSha256 = assertRunIdentityDigest(row.imageSha256, `execObservations[${i}].imageSha256`);
550
+ const argvDigest = assertRunIdentityDigest(row.argvDigest, `execObservations[${i}].argvDigest`);
551
+ const seed = assertRunIdentitySeed(row.seed);
552
+ const address = parseStoreAddress(row.address, { what: `execObservations[${i}].address` });
553
+ const sourceBank = assertEvidSourceBank(row.sourceBank);
554
+ const key = `${imageSha256}${argvDigest}${seed}`;
555
+ let group = execGroups.get(key);
556
+ if (group === undefined) {
557
+ group = { imageSha256, argvDigest, seed, observations: [] };
558
+ execGroups.set(key, group);
559
+ }
560
+ group.observations.push({ address, sourceBank });
561
+ }
562
+ for (const group of execGroups.values()) {
563
+ plan.push({ kind: "execObservationGroup", ...group });
564
+ }
565
+
566
+ // `scopes` is OPTIONAL on the document -- see `StoreExportScopeRow`'s own
567
+ // doc comment. A document from before this field existed has `undefined`
568
+ // here, treated as empty, never as a schema violation.
569
+ const scopeRows = doc.scopes ?? [];
570
+ const sortedScopes = [...scopeRows].map((row, i) => ({ row, i })).sort((a, b) => a.row.start - b.row.start);
571
+ // EXISTING-STORE OVERLAP IS ALSO REFUSED HERE, before any write -- not just
572
+ // overlap among the document's own scopes (checked below). `addScope()`
573
+ // itself refuses a scope that overlaps a scope ALREADY IN THE TARGET STORE,
574
+ // via a live query inside its own transaction, and that refusal used to
575
+ // fire only after ranges/labels/comments/enums/xrefs/exec-observations from
576
+ // this same plan had already been committed -- contradicting the
577
+ // "VALIDATION IS COMPLETE" claim below and the document-internal refusal's
578
+ // own "whole import is refused rather than partially applied" guarantee.
579
+ // Mirroring `addScope()`'s predicate (and its identical-scope idempotence:
580
+ // a byte-identical repeat is an accepted no-op, never an overlap) up here
581
+ // closes that gap without touching `anno-store.ts`'s transaction model.
582
+ const existingScopes = listScopes(handle);
583
+ for (let s = 0; s < sortedScopes.length; s++) {
584
+ const { row, i } = sortedScopes[s]!;
585
+ assertRangeShape(row.start, row.endInclusive, "byte");
586
+ const conflict = existingScopes.find(
587
+ (existing) =>
588
+ !(existing.start === row.start && existing.endInclusive === row.endInclusive) &&
589
+ existing.start <= row.endInclusive &&
590
+ existing.endInclusive >= row.start,
591
+ );
592
+ if (conflict) {
593
+ throw new AnnoStoreExportError(
594
+ `anno-store-export refused: scopes[${i}] (${row.start}..${row.endInclusive}) overlaps a scope already present in the target ` +
595
+ `store (id=${conflict.id} ${conflict.start}..${conflict.endInclusive}) -- nested and overlapping scopes are unsupported by ` +
596
+ `the schema this store mirrors, so the whole import is refused rather than partially applied.`,
597
+ );
598
+ }
599
+ // OVERLAP IS REFUSED using `addScope()`'s own predicate, checked here
600
+ // against the DOCUMENT's own scopes before any write -- a document whose
601
+ // own scopes overlap must never partially apply.
602
+ const prev = s > 0 ? sortedScopes[s - 1]!.row : undefined;
603
+ if (prev && prev.endInclusive >= row.start) {
604
+ throw new AnnoStoreExportError(
605
+ `anno-store-export refused: scopes[${i}] (${row.start}..${row.endInclusive}) overlaps another scope in this same document -- ` +
606
+ `nested and overlapping scopes are unsupported by the schema this store mirrors, so the whole import is refused rather than ` +
607
+ `partially applied.`,
608
+ );
609
+ }
610
+ plan.push({ kind: "scope", start: row.start, endInclusive: row.endInclusive });
611
+ }
612
+
613
+ // VALIDATION IS COMPLETE. Nothing above this line calls a `set*`/`put*`/
614
+ // `insert*` function on `handle` -- everything from here on is applying
615
+ // the already-validated plan.
616
+ const summary: ImportSummary = { ranges: 0, labels: 0, comments: 0, projectEnums: 0, enumUsage: 0, xrefs: 0, execObservations: 0, scopes: 0 };
617
+
618
+ for (const write of plan) {
619
+ switch (write.kind) {
620
+ case "range":
621
+ setDataType(handle, { start: write.start, endInclusive: write.endInclusive, dataType: write.dataType });
622
+ summary.ranges++;
623
+ break;
624
+ case "label":
625
+ setLabel(handle, { address: write.address, name: write.name, kind: write.kind_ });
626
+ summary.labels++;
627
+ break;
628
+ case "comment":
629
+ setComment(handle, { address: write.address, commentType: write.commentType, text: write.text });
630
+ summary.comments++;
631
+ break;
632
+ case "projectEnum":
633
+ createProjectEnum(handle, { name: write.name, variants: write.variants, description: write.description ?? undefined });
634
+ summary.projectEnums++;
635
+ break;
636
+ case "enumUsage":
637
+ applyEnumUsage(handle, { address: write.address, name: write.name });
638
+ summary.enumUsage++;
639
+ break;
640
+ case "xref":
641
+ putXref(handle, { fromAddress: write.fromAddress, toAddress: write.toAddress, accessKind: write.accessKind });
642
+ summary.xrefs++;
643
+ break;
644
+ case "execObservationGroup":
645
+ insertExecObservations(handle, {
646
+ imageSha256: write.imageSha256,
647
+ argvDigest: write.argvDigest,
648
+ seed: write.seed,
649
+ observations: write.observations,
650
+ });
651
+ summary.execObservations += write.observations.length;
652
+ break;
653
+ case "scope":
654
+ addScope(handle, { start: write.start, endInclusive: write.endInclusive });
655
+ summary.scopes++;
656
+ break;
657
+ }
658
+ }
659
+
660
+ return summary;
661
+ }