@henols/vice-mcp 0.2.1 → 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 (108) hide show
  1. package/README.md +4 -3
  2. package/THIRD-PARTY-NOTICES.md +423 -25
  3. package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
  4. package/anno-bank.ts +171 -0
  5. package/anno-cli.ts +3040 -0
  6. package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
  7. package/anno-coverage.ts +2465 -0
  8. package/anno-derive.ts +590 -0
  9. package/anno-details.ts +169 -0
  10. package/anno-enum-gen.ts +919 -0
  11. package/anno-export-asm.ts +2396 -0
  12. package/anno-graphics.ts +338 -0
  13. package/anno-hazard-report.ts +1367 -0
  14. package/anno-import.ts +495 -0
  15. package/anno-index.ts +150 -0
  16. package/anno-join.ts +480 -0
  17. package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
  18. package/anno-provenance-ledger.ts +472 -0
  19. package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
  20. package/{r2000-regbits.json → anno-regbits.json} +2 -2
  21. package/anno-register.ts +399 -0
  22. package/anno-store-export.ts +661 -0
  23. package/anno-store.ts +4002 -0
  24. package/anno-symbols.ts +266 -0
  25. package/anno-tools.ts +3264 -0
  26. package/anno-types.ts +1909 -0
  27. package/backend-detect.mts +124 -312
  28. package/block-class.ts +201 -0
  29. package/build.ts +4 -2
  30. package/capture-predicate.ts +597 -0
  31. package/channel-lock.ts +349 -0
  32. package/disasm-decoder.ts +14 -14
  33. package/disasm-opcodes.ts +4 -4
  34. package/disasm-renderer.ts +2 -2
  35. package/evid-ingest.ts +217 -0
  36. package/evid-reconcile.ts +316 -0
  37. package/host-tool-client.ts +430 -0
  38. package/hostpath.ts +1 -1
  39. package/incident-record.ts +23 -12
  40. package/install-resources.ts +30 -14
  41. package/memmap-lookup.ts +285 -0
  42. package/package.json +48 -23
  43. package/prg-image.ts +118 -0
  44. package/repo-root.ts +107 -8
  45. package/resources/backend-detect.mjs +98 -236
  46. package/resources/broker-control.mjs +189 -16
  47. package/resources/broker-epoch.mjs +1 -1
  48. package/resources/broker-kill.mjs +8 -2
  49. package/resources/broker-launch.mjs +373 -214
  50. package/resources/broker-state.mjs +64 -18
  51. package/resources/container-guard.mjs +1 -1
  52. package/resources/ghidra-project.mjs +790 -0
  53. package/resources/host-tool.mjs +2561 -0
  54. package/resources/vice-broker.mjs +330 -184
  55. package/resources/vice-launcher.sh +130 -12
  56. package/stock-address.ts +6 -6
  57. package/stock-cia.ts +2 -2
  58. package/stock-condition.ts +8 -8
  59. package/stock-connect.ts +10 -6
  60. package/stock-derived.ts +29 -37
  61. package/stock-diagnose.ts +200 -36
  62. package/stock-dispatch.ts +200 -68
  63. package/stock-execution.ts +5 -3
  64. package/stock-handler.ts +1 -1
  65. package/stock-input.ts +9 -9
  66. package/stock-machine.ts +17 -6
  67. package/stock-paths.ts +18 -14
  68. package/stock-petscii.ts +1 -1
  69. package/stock-protocol.ts +17 -12
  70. package/stock-recycle.ts +83 -2
  71. package/stock-registers.ts +54 -29
  72. package/stock-reproducible-run.ts +811 -0
  73. package/stock-run-until.ts +100 -1
  74. package/stock-sprites.ts +3 -3
  75. package/stock-symbols.ts +13 -13
  76. package/stock-timing.ts +2 -2
  77. package/stock-vicii.ts +1 -1
  78. package/stop-oracle.ts +167 -0
  79. package/text-capability-probe.ts +660 -0
  80. package/text-connect.ts +157 -0
  81. package/text-protocol.ts +810 -0
  82. package/text-tools.ts +778 -0
  83. package/textmon-backtrace.ts +385 -0
  84. package/textmon-cpuhistory.ts +335 -0
  85. package/textmon-memmap.ts +494 -0
  86. package/textmon-profile.ts +458 -0
  87. package/textmon-registers.ts +748 -0
  88. package/tools-manifest.stock.json +864 -3
  89. package/version.ts +1 -1
  90. package/vice-broker-client.ts +189 -42
  91. package/vice-errors.ts +268 -0
  92. package/vice-proxy.ts +392 -2175
  93. package/vsf-slice.ts +640 -0
  94. package/capability-registry.ts +0 -388
  95. package/r2000-cli.ts +0 -1103
  96. package/r2000-d64.ts +0 -310
  97. package/r2000-enum-gen.ts +0 -574
  98. package/r2000-launch.ts +0 -357
  99. package/r2000-mcp-client.ts +0 -596
  100. package/r2000-project.ts +0 -190
  101. package/r2000-symbols.ts +0 -388
  102. package/r2000-tools.ts +0 -914
  103. package/r2000-verify.ts +0 -184
  104. package/refresh-manifest.ts +0 -124
  105. package/tools-manifest.json +0 -1223
  106. package/vice-probe.ts +0 -278
  107. package/vice-sync.ts +0 -336
  108. package/vice.ts +0 -772
