@henols/c64-re-tools 0.2.1 → 0.2.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.
@@ -1,665 +0,0 @@
1
- // Coverage for diff-images.mjs: the anchor-proven offset
2
- // search, N-way byte diff, gap-tolerant coalescing, patch counting, and the
3
- // generated ledger tier. Every test here runs with no emulator present --
4
- // small synthetic fixtures for the arithmetic and boundary cases, plus a
5
- // corpus-driven case that runs `anchorSearch` against whatever real committed
6
- // dumps the host project has, skipping when it has none.
7
- import { test } from "node:test";
8
- import assert from "node:assert/strict";
9
- import { readFileSync, existsSync } from "node:fs";
10
- import { createHash } from "node:crypto";
11
- import { fileURLToPath } from "node:url";
12
- import { dirname, join, resolve } from "node:path";
13
-
14
- import {
15
- anchorSearch,
16
- proveOffset,
17
- applyOffset,
18
- diffRanges,
19
- coalesceRanges,
20
- countPatches,
21
- bucketManifest,
22
- findPrintableRuns,
23
- findCracktroRuns,
24
- renderLedger,
25
- enumerateManifests,
26
- splitRangeByManifestKind,
27
- } from "./diff-images.mjs";
28
- import { registryPath } from "../../c64-ram-capture/scripts/releases.mjs";
29
-
30
- const HERE = dirname(fileURLToPath(import.meta.url));
31
- const REPO_ROOT = resolve(HERE, "..", "..", "..", ".."); // scripts -> skill -> skills -> .claude -> repo root
32
-
33
- // ------------------------------------------------------------ anchorSearch
34
-
35
- // Deterministic, non-periodic pseudo-random fill (sha256 counter mode) --
36
- // a linear-congruence fill like `(i*37+11)&0xff` has a short period (256
37
- // here) and produces spurious repeated matches within a few KB, which is
38
- // not what a "distinctive" run means for this test.
39
- function pseudoRandomFill(length, seed) {
40
- const buf = Buffer.alloc(length);
41
- let counter = 0;
42
- let pos = 0;
43
- while (pos < length) {
44
- const chunk = createHash("sha256").update(`${seed}:${counter++}`).digest();
45
- chunk.copy(buf, pos);
46
- pos += chunk.length;
47
- }
48
- return buf.subarray(0, length);
49
- }
50
-
51
- test("anchorSearch finds a unique anchor and reports delta plus neighbour bytes", () => {
52
- // A distinctive, non-repeating pattern across the whole post-volatile-zone
53
- // region (rather than one narrow run in an otherwise-blank buffer) so the
54
- // search's own candidate-selection heuristics can't miss it by construction.
55
- const source = Buffer.alloc(4096, 0x00);
56
- pseudoRandomFill(4096 - 1024, "anchor-fixture").copy(source, 1024);
57
- const target = Buffer.from(source); // offset 0 -- identical placement
58
-
59
- const anchors = anchorSearch(source, target, { minRunLength: 48, count: 4 });
60
- assert.ok(anchors.length > 0, "expected at least one candidate anchor");
61
- const hit = anchors.find((a) => a.unique && a.delta === 0);
62
- assert.ok(hit, "expected at least one unique anchor agreeing on delta 0");
63
- assert.ok(hit.neighbourBytes, "neighbour bytes must be reported for a unique anchor");
64
- assert.equal(hit.neighbourBytes.at, target[hit.matchOffset]);
65
- assert.equal(hit.neighbourBytes.before, target[hit.matchOffset - 1]);
66
- assert.equal(hit.neighbourBytes.after, target[hit.matchOffset + 1]);
67
- });
68
-
69
- test("anchorSearch rejects a trivial constant-byte run as a candidate", () => {
70
- const source = Buffer.alloc(1024, 0xff);
71
- const target = Buffer.alloc(1024, 0xff);
72
- const anchors = anchorSearch(source, target, { minRunLength: 32, count: 4 });
73
- assert.equal(anchors.length, 0, "an all-constant image offers no non-trivial candidate anchors");
74
- });
75
-
76
- test("anchorSearch reports multiple match offsets when a run repeats in the target", () => {
77
- const run = Buffer.from(Array.from({ length: 48 }, (_, i) => (i * 13 + 3) & 0xff));
78
- const source = Buffer.alloc(4096, 0x00);
79
- run.copy(source, 1024);
80
- const target = Buffer.alloc(4096, 0x00);
81
- run.copy(target, 1024);
82
- run.copy(target, 3000); // the same run repeats elsewhere in the target
83
- const anchors = anchorSearch(source, target, { minRunLength: 48, count: 4 });
84
- const hit = anchors.find((a) => a.sourceOffset === 1024);
85
- assert.ok(hit);
86
- assert.equal(hit.matchCount, 2);
87
- assert.equal(hit.unique, false);
88
- assert.equal(hit.delta, null);
89
- });
90
-
91
- // ------------------------------------------------------------- proveOffset
92
-
93
- test("proveOffset accepts a single offset when every unique-match anchor agrees", () => {
94
- const anchors = [
95
- { sourceOffset: 100, unique: true, delta: 5 },
96
- { sourceOffset: 200, unique: true, delta: 5 },
97
- { sourceOffset: 300, unique: true, delta: 5 },
98
- ];
99
- const proof = proveOffset(anchors);
100
- assert.equal(proof.ok, true);
101
- assert.equal(proof.offset, 5);
102
- });
103
-
104
- test("proveOffset FAILS (never a majority vote) when one anchor's delta disagrees", () => {
105
- const anchors = [
106
- { sourceOffset: 100, unique: true, delta: 0 },
107
- { sourceOffset: 200, unique: true, delta: 0 },
108
- { sourceOffset: 300, unique: true, delta: 7 }, // disagrees
109
- ];
110
- const proof = proveOffset(anchors);
111
- assert.equal(proof.ok, false, "must fail rather than return the majority answer (0)");
112
- assert.equal(proof.offset, null);
113
- assert.ok(proof.reason.includes("disagree"));
114
- // The disagreeing anchor set must name every usable anchor, including the majority ones.
115
- assert.equal(proof.disagreeing.length, 3);
116
- assert.ok(proof.disagreeing.some((a) => a.sourceOffset === 300 && a.delta === 7));
117
- });
118
-
119
- test("proveOffset rejects an anchor that matched at more than one target offset", () => {
120
- const anchors = [
121
- { sourceOffset: 100, unique: true, delta: 0 },
122
- { sourceOffset: 200, unique: false, delta: null, matches: [50, 900] },
123
- ];
124
- const proof = proveOffset(anchors);
125
- assert.equal(proof.ok, true, "the remaining unique anchor still proves the offset");
126
- assert.equal(proof.offset, 0);
127
- assert.equal(proof.rejected.length, 1);
128
- assert.equal(proof.rejected[0].sourceOffset, 200);
129
- });
130
-
131
- test("proveOffset fails with an actionable reason when no anchor produced a unique match", () => {
132
- const anchors = [
133
- { sourceOffset: 100, unique: false, delta: null, matches: [1, 2] },
134
- { sourceOffset: 200, unique: false, delta: null, matches: [] },
135
- ];
136
- const proof = proveOffset(anchors);
137
- assert.equal(proof.ok, false);
138
- assert.ok(proof.reason.includes("no anchor produced a unique match"));
139
- });
140
-
141
- // ------------------------------------------------------------- applyOffset
142
-
143
- test("applyOffset excludes an address whose offset-adjusted counterpart falls past $FFFF, with a named reason, no wrap", () => {
144
- const result = applyOffset(0xfff0, 0x20); // 0xfff0 + 0x20 = 0x10010, past $FFFF
145
- assert.equal(result.inRange, false);
146
- assert.ok(result.reason.includes("$0000-$FFFF"));
147
- assert.ok(!result.reason.includes("wrapped") || result.reason.includes("never wrapped"));
148
- assert.equal(result.target, 0x10010, "the raw target must not be silently wrapped modulo 65536");
149
- });
150
-
151
- test("applyOffset covers $0000 and $FFFF inclusive when the proven offset is zero", () => {
152
- const low = applyOffset(0x0000, 0);
153
- const high = applyOffset(0xffff, 0);
154
- assert.equal(low.inRange, true);
155
- assert.equal(low.target, 0);
156
- assert.equal(high.inRange, true);
157
- assert.equal(high.target, 0xffff);
158
- });
159
-
160
- test("applyOffset excludes a negative offset-adjusted address, never wrapping to a positive one", () => {
161
- const result = applyOffset(0x0005, -0x10);
162
- assert.equal(result.inRange, false);
163
- assert.equal(result.target, -11);
164
- });
165
-
166
- // ------------------------------------------------------------- coalesceRanges
167
-
168
- function orig(start, end, agreeing = 2) {
169
- return { start, end, verdict: "ORIGINAL", agreeing_releases: agreeing, evidence: "identical", reason: "" };
170
- }
171
- function unk(start, end, reason = "no signature") {
172
- return { start, end, verdict: "UNKNOWN", agreeing_releases: 0, evidence: "", reason };
173
- }
174
- function patch(start, end, evidence = "loader region") {
175
- return { start, end, verdict: "CRACKER-PATCH", agreeing_releases: 0, evidence, reason: "" };
176
- }
177
-
178
- test("coalesceRanges merges two differing ranges across a gap strictly shorter than the tolerance", () => {
179
- const ranges = [unk(0, 9), orig(10, 19), unk(20, 29)]; // gap length 10 < tolerance 16
180
- const { ranges: out, coalesced } = coalesceRanges(ranges, 16);
181
- assert.equal(out.length, 1);
182
- assert.equal(out[0].start, 0);
183
- assert.equal(out[0].end, 29);
184
- assert.equal(coalesced, 2);
185
- });
186
-
187
- test("coalesceRanges leaves ranges separated by a gap EXACTLY equal to the tolerance as two separate rows -- the boundary is defined, not incidental", () => {
188
- const ranges = [unk(0, 9), orig(10, 25), unk(26, 35)]; // gap length exactly 16
189
- const { ranges: out } = coalesceRanges(ranges, 16);
190
- assert.equal(out.length, 3, "a gap of exactly the tolerance must NOT be swallowed");
191
- assert.deepEqual(
192
- out.map((r) => [r.start, r.end, r.verdict]),
193
- [
194
- [0, 9, "UNKNOWN"],
195
- [10, 25, "ORIGINAL"],
196
- [26, 35, "UNKNOWN"],
197
- ]
198
- );
199
- });
200
-
201
- test("coalesceRanges leaves ranges separated by a gap LONGER than the tolerance as two separate rows", () => {
202
- const ranges = [unk(0, 9), orig(10, 30), unk(31, 40)]; // gap length 21 > 16
203
- const { ranges: out } = coalesceRanges(ranges, 16);
204
- assert.equal(out.length, 3);
205
- });
206
-
207
- test("coalesceRanges merges runs of the SAME verdict directly (no gap) and preserves that verdict", () => {
208
- const ranges = [patch(0, 9), patch(10, 19)];
209
- const { ranges: out } = coalesceRanges(ranges, 16);
210
- assert.equal(out.length, 1);
211
- assert.equal(out[0].verdict, "CRACKER-PATCH");
212
- });
213
-
214
- test("coalesceRanges downgrades a mixed-verdict merge to UNKNOWN -- the conservative choice, never a fabricated stronger verdict", () => {
215
- const ranges = [patch(0, 9), orig(10, 15), unk(16, 25)]; // gap 6 < 16, differing verdicts on each side
216
- const { ranges: out } = coalesceRanges(ranges, 16);
217
- assert.equal(out.length, 1);
218
- assert.equal(out[0].verdict, "UNKNOWN");
219
- assert.ok(out[0].reason.length > 0, "a merged UNKNOWN row must still carry a non-empty reason");
220
- });
221
-
222
- test("coalesceRanges reports kept-vs-coalesced counts in the accumulate-both-counts shape", () => {
223
- const ranges = [orig(0, 100), unk(101, 110)];
224
- const { kept, coalesced } = coalesceRanges(ranges, 16);
225
- assert.equal(kept, 2);
226
- assert.equal(coalesced, 0);
227
- });
228
-
229
- // ---------------------------------------------------------------- diffRanges
230
-
231
- function makeImage(fillByte) {
232
- return Buffer.alloc(65536, fillByte);
233
- }
234
-
235
- test("diffRanges: identical bytes across >=2 releases verdict ORIGINAL, never below agreeing_releases=2", () => {
236
- const a = makeImage(0x11);
237
- const b = makeImage(0x11);
238
- const result = diffRanges(
239
- [
240
- { id: "a", bytes: a, offset: 0, loaderRanges: [], cracktroRuns: [] },
241
- { id: "b", bytes: b, offset: 0, loaderRanges: [], cracktroRuns: [] },
242
- ],
243
- { gapTolerance: 16 }
244
- );
245
- assert.equal(result.ranges.length, 1);
246
- assert.equal(result.ranges[0].verdict, "ORIGINAL");
247
- assert.ok(result.ranges[0].agreeing_releases >= 2);
248
- });
249
-
250
- test("diffRanges: a range only one release covers yields UNKNOWN, never ORIGINAL", () => {
251
- const a = makeImage(0x22);
252
- const result = diffRanges([{ id: "solo", bytes: a, offset: 0, loaderRanges: [], cracktroRuns: [] }], { gapTolerance: 16 });
253
- assert.equal(result.ranges.length, 1);
254
- assert.equal(result.ranges[0].verdict, "UNKNOWN");
255
- assert.ok(result.ranges[0].reason.length > 0);
256
- });
257
-
258
- test("diffRanges: a differing byte inside a release's loader_ranges is classified CRACKER-PATCH with the technique named", () => {
259
- const a = Buffer.alloc(65536, 0x00);
260
- const b = Buffer.alloc(65536, 0x00);
261
- b[100] = 0x99; // differs at address 100
262
- const result = diffRanges(
263
- [
264
- { id: "a", bytes: a, offset: 0, loaderRanges: [{ start: 90, end: 110 }], cracktroRuns: [] },
265
- { id: "b", bytes: b, offset: 0, loaderRanges: [], cracktroRuns: [] },
266
- ],
267
- { gapTolerance: 0 }
268
- );
269
- const hit = result.ranges.find((r) => r.start <= 100 && r.end >= 100 && r.start !== 0);
270
- const patchRange = result.ranges.find((r) => r.verdict === "CRACKER-PATCH");
271
- assert.ok(patchRange, "expected at least one CRACKER-PATCH range");
272
- assert.ok(patchRange.evidence.includes("loader"));
273
- });
274
-
275
- test("diffRanges: a differing byte with no recognised signature is UNKNOWN with a reason naming the ruled-out alternatives", () => {
276
- const a = Buffer.alloc(65536, 0x00);
277
- const b = Buffer.alloc(65536, 0x00);
278
- b[5000] = 0x77; // differs, not inside any loader/cracktro range
279
- const result = diffRanges(
280
- [
281
- { id: "a", bytes: a, offset: 0, loaderRanges: [], cracktroRuns: [] },
282
- { id: "b", bytes: b, offset: 0, loaderRanges: [], cracktroRuns: [] },
283
- ],
284
- { gapTolerance: 0 }
285
- );
286
- const unkRange = result.ranges.find((r) => r.verdict === "UNKNOWN" && r.start <= 5000 && r.end >= 5000);
287
- assert.ok(unkRange);
288
- assert.ok(unkRange.reason.includes("no recognised cracker signature"));
289
- });
290
-
291
- test("diffRanges: ranges union covers exactly $0000-$FFFF with no gap and no overlap", () => {
292
- const a = Buffer.alloc(65536, 0x00);
293
- const b = Buffer.alloc(65536, 0x00);
294
- for (let i = 0; i < 65536; i += 137) b[i] = (b[i] + 1) & 0xff; // scatter some differences
295
- const result = diffRanges(
296
- [
297
- { id: "a", bytes: a, offset: 0, loaderRanges: [], cracktroRuns: [] },
298
- { id: "b", bytes: b, offset: 0, loaderRanges: [], cracktroRuns: [] },
299
- ],
300
- { gapTolerance: 16 }
301
- );
302
- let sum = 0;
303
- let prevEnd = -1;
304
- for (const r of [...result.ranges].sort((x, y) => x.start - y.start)) {
305
- assert.ok(r.start > prevEnd, `range starting at ${r.start} overlaps previous end ${prevEnd}`);
306
- assert.equal(r.start, prevEnd + 1, "no gap between consecutive ranges");
307
- sum += r.end - r.start + 1;
308
- prevEnd = r.end;
309
- }
310
- assert.equal(sum, 65536);
311
- assert.equal(prevEnd, 65535);
312
- });
313
-
314
- test("diffRanges: an out-of-range offset excludes that release from the address's coverage rather than wrapping", () => {
315
- const a = Buffer.alloc(65536, 0x11);
316
- const b = Buffer.alloc(65536, 0x11);
317
- // b's offset pushes it entirely out of range for address 0 -- but in range elsewhere.
318
- const result = diffRanges(
319
- [
320
- { id: "a", bytes: a, offset: 0, loaderRanges: [], cracktroRuns: [] },
321
- { id: "b", bytes: b, offset: -5, loaderRanges: [], cracktroRuns: [] }, // address 0 -> target -5, out of range
322
- ],
323
- { gapTolerance: 0 }
324
- );
325
- const first = result.ranges.find((r) => r.start === 0);
326
- assert.equal(first.verdict, "UNKNOWN", "only one release (a) covers address 0 once b's offset pushes it out of range");
327
- });
328
-
329
- // -------------------------------------------------------------- countPatches
330
-
331
- test("countPatches counts CRACKER-PATCH bytes only within ranges bucketed 'game', per release, deterministically", () => {
332
- const a = Buffer.alloc(65536, 0x00);
333
- const b = Buffer.alloc(65536, 0x00);
334
- b[50000] = 0x42; // a difference inside a 'game'-bucketed region for both
335
- const images = [
336
- { id: "a", bytes: a, offset: 0, loaderRanges: [{ start: 49990, end: 50010 }], cracktroRuns: [] },
337
- { id: "b", bytes: b, offset: 0, loaderRanges: [{ start: 49990, end: 50010 }], cracktroRuns: [] },
338
- ];
339
- const diffResult = diffRanges(images, { gapTolerance: 0 });
340
- const gameManifest = { ranges: [{ start: 0, end: 65535, kind: "game" }] };
341
- const counts1 = countPatches(images, diffResult, { a: gameManifest, b: gameManifest });
342
- const counts2 = countPatches(images, diffResult, { a: gameManifest, b: gameManifest });
343
- assert.deepEqual(counts1, counts2, "must be re-runnable and byte-identical");
344
- assert.equal(counts1.a, 1);
345
- assert.equal(counts1.b, 1);
346
- });
347
-
348
- test("countPatches reports zero for a release whose CRACKER-PATCH bytes fall outside its own 'game' bucket", () => {
349
- const a = Buffer.alloc(65536, 0x00);
350
- const b = Buffer.alloc(65536, 0x00);
351
- b[300] = 0x42;
352
- const images = [
353
- { id: "a", bytes: a, offset: 0, loaderRanges: [{ start: 290, end: 310 }], cracktroRuns: [] },
354
- { id: "b", bytes: b, offset: 0, loaderRanges: [{ start: 290, end: 310 }], cracktroRuns: [] },
355
- ];
356
- const diffResult = diffRanges(images, { gapTolerance: 0 });
357
- const loaderManifest = { ranges: [{ start: 0, end: 65535, kind: "loader" }] };
358
- const counts = countPatches(images, diffResult, { a: loaderManifest, b: loaderManifest });
359
- assert.equal(counts.a, 0);
360
- assert.equal(counts.b, 0);
361
- });
362
-
363
- // -------------------------------------------------------------- findPrintableRuns
364
-
365
- test("findPrintableRuns finds a run of printable-ASCII bytes at least minLength long", () => {
366
- const buf = Buffer.alloc(64, 0x00);
367
- Buffer.from("SOME GAME CRACKED BY").copy(buf, 20);
368
- const runs = findPrintableRuns(buf, { minLength: 8 });
369
- assert.equal(runs.length, 1);
370
- assert.equal(runs[0].start, 20);
371
- });
372
-
373
- test("findPrintableRuns ignores a printable run shorter than minLength", () => {
374
- const buf = Buffer.alloc(64, 0x00);
375
- Buffer.from("HI").copy(buf, 10);
376
- const runs = findPrintableRuns(buf, { minLength: 8 });
377
- assert.equal(runs.length, 0);
378
- });
379
-
380
- // -------------------------------------------------------------- findCracktroRuns
381
-
382
- test("findCracktroRuns matches a printable run containing a recognised crack-credit word", () => {
383
- const buf = Buffer.alloc(64, 0x00);
384
- Buffer.from("SOME GAME CRACKED BY").copy(buf, 10);
385
- const runs = findCracktroRuns(buf, { minLength: 8 });
386
- assert.equal(runs.length, 1);
387
- });
388
-
389
- test("findCracktroRuns does NOT match a game's own title-screen text -- a real false positive found live against a two-release corpus, where the title text differed between releases and a bare printable scan called it cracker credit", () => {
390
- const buf = Buffer.alloc(64, 0x00);
391
- Buffer.from("PUBLISHER PRESENTS").copy(buf, 10);
392
- const runsPlain = findPrintableRuns(buf, { minLength: 8 });
393
- assert.equal(runsPlain.length, 1, "the plain scan does find this run -- it is genuinely printable text");
394
- const runsCracktro = findCracktroRuns(buf, { minLength: 8 });
395
- assert.equal(runsCracktro.length, 0, "but it must not be classified as cracktro credit content -- it's the game's own presentation text");
396
- });
397
-
398
- // -------------------------------------------------------------- bucketManifest
399
-
400
- test("bucketManifest reclassifies an unclassified range against loader_ranges, seeding 'loader' from the registry, never NOTES.md prose", () => {
401
- const image = Buffer.alloc(65536, 0x00);
402
- const manifest = {
403
- schema_version: 1,
404
- classification_state: "ranges-only",
405
- ranges: [
406
- { start: 0, end: 99, kind: "unclassified" },
407
- { start: 100, end: 65535, kind: "unused", note: "power-on pattern" },
408
- ],
409
- };
410
- const bucketed = bucketManifest(image, manifest, { loaderRanges: [{ start: "$0000", end: "$001F", note: "loader scratch", evidence: "disasm" }] });
411
- assert.equal(bucketed.classification_state, "bucketed");
412
- const kinds = new Set(bucketed.ranges.map((r) => r.kind));
413
- assert.ok(![...kinds].includes("unclassified"), "no unclassified range may remain");
414
- const loaderRange = bucketed.ranges.find((r) => r.kind === "loader");
415
- assert.ok(loaderRange);
416
- assert.equal(loaderRange.start, 0);
417
- assert.equal(loaderRange.end, 31);
418
- const gameRange = bucketed.ranges.find((r) => r.kind === "game");
419
- assert.ok(gameRange, "the remainder reached by the trace must be bucketed game");
420
- assert.equal(gameRange.start, 32);
421
- assert.equal(gameRange.end, 99);
422
- });
423
-
424
- test("bucketManifest preserves 'unused' and 'io' ranges verbatim and only touches 'unclassified' ranges", () => {
425
- const image = Buffer.alloc(65536, 0x00);
426
- const manifest = {
427
- ranges: [
428
- { start: 0, end: 15, kind: "unused", note: "power-on" },
429
- { start: 16, end: 31, kind: "io", note: "I/O window" },
430
- { start: 32, end: 63, kind: "unclassified" },
431
- ],
432
- };
433
- const bucketed = bucketManifest(image, manifest, { loaderRanges: [] });
434
- assert.deepEqual(bucketed.ranges.find((r) => r.start === 0), { start: 0, end: 15, kind: "unused", note: "power-on" });
435
- assert.deepEqual(bucketed.ranges.find((r) => r.start === 16), { start: 16, end: 31, kind: "io", note: "I/O window" });
436
- });
437
-
438
- test("bucketManifest's output ranges form a complete, gapless, non-overlapping partition matching the input manifest's own span", () => {
439
- const image = Buffer.alloc(65536, 0x00);
440
- const manifest = {
441
- ranges: [
442
- { start: 0, end: 999, kind: "unclassified" },
443
- { start: 1000, end: 1015, kind: "unused" },
444
- { start: 1016, end: 65535, kind: "unclassified" },
445
- ],
446
- };
447
- const bucketed = bucketManifest(image, manifest, { loaderRanges: [{ start: "$0064", end: "$00C7" }] });
448
- const sorted = [...bucketed.ranges].sort((a, b) => a.start - b.start);
449
- let expected = 0;
450
- for (const r of sorted) {
451
- assert.equal(r.start, expected);
452
- expected = r.end + 1;
453
- }
454
- assert.equal(expected, 65536);
455
- });
456
-
457
- test("bucketManifest is idempotent -- re-running it on an already-bucketed manifest preserves game/loader/cracktro ranges rather than discarding them", () => {
458
- const image = Buffer.alloc(65536, 0x00);
459
- const manifest = {
460
- ranges: [
461
- { start: 0, end: 15, kind: "unused" },
462
- { start: 16, end: 999, kind: "unclassified" },
463
- ],
464
- };
465
- const firstPass = bucketManifest(image, manifest, { loaderRanges: [{ start: "$0020", end: "$003F" }] });
466
- assert.equal(firstPass.classification_state, "bucketed");
467
- const kindsAfterFirst = new Set(firstPass.ranges.map((r) => r.kind));
468
- assert.ok(kindsAfterFirst.has("game"));
469
- assert.ok(kindsAfterFirst.has("loader"));
470
-
471
- // Re-run on the manifest bucketManifest itself just produced.
472
- const secondPass = bucketManifest(image, firstPass, { loaderRanges: [{ start: "$0020", end: "$003F" }] });
473
- const kindsAfterSecond = new Set(secondPass.ranges.map((r) => r.kind));
474
- assert.ok(kindsAfterSecond.has("game"), "a second bucketing pass must not discard the game range");
475
- assert.ok(kindsAfterSecond.has("loader"), "a second bucketing pass must not discard the loader range");
476
- assert.ok(kindsAfterSecond.has("unused"));
477
- assert.deepEqual(
478
- [...firstPass.ranges].sort((a, b) => a.start - b.start),
479
- [...secondPass.ranges].sort((a, b) => a.start - b.start),
480
- "bucketing an already-bucketed manifest must be a no-op"
481
- );
482
- });
483
-
484
- // -------------------------------------------------------------- renderLedger
485
-
486
- test("renderLedger refuses to emit (throws, writes nothing) when a row is UNKNOWN with an empty reason", () => {
487
- const generatedRanges = [
488
- { start: 0, end: 65535, verdict: "UNKNOWN", agreeing_releases: 0, evidence: "", reason: "" },
489
- ];
490
- assert.throws(() => renderLedger({ generatedRanges, gapTolerance: 16, prose: "x" }), /UNKNOWN with an empty reason/);
491
- });
492
-
493
- test("renderLedger refuses to emit when a row is ORIGINAL with agreeing_releases below two", () => {
494
- const generatedRanges = [
495
- { start: 0, end: 65535, verdict: "ORIGINAL", agreeing_releases: 1, evidence: "only one release", reason: "" },
496
- ];
497
- assert.throws(() => renderLedger({ generatedRanges, gapTolerance: 16, prose: "x" }), /agreeing_releases=1/);
498
- });
499
-
500
- test("renderLedger refuses to emit when ranges do not cover exactly $0000-$FFFF", () => {
501
- const generatedRanges = [
502
- { start: 0, end: 65534, verdict: "ORIGINAL", agreeing_releases: 2, evidence: "identical", reason: "" },
503
- ];
504
- assert.throws(() => renderLedger({ generatedRanges, gapTolerance: 16, prose: "x" }), /does not reach \$FFFF/);
505
- });
506
-
507
- test("renderLedger produces byte-identical generated-tier output across two runs from unchanged input", () => {
508
- const generatedRanges = [
509
- { start: 0, end: 32767, kind: "game", verdict: "ORIGINAL", agreeing_releases: 2, evidence: "identical", reason: "" },
510
- { start: 32768, end: 65535, kind: "loader", verdict: "CRACKER-PATCH", agreeing_releases: 0, evidence: "loader replacement", reason: "" },
511
- ];
512
- const md1 = renderLedger({ generatedRanges, gapTolerance: 16, prose: "prose text" });
513
- const md2 = renderLedger({ generatedRanges, gapTolerance: 16, prose: "prose text" });
514
- const tier1 = md1.split("## Prose tier")[0];
515
- const tier2 = md2.split("## Prose tier")[0];
516
- assert.equal(tier1, tier2);
517
- });
518
-
519
- test("renderLedger sorts rows by start address ascending then end address ascending", () => {
520
- const generatedRanges = [
521
- { start: 100, end: 199, kind: "game", verdict: "ORIGINAL", agreeing_releases: 2, evidence: "id", reason: "" },
522
- { start: 0, end: 99, kind: "game", verdict: "ORIGINAL", agreeing_releases: 2, evidence: "id", reason: "" },
523
- { start: 200, end: 65535, kind: "game", verdict: "ORIGINAL", agreeing_releases: 2, evidence: "id", reason: "" },
524
- ];
525
- const md = renderLedger({ generatedRanges, gapTolerance: 16, prose: "x" });
526
- const tier = md.split("## Prose tier")[0];
527
- const firstIdx = tier.indexOf("$0000");
528
- const secondIdx = tier.indexOf("$0064");
529
- const thirdIdx = tier.indexOf("$00C8");
530
- assert.ok(firstIdx < secondIdx && secondIdx < thirdIdx, "rows must appear in start-ascending order");
531
- });
532
-
533
- // -------------------------------------------------------------- enumerateManifests
534
-
535
- test("enumerateManifests reads every dumps[] entry's range_manifest from the registry, never a hardcoded pair", () => {
536
- const fakeRegistry = {
537
- releases: [
538
- { id: "a", dumps: [{ label: "run1", range_manifest: "recovery/a/dumps/a-run1.map.json" }, { label: "run2", range_manifest: "recovery/a/dumps/a-run2.map.json" }] },
539
- { id: "b", dumps: [{ label: "run1", range_manifest: "recovery/b/dumps/b-run1.map.json" }] },
540
- ],
541
- };
542
- const list = enumerateManifests(fakeRegistry);
543
- assert.equal(list.length, 3);
544
- assert.ok(list.some((m) => m.release === "a" && m.label === "run2"));
545
- });
546
-
547
- // -------------------------------------------------------- splitRangeByManifestKind
548
-
549
- test("splitRangeByManifestKind splits a range spanning a manifest kind boundary into per-kind sub-ranges", () => {
550
- // A real bug found live: a coalesced diff range can span straight through
551
- // a manifest's own kind boundary (e.g. a wide ORIGINAL range ORIGINAL range
552
- // crosses its own $0340-$035E loader sub-range), and resolving kind from
553
- // only the range's start address silently mislabels everything past the
554
- // first boundary.
555
- const manifestRanges = [
556
- { start: 0, end: 831, kind: "game" },
557
- { start: 832, end: 862, kind: "loader" },
558
- { start: 863, end: 65535, kind: "game" },
559
- ];
560
- const diffRange = { start: 828, end: 18288, verdict: "ORIGINAL", agreeing_releases: 2, evidence: "identical", reason: "" };
561
- const split = splitRangeByManifestKind(diffRange, manifestRanges);
562
- assert.equal(split.length, 3);
563
- assert.deepEqual(split.map((r) => [r.start, r.end, r.kind]), [
564
- [828, 831, "game"],
565
- [832, 862, "loader"],
566
- [863, 18288, "game"],
567
- ]);
568
- // Every sub-range must keep the original verdict/evidence, only start/end/kind change.
569
- for (const r of split) {
570
- assert.equal(r.verdict, "ORIGINAL");
571
- assert.equal(r.agreeing_releases, 2);
572
- assert.equal(r.evidence, "identical");
573
- }
574
- });
575
-
576
- test("splitRangeByManifestKind returns the range unchanged (one sub-range) when it doesn't cross a kind boundary", () => {
577
- const manifestRanges = [{ start: 0, end: 65535, kind: "game" }];
578
- const diffRange = { start: 100, end: 200, verdict: "UNKNOWN", agreeing_releases: 0, evidence: "", reason: "no signature" };
579
- const split = splitRangeByManifestKind(diffRange, manifestRanges);
580
- assert.equal(split.length, 1);
581
- assert.deepEqual(split[0], { ...diffRange, kind: "game" });
582
- });
583
-
584
- // ------------------------------------------------- real-dump integration case
585
-
586
- // Corpus-driven: needs two committed primary dumps from different releases.
587
- // Positional, never a release-id comparison -- the reference is simply the first
588
- // release, and "the other one" is anything that isn't it. Skips when the host
589
- // project has fewer than two releases with an existing run1 .bin.
590
- const PAIR = (() => {
591
- let reg;
592
- try {
593
- reg = JSON.parse(readFileSync(registryPath, "utf8"));
594
- } catch {
595
- return null;
596
- }
597
- const withRun1 = (reg.releases ?? [])
598
- .map((r) => ({ r, d: (r.dumps ?? []).find((d) => d.label === "run1") }))
599
- .filter((x) => x.d && x.d.bin && existsSync(join(REPO_ROOT, x.d.bin)));
600
- return withRun1.length >= 2 ? [withRun1[0], withRun1[1]] : null;
601
- })();
602
-
603
- test("anchorSearch against two real committed primary dumps finds unique agreeing anchors",
604
- { skip: PAIR ? false : "fewer than two committed run1 dumps in this project -- integration case skipped" }, () => {
605
- const source = readFileSync(join(REPO_ROOT, PAIR[0].d.bin));
606
- const target = readFileSync(join(REPO_ROOT, PAIR[1].d.bin));
607
- assert.equal(source.length, 65536);
608
- assert.equal(target.length, 65536);
609
-
610
- const { anchorSearch: realAnchorSearch, proveOffset: realProveOffset } = { anchorSearch, proveOffset };
611
- const anchors = realAnchorSearch(source, target);
612
- const proof = realProveOffset(anchors);
613
- assert.ok(anchors.length > 0, "expected at least one candidate anchor from the real dumps");
614
- assert.ok(proof.usable.length > 0, "expected at least one anchor with a unique match against the real target dump");
615
- assert.equal(proof.ok, true, `expected the real dumps to agree on a single offset; got: ${proof.reason}`);
616
- });
617
-
618
- // ------------------------------------------------- import-purity guard
619
-
620
- function stripComments(src) {
621
- return src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
622
- }
623
-
624
- function importSpecifiers(src) {
625
- const specs = [];
626
- const re = /import\s+(?:[^'"]+?\s+from\s+)?["']([^"']+)["']/g;
627
- let m;
628
- while ((m = re.exec(src))) specs.push(m[1]);
629
- return specs;
630
- }
631
-
632
- // The invariant here is NOT "imports must be siblings" -- it is "this module
633
- // cannot acquire an outside dependency", which is the mechanical proof that it
634
- // never reaches the emulator by importing a transport module and never pulls a
635
- // third-party package. The toolkit ships as a bundle of skills that may import
636
- // each other, so a sibling *skill*'s scripts dir is legitimate; anything beyond
637
- // the skills tree is not. Widened deliberately when the recovery pipeline moved
638
- // out of `tools/` into the skills (2026-08-04) -- widened to the bundle boundary,
639
- // not removed.
640
- const SKILLS_ROOT = resolve(HERE, "..", "..");
641
-
642
- test("every import specifier in diff-images.mjs is a node: built-in or a module inside the skills bundle -- the mechanical proof of the one permitted route", () => {
643
- const src = stripComments(readFileSync(join(HERE, "diff-images.mjs"), "utf8"));
644
- const specs = importSpecifiers(src);
645
- assert.ok(specs.length > 0, "diff-images.mjs should have at least one import specifier");
646
- for (const spec of specs) {
647
- const isNodeBuiltin = spec.startsWith("node:");
648
- const isRelativePath = spec.startsWith("./") || spec.startsWith("../");
649
- assert.ok(
650
- isNodeBuiltin || isRelativePath,
651
- `diff-images.mjs imports "${spec}", which is neither a node: built-in nor a relative path -- a bare specifier means a third-party package`,
652
- );
653
- if (isRelativePath) {
654
- const resolvedPath = resolve(HERE, spec);
655
- assert.ok(
656
- resolvedPath.startsWith(SKILLS_ROOT + "/"),
657
- `diff-images.mjs's import "${spec}" resolves to ${resolvedPath}, outside the skills bundle at ${SKILLS_ROOT}`,
658
- );
659
- assert.ok(
660
- /\/scripts\//.test(resolvedPath),
661
- `diff-images.mjs's import "${spec}" must resolve into some skill's scripts/ dir, not ${resolvedPath}`,
662
- );
663
- }
664
- }
665
- });