@henols/c64-re-tools 1.0.0 → 2.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +190 -37
- package/distribution/package.ts +36 -0
- package/distribution/plugin.ts +120 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +73 -0
- package/skills/c64-assembler/scripts/assemble.ts +103 -0
- package/skills/c64-basic/SKILL.md +67 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +79 -0
- package/skills/c64-disk/scripts/disk.ts +65 -0
- package/skills/c64-emulator/SKILL.md +124 -0
- package/skills/c64-knowledge/SKILL.md +102 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
- package/skills/c64-memory-map/SKILL.md +76 -0
- package/skills/c64-memory-map/scripts/memmap.ts +81 -0
- package/skills/c64-memory-map/scripts/platform.ts +424 -0
- package/skills/c64-provenance/SKILL.md +76 -0
- package/skills/c64-provenance/scripts/compare.ts +92 -0
- package/skills/c64-provenance/scripts/provenance.ts +83 -0
- package/skills/c64-reverse-engineering/SKILL.md +90 -0
- package/skills/c64-static-analysis/SKILL.md +96 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
- package/skills/c64-static-analysis/scripts/findings.ts +73 -0
- package/skills/c64-testing/SKILL.md +79 -0
- package/skills/c64-testing/references/scenario-format.md +76 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/scenario.ts +473 -0
- package/skills/c64-testing/scripts/test.ts +55 -0
- package/skills/c64-unpacker/SKILL.md +82 -0
- package/skills/c64-unpacker/scripts/evidence.ts +135 -0
- package/skills/c64-unpacker/scripts/unpack.ts +216 -0
- package/src/c64.ts +281 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +122 -0
- package/src/cli/status.ts +34 -0
- package/src/host/main.ts +121 -0
- package/src/host/server.ts +409 -0
- package/src/host/tools/c1541.ts +255 -0
- package/src/host/tools/index.ts +31 -0
- package/src/host/tools/petcat.ts +70 -0
- package/src/host/tools/status.ts +30 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +761 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +108 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +402 -0
- package/src/host/vice/screen.ts +183 -0
- package/src/host/vice/session.ts +1316 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +100 -0
- package/src/host-client/vice-session.ts +254 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +128 -0
- package/src/knowledge/import.ts +344 -0
- package/src/knowledge/read.ts +191 -0
- package/src/knowledge/schema.ts +140 -0
- package/src/knowledge/write.ts +348 -0
- package/src/mcp/main.ts +109 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/tools/debug.ts +237 -0
- package/src/mcp/tools/execution.ts +116 -0
- package/src/mcp/tools/input.ts +69 -0
- package/src/mcp/tools/machine.ts +97 -0
- package/src/mcp/tools/media.ts +106 -0
- package/src/mcp/tools/memory.ts +203 -0
- package/src/mcp/tools/video.ts +260 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +141 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +157 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +242 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +101 -0
- package/src/native/types.ts +228 -0
- package/src/native/watchdog.ts +78 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +252 -0
- package/src/protocol/tools.ts +247 -0
- package/src/protocol/vice.ts +1235 -0
- package/src/script.ts +59 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +162 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- 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
|
-
}
|