package/anno-join.ts ADDED
@@ -0,0 +1,480 @@
1
+ #!/usr/bin/env node
2
+ // anno-join.ts
3
+ //
4
+ // Phase 37, plan 37-01 (AUTO-01): the mechanical join between stored
5
+ // cross-references and `memmap.json`, with no agent call, no queue walk and
6
+ // no skill invocation anywhere in the loop.
7
+ //
8
+ // THIS MODULE RECEIVES AN ALREADY-OPEN STORE HANDLE, exactly like
9
+ // `anno-import.ts` -- there is no second store session anywhere in this
10
+ // file. It never names `node:sqlite`, never calls `openStore()`/`closeStore()`
11
+ // itself, and never imports `hostpath.ts`/`containerpath.ts`: the image range
12
+ // it needs arrives as plain numbers (`imageOrigin`, `imageByteLength`) that
13
+ // the caller has already derived from a loaded image, mirroring
14
+ // `dxa-blocks.ts`'s own header posture of reading only already-fetched facts
15
+ // a caller passes in.
16
+ //
17
+ // WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: reading `listXrefs()`'s
18
+ // distinct target addresses, classifying each one (inside the loaded image,
19
+ // no `memmap.json` entry, or annotated) and writing the resulting comment
20
+ // through `setComment()`. `AUTO-01`'s own criterion -- no agent, no queue
21
+ // walk, no skill invocation -- is checked STRUCTURALLY over this module's own
22
+ // source in `anno-join.test.ts` (plan 37 task 3), not merely asserted here in
23
+ // prose.
24
+ //
25
+ // WHAT NOT TO DO:
26
+ // - Never import anything under `src/skills/`. This function's entire
27
+ // point is that annotating an address costs one mechanical call, not an
28
+ // agent turn.
29
+ // - Never spawn a child process from this module.
30
+ // - Never carry a bank value forward past a point where two paths
31
+ // disagree, and never default an unresolved bank state to the power-on
32
+ // value (T-37-26). A reaching-values set of size other than one, or one
33
+ // that decodes to more than one region, DECLINES -- see the bank-state
34
+ // block below.
35
+ //
36
+ // Phase 37, plan 37-06 (AUTO-04/AUTO-05): `runMemmapJoin()` gains an
37
+ // OPTIONAL `constWrites` argument. When `undefined` (every pre-37-06 call
38
+ // site, and every existing test in this file), the bank-state machinery
39
+ // below is a complete no-op and every address resolves EXACTLY as it did
40
+ // before this plan -- the candidate-constraint argument must not change any
41
+ // unconstrained selection's answer. Only when a caller explicitly supplies
42
+ // an array (even an empty one) does the reaching-values/decline logic
43
+ // activate for addresses inside `BANK_CONDITIONAL_RANGES`. D-37-24: the
44
+ // reaching-values computation is deliberately conservative -- it is NOT a
45
+ // dataflow analysis. It uses only what the export gives: the recovered
46
+ // constant stores to the processor port (each with its own address), and
47
+ // the cross-reference graph the importer already stored via `putXref()`.
48
+ // The set of recovered values reaching a given address is the set of
49
+ // `constWrites` whose OWN store address can reach that address, walked
50
+ // forward over that same graph. Where the graph does not connect a store to
51
+ // the address at all, that value is simply not in the reaching set --
52
+ // D-37-23's decline-on-empty-or-disagreement rule (below) is what turns
53
+ // "nothing reaches this point" into a stated absence rather than a silent
54
+ // default.
55
+
56
+ import { listXrefs, setComment, setDataType } from "./anno-store.ts";
57
+ import type { AnnoStoreHandle } from "./anno-store.ts";
58
+ import {
59
+ decodeBankState,
60
+ isBankConditionalAddress,
61
+ regionAdmitsEntry,
62
+ resolveBankedRegion,
63
+ } from "./anno-bank.ts";
64
+ import type { BankedRegion } from "./anno-bank.ts";
65
+ import type { ConstWriteFact } from "./anno-import.ts";
66
+ import type { ContradictedComment, SplitTableReinterpretation, XrefRow } from "./anno-types.ts";
67
+ import { loadMemmap, memmapDigest, PROVENANCE_TOKEN_PREFIX, selectMemmapEntry } from "./memmap-lookup.ts";
68
+ import type { MemmapEntry, MemmapSelection } from "./memmap-lookup.ts";
69
+ // Phase 37, plan 37-08 (AUTO-07): the graphics write-back. deriveGraphicsRanges()
70
+ // is structurally typed against ConstWriteFact -- GraphicsConstWriteFact's own
71
+ // shape is identical ({storeAddress, targetAddress, value}) -- so THIS module's
72
+ // own constWrites argument, already threaded for D-37-24's bank-state block, is
73
+ // handed straight through with no translation layer (37-07-SUMMARY.md's own
74
+ // "Next Phase Readiness" note names this directly).
75
+ import { deriveGraphicsRanges } from "./anno-graphics.ts";
76
+ import type { GraphicsMap } from "./anno-graphics.ts";
77
+
78
+ /** D-37-25: the axis-qualified provenance marker an annotated bank-conditional
79
+ * comment carries, ALWAYS before `PROVENANCE_TOKEN_PREFIX`'s own digest
80
+ * token (which stays last -- D-37-13). Names the axis ("processor-port")
81
+ * explicitly, so a later phase adding the VIC banking axis is additive
82
+ * rather than ambiguous about which axis a given token names. */
83
+ const BANK_PROVENANCE_PREFIX = "[processor-port:";
84
+
85
+ /**
86
+ * The reaching-values computation's own result shape (D-37-24): exactly one
87
+ * recovered value reaches the address; several do (an array -- length 0
88
+ * means none reach at all, length 2+ means genuine disagreement-or-agreement
89
+ * to resolve); or the computation cannot decide at all. Nothing in this
90
+ * project's current data model (a plain directed graph of already-resolved
91
+ * `XrefRow`s) can currently produce `"unknown"` -- there is no signal here
92
+ * for a dropped or unresolved reference (D-37-24's own stated limit) -- but
93
+ * the shape is kept complete for a future importer that DOES emit such a
94
+ * signal, rather than silently folding that case into "empty".
95
+ */
96
+ type ReachingValues = { kind: "one"; value: number } | { kind: "several"; values: number[] } | { kind: "unknown" };
97
+
98
+ /** Builds a forward adjacency map from `xrefs`, ONCE per join run, reused for
99
+ * every address's own reachability walk below. */
100
+ function buildAdjacency(xrefs: readonly XrefRow[]): Map<number, number[]> {
101
+ const adjacency = new Map<number, number[]>();
102
+ for (const xref of xrefs) {
103
+ const list = adjacency.get(xref.fromAddress);
104
+ if (list) list.push(xref.toAddress);
105
+ else adjacency.set(xref.fromAddress, [xref.toAddress]);
106
+ }
107
+ return adjacency;
108
+ }
109
+
110
+ /** Plain forward reachability (DFS, visited-set guarded against cycles):
111
+ * can `from` reach `target` by following zero or more `adjacency` edges? */
112
+ function canReach(from: number, target: number, adjacency: Map<number, number[]>): boolean {
113
+ if (from === target) return true;
114
+ const visited = new Set<number>([from]);
115
+ const stack = [from];
116
+ while (stack.length > 0) {
117
+ const current = stack.pop()!;
118
+ for (const next of adjacency.get(current) ?? []) {
119
+ if (next === target) return true;
120
+ if (!visited.has(next)) {
121
+ visited.add(next);
122
+ stack.push(next);
123
+ }
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+
129
+ /** D-37-24: the set of recovered processor-port values whose OWN store
130
+ * address can reach `targetAddress`, over `adjacency`. Never a dataflow
131
+ * analysis -- purely "does the stored cross-reference graph connect this
132
+ * store to this address". */
133
+ function computeReachingValues(
134
+ targetAddress: number,
135
+ constWrites: readonly ConstWriteFact[],
136
+ adjacency: Map<number, number[]>,
137
+ ): ReachingValues {
138
+ const reaching: number[] = [];
139
+ for (const write of constWrites) {
140
+ if (canReach(write.storeAddress, targetAddress, adjacency)) reaching.push(write.value);
141
+ }
142
+ if (reaching.length === 1) return { kind: "one", value: reaching[0]! };
143
+ return { kind: "several", values: reaching };
144
+ }
145
+
146
+ /** Raised when `runMemmapJoin()`'s own inputs cannot support a join at all --
147
+ * currently only the zero-byte-image refusal below. Follows `anno-import.ts`'s
148
+ * `AnnoImportError` construction idiom (a bare `Error` subclass, not
149
+ * `AnnoStoreError`, since this module never touches the store's own
150
+ * persistence). */
151
+ export class AnnoJoinError extends Error {
152
+ constructor(message: string) {
153
+ super(message);
154
+ this.name = "AnnoJoinError";
155
+ }
156
+ }
157
+
158
+ /** What one `runMemmapJoin()` call reports. `addressesConsidered` is always
159
+ * the sum of the next four fields. The three `graphics*` fields (plan 37-08,
160
+ * AUTO-07) are always present and `0` when `constWrites` is omitted or when
161
+ * the selected map derives zero ranges -- never absent, so a caller reads
162
+ * them unconditionally instead of guarding on them, mirroring
163
+ * `SetDataTypeResult`'s own "always present, often empty" convention for
164
+ * `contradictedComments`/`reinterpretedSplitTables`. */
165
+ export interface JoinCounts {
166
+ addressesConsidered: number;
167
+ annotated: number;
168
+ skippedInImage: number;
169
+ skippedNoMapEntry: number;
170
+ declined: number;
171
+ commentsChanged: number;
172
+ graphicsRangesWritten: number;
173
+ graphicsContradictedComments: number;
174
+ graphicsReinterpretedSplitTables: number;
175
+ }
176
+
177
+ /**
178
+ * What the graphics write-back (plan 37-08, AUTO-07) reports, in full --
179
+ * `JoinCounts`'s own `graphics*` fields are the COUNTS of these same
180
+ * `contradictedComments`/`reinterpretedSplitTables` arrays; this record
181
+ * carries the disclosures themselves so neither is dropped (must_haves.truths:
182
+ * "the join's returned counts include the contradicted-comment and
183
+ * fragmented-split-table disclosures the range write reported; neither is
184
+ * dropped"). `mapIndex` records WHICH of `deriveGraphicsRanges()`'s several
185
+ * maps was written -- D-37-27's own rule (several valid combinations are
186
+ * several maps, never one merged map) means writing more than one would
187
+ * write mutually-contradicting ranges into the SAME store, so exactly one is
188
+ * ever written and this field is the record of which. */
189
+ export interface GraphicsWriteBack {
190
+ mapIndex: number;
191
+ rangesWritten: number;
192
+ contradictedComments: readonly ContradictedComment[];
193
+ reinterpretedSplitTables: readonly SplitTableReinterpretation[];
194
+ }
195
+
196
+ /** One address's own outcome. A skip or a decline always carries a non-empty
197
+ * `reason` naming WHY in the join's own vocabulary; an annotation carries the
198
+ * `label` it wrote. */
199
+ export interface JoinDecision {
200
+ address: number;
201
+ outcome: "annotated" | "skipped-in-image" | "skipped-no-entry" | "declined";
202
+ reason?: string;
203
+ label?: string;
204
+ }
205
+
206
+ export interface RunMemmapJoinArgs {
207
+ imageOrigin: number;
208
+ imageByteLength: number;
209
+ /** D-37-24/AUTO-04/AUTO-05: the recovered `$01` const-write facts this run
210
+ * has evidence for, typically `parseConstWrites()`'s own output over one
211
+ * imported export. `undefined` (every pre-37-06 call site) means "this run
212
+ * carries no bank-state evidence at all" -- the bank-state machinery is a
213
+ * complete no-op and every address resolves EXACTLY as before this plan.
214
+ * An explicit array (even `[]`) activates it for addresses inside
215
+ * `BANK_CONDITIONAL_RANGES`. */
216
+ constWrites?: readonly ConstWriteFact[];
217
+ /** Plan 37-08 (AUTO-07, D-37-27): which of `deriveGraphicsRanges()`'s
218
+ * several maps to write back, when `constWrites` derives more than one
219
+ * distinct register-value combination. Defaults to `0`. Consulted ONLY
220
+ * when `constWrites` is supplied AT ALL (the SAME gate that activates the
221
+ * bank-state block above) -- omitting `constWrites` entirely skips the
222
+ * graphics write-back completely, exactly like the bank-state block. Out
223
+ * of range for the derived map count refuses BY NAME (`AnnoJoinError`)
224
+ * rather than silently clamping or picking a default. */
225
+ graphicsMapIndex?: number;
226
+ }
227
+
228
+ /**
229
+ * Joins every DISTINCT `toAddress` `listXrefs()` holds against `memmap.json`,
230
+ * in ascending address order. An address inside the caller's own loaded
231
+ * image range is a program address, not a hardware/memory-map feature, and
232
+ * is skipped WITHOUT a `memmap.json` lookup -- the membership test runs
233
+ * BEFORE `selectEntry()` is called at all (D-37-12), so the guard is a
234
+ * control-flow fact rather than a result-filtering one: `AUTO-03` says an
235
+ * in-image address is "never looked up in memmap.json", a claim about what
236
+ * runs, not merely about what the caller sees back. An address with no
237
+ * containing `memmap.json` entry is skipped for that reason instead;
238
+ * everything else is annotated with the selected entry's label via
239
+ * `setComment()`. Running this twice over an unchanged store re-classifies
240
+ * every address identically and reports `commentsChanged: 0` on the second
241
+ * run, because `setComment()` itself reports `changed: false` for a
242
+ * byte-identical repeat.
243
+ *
244
+ * `selectEntry` defaults to the real `selectMemmapEntry` and exists as an
245
+ * injection point for exactly one reason: it is the seam that makes "never
246
+ * looked up" checkable at all. Without it, the only available assertion is
247
+ * on the RESULT, and a result-filtering implementation (compute the
248
+ * selection, then discard it for an in-image address) would pass that
249
+ * assertion while still violating the requirement. A test passes a counting
250
+ * spy in its place and asserts zero calls for an all-in-image store.
251
+ */
252
+ export function runMemmapJoin(
253
+ handle: AnnoStoreHandle,
254
+ args: RunMemmapJoinArgs,
255
+ entries: readonly MemmapEntry[] = loadMemmap(),
256
+ selectEntry: (address: number, entries: readonly MemmapEntry[]) => MemmapSelection | undefined = selectMemmapEntry,
257
+ ): { counts: JoinCounts; decisions: JoinDecision[]; graphics?: GraphicsWriteBack } {
258
+ if (args.imageByteLength === 0) {
259
+ throw new AnnoJoinError(
260
+ "runMemmapJoin refused: the image's own body length is 0 -- an image with no bytes has no range, and treating " +
261
+ "it as a zero-width range at its own origin would silently make one address (the origin) read as in-image.",
262
+ );
263
+ }
264
+
265
+ // D-37-13: computed ONCE per join run and reused for every annotated row,
266
+ // never recomputed per row -- two comments written in the same run are
267
+ // therefore GUARANTEED to carry byte-identical tokens, not merely likely
268
+ // to (the file cannot change mid-run, but a per-row recompute would still
269
+ // be wasted work re-reading and re-hashing the same bytes for nothing).
270
+ const digest = memmapDigest();
271
+
272
+ const xrefs = listXrefs(handle);
273
+ const targets = [...new Set(xrefs.map((xref) => xref.toAddress))].sort((a, b) => a - b);
274
+
275
+ // D-37-24: built ONCE per run, over the SAME xref graph the unconstrained
276
+ // path already reads via `listXrefs()` above -- reused for every address's
277
+ // own reachability walk below. `undefined` `args.constWrites` means the
278
+ // bank-state block is never entered at all, so this adjacency map is built
279
+ // but simply never consulted -- negligible cost, and keeps the "no
280
+ // constWrites -> no-op" contract a single conditional rather than two
281
+ // divergent code paths.
282
+ const bankAdjacency = buildAdjacency(xrefs);
283
+
284
+ // The inclusive image range, computed ONCE from the LoadedImage's own body
285
+ // bytes (D-37-12) -- never from `totalBytes` (the file's own byte count,
286
+ // which on the .prg route includes the two-byte load-address header and
287
+ // would shift this whole range by two bytes) and never a caller-supplied
288
+ // number pair that could silently widen or narrow the program's own
289
+ // extent.
290
+ const imageStart = args.imageOrigin;
291
+ const imageEnd = args.imageOrigin + args.imageByteLength - 1;
292
+
293
+ const decisions: JoinDecision[] = [];
294
+ let annotated = 0;
295
+ let skippedInImage = 0;
296
+ let skippedNoMapEntry = 0;
297
+ let declined = 0;
298
+ let commentsChanged = 0;
299
+
300
+ for (const address of targets) {
301
+ // THE GUARD: one early-return, before selectEntry() is ever called. A
302
+ // single textual deletion of this block removes it cleanly -- that
303
+ // deletion is plan 37-05's own observed-red control (row 4).
304
+ if (address >= imageStart && address <= imageEnd) {
305
+ skippedInImage += 1;
306
+ decisions.push({
307
+ address,
308
+ outcome: "skipped-in-image",
309
+ reason:
310
+ `address $${address.toString(16)} lies inside the loaded image's own range ($${imageStart.toString(16)}-` +
311
+ `$${imageEnd.toString(16)}) and is therefore a program address, never looked up in memmap.json`,
312
+ });
313
+ continue;
314
+ }
315
+
316
+ // THE BANK-STATE BLOCK (D-37-22/D-37-23/D-37-24, AUTO-04/AUTO-05). Only
317
+ // entered when the caller supplied `constWrites` AT ALL (`undefined`
318
+ // skips this whole block, falling through to the unconstrained path
319
+ // below exactly as pre-37-06) AND the address is inside one of the
320
+ // three bank-conditional ranges -- outside them, bank state is
321
+ // irrelevant and the candidate set stays unconstrained regardless of
322
+ // `constWrites` (D-37-22).
323
+ if (args.constWrites !== undefined && isBankConditionalAddress(address)) {
324
+ const reaching = computeReachingValues(address, args.constWrites, bankAdjacency);
325
+
326
+ if (reaching.kind === "unknown") {
327
+ declined += 1;
328
+ decisions.push({
329
+ address,
330
+ outcome: "declined",
331
+ reason: `bank state at $${address.toString(16)} could not be determined -- the reaching-values computation could not decide`,
332
+ });
333
+ continue;
334
+ }
335
+
336
+ // Resolves ONE region (or refuses) for a single reaching value, applies
337
+ // it as a candidate constraint BEFORE selection runs (never a
338
+ // post-filter -- D-37-22), and pushes the matching decision. Shared by
339
+ // both the single-value and the several-values-same-region branches
340
+ // below, so the annotate path is written exactly once.
341
+ const annotateUnderRegion = (region: Exclude<BankedRegion, "not_applicable">, bankNote: string): void => {
342
+ const constrained = entries.filter((entry) => regionAdmitsEntry(entry, region));
343
+ const selection = selectEntry(address, constrained);
344
+ if (selection === undefined) {
345
+ skippedNoMapEntry += 1;
346
+ decisions.push({
347
+ address,
348
+ outcome: "skipped-no-entry",
349
+ reason: `no memmap.json entry admits the resolved ${region} region for $${address.toString(16)}`,
350
+ });
351
+ return;
352
+ }
353
+ const commentText = `${selection.entry.label} ${BANK_PROVENANCE_PREFIX}${bankNote}] ${PROVENANCE_TOKEN_PREFIX}${digest}`;
354
+ const write = setComment(handle, { address, commentType: "line", text: commentText });
355
+ annotated += 1;
356
+ if (write.changed) commentsChanged += 1;
357
+ decisions.push({
358
+ address,
359
+ outcome: "annotated",
360
+ label: selection.entry.label,
361
+ ...(reaching.kind === "several" ? { reason: `reached under differing processor-port values (${bankNote}) that all resolve to the same region (${region})` } : {}),
362
+ });
363
+ };
364
+
365
+ if (reaching.kind === "one") {
366
+ const region = resolveBankedRegion(address, decodeBankState(reaching.value));
367
+ annotateUnderRegion(region as Exclude<BankedRegion, "not_applicable">, `$${reaching.value.toString(16)}`);
368
+ continue;
369
+ }
370
+
371
+ // reaching.kind === "several"
372
+ if (reaching.values.length === 0) {
373
+ declined += 1;
374
+ decisions.push({
375
+ address,
376
+ outcome: "declined",
377
+ reason: `no recovered processor-port value reaches $${address.toString(16)} -- declining rather than defaulting to the power-on state`,
378
+ });
379
+ continue;
380
+ }
381
+
382
+ const uniqueValues = [...new Set(reaching.values)].sort((a, b) => a - b);
383
+ const regionsByValue = uniqueValues.map((value) => resolveBankedRegion(address, decodeBankState(value)));
384
+ const uniqueRegions = new Set(regionsByValue);
385
+ if (uniqueRegions.size > 1) {
386
+ declined += 1;
387
+ const named = uniqueValues.map((value, i) => `$${value.toString(16)}(${regionsByValue[i]})`).join(", ");
388
+ decisions.push({
389
+ address,
390
+ outcome: "declined",
391
+ reason: `$${address.toString(16)} is reached under disagreeing processor-port values: ${named}`,
392
+ });
393
+ continue;
394
+ }
395
+
396
+ const region = regionsByValue[0]! as Exclude<BankedRegion, "not_applicable">;
397
+ const valuesNote = uniqueValues.map((value) => `$${value.toString(16)}`).join(",");
398
+ annotateUnderRegion(region, valuesNote);
399
+ continue;
400
+ }
401
+
402
+ const selection = selectEntry(address, entries);
403
+ if (selection === undefined) {
404
+ skippedNoMapEntry += 1;
405
+ decisions.push({
406
+ address,
407
+ outcome: "skipped-no-entry",
408
+ reason: "no memmap.json entry contains this address",
409
+ });
410
+ continue;
411
+ }
412
+
413
+ // The full comment text: the selected entry's label, one space, the
414
+ // provenance prefix, then the full 64-character digest -- always LAST,
415
+ // never truncated (D-37-13). setComment() -> assertCommentText() refuses
416
+ // (never truncates) a text that overflows MAX_COMMENT_BYTES; that
417
+ // refusal is left to propagate here rather than being pre-checked and
418
+ // silently worked around, because a truncated provenance token would be
419
+ // a wrong answer that reports success.
420
+ const commentText = `${selection.entry.label} ${PROVENANCE_TOKEN_PREFIX}${digest}`;
421
+ const write = setComment(handle, { address, commentType: "line", text: commentText });
422
+ annotated += 1;
423
+ if (write.changed) commentsChanged += 1;
424
+ decisions.push({ address, outcome: "annotated", label: selection.entry.label });
425
+ }
426
+
427
+ // THE GRAPHICS WRITE-BACK (D-37-32/D-37-33, AUTO-07). Runs AFTER the main
428
+ // per-address loop above, as its own step -- graphics ranges are derived
429
+ // from register VALUES, never from the cross-reference targets the loop
430
+ // above walks, so there is no reason to interleave the two. Gated on the
431
+ // SAME `constWrites !== undefined` condition the bank-state block uses:
432
+ // omitting `constWrites` entirely is a complete no-op here too, exactly
433
+ // like the bank-state block above.
434
+ let graphics: GraphicsWriteBack | undefined;
435
+ if (args.constWrites !== undefined) {
436
+ const maps = deriveGraphicsRanges(args.constWrites);
437
+ const mapIndex = args.graphicsMapIndex ?? 0;
438
+ const selectedMap: GraphicsMap | undefined = maps[mapIndex];
439
+ if (selectedMap === undefined) {
440
+ throw new AnnoJoinError(
441
+ `runMemmapJoin refused: graphicsMapIndex ${mapIndex} is out of range -- deriveGraphicsRanges() produced ` +
442
+ `${maps.length} map(s) for this run's own constWrites`,
443
+ );
444
+ }
445
+
446
+ // D-37-27: write ONLY the selected map's own ranges -- never every map
447
+ // deriveGraphicsRanges() returned. Several distinct register-value
448
+ // combinations describe MUTUALLY CONTRADICTING layouts (D-37-27's own
449
+ // reason several maps exist at all); writing more than one into the
450
+ // same store would write ranges that disagree with each other by
451
+ // construction.
452
+ let rangesWritten = 0;
453
+ const contradictedComments: ContradictedComment[] = [];
454
+ const reinterpretedSplitTables: SplitTableReinterpretation[] = [];
455
+ for (const range of selectedMap.ranges) {
456
+ const write = setDataType(handle, { start: range.start, endInclusive: range.endInclusive, dataType: range.dataType });
457
+ rangesWritten += 1;
458
+ // Disclosures a successful range write can carry -- surfaced, never
459
+ // dropped (must_haves.truths): a comment whose recorded confidence now
460
+ // contradicts the type this write just assigned, or a split table this
461
+ // write fragmented.
462
+ contradictedComments.push(...write.contradictedComments);
463
+ reinterpretedSplitTables.push(...write.reinterpretedSplitTables);
464
+ }
465
+ graphics = { mapIndex, rangesWritten, contradictedComments, reinterpretedSplitTables };
466
+ }
467
+
468
+ const counts: JoinCounts = {
469
+ addressesConsidered: targets.length,
470
+ annotated,
471
+ skippedInImage,
472
+ skippedNoMapEntry,
473
+ declined,
474
+ commentsChanged,
475
+ graphicsRangesWritten: graphics?.rangesWritten ?? 0,
476
+ graphicsContradictedComments: graphics?.contradictedComments.length ?? 0,
477
+ graphicsReinterpretedSplitTables: graphics?.reinterpretedSplitTables.length ?? 0,
478
+ };
479
+ return graphics === undefined ? { counts, decisions } : { counts, decisions, graphics };
480
+ }