@henols/vice-mcp 0.2.2 → 0.2.4

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 (84) 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 +1736 -163
  5. package/anno-confidence.ts +2 -2
  6. package/anno-derive.ts +6 -6
  7. package/anno-details.ts +4 -4
  8. package/anno-enum-gen.ts +416 -30
  9. package/anno-export-asm.ts +1211 -126
  10. package/anno-graphics.ts +338 -0
  11. package/anno-hazard-report.ts +1367 -0
  12. package/anno-import.ts +495 -0
  13. package/anno-index.ts +8 -8
  14. package/anno-join.ts +480 -0
  15. package/anno-memmap-render.ts +22 -21
  16. package/anno-provenance-ledger.ts +472 -0
  17. package/anno-regbits-gen.ts +13 -13
  18. package/anno-register.ts +159 -0
  19. package/anno-store-export.ts +661 -0
  20. package/anno-store.ts +635 -124
  21. package/anno-symbols.ts +7 -7
  22. package/anno-tools.ts +1169 -16
  23. package/anno-types.ts +313 -40
  24. package/backend-detect.mts +124 -312
  25. package/build.ts +3 -1
  26. package/capture-predicate.ts +597 -0
  27. package/channel-lock.ts +349 -0
  28. package/evid-ingest.ts +217 -0
  29. package/evid-reconcile.ts +316 -0
  30. package/host-tool-client.ts +430 -0
  31. package/incident-record.ts +23 -12
  32. package/install-resources.ts +29 -13
  33. package/memmap-lookup.ts +285 -0
  34. package/package.json +27 -8
  35. package/prg-image.ts +1 -2
  36. package/repo-root.ts +87 -3
  37. package/resources/backend-detect.mjs +98 -236
  38. package/resources/broker-control.mjs +220 -54
  39. package/resources/broker-epoch.mjs +7 -8
  40. package/resources/broker-kill.mjs +36 -31
  41. package/resources/broker-launch.mjs +511 -374
  42. package/resources/broker-state.mjs +69 -24
  43. package/resources/container-guard.mjs +1 -1
  44. package/resources/ghidra-project.mjs +790 -0
  45. package/resources/host-tool.mjs +2533 -0
  46. package/resources/vice-broker.mjs +434 -290
  47. package/resources/vice-launcher.sh +127 -9
  48. package/stock-address.ts +1 -1
  49. package/stock-condition.ts +1 -1
  50. package/stock-connect.ts +9 -5
  51. package/stock-derived.ts +29 -37
  52. package/stock-diagnose.ts +200 -36
  53. package/stock-dispatch.ts +179 -77
  54. package/stock-handler.ts +1 -1
  55. package/stock-paths.ts +18 -14
  56. package/stock-petscii.ts +1 -1
  57. package/stock-protocol.ts +1 -1
  58. package/stock-recycle.ts +83 -2
  59. package/stock-reproducible-run.ts +811 -0
  60. package/stock-run-until.ts +100 -1
  61. package/stock-symbols.ts +4 -4
  62. package/stock-timing.ts +1 -1
  63. package/stop-oracle.ts +167 -0
  64. package/text-capability-probe.ts +660 -0
  65. package/text-connect.ts +157 -0
  66. package/text-protocol.ts +810 -0
  67. package/text-tools.ts +778 -0
  68. package/textmon-backtrace.ts +385 -0
  69. package/textmon-cpuhistory.ts +335 -0
  70. package/textmon-memmap.ts +494 -0
  71. package/textmon-profile.ts +458 -0
  72. package/textmon-registers.ts +748 -0
  73. package/tools-manifest.stock.json +864 -3
  74. package/vice-broker-client.ts +253 -108
  75. package/vice-errors.ts +268 -0
  76. package/vice-proxy.ts +339 -2144
  77. package/vsf-slice.ts +640 -0
  78. package/anno-d64.ts +0 -310
  79. package/capability-registry.ts +0 -390
  80. package/refresh-manifest.ts +0 -124
  81. package/tools-manifest.json +0 -1223
  82. package/vice-probe.ts +0 -278
  83. package/vice-sync.ts +0 -336
  84. package/vice.ts +0 -772
