@henols/c64-re-tools 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-disk/scripts/lib/containers.ts +2 -1
  3. package/skills/c64-emulator/SKILL.md +2 -0
  4. package/skills/c64-knowledge/SKILL.md +6 -2
  5. package/skills/c64-knowledge/scripts/knowledge.ts +6 -12
  6. package/skills/c64-knowledge/scripts/lib/listing.ts +8 -1
  7. package/skills/c64-memory-map/SKILL.md +2 -0
  8. package/skills/c64-memory-map/scripts/memory-map.ts +22 -3
  9. package/skills/c64-provenance/SKILL.md +4 -3
  10. package/skills/c64-provenance/scripts/lib/library.ts +28 -5
  11. package/skills/c64-provenance/scripts/lib/separate.ts +34 -6
  12. package/skills/c64-provenance/scripts/provenance.ts +13 -4
  13. package/skills/c64-source/SKILL.md +1 -1
  14. package/skills/c64-source/scripts/lib/source.ts +18 -4
  15. package/skills/c64-source/scripts/source.ts +9 -13
  16. package/skills/c64-static-analysis/SKILL.md +4 -3
  17. package/skills/c64-static-analysis/scripts/lib/findings.ts +23 -8
  18. package/skills/c64-static-analysis/scripts/lib/pointers.ts +38 -25
  19. package/skills/c64-static-analysis/scripts/static-analysis.ts +17 -14
  20. package/skills/c64-testing/SKILL.md +6 -3
  21. package/skills/c64-testing/references/scenario-format.md +5 -3
  22. package/skills/c64-testing/scripts/lib/diff-files.ts +27 -8
  23. package/skills/c64-testing/scripts/lib/runs.ts +23 -7
  24. package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
  25. package/skills/c64-testing/scripts/testing.ts +5 -2
  26. package/skills/c64-unpacker/SKILL.md +2 -2
  27. package/skills/c64-unpacker/scripts/lib/loads.ts +4 -1
  28. package/skills/c64-unpacker/scripts/lib/machine.ts +31 -0
  29. package/skills/c64-unpacker/scripts/unpacker.ts +4 -1
  30. package/src/host/vice/session.ts +98 -11
  31. package/src/host-client/image.ts +22 -8
  32. package/src/host-client/steps.ts +2 -2
  33. package/src/host-client/tools.ts +4 -3
  34. package/src/host-client/transfer.ts +8 -3
  35. package/src/knowledge/write.ts +64 -28
  36. package/src/mcp/output.ts +1 -1
  37. package/src/mcp/server.ts +7 -0
  38. package/src/mcp/tools/debug.ts +2 -1
  39. package/src/mcp/tools/media.ts +5 -1
  40. package/src/media.ts +15 -0
  41. package/src/opcodes.ts +31 -1
  42. package/src/platform.ts +14 -1
  43. package/src/project.ts +8 -1
  44. package/src/script.ts +63 -2
@@ -12,10 +12,12 @@ import { applyEntryState, parseEntryState } from "#src/host-client/entry-state.t
12
12
  import { readPrg } from "#src/host-client/image.ts";
13
13
  import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
14
14
  import { readMemoryInParts, type ViceSessionClient } from "#src/host-client/vice-session.ts";
15
+ import { checkSnapshotFile } from "#src/media.ts";
15
16
  import { openForRead } from "#src/knowledge/database.ts";
16
17
  import { nameTable } from "#src/knowledge/names.ts";
17
18
  import { DEFAULT_SPACE } from "#src/knowledge/schema.ts";
18
19
  import { programImage } from "#src/knowledge/program.ts";
20
+ import { listed } from "#src/script.ts";
19
21
 
20
22
  export type Side = "original" | "rebuild";
21
23
  export const SIDES: Side[] = ["original", "rebuild"];
