@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.
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1674 -99
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1175 -89
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-join.ts +480 -0
- package/anno-provenance-ledger.ts +472 -0
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +518 -2
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +275 -2
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +365 -210
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- 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
|
+
}
|