@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11

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 (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -1,525 +0,0 @@
1
- #!/usr/bin/env node
2
- // Cross-binary RAM/chip-state comparison: classify every difference between
3
- // two captures of DIFFERENT binaries and print a single VERDICT line.
4
- //
5
- // Pure logic. This module reads files the agent already produced and does
6
- // arithmetic over them. It contacts nothing: the mcp__plugin_c64-re-tools_vice__*
7
- // tools are the only route to the emulator (.claude/CLAUDE.md § Emulator
8
- // Access), and nothing here opens a connection, reads broker state, or shells
9
- // out.
10
- //
11
- // The narrowed volatile mask below is committed here, in this commit, before
12
- // any rebuild is compared under it (ROADMAP Phase 50 criterion 1). The mask
13
- // may be narrowed further later. It must never be widened just to make a
14
- // rebuild pass -- the same discipline `compare.mjs` and `derive-transients.mjs`
15
- // already carry for their own masks/allow-lists.
16
- //
17
- // This is a SIBLING of `compare.mjs`, not an extension of it. `compare.mjs`'s
18
- // own rules -- a blanket $D000-$DFFF mask and a one-bit "drift" tolerance --
19
- // are correct for its own job: two captures of the SAME binary at the same
20
- // checkpoint. Both rules are wrong for this module's job, which is two
21
- // captures of DIFFERENT binaries, where a one-bit difference at a masked
22
- // address (e.g. `lda #$02` -> `lda #$03` ahead of `sta $d020`) is exactly the
23
- // kind of regression a rebuild must not be allowed to hide. This module does
24
- // not import from, edit, or re-export `compare.mjs`; its rules stay frozen
25
- // for its own, still-correct, same-binary job. The three-bucket shape and the
26
- // classification core are duplicated here DELIBERATELY, not shared, so a
27
- // future edit to one module's rules cannot silently change the other's.
28
- //
29
- // Three rule departures from compare.mjs, each load-bearing:
30
- //
31
- // (a) NO DRIFT BUCKET. Across two different binaries a one-bit difference
32
- // is a real difference. `drift` exists to absorb sampling noise between
33
- // two runs of the SAME binary; applied across binaries it would absorb
34
- // an `lda #$02` to `lda #$03` regression whole. Every non-volatile,
35
- // non-allowlisted difference is a divergence here, regardless of bit
36
- // count.
37
- // (b) NARROWED I/O MASK. `compare.mjs` masks $D000-$DFFF entirely. This
38
- // module masks only the addresses and register fields that genuinely
39
- // cannot be stable, listed individually below with a reason each.
40
- // (c) ROUTE AWARENESS. A capture carries a declared route of "snapshot" or
41
- // "memory-read". On the snapshot route the bytes at $D000-$DFFF in the
42
- // image are RAM under I/O, not the register read view (SKILL.md,
43
- // "Slice the image out of a snapshot instead of transcribing it"); on
44
- // the memory-read route they are the register read view. Comparing one
45
- // against the other is meaningless, so this module refuses the pair.
46
- //
47
- // Departure from compare.mjs's own CLI-dispatch shape: the dispatch tail at
48
- // the bottom of this file is GUARDED
49
- // (`if (process.argv[1] === fileURLToPath(import.meta.url))`), unlike
50
- // compare.mjs's unguarded tail. This module exports IMAGE_VOLATILE and
51
- // IO_VOLATILE for reuse by this file's own test file and by later Phase 50
52
- // plans; an unguarded tail would call process.exit() the instant anything
53
- // imports this module, killing whatever process did the importing.
54
- // `dump-artifacts.mjs`, the newer script in this same directory, already
55
- // carries the identical guard for the identical reason.
56
-
57
- import { readFileSync } from "node:fs";
58
- import { createHash } from "node:crypto";
59
- import { basename, resolve } from "node:path";
60
- import { fileURLToPath } from "node:url";
61
-
62
- const IMAGE_BYTES = 65536;
63
-
64
- // A hand-bumped version string, printed into every report as
65
- // `MASK_NARROWED_AT:` so a transcript records which mask produced it. Bump
66
- // this whenever a mask span in IMAGE_VOLATILE or IO_VOLATILE changes --
67
- // never silently, and never to make a rebuild pass.
68
- const MASK_VERSION = "compare-cross-binary-mask-v1";
69
-
70
- // ---------------------------------------------------------------- IMAGE_VOLATILE
71
-
72
- // Spans of the 64K image that are excluded from the verdict on every route.
73
- // Carried forward from compare.mjs's own VOLATILE table, same reasons.
74
- export const IMAGE_VOLATILE = [
75
- { lo: 0x0000, hi: 0x0001, reason: "CPU port" },
76
- { lo: 0x0100, hi: 0x01ff, reason: "stack page" },
77
- { lo: 0x0200, hi: 0x03ff, reason: "KERNAL work area / BASIC input buffer" },
78
- ];
79
-
80
- // ------------------------------------------------------------------- IO_VOLATILE
81
-
82
- // Register offsets and ranges that stay excluded, each with its own one-line
83
- // hardware reason. The VIC-II only decodes 6 address bits, so its registers
84
- // at $D000-$D02E mirror every $40 bytes across the whole $D000-$D3FF window
85
- // -- entries here for that window are expressed as the CANONICAL address in
86
- // the first $D000-$D03F block and folded across all sixteen mirrors by
87
- // (addr & 0x3f) in isIoVolatile()/imageMaskEntryFor() below, so a regression
88
- // at $D020 is caught at $D020 and at all fifteen of its mirrors.
89
- //
90
- // Everything else in $D000-$D3FF -- $D000-$D010, $D013-$D018, $D01A-$D01D
91
- // and $D020-$D02E -- is deliberately NOT masked. $D015, $D018 and $D020 are
92
- // the three ROADMAP criterion 1 names inside that carve-out.
93
- const VIC_MIRRORED_VOLATILE = [
94
- {
95
- lo: 0xd011,
96
- hi: 0xd011,
97
- reason: "bit 7 is the raster counter's ninth bit, so the whole byte is masked",
98
- },
99
- { lo: 0xd012, hi: 0xd012, reason: "the raster counter" },
100
- { lo: 0xd019, hi: 0xd019, reason: "the interrupt latch" },
101
- { lo: 0xd01e, hi: 0xd01f, reason: "the collision latches, cleared by reading them" },
102
- ];
103
-
104
- // Ranges outside the VIC-II mirrored window. These do not mirror the same
105
- // way -- SID/colour-RAM/CIA/expansion each occupy their own flat span -- so
106
- // no offset-folding applies; the whole range is excluded.
107
- const FLAT_VOLATILE = [
108
- { lo: 0xd400, hi: 0xd7ff, reason: "SID is write-only in hardware, read-back is unrecoverable" },
109
- { lo: 0xd800, hi: 0xdbff, reason: "colour RAM whose high nibble is open bus" },
110
- {
111
- lo: 0xdc00,
112
- hi: 0xdcff,
113
- reason: "CIA1 -- timers, time-of-day and interrupt-control registers all move without the program touching them",
114
- },
115
- {
116
- lo: 0xdd00,
117
- hi: 0xddff,
118
- reason: "CIA2 -- timers, time-of-day and interrupt-control registers all move without the program touching them",
119
- },
120
- { lo: 0xde00, hi: 0xdfff, reason: "I/O expansion area and reads open bus" },
121
- ];
122
-
123
- export const IO_VOLATILE = [...VIC_MIRRORED_VOLATILE, ...FLAT_VOLATILE];
124
-
125
- function ioMaskEntryFor(addr) {
126
- if (addr >= 0xd000 && addr <= 0xd3ff) {
127
- const canonical = 0xd000 + (addr & 0x3f);
128
- return VIC_MIRRORED_VOLATILE.find(({ lo, hi }) => canonical >= lo && canonical <= hi) ?? null;
129
- }
130
- return FLAT_VOLATILE.find(({ lo, hi }) => addr >= lo && addr <= hi) ?? null;
131
- }
132
-
133
- export function isIoVolatile(addr) {
134
- return ioMaskEntryFor(addr) !== null;
135
- }
136
-
137
- function imageMaskEntryFor(addr, route) {
138
- const base = IMAGE_VOLATILE.find(({ lo, hi }) => addr >= lo && addr <= hi);
139
- if (base) return base;
140
- if (route === "memory-read" && addr >= 0xd000 && addr <= 0xdfff) {
141
- return ioMaskEntryFor(addr);
142
- }
143
- // snapshot route: $D000-$DFFF in the image is ordinary RAM under I/O, not
144
- // the register read view -- not masked at all. See SKILL.md, "Slice the
145
- // image out of a snapshot instead of transcribing it".
146
- return null;
147
- }
148
-
149
- export function isImageVolatile(addr, route) {
150
- return imageMaskEntryFor(addr, route) !== null;
151
- }
152
-
153
- // -------------------------------------------------------------------- format
154
-
155
- const hex4 = (n) => "$" + n.toString(16).toUpperCase().padStart(4, "0");
156
- const hex2 = (n) => "$" + n.toString(16).toUpperCase().padStart(2, "0");
157
- const bin8 = (n) => "%" + n.toString(2).padStart(8, "0");
158
-
159
- function loadImage(path) {
160
- const buf = readFileSync(path);
161
- if (buf.length !== IMAGE_BYTES) {
162
- throw new Error(`${path}: ${buf.length} bytes, expected ${IMAGE_BYTES} — not a full 64K image`);
163
- }
164
- return buf;
165
- }
166
-
167
- const sha256 = (buf) => createHash("sha256").update(buf).digest("hex");
168
-
169
- // ---------------------------------------------------------------------- state
170
-
171
- // --state <a.state.json> loads a per-capture sidecar this module defines its
172
- // own way -- distinct from, but inspired by, `dump-artifacts.mjs`'s
173
- // chip-state output shape (`registers`/`sprites`/`cpu`), since no committed
174
- // chip-state sidecar carries $Dxxx register values directly today. The shape
175
- // this module reads:
176
- //
177
- // {
178
- // "route": "snapshot" | "memory-read",
179
- // "registers": { "$D011": 27, "53272": 21, "0xD020": 14 }
180
- // }
181
- //
182
- // `registers` keys may be a "$Dxxx" hex string, a "0xNNNN" hex string, or a
183
- // bare decimal string -- whatever the caller already has to hand. Every key
184
- // must parse as an address or the sidecar is refused by name. `registers` is
185
- // optional: a capture with no chip-state evidence compares on the image
186
- // alone.
187
- function loadState(path) {
188
- return JSON.parse(readFileSync(path, "utf8"));
189
- }
190
-
191
- function parseAddrKey(k) {
192
- if (/^\$[0-9a-fA-F]+$/.test(k)) return parseInt(k.slice(1), 16);
193
- if (/^0x[0-9a-fA-F]+$/i.test(k)) return parseInt(k, 16);
194
- if (/^\d+$/.test(k)) return parseInt(k, 10);
195
- return null;
196
- }
197
-
198
- function normalizeRegisters(stateDoc, path) {
199
- const map = new Map();
200
- const raw = stateDoc?.registers;
201
- if (!raw) return map;
202
- for (const [k, v] of Object.entries(raw)) {
203
- const addr = parseAddrKey(k);
204
- if (addr === null) {
205
- throw new Error(`${path}: register key "${k}" is not a parseable address -- refused`);
206
- }
207
- map.set(addr, v);
208
- }
209
- return map;
210
- }
211
-
212
- // ----------------------------------------------------------------- allowlist
213
-
214
- // --allowlist <path> loads the intentional-difference document. Shape:
215
- //
216
- // {
217
- // "checkpoint": "hazard_raster_entry",
218
- // "subjects": { "hazard-subject-modified": "<sha256 of the .prg>" },
219
- // "entries": [
220
- // { "start": 8221, "endInclusive": 8221, "domain": "image", "why": "..." }
221
- // ]
222
- // }
223
- //
224
- // Every entry is validated at load time and refused BY NAME on: a missing or
225
- // whitespace-only `why`; a `start` above `endInclusive`; a range that
226
- // intersects a masked span (mask wins -- an allowlist can never re-admit a
227
- // hardware-volatile address, so a real difference can never be smuggled in
228
- // under a masked address's cover); and a document `checkpoint` that does not
229
- // match the checkpoint the two captures themselves declare. This is
230
- // semantically distinct from the mask tables above -- hardware noise versus a
231
- // deliberate difference -- and is kept in its own bucket, never folded into
232
- // IMAGE_VOLATILE/IO_VOLATILE, so ROADMAP criterion 2's distinction stays
233
- // visible in the record.
234
- function loadAllowlist(path, { route, checkpointName }) {
235
- const doc = JSON.parse(readFileSync(path, "utf8"));
236
- const entries = doc.entries ?? [];
237
-
238
- if (checkpointName && doc.checkpoint && doc.checkpoint !== checkpointName) {
239
- throw new Error(
240
- `${path}: allowlist declares checkpoint "${doc.checkpoint}", captures declare "${checkpointName}" -- refused`,
241
- );
242
- }
243
-
244
- for (const e of entries) {
245
- const rangeLabel =
246
- e.start === e.endInclusive ? hex4(e.start) : `${hex4(e.start)}-${hex4(e.endInclusive)}`;
247
- if (!e.why || !String(e.why).trim()) {
248
- throw new Error(
249
- `${path}: allowlist entry ${rangeLabel} has no why -- an allowlist entry without a non-empty why string is refused`,
250
- );
251
- }
252
- if (!(e.start <= e.endInclusive)) {
253
- throw new Error(`${path}: allowlist entry ${rangeLabel} has start above endInclusive -- refused`);
254
- }
255
- const domain = e.domain === "register" ? "register" : "image";
256
- for (let addr = e.start; addr <= e.endInclusive; addr++) {
257
- const maskEntry = domain === "register" ? ioMaskEntryFor(addr) : imageMaskEntryFor(addr, route);
258
- if (maskEntry) {
259
- throw new Error(
260
- `${path}: allowlist entry ${rangeLabel} (${domain}) overlaps the masked span ` +
261
- `${hex4(maskEntry.lo)}-${hex4(maskEntry.hi)} (${maskEntry.reason}) -- overlap between an ` +
262
- `allowlist entry and the volatile mask is refused`,
263
- );
264
- }
265
- }
266
- }
267
-
268
- return { checkpoint: doc.checkpoint ?? null, subjects: doc.subjects ?? {}, entries };
269
- }
270
-
271
- function findAllowlistEntry(allowlist, addr, domain) {
272
- if (!allowlist) return null;
273
- for (const e of allowlist.entries) {
274
- const eDomain = e.domain === "register" ? "register" : "image";
275
- if (eDomain !== domain) continue;
276
- if (addr >= e.start && addr <= e.endInclusive) return e;
277
- }
278
- return null;
279
- }
280
-
281
- // -------------------------------------------------------------- classification
282
-
283
- /**
284
- * Classify every differing address between two captures -- image bytes plus,
285
- * when both sides carry a chip-state sidecar, register values. Precedence is
286
- * volatile first, then allowlisted, then divergence -- the same "mask wins
287
- * over everything else" precedence compare.mjs already uses for isVolatile,
288
- * extended one step further: allowlist wins over bit-count too. There is no
289
- * fourth bucket and no bit-count branch: every non-volatile,
290
- * non-allowlisted difference is a divergence, one bit or many.
291
- */
292
- export function classify({ imgA, imgB, route, regMapA, regMapB, allowlist }) {
293
- const volatile_ = [];
294
- const allowlisted = [];
295
- const divergence = [];
296
-
297
- for (let addr = 0; addr < IMAGE_BYTES; addr++) {
298
- const x = imgA[addr];
299
- const y = imgB[addr];
300
- if (x === y) continue;
301
-
302
- const rec = { addr, a: x, b: y, domain: "image" };
303
- if (isImageVolatile(addr, route)) {
304
- volatile_.push(rec);
305
- continue;
306
- }
307
- const aw = findAllowlistEntry(allowlist, addr, "image");
308
- if (aw) {
309
- allowlisted.push({ ...rec, why: aw.why });
310
- continue;
311
- }
312
- divergence.push(rec);
313
- }
314
-
315
- if (regMapA && regMapB && regMapA.size && regMapB.size) {
316
- const addrs = new Set([...regMapA.keys(), ...regMapB.keys()]);
317
- for (const addr of [...addrs].sort((p, q) => p - q)) {
318
- const x = regMapA.get(addr);
319
- const y = regMapB.get(addr);
320
- // Present on only one side -- not comparable, not counted either way.
321
- if (x === undefined || y === undefined) continue;
322
- if (x === y) continue;
323
-
324
- const rec = { addr, a: x, b: y, domain: "register" };
325
- if (isIoVolatile(addr)) {
326
- volatile_.push(rec);
327
- continue;
328
- }
329
- const aw = findAllowlistEntry(allowlist, addr, "register");
330
- if (aw) {
331
- allowlisted.push({ ...rec, why: aw.why });
332
- continue;
333
- }
334
- divergence.push(rec);
335
- }
336
- }
337
-
338
- return { volatile: volatile_, allowlisted, divergence, pass: divergence.length === 0 };
339
- }
340
-
341
- // ------------------------------------------------------------------- printing
342
-
343
- const fmtDiffRow = (r) =>
344
- ` ${hex4(r.addr)} ${hex2(r.a)} ${bin8(r.a)} -> ${hex2(r.b)} ${bin8(r.b)} [${r.domain}]`;
345
-
346
- function printList(title, rows, limit) {
347
- console.log(`\n${title}: ${rows.length}`);
348
- if (!rows.length) return;
349
- // --limit 0 means unlimited, matching the usage text. Anything else caps.
350
- const shown = limit ? rows.slice(0, limit) : rows;
351
- for (const r of shown) {
352
- const why = r.why ? ` -- ${r.why}` : "";
353
- console.log(fmtDiffRow(r) + why);
354
- }
355
- if (shown.length < rows.length) {
356
- console.log(` … ${rows.length - shown.length} more (--limit 0 for all)`);
357
- }
358
- }
359
-
360
- // ---------------------------------------------------------------------- cross
361
-
362
- function parseCrossArgs(argv) {
363
- const positional = [];
364
- let statePaths = null;
365
- let allowlistPath = null;
366
- let noAllowlist = false;
367
- let checkpointAssert = null;
368
- let routeAssert = null;
369
- let limit;
370
-
371
- for (let i = 0; i < argv.length; i++) {
372
- const a = argv[i];
373
- if (a === "--state") {
374
- statePaths = [argv[++i], argv[++i]];
375
- } else if (a === "--allowlist") {
376
- allowlistPath = argv[++i];
377
- } else if (a === "--no-allowlist") {
378
- noAllowlist = true;
379
- } else if (a === "--checkpoint") {
380
- checkpointAssert = argv[++i];
381
- } else if (a === "--route") {
382
- routeAssert = argv[++i];
383
- } else if (a === "--limit") {
384
- const n = Number(argv[++i]);
385
- if (!Number.isInteger(n) || n < 0) throw new Error("--limit needs a non-negative integer");
386
- limit = n;
387
- } else {
388
- positional.push(a);
389
- }
390
- }
391
-
392
- if (positional.length !== 2) throw new Error("cross needs exactly two image paths");
393
- return {
394
- imagePaths: positional,
395
- statePaths,
396
- allowlistPath,
397
- noAllowlist,
398
- checkpointAssert,
399
- routeAssert,
400
- limit,
401
- };
402
- }
403
-
404
- function cmdCross(argv) {
405
- const opts = parseCrossArgs(argv);
406
- const [pa, pb] = opts.imagePaths;
407
- const imgA = loadImage(pa);
408
- const imgB = loadImage(pb);
409
-
410
- const stateA = opts.statePaths ? loadState(opts.statePaths[0]) : null;
411
- const stateB = opts.statePaths ? loadState(opts.statePaths[1]) : null;
412
-
413
- const routeA = stateA?.route ?? opts.routeAssert ?? "memory-read";
414
- const routeB = stateB?.route ?? opts.routeAssert ?? "memory-read";
415
- if (routeA !== routeB) {
416
- throw new Error(
417
- `capture routes differ -- A declares "${routeA}", B declares "${routeB}". Comparing a ` +
418
- `snapshot-route capture against a memory-read-route capture is meaningless; refused.`,
419
- );
420
- }
421
- const route = routeA;
422
-
423
- const checkpointA = stateA?.checkpoint_name ?? null;
424
- const checkpointB = stateB?.checkpoint_name ?? null;
425
- if (checkpointA && checkpointB && checkpointA !== checkpointB) {
426
- throw new Error(
427
- `logical checkpoints differ -- A resolved "${checkpointA}", B resolved "${checkpointB}". A ` +
428
- `comparison whose two captures name different logical checkpoints is refused.`,
429
- );
430
- }
431
- const checkpointName = checkpointA ?? checkpointB ?? null;
432
- if (opts.checkpointAssert && checkpointName && opts.checkpointAssert !== checkpointName) {
433
- throw new Error(
434
- `--checkpoint asserted "${opts.checkpointAssert}", captures declare "${checkpointName}" -- refused`,
435
- );
436
- }
437
-
438
- const regMapA = stateA ? normalizeRegisters(stateA, opts.statePaths[0]) : null;
439
- const regMapB = stateB ? normalizeRegisters(stateB, opts.statePaths[1]) : null;
440
-
441
- const allowlist =
442
- opts.allowlistPath && !opts.noAllowlist
443
- ? loadAllowlist(opts.allowlistPath, { route, checkpointName })
444
- : null;
445
-
446
- const haA = sha256(imgA);
447
- const haB = sha256(imgB);
448
-
449
- console.log(`A ${basename(pa)} sha256 ${haA}`);
450
- console.log(`B ${basename(pb)} sha256 ${haB}`);
451
- console.log(`MASK_NARROWED_AT: ${MASK_VERSION}`);
452
- if (checkpointA) {
453
- console.log(`A checkpoint ${checkpointA} @ ${hex4(stateA.checkpoint_address ?? 0)}`);
454
- }
455
- if (checkpointB) {
456
- console.log(`B checkpoint ${checkpointB} @ ${hex4(stateB.checkpoint_address ?? 0)}`);
457
- }
458
-
459
- const byteIdentical = haA === haB;
460
- console.log(`\nBYTE_IDENTICAL: ${byteIdentical ? "yes" : "no"}`);
461
- console.log(" (a recorded extra -- the VERDICT line below is the acceptance signal, not this one)");
462
-
463
- // Always classify, even when the images are byte-identical: a chip-state-
464
- // only regression must still be caught. Unlike compare.mjs's cmdCompare(),
465
- // there is no early return on equal digests here -- see this plan's
466
- // assumption_delta_decision, which demotes byte-identity to a recorded
467
- // extra and promotes behavioural equivalence (the full classification) to
468
- // the actual acceptance criterion.
469
- const r = classify({ imgA, imgB, route, regMapA, regMapB, allowlist });
470
-
471
- printList("volatile (excluded from the verdict)", r.volatile, opts.limit);
472
- printList("allowlisted (intentional difference, excluded from the verdict)", r.allowlisted, opts.limit);
473
- printList("DIVERGENCE — fails the comparison", r.divergence, opts.limit);
474
-
475
- const total = r.volatile.length + r.allowlisted.length + r.divergence.length;
476
- console.log(`\ntotal differing addresses (image + register): ${total}`);
477
- console.log(`\nVERDICT: ${r.pass ? "PASS" : "FAIL"}`);
478
- return r.pass ? 0 : 1;
479
- }
480
-
481
- // -------------------------------------------------------------------- dispatch
482
-
483
- const commands = { cross: cmdCross };
484
-
485
- function usage() {
486
- return `usage: node compare-cross-binary.mjs <command>
487
-
488
- cross <a.bin> <b.bin> [--state <a.state.json> <b.state.json>]
489
- [--allowlist <path>] [--no-allowlist] [--checkpoint <name>]
490
- [--route <snapshot|memory-read>] [--limit N]
491
- classify every difference between two captures of DIFFERENT binaries and
492
- print a single VERDICT line.
493
-
494
- No drift bucket here: every non-volatile, non-allowlisted difference fails,
495
- regardless of bit count -- unlike compare.mjs, which is for two captures of
496
- the SAME binary. Volatile registers (excluded): $D011, $D012, $D019,
497
- $D01E-$D01F, $D400-$D7FF, $D800-$DBFF, $DC00-$DCFF, $DD00-$DDFF, $DE00-$DFFF
498
- (mirrored across $D000-$D3FF where applicable). $D015, $D018 and $D020 are
499
- deliberately NOT masked.
500
- --limit 0 prints every row. Exit status is 1 on a FAIL verdict, non-zero on
501
- any refusal (mismatched route, mismatched checkpoint, a malformed allowlist,
502
- a malformed image).
503
-
504
- Captures come from the procedure in c64-ram-capture/SKILL.md, via
505
- mcp__plugin_c64-re-tools_vice__*. This script contacts nothing.`;
506
- }
507
-
508
- function main() {
509
- const [cmd, ...rest] = process.argv.slice(2);
510
- if (!cmd || !commands[cmd]) {
511
- console.error(usage());
512
- process.exit(cmd ? 1 : 0);
513
- return;
514
- }
515
- try {
516
- process.exit(commands[cmd](rest));
517
- } catch (e) {
518
- console.error(`error: ${e.message}`);
519
- process.exit(1);
520
- }
521
- }
522
-
523
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
524
- main();
525
- }