@@ -70,11 +72,24 @@ export interface Scenario {
70
72
 
71
73
  export type Outcome = "PASS" | "FAIL" | "INCONCLUSIVE";
72
74
 
75
+ /** A byte of a memory item that differs: its address, and its value on each side as two hex digits. */
76
+ export interface DifferingByte {
77
+ address: string;
78
+ original: string;
79
+ rebuild: string;
80
+ }
81
+
82
+ /**
83
+ * A difference at a checkpoint. A memory item gives the bytes that differ
84
+ * (at most --max-listed of them, and their count); every other item gives
85
+ * its value on each side.
86
+ */
73
87
  export interface Difference {
74
88
  checkpoint: string;
75
89
  what: string;
76
- original: unknown;
77
- rebuild: unknown;
90
+ original?: unknown;
91
+ rebuild?: unknown;
92
+ bytes?: { count: number; listed: DifferingByte[]; rest?: string };
78
93
  }
79
94
 
80
95
  export interface ScenarioResult {
@@ -290,8 +305,9 @@ const PROGRAM_REMEDY = "A scenario program is a PRG. Get the PRG out of the disk
290
305
  /**
291
306
  * Checks each file that the scenario names before an emulator starts: the
292
307
  * program and the symbols of each side, and each snapshot file. A missing
293
- * file is not-found; a program that is no PRG and a symbols file with no
294
- * names are invalid-input. Gives the names of each side.
308
+ * file is not-found; a program that is no PRG, a symbols file with no names
309
+ * and a snapshot step whose file is no VICE snapshot file are invalid-input.
310
+ * A refusal names the field or the step. Gives the names of each side.
295
311
  */
296
312
  export function scenarioNames(scenario: Scenario): Map<Side, Map<string, number>> {
297
313
  const names = new Map<Side, Map<string, number>>();
@@ -307,7 +323,9 @@ export function scenarioNames(scenario: Scenario): Map<Side, Map<string, number>
307
323
  named(`${side}.program`, () => readPrg(scenario[side].program, PROGRAM_REMEDY));
308
324
  names.set(side, named(`${side}.symbols`, () => namesOf(scenario[side].symbols)));
309
325
  }
310
- for (const step of scenario.steps) if ("snapshot" in step) readProjectFile(step.snapshot);
326
+ scenario.steps.forEach((step, index) => {
327
+ if ("snapshot" in step) named(`step ${index + 1}: snapshot`, () => checkSnapshotFile(step.snapshot, readProjectFile(step.snapshot).bytes));
328
+ });
311
329
  return names;
312
330
  }
313
331
 
@@ -368,7 +386,6 @@ async function startProgram(client: ViceSessionClient, program: string, side: Si
368
386
  }
369
387
 
370
388
  async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side, symbols: Map<string, number>, files: boolean): Promise<Map<string, Observation>> {
371
- const subject = scenario[side];
372
389
  const observations = new Map<string, Observation>();
373
390
  const at = (location: Location) => resolve(location, symbols, side);
374
391
  // Each side starts from a hard reset, stopped at the reset vector, with the control ports released.
@@ -376,84 +393,117 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
376
393
  await client.joystick({ port: 1, direction: "center", fire: false });
377
394
  await client.joystick({ port: 2, direction: "center", fire: false });
378
395
  let checkpoint = 0;
379
- for (const step of scenario.steps) {
396
+ for (const [index, step] of scenario.steps.entries()) {
380
397
  if (step.side !== undefined && step.side !== side) continue;
381
- // Observe first: an observe step may also carry "registers" or "memory".
382
- if ("observe" in step) {
383
- checkpoint++;
384
- const observation: Observation = { memory: [], registers: {} };
385
- for (const item of step.memory ?? []) {
386
- const spec: MemoryObservation = typeof item === "string" ? { address: item } : item;
387
- const address = at(spec.address);
388
- const size = spec.size ?? 1;
389
- const read = { data: (await readMemoryInParts(client, address, size, spec.view ?? "cpu")).toString("hex") };
390
- const excluded = (spec.exclude ?? []).map((exclude) => {
391
- const offset = at(exclude.address) - address;
392
- if (offset < 0 || offset >= size) throw new Refusal("invalid-input", `the exclude ${exclude.address} lies outside the observed ${spec.address} (${size} bytes)`);
393
- return { offset, address: exclude.address, reason: exclude.reason };
394
- });
395
- observation.memory.push({ label: size > 1 ? `${spec.address} (${size} bytes)` : spec.address, address, bytes: [...Buffer.from(read.data, "hex")], tolerance: spec.tolerance ?? 0, excluded });
396
- }
397
- if (step.registers !== undefined) {
398
- const registers = await client.registersGet("c64");
399
- for (const name of step.registers) observation.registers[name] = name === "pc" ? formatC64Address(registers.pc) : registers[name];
400
- }
401
- if (step.vicii) {
402
- // The raster line changes from one moment to the next; the comparison leaves it out.
403
- const { rasterLine: _rasterLine, ...state } = await client.vicii();
404
- observation.vicii = formatVicii(state);
405
- }
406
- if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites.map(formatSprite);
407
- if (step.screen !== undefined) {
408
- const baseline = baselineName(checkpoint, step.observe);
409
- if (side === "original") {
410
- const shot = await client.screenCapture(baseline);
411
- if (files) observation.png = shot.png;
412
- } else {
413
- const compared = await client.screenCompare({ baseline, maxMismatchRatio: step.screen.maxMismatchRatio ?? 0, mask: step.screen.mask ?? [], includeDiff: files });
414
- observation.screen = { match: compared.match, mismatchingPixels: compared.mismatchingPixels, comparedPixels: compared.comparedPixels, mismatchRatio: compared.mismatchRatio, ...(compared.bounds === undefined ? {} : { bounds: compared.bounds }) };
415
- if (files) {
416
- observation.png = (await client.screenCapture()).png;
417
- if (compared.diffPng !== undefined) observation.diffPng = compared.diffPng;
418
- }
419
- await client.screenDiscard(baseline);
398
+ try {
399
+ checkpoint = await runStep(client, scenario, side, step, at, checkpoint, files, observations);
400
+ } catch (error) {
401
+ // The step in the reason or the message: the scenario file has many steps.
402
+ const where = `step ${index + 1} (${stepKindOf(step)})`;
403
+ if (error instanceof Inconclusive) throw new Inconclusive(`${where}: ${error.message}`);
404
+ if (error instanceof WireFailure) throw new WireFailure(error.code, `${where}: ${error.message}`);
405
+ if (error instanceof Refusal) throw new Refusal(error.code, `${where}: ${error.message}`);
406
+ throw error;
407
+ }
408
+ }
409
+ return observations;
410
+ }
411
+
412
+ /** The kind of a checked step: its one key from the step kinds. */
413
+ const stepKindOf = (step: Step) => Object.keys(step).find((key) => STEP_KINDS.includes(key)) ?? "step";
414
+
415
+ /** Runs one step of one side. Gives the number of the last checkpoint. */
416
+ async function runStep(client: ViceSessionClient, scenario: Scenario, side: Side, step: Step, at: (location: Location) => number, last: number, files: boolean, observations: Map<string, Observation>): Promise<number> {
417
+ const subject = scenario[side];
418
+ let checkpoint = last;
419
+ // Observe first: an observe step may also carry "registers" or "memory".
420
+ if ("observe" in step) {
421
+ checkpoint++;
422
+ const observation: Observation = { memory: [], registers: {} };
423
+ for (const item of step.memory ?? []) {
424
+ const spec: MemoryObservation = typeof item === "string" ? { address: item } : item;
425
+ const address = at(spec.address);
426
+ const size = spec.size ?? 1;
427
+ const read = { data: (await readMemoryInParts(client, address, size, spec.view ?? "cpu")).toString("hex") };
428
+ const excluded = (spec.exclude ?? []).map((exclude) => {
429
+ const offset = at(exclude.address) - address;
430
+ if (offset < 0 || offset >= size) throw new Refusal("invalid-input", `the exclude ${exclude.address} lies outside the observed ${spec.address} (${size} bytes)`);
431
+ return { offset, address: exclude.address, reason: exclude.reason };
432
+ });
433
+ observation.memory.push({ label: size > 1 ? `${spec.address} (${size} bytes)` : spec.address, address, bytes: [...Buffer.from(read.data, "hex")], tolerance: spec.tolerance ?? 0, excluded });
434
+ }
435
+ if (step.registers !== undefined) {
436
+ const registers = await client.registersGet("c64");
437
+ for (const name of step.registers) observation.registers[name] = name === "pc" ? formatC64Address(registers.pc) : registers[name];
438
+ }
439
+ if (step.vicii) {
440
+ // The raster line changes from one moment to the next; the comparison leaves it out.
441
+ const { rasterLine: _rasterLine, ...state } = await client.vicii();
442
+ observation.vicii = formatVicii(state);
443
+ }
444
+ if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites.map(formatSprite);
445
+ if (step.screen !== undefined) {
446
+ const baseline = baselineName(checkpoint, step.observe);
447
+ if (side === "original") {
448
+ const shot = await client.screenCapture(baseline);
449
+ if (files) observation.png = shot.png;
450
+ } else {
451
+ const compared = await client.screenCompare({ baseline, maxMismatchRatio: step.screen.maxMismatchRatio ?? 0, mask: step.screen.mask ?? [], includeDiff: files });
452
+ observation.screen = { match: compared.match, mismatchingPixels: compared.mismatchingPixels, comparedPixels: compared.comparedPixels, mismatchRatio: compared.mismatchRatio, ...(compared.bounds === undefined ? {} : { bounds: compared.bounds }) };
453
+ if (files) {
454
+ observation.png = (await client.screenCapture()).png;
455
+ if (compared.diffPng !== undefined) observation.diffPng = compared.diffPng;
420
456
  }
421
- }
422
- observations.set(step.observe, observation);
423
- } else if ("reset" in step) {
424
- // Stopped at the reset vector, so frame counts from here are exact.
425
- await client.reset({ mode: step.reset, run: false });
426
- } else if ("load" in step) {
427
- await client.programLoad({ path: subject.program });
428
- } else if ("autostart" in step) {
429
- await startProgram(client, subject.program, side);
430
- } else if ("snapshot" in step) {
431
- await client.snapshotRestore(readProjectFile(step.snapshot).bytes);
432
- } else if ("normalize" in step) {
433
- await normalize(client);
434
- } else {
435
- const { side: _side, ...action } = step;
436
- const notReached = await runActionStep(client, action as ActionStep, at);
437
- if (notReached !== undefined && "runUntil" in step) {
438
- throw new Inconclusive(`the ${side} side did not reach the checkpoint before ${step.runUntil.timeoutFrames} frames (it stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)})`);
457
+ await client.screenDiscard(baseline);
439
458
  }
440
459
  }
460
+ observations.set(step.observe, observation);
461
+ } else if ("reset" in step) {
462
+ // Stopped at the reset vector, so frame counts from here are exact.
463
+ await client.reset({ mode: step.reset, run: false });
464
+ } else if ("load" in step) {
465
+ await client.programLoad({ path: subject.program });
466
+ } else if ("autostart" in step) {
467
+ await startProgram(client, subject.program, side);
468
+ } else if ("snapshot" in step) {
469
+ await client.snapshotRestore(readProjectFile(step.snapshot).bytes);
470
+ } else if ("normalize" in step) {
471
+ await normalize(client);
472
+ } else {
473
+ const { side: _side, ...action } = step;
474
+ const notReached = await runActionStep(client, action as ActionStep, at);
475
+ if (notReached !== undefined && "runUntil" in step) {
476
+ throw new Inconclusive(`the ${side} side did not reach the checkpoint before ${step.runUntil.timeoutFrames} frames (it stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)})`);
477
+ }
441
478
  }
442
- return observations;
479
+ return checkpoint;
443
480
  }
444
481
 
445
- /** The differences between the two sides' observations at each checkpoint, under the scenario's rules. */
446
- export function compare(original: Map<string, Observation>, rebuild: Map<string, Observation>): Difference[] {
482
+ type MemoryItem = Observation["memory"][number];
483
+
484
+ /** The bytes of a memory item that differ by more than its tolerance, without the excluded bytes. */
485
+ function differingBytes(item: MemoryItem, other: MemoryItem): DifferingByte[] {
486
+ const hex = (byte: number) => byte.toString(16).padStart(2, "0");
487
+ const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
488
+ return item.bytes.flatMap((byte, offset) => (!skipped.has(offset) && Math.abs(byte - other.bytes[offset]!) > item.tolerance ? [{ address: formatC64Address(item.address + offset), original: hex(byte), rebuild: hex(other.bytes[offset]!) }] : []));
489
+ }
490
+
491
+ /** The most differing bytes of one memory item that a result lists when no --max-listed is given. */
492
+ export const DEFAULT_LISTED_BYTES = 50;
493
+
494
+ /**
495
+ * The differences between the two sides' observations at each checkpoint,
496
+ * under the scenario's rules. A memory item lists at most `max` differing
497
+ * bytes; `rest` tells where the others are.
498
+ */
499
+ export function compare(original: Map<string, Observation>, rebuild: Map<string, Observation>, options: { max?: number; rest?: string } = {}): Difference[] {
447
500
  const differences: Difference[] = [];
501
+ const max = options.max ?? DEFAULT_LISTED_BYTES;
448
502
  for (const [checkpoint, left] of original) {
449
503
  const right = rebuild.get(checkpoint)!;
450
504
  left.memory.forEach((item, index) => {
451
- const other = right.memory[index]!;
452
- const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
453
- if (item.bytes.some((byte, at) => !skipped.has(at) && Math.abs(byte - other.bytes[at]!) > item.tolerance)) {
454
- const show = (bytes: number[]) => Buffer.from(bytes).toString("hex");
455
- differences.push({ checkpoint, what: `memory ${item.label}`, original: show(item.bytes), rebuild: show(other.bytes) });
456
- }
505
+ const bytes = differingBytes(item, right.memory[index]!);
506
+ if (bytes.length > 0) differences.push({ checkpoint, what: `memory ${item.label}`, bytes: listed(bytes, max, undefined, options.rest) });
457
507
  });
458
508
  for (const [name, value] of Object.entries(left.registers)) {
459
509
  const other = right.registers[name as ObservedRegister];
@@ -469,15 +519,15 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
469
519
 
470
520
  /**
471
521
  * Writes the files of a FAIL into `out`: for a screen difference both
472
- * screens and the difference image, and memory.json with each differing byte.
522
+ * screens and the difference image, and memory.json with each differing
523
+ * memory item: all its bytes on each side, and each byte that differs.
473
524
  */
474
525
  function writeFailFiles(out: string, original: Map<string, Observation>, rebuild: Map<string, Observation>, differences: Difference[]): Written[] {
475
526
  const files: Written[] = [];
476
527
  const write = (name: string, bytes: Uint8Array) => {
477
528
  files.push(writeReported(`${out}/${name}`, bytes, { what: "--out" }));
478
529
  };
479
- const memory: Array<{ checkpoint: string; what: string; bytes: Array<{ address: string; original: string; rebuild: string }> }> = [];
480
- const hex = (byte: number) => byte.toString(16).padStart(2, "0");
530
+ const memory: Array<{ checkpoint: string; what: string; address: string; original: string; rebuild: string; bytes: DifferingByte[] }> = [];
481
531
  for (const difference of differences) {
482
532
  const left = original.get(difference.checkpoint)!;
483
533
  const right = rebuild.get(difference.checkpoint)!;
@@ -491,9 +541,8 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
491
541
  const index = left.memory.findIndex((item) => `memory ${item.label}` === difference.what);
492
542
  const item = left.memory[index]!;
493
543
  const other = right.memory[index]!;
494
- const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
495
- const bytes = item.bytes.flatMap((byte, offset) => (!skipped.has(offset) && Math.abs(byte - other.bytes[offset]!) > item.tolerance ? [{ address: formatC64Address(item.address + offset), original: hex(byte), rebuild: hex(other.bytes[offset]!) }] : []));
496
- memory.push({ checkpoint: difference.checkpoint, what: difference.what, bytes });
544
+ const show = (bytes: number[]) => Buffer.from(bytes).toString("hex");
545
+ memory.push({ checkpoint: difference.checkpoint, what: difference.what, address: formatC64Address(item.address), original: show(item.bytes), rebuild: show(other.bytes), bytes: differingBytes(item, other) });
497
546
  }
498
547
  }
499
548
  if (memory.length > 0) write("memory.json", Buffer.from(`${JSON.stringify(memory, null, 2)}\n`));
@@ -501,7 +550,7 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
501
550
  }
502
551
 
503
552
  /** Runs the scenario on the original, then on the rebuild, in one VICE session. With out, a FAIL leaves files there. */
504
- export async function runScenario(client: ViceSessionClient, scenario: Scenario, options: { out?: string; names?: Map<Side, Map<string, number>> } = {}): Promise<ScenarioResult> {
553
+ export async function runScenario(client: ViceSessionClient, scenario: Scenario, options: { out?: string; names?: Map<Side, Map<string, number>>; max?: number } = {}): Promise<ScenarioResult> {
505
554
  const checkpoints = scenario.steps.flatMap((step) => ("observe" in step ? [step.observe] : []));
506
555
  const observed = new Map<Side, Map<string, Observation>>();
507
556
  // The names of both sides before either side runs: a bad symbols file is an error, not a result.
@@ -519,7 +568,7 @@ export async function runScenario(client: ViceSessionClient, scenario: Scenario,
519
568
  }
520
569
  const original = observed.get("original")!;
521
570
  const rebuild = observed.get("rebuild")!;
522
- const differences = compare(original, rebuild);
571
+ const differences = compare(original, rebuild, { ...(options.max === undefined ? {} : { max: options.max }), rest: options.out === undefined ? "a larger --max-listed, or --out <dir> for memory.json with every byte" : `${options.out}/memory.json` });
523
572
  const excluded = [...original].flatMap(([checkpoint, observation]) => observation.memory.flatMap((item) => item.excluded.map((excluded) => ({ checkpoint, address: excluded.address, reason: excluded.reason }))));
524
573
  const files = differences.length > 0 && options.out !== undefined ? writeFailFiles(options.out, original, rebuild, differences) : [];
525
574
  return {
@@ -47,7 +47,7 @@ async function test(call: Call): Promise<ScenarioResult> {
47
47
  return { result: "INCONCLUSIVE", scenario: scenario.name, reason: `no emulator: ${error.message}`, checkpoints: [], differences: [] };
48
48
  }
49
49
  try {
50
- return await runScenario(client, scenario, { names, ...(out === undefined ? {} : { out }) });
50
+ return await runScenario(client, scenario, { names, max: call.count("max-listed")!, ...(out === undefined ? {} : { out }) });
51
51
  } finally {
52
52
  await client.close();
53
53
  }
@@ -123,7 +123,10 @@ const COMMANDS: CommandSpec[] = [
123
123
  name: "test",
124
124
  summary: "Runs a scenario on the original and then on the rebuild in one emulator. result is PASS, FAIL with the differences, or INCONCLUSIVE with the reason.",
125
125
  subjects: [{ name: "<scenario.json>", help: "a scenario file, relative to the project folder. See references/scenario-format.md" }],
126
- options: { out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "with a FAIL, write both screens, the difference image and memory.json with each differing byte into this folder" } },
126
+ options: {
127
+ out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "with a FAIL, write both screens, the difference image and memory.json into this folder: each differing memory item with all its bytes, and each byte that differs" },
128
+ "max-listed": { ...OPTIONS["max-listed"], help: "list at most n differing bytes of each memory item (default 50). The count is always given" },
129
+ },
127
130
  run: test,
128
131
  },
129
132
  {
@@ -109,7 +109,7 @@ The scripts refuse a step in the form of version 2.0, for example `{ "runUntil":
109
109
  4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
110
110
  - `image.bin`: all 64 KiB of RAM at the entry.
111
111
  - `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote, also a small block. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. A block that a stage before the program wrote in such a block is also not a segment, for example a variable of a loader. A block that is fully in pages 0 to 3 is not a segment, because `entry_state.json` holds these pages. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
112
- - One PRG for each segment.
112
+ - One PRG for each segment. A capture into a directory that has a capture removes the segment PRGs that the new `image.json` does not list. `removed` in the result names them. The capture keeps all other files.
113
113
  - `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `cia` gives the CIA timer latches and interrupt masks. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
114
114
  5. Make sure that the result is the application: the code at the entry must not be the unpacker again. A segment can be data that a loader left, for example intro graphics. Keep it until other evidence shows that the program does not use it. Then capture again with `--exclude` for that range. Thus the source and the loader do not include the range.
115
115
  6. Put the capture into the knowledge with `image <out>/image.json` of the c64-knowledge skill. The knowledge keeps the segments, the entry, the entry state and the stages. The other skills read them there. The files in `<out>` are not necessary after that.
@@ -132,7 +132,7 @@ A multi-load program loads more parts from the disk while it runs, for example o
132
132
  ```
133
133
 
134
134
  2. Run `loads` with the steps file and a directory in `--out`. Add `--drive` when the program has its own drive code, for example a fast loader.
135
- 3. Each capture writes `<out>/<name>/` with `image.bin`, `image.json`, `entry_state.json` and a PRG for each segment. A segment is a block of RAM from `$0400` that the program changed after the capture before. The first capture also has the program itself.
135
+ 3. Each capture writes `<out>/<name>/` with `image.bin`, `image.json`, `entry_state.json` and a PRG for each segment. It removes the segment PRGs and the `drive8.prg` of an earlier capture that the new `image.json` does not list. A segment is a block of RAM from `$0400` that the program changed after the capture before. The first capture also has the program itself.
136
136
  4. Put the part that you work on into the knowledge with `image <out>/<name>/image.json` of the c64-knowledge skill.
137
137
  5. With `--drive`, `drive8.prg` is the drive RAM at `$0000`-`$07ff`. Put it into the knowledge with `image <out>/<name>/drive8.prg --space drive8`. Then analyze it with c64-static-analysis and `--space drive8`.
138
138
 
@@ -13,7 +13,7 @@ import { WireFailure } from "#src/host-client/tools.ts";
13
13
  import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
14
14
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
15
15
  import { UsageError } from "#src/script.ts";
16
- import { entryStateFile, json, machineState, readRam, readyState, vectorsOf } from "./machine.ts";
16
+ import { entryStateFile, json, machineState, readRam, readyState, removeStaleFiles, vectorsOf } from "./machine.ts";
17
17
  import { FIRST_SEGMENT_ADDRESS } from "./stages.ts";
18
18
 
19
19
 
@@ -190,11 +190,14 @@ export async function loads(program: string, options: { out: string; steps: Load
190
190
  );
191
191
  const entry = entryStateFile(registers, state, vectors, clean);
192
192
  write("entry_state.json", entry.bytes);
193
+ // A capture again into the folder: the files of the earlier capture that image.json no longer lists go.
194
+ const removed = removeStaleFiles(directory, [...files, ...(drive === undefined ? [] : ["drive8.prg"])]);
193
195
  const keyWait = registers.pc >= KERNAL_KEY_WAIT.start && registers.pc <= KERNAL_KEY_WAIT.end;
194
196
  captures.push({
195
197
  capture: step.capture,
196
198
  out: directory,
197
199
  files: written,
200
+ ...(removed.length === 0 ? {} : { removed }),
198
201
  pc: formatC64Address(registers.pc),
199
202
  ...(keyWait ? { keyWait: true } : {}),
200
203
  segments: segments.map(showRange),
@@ -2,9 +2,14 @@
2
2
  // 0-3, the chip registers read without side effects, and the vectors. The
3
3
  // capture and loads commands of the unpacker share it.
4
4
 
5
+ import { readdirSync, rmSync } from "node:fs";
6
+ import { join } from "node:path";
7
+
5
8
  import { formatC64Address } from "#src/c64.ts";
6
9
  import { resetToReady } from "#src/host-client/entry-state.ts";
10
+ import { writeRefusal } from "#src/host-client/transfer.ts";
7
11
  import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
12
+ import { projectRoot, resolveProjectPath } from "#src/project.ts";
8
13
 
9
14
  type Registers = Awaited<ReturnType<ViceSessionClient["registersGet"]>>;
10
15
 
@@ -102,4 +107,30 @@ function entryState(registers: Registers, state: MachineState, vectors: ReturnTy
102
107
  };
103
108
  }
104
109
 
110
+ /** The names of the files that a capture writes for its content: a PRG for each segment, and the drive RAM. */
111
+ const CAPTURE_FILE = /^(?:segment-[0-9a-f]{4}-[0-9a-f]{4}\.prg|drive8\.prg)$/;
112
+
113
+ /**
114
+ * Removes from the capture folder `folder` (a project path) each file that an
115
+ * earlier capture wrote and this capture did not: a segment PRG of another
116
+ * range, or the drive RAM. image.json lists only the files of this capture.
117
+ * Every other file stays. Gives the removed files as project paths.
118
+ */
119
+ export function removeStaleFiles(folder: string, written: readonly string[]): string[] {
120
+ const path = resolveProjectPath(folder, projectRoot());
121
+ const removed: string[] = [];
122
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
123
+ // A symbolic link is not a file of a capture: it stays.
124
+ if (!entry.isFile() || !CAPTURE_FILE.test(entry.name) || written.includes(entry.name)) continue;
125
+ const file = `${folder}/${entry.name}`;
126
+ try {
127
+ rmSync(join(path, entry.name));
128
+ } catch (error) {
129
+ throw writeRefusal(error, file, "--out") ?? error;
130
+ }
131
+ removed.push(file);
132
+ }
133
+ return removed.sort();
134
+ }
135
+
105
136
  export const json = (value: unknown) => Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
@@ -19,7 +19,7 @@ import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec, ty
19
19
  import { basicStart, inspect, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
20
20
  import { loads, readLoadSteps } from "./lib/loads.ts";
21
21
  import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges } from "./lib/stages.ts";
22
- import { entryStateFile, machineState, readRam, readyState, vectorsOf } from "./lib/machine.ts";
22
+ import { entryStateFile, machineState, readRam, readyState, removeStaleFiles, vectorsOf } from "./lib/machine.ts";
23
23
 
24
24
  /** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
25
25
  const UNPACKED_BYTES = 256;
@@ -537,12 +537,15 @@ async function capture(
537
537
  // A clean machine at the READY prompt shows what the loader and the stages changed.
538
538
  const entry = entryStateFile(registers, state, vectors, await readyState(client));
539
539
  write("entry_state.json", entry.bytes);
540
+ // A capture again into the folder: the segment PRGs of the earlier capture that image.json no longer lists go.
541
+ const removed = removeStaleFiles(options.out, files);
540
542
  return {
541
543
  output: {
542
544
  captured: true,
543
545
  pc: formatC64Address(registers.pc),
544
546
  out: options.out,
545
547
  files: written,
548
+ ...(removed.length === 0 ? {} : { removed }),
546
549
  segments: segments.map(showRange),
547
550
  ...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
548
551
  stages: followed.stages.length,