@@ -0,0 +1,472 @@
1
+ // anno-provenance-ledger.ts -- the ONE place `recovery/PROVENANCE.md`'s
2
+ // generated tier is turned back into typed rows.
3
+ //
4
+ // ---------------------------------------------------------------------------
5
+ // WHY THIS FILE EXISTS
6
+ // ---------------------------------------------------------------------------
7
+ // Nothing in this repository has ever READ the provenance ledger. It has one
8
+ // writer -- `renderLedger()` in `src/skills/c64-provenance-diff/scripts/
9
+ // diff-images.mjs` -- and zero readers, because the requirement governing this
10
+ // module requires the verdict be READ from the existing ledger, "never
11
+ // re-derived". Recomputing it in-process would force the
12
+ // exporter to import a registry-resolution path it has no other reason to
13
+ // know about, and would make "ledger absent" ambiguous between three
14
+ // different missing things. This module exists so "read" has an implementation
15
+ // distinct from "recompute".
16
+ //
17
+ // ---------------------------------------------------------------------------
18
+ // WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR
19
+ // ---------------------------------------------------------------------------
20
+ // Turning `renderLedger()`'s emitted Markdown table -- the generated tier of
21
+ // `recovery/PROVENANCE.md` -- back into typed `ProvenanceLedgerRow` values,
22
+ // plus the one pure address-range join (`provenanceForRange()`) a caller uses
23
+ // to find which rows overlap a block it is about to emit.
24
+ //
25
+ // ---------------------------------------------------------------------------
26
+ // WHAT NOT TO DO
27
+ // ---------------------------------------------------------------------------
28
+ // - Never recompute a verdict. This module parses TEXT `renderLedger()`
29
+ // already wrote; it never calls `diffRanges()`, never touches a release
30
+ // registry, and never derives a Verdict or Confidence value from bytes.
31
+ // - Never repair a malformed row. A row that does not split into exactly
32
+ // seven pipe-delimited cells, or whose Start/End cell is not the `$XXXX`
33
+ // shape `renderLedger()`'s own `hex4()` writes, is REFUSED -- never
34
+ // best-effort-parsed, never defaulted, never silently skipped. No
35
+ // trimming a cell into shape, no defaulting a missing column, no
36
+ // coercing a bad address to zero, no dropping a bad row and continuing
37
+ // with the rest: a partially-parsed ledger that reports success is worse
38
+ // than a refusal, because every downstream verdict then looks
39
+ // authoritative.
40
+ // - Never interpolate the ledger's own row text into an error message. A
41
+ // path, a line number and an expected shape are facts ABOUT a file; its
42
+ // contents are not, and quoting them turns a refusal into a
43
+ // content-disclosure oracle -- the same rule `anno-export-asm.ts`'s own
44
+ // header states and this module's refusals below all follow. THE
45
+ // PERMITTED VOCABULARY, enumerated rather than left as a general
46
+ // instruction so a later contributor adding a refusal has it in front of
47
+ // them, is exactly these five fact kinds and nothing else:
48
+ // 1. the ledger's PATH.
49
+ // 2. a 1-based LINE NUMBER, when the refusal is about one row.
50
+ // 3. a column NAME (`"Start"`, `"End"`, or one of
51
+ // `PROVENANCE_LEDGER_HEADER_CELLS`).
52
+ // 4. a cell COUNT (how many cells a row split into, vs. how many were
53
+ // expected).
54
+ // 5. a parsed ADDRESS (a `number` this module itself derived by calling
55
+ // `parseHexAddress()`, formatted back through this module's own
56
+ // `hex4()` -- never a cell's raw text passed through unexamined).
57
+ // Cell TEXT -- Kind, Verdict, Confidence, Agreeing releases, Evidence /
58
+ // Reason, or any cell that failed to parse -- is not on this list and
59
+ // must never appear in a thrown message.
60
+ // - Never re-derive `renderLedger()`'s CONTENT-shaped preconditions here.
61
+ // `renderLedger()` refuses to EMIT under three conditions: an UNKNOWN row
62
+ // with an empty reason, an ORIGINAL row with `agreeing_releases` below 2,
63
+ // and a generated tier that does not tile `$0000-$FFFF` exactly. The
64
+ // first two are claims about what a Verdict cell's CONTENT means --
65
+ // asserting them here would make this reader adjudicate a verdict's
66
+ // substance, exactly the "tool is the decider" shape this phase exists
67
+ // to make structurally unreachable. Coverage, disjointness and ascending
68
+ // order (the third condition, and the ones this module DOES assert
69
+ // below) are claims about ADDRESSES only, which is the one thing a join
70
+ // over ranges is allowed to reason about. A file violating an
71
+ // ADDRESS-shaped invariant was never producible by the one writer this
72
+ // format has, so the violation is evidence of a hand edit or corruption,
73
+ // not of a reader bug -- accepting it would mean joining a store range
74
+ // against a table that does not describe the whole address space.
75
+ //
76
+ // This module imports NOTHING from `src/skills/` (the shipped `@henols/
77
+ // vice-mcp` tarball does not contain that tree, and `anno-join.ts:26` already
78
+ // states the rule for this module family) and nothing from
79
+ // `hostpath.ts`/`containerpath.ts` -- every path this module reads is handed
80
+ // to it already resolved, exactly as `anno-export-asm.ts`'s own `imagePath`
81
+ // is documented: the CALLER owns confinement, this module only reads.
82
+ import { readFileSync } from "node:fs";
83
+
84
+ /**
85
+ * The seven column names `renderLedger()` writes, in this exact order and
86
+ * spelling (`diff-images.mjs`'s own header row). ONE declaration, one
87
+ * spelling -- `readProvenanceLedger()` below locates the header row by
88
+ * matching every cell against this array, so a header row that drifted from
89
+ * this exact wording is treated as "no header found" rather than guessed at.
90
+ */
91
+ export const PROVENANCE_LEDGER_HEADER_CELLS = Object.freeze([
92
+ "Start",
93
+ "End",
94
+ "Kind",
95
+ "Verdict",
96
+ "Confidence",
97
+ "Agreeing releases",
98
+ "Evidence / Reason",
99
+ ] as const);
100
+
101
+ /**
102
+ * One row of the ledger's generated tier, copied verbatim off the table --
103
+ * every string field is the cell's own text, UN-ESCAPED (`\|` back to `|`)
104
+ * but never trimmed of meaning, normalised, upper-cased or mapped. An
105
+ * unrecognised Verdict or Confidence string is carried through exactly as
106
+ * written; this module does not know the vocabulary and does not check it.
107
+ */
108
+ export interface ProvenanceLedgerRow {
109
+ /** First address the row covers, parsed from the `$XXXX` form `hex4()`
110
+ * writes. */
111
+ start: number;
112
+ /** Last address the row covers, INCLUSIVE -- `renderLedger()` writes
113
+ * `hex4(r.end)` and advances its own coverage cursor with `r.end + 1`, so
114
+ * the column this module reads is already an inclusive end. */
115
+ endInclusive: number;
116
+ /** The Kind cell, copied verbatim -- `game`/`loader`/`cracktro`/`io`/
117
+ * `unused`/`unresolved` in a well-formed ledger, but this module does not
118
+ * check membership; it copies whatever text is there. */
119
+ kind: string;
120
+ /** The Verdict cell, copied verbatim. Never normalised: an unrecognised
121
+ * string is carried through as text, exactly as the plan's own prohibition
122
+ * requires. */
123
+ verdict: string;
124
+ /** The Confidence cell, copied verbatim, on the same terms as `verdict`. */
125
+ confidence: string;
126
+ /**
127
+ * The Agreeing releases cell, carried as a STRING, never parsed to a
128
+ * number. `renderLedger()` interpolates `${r.agreeing_releases}` with no
129
+ * guard, so a `CRACKER-PATCH` or `UNKNOWN` row can legitimately carry the
130
+ * literal text `undefined` in this column -- inventing a number there
131
+ * would be this module deciding something the ledger itself never
132
+ * committed to.
133
+ */
134
+ agreeingReleases: string;
135
+ /** The Evidence / Reason cell, un-escaped (`\|` back to `|`) but
136
+ * otherwise verbatim. */
137
+ evidence: string;
138
+ }
139
+
140
+ /** One parsed ledger: the path it was read from, and every row the generated
141
+ * tier carried, in file order. */
142
+ export interface ProvenanceLedger {
143
+ path: string;
144
+ rows: readonly ProvenanceLedgerRow[];
145
+ }
146
+
147
+ export interface ProvenanceLedgerErrorOptions {
148
+ path: string;
149
+ /** 1-based line number of the offending row, when the refusal is about one
150
+ * specific row rather than the file as a whole. */
151
+ lineNumber?: number;
152
+ }
153
+
154
+ /**
155
+ * Thrown by `readProvenanceLedger()`. Follows `ViceError`'s shape convention
156
+ * (`vice.ts`) -- plain public fields, no sanitising -- rather than a
157
+ * message-only `Error`, so a caller that wants the path or the line number
158
+ * programmatically (a CLI wrapper printing its own summary, say) does not
159
+ * have to parse them back out of prose.
160
+ */
161
+ export class ProvenanceLedgerError extends Error {
162
+ path: string;
163
+ lineNumber?: number;
164
+
165
+ constructor(message: string, { path, lineNumber }: ProvenanceLedgerErrorOptions) {
166
+ super(message);
167
+ this.name = "ProvenanceLedgerError";
168
+ this.path = path;
169
+ this.lineNumber = lineNumber;
170
+ }
171
+ }
172
+
173
+ /** The remedy every refusal below names -- one sentence, one place, so the
174
+ * two named routes stay in sync with each other rather than being retyped at
175
+ * every throw site. */
176
+ const LEDGER_REMEDY =
177
+ 'regenerate it with c64-provenance-diff\'s "ledger" verb, or omit --ledger to export without provenance annotation';
178
+
179
+ /**
180
+ * Splits one line into its pipe-delimited cells, or returns `undefined` when
181
+ * the line is not shaped like a table row at all (does not start with `|`
182
+ * once trimmed -- true of the blank line and the `## Prose tier` heading that
183
+ * follow the generated tier, and of every prose line before the table).
184
+ *
185
+ * `\|` is protected before splitting and restored afterward in every cell,
186
+ * matching the one escape `renderLedger()`'s writer applies (only ever to the
187
+ * Evidence/Reason cell in practice, but restored uniformly here since a
188
+ * split-time placeholder is an artefact of splitting, not a second escaping
189
+ * rule).
190
+ *
191
+ * `PLACEHOLDER` is a one-character NUL string, built at RUNTIME with
192
+ * String.fromCharCode(0) rather than written as a literal byte -- or any
193
+ * backslash escape sequence naming that byte -- anywhere in this file's
194
+ * own source text. A raw NUL byte on disk makes `git diff` render this
195
+ * module as `Bin 0 -> N bytes` with no line-level diff ever again, and
196
+ * hides the file from a plain (non-`-a`) `grep` census -- the exact
197
+ * hazard `anno-memmap-render.ts` already carries and that this module
198
+ * must not add a second instance of. `String.fromCharCode(0)` produces
199
+ * the IDENTICAL runtime character while keeping every byte of the
200
+ * source file printable ASCII, with no escape-sequence spelling for a
201
+ * reviewing tool (or a lossy copy-paste) to silently collapse into the
202
+ * raw byte it names. A plain space is NOT a safe substitute for the
203
+ * sentinel value: ordinary ledger prose ("Agreeing releases", "Evidence
204
+ * / Reason") already contains spaces, so restoring a space-shaped
205
+ * placeholder back to `|` would corrupt every cell that happens to
206
+ * contain one -- reproduced against this file's own header-row match
207
+ * failing outright. NUL is chosen because ordinary ledger text -- a
208
+ * Markdown table written by renderLedger(), plausibly hand-edited --
209
+ * cannot contain it.
210
+ *
211
+ * The leading and trailing empty cells a `| a | b |`-shaped line produces are
212
+ * dropped ONLY when they are genuinely empty -- a row missing its closing `|`
213
+ * is left with a non-empty last cell, so the seven-cell count check at the
214
+ * call site catches the malformed shape rather than this function silently
215
+ * repairing it.
216
+ *
217
+ * A raw NUL byte already present in the line -- e.g. in a hand-edited or
218
+ * corrupted ledger's Evidence/Reason cell -- is REFUSED rather than restored:
219
+ * without this check, an existing NUL would collide with the placeholder
220
+ * this function inserts for its own `\|` escaping, and the unconditional
221
+ * `.join("|")` restoration below would silently turn that pre-existing NUL
222
+ * into an extra, unescaped `|` that never delimited anything in the source
223
+ * file. That is exactly the "repair a malformed row" outcome this module's
224
+ * header forbids, so it is named and refused like every other malformed
225
+ * shape this function's caller checks for -- never coerced, never dropped.
226
+ * `ledgerPath` and `lineNumber` are threaded through only to build that
227
+ * refusal's message; they change no other behaviour of this function.
228
+ */
229
+ function splitTableRow(line: string, ledgerPath: string, lineNumber: number): string[] | undefined {
230
+ const trimmed = line.trim();
231
+ if (!trimmed.startsWith("|")) return undefined;
232
+
233
+ const PLACEHOLDER = String.fromCharCode(0);
234
+ if (trimmed.includes(PLACEHOLDER)) {
235
+ throw new ProvenanceLedgerError(
236
+ `anno-provenance-ledger: row ${lineNumber} of "${ledgerPath}" contains a raw NUL byte -- refusing to parse a row that ` +
237
+ `could collide with this module's own internal escape-placeholder sentinel rather than silently turning it into an ` +
238
+ `unescaped "|". ${LEDGER_REMEDY}.`,
239
+ { path: ledgerPath, lineNumber },
240
+ );
241
+ }
242
+ const protectedLine = trimmed.replace(/\\\|/g, PLACEHOLDER);
243
+ let cells = protectedLine.split("|");
244
+ if (cells[0] === "") cells = cells.slice(1);
245
+ if (cells.length > 0 && cells[cells.length - 1] === "") cells = cells.slice(0, -1);
246
+
247
+ return cells.map((cell) => cell.trim().split(PLACEHOLDER).join("|"));
248
+ }
249
+
250
+ /** Parses a `$XXXX` cell (exactly `hex4()`'s own output shape) into a
251
+ * number, or `undefined` when the cell does not match -- the caller turns
252
+ * `undefined` into a named refusal rather than this function guessing. */
253
+ function parseHexAddress(cell: string): number | undefined {
254
+ const match = /^\$([0-9a-fA-F]{4})$/.exec(cell);
255
+ if (!match) return undefined;
256
+ return Number.parseInt(match[1]!, 16);
257
+ }
258
+
259
+ /** Formats a PARSED address (never a cell's raw text) as `$XXXX`, for use in
260
+ * refusal messages -- the one fact kind on the permitted-vocabulary list that
261
+ * is a number this module derived itself, not text copied off the file. */
262
+ function hex4(value: number): string {
263
+ return `$${(value & 0xffff).toString(16).padStart(4, "0")}`;
264
+ }
265
+
266
+ /**
267
+ * Throws the shared bad-address refusal for row `lineNumber`'s `column`
268
+ * cell (`"Start"` or `"End"`) -- ONE template for both columns, since the
269
+ * only fact that differs between them is the column NAME, which is on the
270
+ * permitted vocabulary list. The cell's own text is never read here or by
271
+ * either call site: the caller passes only the column name, never the
272
+ * string that failed to parse.
273
+ */
274
+ function refuseBadAddress(ledgerPath: string, lineNumber: number, column: "Start" | "End"): never {
275
+ throw new ProvenanceLedgerError(
276
+ `anno-provenance-ledger: row ${lineNumber} of "${ledgerPath}" has a ${column} cell that is not the "$XXXX" shape ` +
277
+ `renderLedger()'s own hex4() writes -- refusing to parse an address this module cannot be sure of, and never guessing ` +
278
+ `one from the cell's own text. ${LEDGER_REMEDY}.`,
279
+ { path: ledgerPath, lineNumber },
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Reads and parses `ledgerPath`'s generated tier.
285
+ *
286
+ * Locates the header row by matching every cell against
287
+ * `PROVENANCE_LEDGER_HEADER_CELLS`, skips the `|---|` separator line
288
+ * immediately below it without re-validating its own shape, then parses
289
+ * every following pipe-delimited line into a `ProvenanceLedgerRow` until the
290
+ * first line that is not a table row at all, then asserts the accepted rows
291
+ * as a WHOLE are strictly ascending, disjoint, and tile exactly
292
+ * `$0000-$FFFF`.
293
+ *
294
+ * Refuses (`ProvenanceLedgerError`) for eight distinct reasons, and REJECTS
295
+ * rather than repairs at every one of them -- no trimming, no defaulting, no
296
+ * coercing a bad address to zero, no dropping a bad row and continuing with
297
+ * the rest. Listed here in RULE order rather than strict execution order
298
+ * (the zero-data-rows check can only run once the per-row loop below has
299
+ * finished, so it fires textually after checks 4-6 even though it is
300
+ * conceptually "does the table hold any rows at all"):
301
+ * 1. the file cannot be read.
302
+ * 2. no header row matching all seven cells is found.
303
+ * 3. the header and separator are present but zero data rows follow.
304
+ * 4. a candidate row does not split into exactly seven cells.
305
+ * 5. a candidate row's Start or End cell is not the `$XXXX` shape
306
+ * `hex4()` writes.
307
+ * 6. a candidate row's End is below its Start.
308
+ * 7. two rows overlap, or are not strictly ascending by Start.
309
+ * 8. the accepted rows do not begin at `$0000`, leave a gap, or stop
310
+ * below `$FFFF`.
311
+ *
312
+ * Every refusal names the path and, for a single-row problem, the 1-based
313
+ * line number -- never the row's own cell text. See the module header's
314
+ * "WHAT NOT TO DO" section for the full permitted-fact vocabulary, and for
315
+ * why checks 7 and 8 are the ONLY two that mirror `renderLedger()`'s own
316
+ * emit-time preconditions: they are the ADDRESS-shaped ones, and the two
317
+ * CONTENT-shaped ones (UNKNOWN-with-empty-reason, ORIGINAL-agreeing-below-2)
318
+ * are deliberately never re-asserted here.
319
+ */
320
+ export function readProvenanceLedger(ledgerPath: string): ProvenanceLedger {
321
+ let raw: string;
322
+ try {
323
+ raw = readFileSync(ledgerPath, "utf8");
324
+ } catch (err) {
325
+ throw new ProvenanceLedgerError(
326
+ `anno-provenance-ledger: could not read the ledger at "${ledgerPath}" (${err instanceof Error ? err.message : String(err)}) -- ` +
327
+ `refusing to annotate without it. ${LEDGER_REMEDY}.`,
328
+ { path: ledgerPath },
329
+ );
330
+ }
331
+
332
+ const lines = raw.split(/\r?\n/);
333
+
334
+ let headerLineIndex = -1;
335
+ for (let i = 0; i < lines.length; i++) {
336
+ const cells = splitTableRow(lines[i]!, ledgerPath, i + 1);
337
+ if (
338
+ cells !== undefined &&
339
+ cells.length === PROVENANCE_LEDGER_HEADER_CELLS.length &&
340
+ cells.every((cell, idx) => cell === PROVENANCE_LEDGER_HEADER_CELLS[idx])
341
+ ) {
342
+ headerLineIndex = i;
343
+ break;
344
+ }
345
+ }
346
+ if (headerLineIndex === -1) {
347
+ throw new ProvenanceLedgerError(
348
+ `anno-provenance-ledger: no header row matching the seven expected columns (${PROVENANCE_LEDGER_HEADER_CELLS.join(", ")}) ` +
349
+ `was found in "${ledgerPath}" -- refusing to guess which row starts the table. ${LEDGER_REMEDY}.`,
350
+ { path: ledgerPath },
351
+ );
352
+ }
353
+
354
+ const rows: ProvenanceLedgerRow[] = [];
355
+ /** 1-based source line number for `rows[k]`, kept parallel to `rows` so the
356
+ * ordering/overlap check below can name both offending rows without
357
+ * re-scanning `lines` -- an internal bookkeeping array, never exposed on
358
+ * `ProvenanceLedgerRow` itself. */
359
+ const rowLineNumbers: number[] = [];
360
+ // headerLineIndex + 1 is the `|---|` separator -- skipped, not
361
+ // re-validated. Parsing starts one line further on.
362
+ for (let i = headerLineIndex + 2; i < lines.length; i++) {
363
+ const line = lines[i]!;
364
+ const cells = splitTableRow(line, ledgerPath, i + 1);
365
+ if (cells === undefined) break; // first line that is not a table row at all ends the generated tier
366
+
367
+ if (cells.length !== PROVENANCE_LEDGER_HEADER_CELLS.length) {
368
+ throw new ProvenanceLedgerError(
369
+ `anno-provenance-ledger: row ${i + 1} of "${ledgerPath}" splits into ${cells.length} pipe-delimited cells, not the ` +
370
+ `${PROVENANCE_LEDGER_HEADER_CELLS.length} renderLedger() always writes -- refusing to parse a row whose own shape ` +
371
+ `is wrong. ${LEDGER_REMEDY}.`,
372
+ { path: ledgerPath, lineNumber: i + 1 },
373
+ );
374
+ }
375
+
376
+ const [startCell, endCell, kind, verdict, confidence, agreeingReleases, evidence] = cells as [
377
+ string,
378
+ string,
379
+ string,
380
+ string,
381
+ string,
382
+ string,
383
+ string,
384
+ ];
385
+ const start = parseHexAddress(startCell);
386
+ if (start === undefined) refuseBadAddress(ledgerPath, i + 1, "Start");
387
+ const end = parseHexAddress(endCell);
388
+ if (end === undefined) refuseBadAddress(ledgerPath, i + 1, "End");
389
+
390
+ if (end < start) {
391
+ throw new ProvenanceLedgerError(
392
+ `anno-provenance-ledger: row ${i + 1} of "${ledgerPath}" has End ${hex4(end)} below its Start ${hex4(start)} -- ` +
393
+ `refusing to accept a row whose own span is inverted. ${LEDGER_REMEDY}.`,
394
+ { path: ledgerPath, lineNumber: i + 1 },
395
+ );
396
+ }
397
+
398
+ rows.push({ start, endInclusive: end, kind, verdict, confidence, agreeingReleases, evidence });
399
+ rowLineNumbers.push(i + 1);
400
+ }
401
+
402
+ if (rows.length === 0) {
403
+ throw new ProvenanceLedgerError(
404
+ `anno-provenance-ledger: the header and separator in "${ledgerPath}" are present but zero data rows follow -- ` +
405
+ `refusing to treat a table that parsed to no rows the same as no ledger having been asked for at all. ${LEDGER_REMEDY}.`,
406
+ { path: ledgerPath },
407
+ );
408
+ }
409
+
410
+ // CHECK 7 -- ADDRESS-SHAPED, mirrors `renderLedger()`'s own "gap or
411
+ // overlap" precondition. Two rows overlap, or the rows are not strictly
412
+ // ascending by Start, iff a later row's Start does not come strictly after
413
+ // the earlier row's own Start AND End. `rows` is in FILE order (not
414
+ // re-sorted), so this also catches a ledger whose rows were reordered by a
415
+ // hand edit, not only a genuine address collision.
416
+ for (let idx = 1; idx < rows.length; idx++) {
417
+ const prev = rows[idx - 1]!;
418
+ const cur = rows[idx]!;
419
+ if (cur.start <= prev.start || cur.start <= prev.endInclusive) {
420
+ throw new ProvenanceLedgerError(
421
+ `anno-provenance-ledger: rows ${rowLineNumbers[idx - 1]} and ${rowLineNumbers[idx]} of "${ledgerPath}" are not ` +
422
+ `strictly ascending and disjoint by Start -- row ${rowLineNumbers[idx - 1]} spans ${hex4(prev.start)}..${hex4(prev.endInclusive)} ` +
423
+ `and row ${rowLineNumbers[idx]} spans ${hex4(cur.start)}..${hex4(cur.endInclusive)} -- refusing to join a store range against rows ` +
424
+ `this module cannot order unambiguously. ${LEDGER_REMEDY}.`,
425
+ { path: ledgerPath, lineNumber: rowLineNumbers[idx] },
426
+ );
427
+ }
428
+ }
429
+
430
+ // CHECK 8 -- ADDRESS-SHAPED, mirrors `renderLedger()`'s own two coverage
431
+ // preconditions (starts at $0000 with no gap; reaches $FFFF). Run only
432
+ // after check 7 has already established the rows are disjoint and
433
+ // ascending, so `expected` is a running "next address a row must start at"
434
+ // cursor rather than a re-derivation of the ordering check above.
435
+ let expected = 0;
436
+ for (const row of rows) {
437
+ if (row.start !== expected) {
438
+ throw new ProvenanceLedgerError(
439
+ `anno-provenance-ledger: "${ledgerPath}"'s accepted rows do not cover $0000..$FFFF -- address ${hex4(expected)} is not ` +
440
+ `covered by any row -- refusing to join a store range against a table that does not describe the whole address space. ` +
441
+ `${LEDGER_REMEDY}.`,
442
+ { path: ledgerPath },
443
+ );
444
+ }
445
+ expected = row.endInclusive + 1;
446
+ }
447
+ if (expected !== 0x10000) {
448
+ throw new ProvenanceLedgerError(
449
+ `anno-provenance-ledger: "${ledgerPath}"'s accepted rows stop at ${hex4(expected - 1)}, not $FFFF -- refusing to join a ` +
450
+ `store range against a table that does not describe the whole address space. ${LEDGER_REMEDY}.`,
451
+ { path: ledgerPath },
452
+ );
453
+ }
454
+
455
+ return { path: ledgerPath, rows };
456
+ }
457
+
458
+ /**
459
+ * Every row of `ledger` overlapping `[start, endInclusive]`, both ends
460
+ * inclusive, in ascending `start` order.
461
+ *
462
+ * Two ranges overlap iff each starts at or before the other ends -- the same
463
+ * `>=` reasoning `addScope()` in `anno-store.ts` already documents for
464
+ * scopes, so a row ending exactly at `start - 1` does NOT overlap. Returns
465
+ * EVERY match and never picks one: multiplicity is the caller's to record,
466
+ * never this function's to resolve. Takes no threshold, no filter predicate
467
+ * and no default -- the join is address-only, and no Verdict, Confidence or
468
+ * Kind value is ever read here.
469
+ */
470
+ export function provenanceForRange(ledger: ProvenanceLedger, start: number, endInclusive: number): readonly ProvenanceLedgerRow[] {
471
+ return ledger.rows.filter((row) => row.start <= endInclusive && row.endInclusive >= start).sort((a, b) => a.start - b.start);
472
+ }
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // anno-regbits-gen.ts -- the ONE authoritative place in this repo that turns
3
3
  // c64-memory-mapping's memmap.json into the curated address->bit-name table
4
- // anno-enum-gen.ts decodes register values against (D-22, ANNO-13).
4
+ // anno-enum-gen.ts decodes register values against.
5
5
  //
6
- // WHY THIS EXISTS (D-22): neither register the phase's own pinned criterion-3
6
+ // WHY THIS EXISTS: neither register the phase's own pinned criterion-3
7
7
  // target needs ($D011) nor the registers a real game writes to constantly
8
8
  // ($D015/$D017/$D01A-$D01D) can be named from memmap.json's own `bits` prose
9
9
  // alone -- some of that prose is OCR-damaged ("O = Blank" uses a letter O for
@@ -54,11 +54,11 @@ const HERE = dirname(fileURLToPath(import.meta.url));
54
54
 
55
55
  /** The sole read of c64-memory-mapping's own memmap.json -- this generator is
56
56
  * its only consumer for this purpose (per this plan's key_links entry).
57
- * 2026-08-22 (plan 16-01): the skills tree moved from `.claude/skills/`
57
+ * 2026-08-22: the skills tree moved from `.claude/skills/`
58
58
  * (two levels up from `.claude/mcp/vice`) to `src/skills/` (three levels up,
59
59
  * since `src/` sits directly under the repo root rather than under `.claude/`).
60
- * This literal was not in plan 16-01's own enumerated consumer list and its
61
- * test failures caught the gap live -- see 16-01-SUMMARY.md deviations. */
60
+ * This literal was not in the original enumerated consumer list, and a
61
+ * test failure caught the gap live. */
62
62
  const MEMMAP_PATH = join(HERE, "..", "..", "..", "src", "skills", "c64-memory-mapping", "memmap.json");
63
63
 
64
64
  /** Where the generated, committed artifact lives -- always a sibling of this
@@ -163,14 +163,14 @@ export interface RegbitsRegisterOverride {
163
163
  * entries by their `bit` string. */
164
164
  fields?: readonly RegbitsFieldOverride[];
165
165
  /** A COMPLETE field list for an address memmap.json's `io` parser produced
166
- * no `bits` entry for at all (D-22's known gap) -- used only when no
166
+ * no `bits` entry for at all -- used only when no
167
167
  * memmap entry exists for this address, never to replace one that does. */
168
168
  synthetic?: readonly RegBitsField[];
169
169
  }
170
170
 
171
171
  /** Builds the 8 independent "bit N = sprite N" flag fields the five VIC
172
172
  * sprite-plane registers ($D015/$D017/$D01B/$D01C/$D01D) all share the same
173
- * shape for (D-22's named gap) -- each bit is silent (empty token) when
173
+ * shape for -- each bit is silent (empty token) when
174
174
  * clear and names the specific sprite when set, so a typical enum (most
175
175
  * sprites off, one or two on) renders as a short, readable name instead of
176
176
  * naming all eight sprites' negative state every time.
@@ -236,15 +236,15 @@ export const OVERRIDES: readonly RegbitsRegisterOverride[] = [
236
236
  label: "CIA Interrupt Control Register (Read NMIs/Write Mask)",
237
237
  },
238
238
  {
239
- // WHY: memmap.json's `io` parser produced no `bits` entry at all for this address (D-22's
240
- // named gap) even though a real game writes to it constantly -- sprite enable, one flag bit
239
+ // WHY: memmap.json's `io` parser produced no `bits` entry at all for this address,
240
+ // even though a real game writes to it constantly -- sprite enable, one flag bit
241
241
  // per sprite, is trivially regular and does not need memmap's own prose to describe correctly.
242
242
  address: 53269, // $D015 -- Sprite Enable
243
243
  label: "Sprite Enable",
244
244
  synthetic: spriteBitFields("EN"),
245
245
  },
246
246
  {
247
- // WHY: same D-22 gap as $D015 -- Sprite Y-Expand, one flag bit per sprite.
247
+ // WHY: same missing-`bits`-entry gap as $D015 -- Sprite Y-Expand, one flag bit per sprite.
248
248
  address: 53271, // $D017 -- Sprite Y-Expand
249
249
  label: "Sprite Y-Expand",
250
250
  synthetic: spriteBitFields("YEXP"),
@@ -264,20 +264,20 @@ export const OVERRIDES: readonly RegbitsRegisterOverride[] = [
264
264
  ],
265
265
  },
266
266
  {
267
- // WHY: same D-22 gap as $D015 -- Sprite Priority (behind/in-front of background), one flag bit
267
+ // WHY: same missing-`bits`-entry gap as $D015 -- Sprite Priority (behind/in-front of background), one flag bit
268
268
  // per sprite.
269
269
  address: 53275, // $D01B -- Sprite Data Priority
270
270
  label: "Sprite Data Priority",
271
271
  synthetic: spriteBitFields("BG"),
272
272
  },
273
273
  {
274
- // WHY: same D-22 gap as $D015 -- Sprite Multicolor, one flag bit per sprite.
274
+ // WHY: same missing-`bits`-entry gap as $D015 -- Sprite Multicolor, one flag bit per sprite.
275
275
  address: 53276, // $D01C -- Sprite Multicolor
276
276
  label: "Sprite Multicolor",
277
277
  synthetic: spriteBitFields("MC"),
278
278
  },
279
279
  {
280
- // WHY: same D-22 gap as $D015 -- Sprite X-Expand, one flag bit per sprite.
280
+ // WHY: same missing-`bits`-entry gap as $D015 -- Sprite X-Expand, one flag bit per sprite.
281
281
  address: 53277, // $D01D -- Sprite X-Expand
282
282
  label: "Sprite X-Expand",
283
283
  synthetic: spriteBitFields("XEXP"),