@henols/c64-re-tools 2.0.1 → 2.1.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.
Files changed (104) hide show
  1. package/README.md +8 -4
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +24 -9
  4. package/skills/c64-assembler/scripts/assembler.ts +95 -0
  5. package/skills/c64-basic/SKILL.md +17 -9
  6. package/skills/c64-basic/scripts/basic.ts +26 -36
  7. package/skills/c64-disk/SKILL.md +17 -7
  8. package/skills/c64-disk/scripts/disk.ts +130 -104
  9. package/skills/c64-disk/scripts/lib/containers.ts +10 -12
  10. package/skills/c64-docs/SKILL.md +47 -28
  11. package/skills/c64-docs/references/style.md +6 -6
  12. package/skills/c64-docs/scripts/docs.ts +323 -0
  13. package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
  14. package/skills/c64-emulator/SKILL.md +11 -7
  15. package/skills/c64-knowledge/SKILL.md +34 -21
  16. package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
  17. package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
  18. package/skills/c64-memory-map/SKILL.md +18 -10
  19. package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
  20. package/skills/c64-provenance/SKILL.md +82 -26
  21. package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
  22. package/skills/c64-provenance/scripts/lib/leftovers.ts +442 -0
  23. package/skills/c64-provenance/scripts/lib/library.ts +15 -25
  24. package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
  25. package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
  26. package/skills/c64-provenance/scripts/provenance.ts +222 -252
  27. package/skills/c64-reverse-engineering/SKILL.md +18 -15
  28. package/skills/c64-source/SKILL.md +28 -16
  29. package/skills/c64-source/scripts/lib/loader.ts +52 -77
  30. package/skills/c64-source/scripts/lib/source.ts +33 -42
  31. package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
  32. package/skills/c64-static-analysis/SKILL.md +24 -13
  33. package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
  34. package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
  35. package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
  36. package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
  37. package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
  38. package/skills/c64-testing/SKILL.md +94 -19
  39. package/skills/c64-testing/references/recording-format.md +125 -0
  40. package/skills/c64-testing/references/scenario-format.md +24 -19
  41. package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
  42. package/skills/c64-testing/scripts/lib/diff.ts +5 -3
  43. package/skills/c64-testing/scripts/lib/play.ts +334 -0
  44. package/skills/c64-testing/scripts/lib/recording.ts +662 -0
  45. package/skills/c64-testing/scripts/lib/replay.ts +105 -0
  46. package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
  47. package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
  48. package/skills/c64-testing/scripts/testing.ts +189 -0
  49. package/skills/c64-unpacker/SKILL.md +42 -25
  50. package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
  51. package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
  52. package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
  53. package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
  54. package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
  55. package/src/c64.ts +35 -20
  56. package/src/cli/main.ts +2 -2
  57. package/src/errors.ts +38 -0
  58. package/src/graphics.ts +3 -1
  59. package/src/host/server.ts +3 -2
  60. package/src/host/vice/session.ts +4 -4
  61. package/src/host-client/entry-state.ts +106 -0
  62. package/src/host-client/image.ts +21 -10
  63. package/src/host-client/steps.ts +285 -108
  64. package/src/host-client/transfer.ts +21 -15
  65. package/src/host-client/vice-session.ts +1 -1
  66. package/src/knowledge/database.ts +3 -23
  67. package/src/knowledge/expression.ts +8 -11
  68. package/src/knowledge/history.ts +2 -2
  69. package/src/knowledge/import.ts +13 -14
  70. package/src/knowledge/names.ts +50 -0
  71. package/src/knowledge/program.ts +11 -10
  72. package/src/knowledge/read.ts +16 -21
  73. package/src/knowledge/schema.ts +9 -3
  74. package/src/knowledge/text.ts +5 -5
  75. package/src/knowledge/write.ts +93 -137
  76. package/src/mcp/inputs.ts +105 -0
  77. package/src/mcp/output.ts +2 -13
  78. package/src/mcp/server.ts +12 -60
  79. package/src/mcp/tools/debug.ts +17 -12
  80. package/src/mcp/tools/execution.ts +5 -32
  81. package/src/mcp/tools/input.ts +7 -32
  82. package/src/mcp/tools/media.ts +6 -5
  83. package/src/mcp/tools/memory.ts +11 -38
  84. package/src/mcp/tools/video.ts +6 -5
  85. package/src/native/c64run.ts +162 -0
  86. package/src/native/discover.ts +8 -0
  87. package/src/native/dxa.ts +7 -6
  88. package/src/native/ghidra/analyze.ts +2 -1
  89. package/src/native/ghidra/index.ts +2 -1
  90. package/src/native/processes.ts +4 -3
  91. package/src/native/status.ts +25 -4
  92. package/src/native/types.ts +3 -7
  93. package/src/opcodes.ts +13 -13
  94. package/src/platform.ts +9 -9
  95. package/src/project.ts +16 -26
  96. package/src/protocol/messages.ts +3 -21
  97. package/src/protocol/vice.ts +7 -7
  98. package/src/script.ts +491 -49
  99. package/skills/c64-assembler/scripts/assemble.ts +0 -138
  100. package/skills/c64-docs/scripts/generate.ts +0 -109
  101. package/skills/c64-docs/scripts/link.ts +0 -239
  102. package/skills/c64-memory-map/scripts/memmap.ts +0 -92
  103. package/skills/c64-testing/scripts/checklist.ts +0 -51
  104. package/skills/c64-testing/scripts/test.ts +0 -65
@@ -0,0 +1,189 @@
1
+ // The c64-testing script. test runs a scenario on the original and the
2
+ // rebuild in one emulator; run runs steps on one side and writes what they
3
+ // ask for; record and replay play a session in the replay core (c64run) and
4
+ // compare the rebuild with it; diff shows what changed between two states;
5
+ // checklist gives the Markdown list for a human tester.
6
+
7
+ import { COVERAGE } from "#src/c64.ts";
8
+ import { WireFailure } from "#src/host-client/tools.ts";
9
+ import { writeReported } from "#src/host-client/transfer.ts";
10
+ import { ViceSessionClient } from "#src/host-client/vice-session.ts";
11
+ import { C64RunSession, PAL_FRAME_CYCLES } from "#src/native/c64run.ts";
12
+ import { localToolContext } from "#src/native/local.ts";
13
+ import { Workspace } from "#src/native/staging.ts";
14
+ import { knowledgeNames } from "#src/knowledge/names.ts";
15
+ import { OPTIONS, runScript, UsageError, type Call, type CommandSpec } from "#src/script.ts";
16
+ import { diffFiles } from "./lib/diff-files.ts";
17
+ import { play } from "./lib/play.ts";
18
+ import { readRecordFile, record, RECORDING_FILES } from "./lib/recording.ts";
19
+ import { replay } from "./lib/replay.ts";
20
+ import { runFile } from "./lib/runs.ts";
21
+ import { readScenario, runScenario, type Scenario, type ScenarioResult } from "./lib/scenario.ts";
22
+
23
+ /** The default port of the --play page: next to the Host Runtime's 6464. */
24
+ const DEFAULT_HTTP_PORT = 6465;
25
+
26
+ /** Behavior that only a person can judge. */
27
+ const ALWAYS = [
28
+ "The controls respond as fast as in the original.",
29
+ "The animation is smooth and has no glitches.",
30
+ "The music and the sound effects sound the same as in the original.",
31
+ "The game flow and the level changes work from start to end.",
32
+ "Nothing unexpected happens outside the tested scenarios.",
33
+ ];
34
+
35
+ async function test(call: Call): Promise<ScenarioResult> {
36
+ const scenario = readScenario(call.subject(0));
37
+ const out = call.string("out");
38
+ let client: ViceSessionClient;
39
+ try {
40
+ client = await ViceSessionClient.open({ videoStandard: scenario.videoStandard ?? "pal" });
41
+ } catch (error) {
42
+ if (!(error instanceof WireFailure)) throw error;
43
+ return { result: "INCONCLUSIVE", scenario: scenario.name, reason: `no emulator: ${error.message}`, checkpoints: [], differences: [] };
44
+ }
45
+ try {
46
+ return await runScenario(client, scenario, out === undefined ? {} : { out });
47
+ } finally {
48
+ await client.close();
49
+ }
50
+ }
51
+
52
+ async function recordCommand(call: Call): Promise<Record<string, unknown>> {
53
+ const playing = call.flag("play");
54
+ if (!playing && (call.count("joystick") !== undefined || call.count("http-port") !== undefined)) throw new UsageError("--joystick and --http-port are for --play");
55
+ const joystick = (call.count("joystick") ?? 2) as 1 | 2;
56
+ const httpPort = call.count("http-port") ?? DEFAULT_HTTP_PORT;
57
+ const out = call.string("out")!;
58
+ const file = readRecordFile(call.subject(0));
59
+ const context = localToolContext();
60
+ const core = await C64RunSession.open(context);
61
+ const workspace = Workspace.create(context.supervisor, "c64-re-tools-record-");
62
+ try {
63
+ const person = playing
64
+ ? (session: C64RunSession, restore: () => Promise<string | undefined>) => {
65
+ const ready = (url: string) =>
66
+ process.stderr.write("Open " + url + ". The start screen is frozen. Enter or Start begins the play, F12 or End saves the recording, F11 or Cancel ends without one. F9 changes the joystick port.\n");
67
+ return play(session, { httpPort, joystick, ready, restore });
68
+ }
69
+ : undefined;
70
+ const outcome = await record(core, file, workspace, person);
71
+ const summary = {
72
+ name: file.name,
73
+ frames: Math.round(outcome.recording.ticks / PAL_FRAME_CYCLES),
74
+ inputSites: outcome.recording.sites.length,
75
+ events: outcome.recording.events.length,
76
+ // The instructions in the coverage map: 0 when the program never read its input from RAM.
77
+ executed: outcome.coverage.reduce((count, byte) => count + ((byte & COVERAGE.ramExecute) === 0 ? 0 : 1), 0),
78
+ };
79
+ if (outcome.reason !== undefined) return { result: "FAIL", recorded: false, reason: outcome.reason, ...summary };
80
+ const files = [
81
+ writeReported(`${out}/${RECORDING_FILES.recording}`, `${JSON.stringify(outcome.recording, null, 2)}\n`, { what: "--out" }),
82
+ writeReported(`${out}/${RECORDING_FILES.reads}`, outcome.reads, { what: "--out" }),
83
+ writeReported(`${out}/${RECORDING_FILES.coverage}`, outcome.coverage, { what: "--out" }),
84
+ ];
85
+ return { result: "PASS", recorded: true, out, ...summary, files };
86
+ } finally {
87
+ await core.close();
88
+ workspace.remove();
89
+ }
90
+ }
91
+
92
+ function checklist(call: Call): { text: string } {
93
+ const lines = ["# Playtest checklist", ""];
94
+ for (const path of call.subjects) {
95
+ const scenario: Scenario = readScenario(path);
96
+ lines.push(`## ${scenario.name}`, "");
97
+ for (const step of scenario.steps) if ("observe" in step) lines.push(`- [ ] At "${step.observe}", the rebuild looks and behaves as the original.`);
98
+ lines.push("");
99
+ }
100
+ lines.push("## Application", "");
101
+ for (const item of call.strings("item")) lines.push(`- [ ] ${item}`);
102
+ for (const item of ALWAYS) lines.push(`- [ ] ${item}`);
103
+ return { text: `${lines.join("\n")}\n` };
104
+ }
105
+
106
+ const STATE_ITEM = "a name or a $ address, with :size for more than one byte, for example room:2";
107
+
108
+ const COMMANDS: CommandSpec[] = [
109
+ {
110
+ name: "test",
111
+ 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.",
112
+ subjects: [{ name: "<scenario.json>", help: "a scenario file, relative to the project folder. See references/scenario-format.md" }],
113
+ options: { out: { ...OPTIONS.out, value: "<dir>", help: "with a FAIL, write both screens, the difference image and memory.json with each differing byte into this folder" } },
114
+ run: test,
115
+ },
116
+ {
117
+ name: "run",
118
+ summary: "Runs the steps of a run file in its own emulator and writes what they ask for into --out: screens, coverage maps, snapshot files, traces, and results.json with the observations. result is FAIL when a step did not run.",
119
+ subjects: [{ name: "<run.json>", help: "a run file, relative to the project folder. See references/scenario-format.md, section Runs" }],
120
+ options: { out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the files of the steps and results.json" } },
121
+ run: async (call) => {
122
+ const { output, failed } = await runFile(call.subject(0), call.string("out")!);
123
+ return { result: failed ? "FAIL" : "PASS", ...output };
124
+ },
125
+ },
126
+ {
127
+ name: "record",
128
+ summary: "Plays the steps of a record file on the original in the replay core (c64run), and writes the session as a recording folder: recording.json, reads.txt and coverage.bin. result is FAIL when the session did not complete.",
129
+ subjects: [{ name: "<record.json>", help: "a record file, relative to the project folder. See references/recording-format.md" }],
130
+ options: {
131
+ out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the recording folder, for example tests/recordings/<name>" },
132
+ play: { kind: "flag", help: "a person plays after the steps: the script prints the address of a page on stderr. The page shows the start screen frozen until Start (Enter). End (F12) saves the recording, Cancel (F11) ends without one" },
133
+ joystick: { kind: "count", value: "<1|2>", min: 1, max: 2, help: "with --play: the joystick port that the program reads, at the start (default 2). F9 on the page changes it" },
134
+ "http-port": { kind: "count", value: "<n>", min: 0, max: 65535, help: `with --play: the port of the page (default ${DEFAULT_HTTP_PORT})` },
135
+ },
136
+ run: recordCommand,
137
+ },
138
+ {
139
+ name: "replay",
140
+ summary: "Replays a recording on the original, which must end with the recorded state, and with --rebuild on the rebuild, which must have the state of the original at each checkpoint. result is PASS, FAIL or INCONCLUSIVE.",
141
+ subjects: [{ name: "<recording>", help: "a folder that record wrote, relative to the project folder" }],
142
+ options: {
143
+ checkpoint: { kind: "text", value: "<routine>", help: "the name or $ address of a routine: each entry is a checkpoint. Without it, the comparison of the record file" },
144
+ state: { kind: "text", value: "<item>", multiple: true, help: `the state to compare at each checkpoint: ${STATE_ITEM}` },
145
+ pin: { kind: "text", value: "<item>", multiple: true, help: `random state in RAM, in the form of --state: at each checkpoint the rebuild gets the original's values there` },
146
+ rebuild: { kind: "path", value: "<prg>", help: "the rebuild to compare with the original" },
147
+ symbols: { ...OPTIONS.symbols, help: "the names of the rebuild: the c64-assembler output (default: the symbols of the recording)" },
148
+ out: { ...OPTIONS.out, value: "<dir>", help: "with a FAIL, write the RAM of both sides at the first different checkpoint into this folder" },
149
+ },
150
+ notes: "A recording without names takes the names of the knowledge for the original. A $ address is an address of the original: on the rebuild it moves with the name before it.",
151
+ run: (call) => {
152
+ const [checkpoint, rebuild, symbols, out] = [call.string("checkpoint"), call.string("rebuild"), call.string("symbols"), call.string("out")];
153
+ return replay({ folder: call.subject(0), state: call.strings("state"), pin: call.strings("pin"), ...(checkpoint === undefined ? {} : { checkpoint }), ...(rebuild === undefined ? {} : { rebuild }), ...(symbols === undefined ? {} : { symbols }), ...(out === undefined ? {} : { out }) });
154
+ },
155
+ },
156
+ {
157
+ name: "diff",
158
+ summary: "What changed between two machine states of one kind: two snapshot files (snapshot.vsf), two memory files of runs (memory.bin), or two observations of runs (results.json:<name>).",
159
+ subjects: [
160
+ { name: "<a>", help: "the first state, relative to the project folder" },
161
+ { name: "<b>", help: "the second state" },
162
+ ],
163
+ options: {
164
+ gap: { kind: "count", value: "<n>", min: 0, max: 0xffff, default: 0, help: "join changed ranges across at most n equal bytes (default 0)" },
165
+ "max-listed": OPTIONS["max-listed"],
166
+ ntsc: { kind: "flag", help: "the snapshots are of an NTSC machine (default PAL)" },
167
+ address: { kind: "address", value: "<address>", help: "the address of the first byte of a memory file that is shorter than 64 KiB" },
168
+ out: { ...OPTIONS.out, value: "<file>", help: "also write all ranges to this file" },
169
+ },
170
+ run: (call) => {
171
+ const [address, out] = [call.address("address"), call.string("out")];
172
+ return diffFiles(call.subject(0), call.subject(1), { gap: call.count("gap")!, max: call.count("max-listed")!, ntsc: call.flag("ntsc"), ...(address === undefined ? {} : { at: address }), ...(out === undefined ? {} : { out }) });
173
+ },
174
+ },
175
+ {
176
+ name: "checklist",
177
+ summary: "The playtest checklist for a human tester as Markdown in text: the checkpoints of the scenarios, and the behavior that automated scenarios cannot check.",
178
+ subjects: [{ name: "<scenario.json>", help: "a scenario file", list: true }],
179
+ options: { item: { kind: "text", value: "<text>", multiple: true, help: "an application-specific line" } },
180
+ run: checklist,
181
+ },
182
+ ];
183
+
184
+ await runScript({
185
+ name: "testing",
186
+ summary: "Tests a rebuild against the original: scenarios in VICE, runs, recorded play sessions in the replay core, state differences, and a checklist for a human tester.",
187
+ names: knowledgeNames,
188
+ commands: COMMANDS,
189
+ });
@@ -19,47 +19,64 @@ This skill does not change project knowledge.
19
19
  ## Tools / execution path
20
20
 
21
21
  ```
22
- node <skill>/scripts/unpack.ts inspect original/game.prg
23
- node <skill>/scripts/unpack.ts trace original/game.prg
24
- node <skill>/scripts/unpack.ts capture original/game.prg --until '$c000' --out .c64-re-tools/local/capture
25
- node <skill>/scripts/unpack.ts stages original/game.prg --steps .c64-re-tools/local/input.json
26
- node <skill>/scripts/unpack.ts loads original/game.d64 --steps .c64-re-tools/local/parts.json --out .c64-re-tools/local/parts --drive
22
+ node <skill>/scripts/unpacker.ts inspect original/game.prg
23
+ node <skill>/scripts/unpacker.ts trace original/game.prg
24
+ node <skill>/scripts/unpacker.ts stages original/game.prg --steps .c64-re-tools/local/input.json
25
+ node <skill>/scripts/unpacker.ts capture original/game.prg --until '$c000' --out .c64-re-tools/local/capture
26
+ node <skill>/scripts/unpacker.ts loads original/game.d64 --steps .c64-re-tools/local/parts.json --out .c64-re-tools/local/parts --drive
27
27
  ```
28
28
 
29
- `inspect` examines the file locally. `trace` and `capture` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `capture` stops the program when the CPU gets to the `--until` address. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). In `stages` and `capture`, a `frames` step and a `runUntil` step with only `at` count as run time. `stages` refuses a `runUntil` step with `raster` or `memory`.
29
+ `inspect` examines the file locally. `trace`, `stages`, `capture` and `loads` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). `capture` stops the program when the CPU gets to the `--until` address. In `stages` and `capture`, a `frames` step and a `runUntil` step to an address without a condition count as run time. `stages` refuses a different `runUntil` step.
30
30
 
31
31
  | Option | Meaning |
32
32
  |---|---|
33
- | `--until '$c000'` | The address where the unpacked program starts. The capture stops there after the steps. To capture a later arrival at the address, add a `runUntil` step to an address after the first arrival. |
34
- | `--out .c64-re-tools/local/capture` | Where to write the result, relative to the project folder. |
35
- | `--range '$c000' '$cfff'` | Write this range as a PRG. Without it, the script writes all 64 KiB of RAM as a flat file. |
33
+ | `--until '$c000'` | `capture`: the address where the unpacked program starts. The capture stops there after the steps. To capture a later arrival at the address, add a `runUntil` step to an address after the first arrival. |
34
+ | `--out .c64-re-tools/local/capture` | `capture` and `loads`: the folder for the result, relative to the project folder. |
35
+ | `--range '$c000-$cfff'` | `capture` only: write this range as the one segment. Without it, the segments are the blocks that the stages wrote. |
36
36
  | `--exclude '$a000-$a9ff'` | `capture` only: remove this range from the segments. You can give it more than once. `image.json` lists the ranges in `excluded`. The entry must not be in the range. |
37
- | `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
38
- | `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
37
+ | `--timeout-frames 3000` | `capture` only: how many frames the program can run before the capture stops (default 3000). |
38
+ | `--frames 3000` | `trace` and `stages`: how many frames the program runs (default 3000). |
39
39
  | `--entry '$c000'` | `trace`, `stages` and `capture`: start the program at this address. Without it, the script types RUN. A file that is not a BASIC program at `$0801` needs it. |
40
- | `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. The scripts start the program as a c64-testing scenario does. Thus the same steps in a scenario get the program to the entry at the same time. |
40
+ | `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. |
41
41
  | `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
42
42
  | `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
43
43
  | `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
44
44
 
45
- The capture reads RAM, also the RAM under the ROMs and the I/O area. The script prints one JSON object. The script refuses an option that the command does not use.
45
+ The capture reads RAM, also the RAM under the ROMs and the I/O area. The script refuses an option that the command does not use.
46
46
 
47
- A steps file has the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of the c64-testing skill. Addresses are `$` and four hex digits. For example, wait 50 frames and press the space bar:
47
+ A steps file is a JSON list of steps. Each step has the input of the MCP tool of the same action. An address in a step can be a name. For example, wait 50 frames and type a space:
48
48
 
49
49
  ```json
50
- [{ "frames": 50 }, { "type": " " }]
50
+ [{ "frames": 50 }, { "type": { "mode": "text", "text": " " } }]
51
51
  ```
52
52
 
53
- A `frames` step runs 1 to 30000 frames. A `runUntil` step has `timeoutFrames` from 1 to 30000.
53
+ - `{ "write": { "address": "$c000", "data": "0102" } }` writes bytes.
54
+ - `{ "registers": { "pc": "start", "a": 1 } }` sets registers. Only `pc` takes a name.
55
+ - `{ "joystick": { "port": 2, "direction": "up", "fire": false } }` sets a joystick.
56
+ - `{ "type": { "mode": "text", "text": "RUN\n" } }` or `{ "type": { "mode": "petscii", "bytes": [147] } }` puts keys in the keyboard queue of the KERNAL.
57
+ - `{ "frames": 50 }` runs 1 to 30000 frames.
58
+ - `{ "runUntil": { "target": { "kind": "address", "address": "main_loop" }, "timeoutFrames": 300 } }` runs until the target. The target kinds are `address`, `memory` and `raster`. `timeoutFrames` is 1 to 30000 (default 3000).
54
59
 
55
- A `type` step puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware does not see them. A step cannot press one specific key of the keyboard matrix. A wait for any key that reads all keyboard columns can end without a key.
60
+ A program that reads the keyboard matrix does not see the keys of a `type` step. These scripts refuse a `press` step. A wait for any key that reads all keyboard columns can end without a key.
61
+
62
+ ### Calling the script
63
+
64
+ - Call a script as `node <skill>/scripts/<script>.ts <command> [subjects] [options]`. The command comes first.
65
+ - `<script>.ts --help` lists the commands. `<script>.ts <command> --help` gives the subjects and the options of one command. The help is JSON: `{"text": "..."}`.
66
+ - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
67
+ - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
68
+ - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
69
+ - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
70
+ - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
71
+ - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
56
72
 
57
73
  ### Project folder
58
74
 
59
75
  - Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
60
76
  - If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
61
77
  - Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
62
- - Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence` of c64-knowledge. `--evidence` refuses a file in `.c64-re-tools/local/`.
78
+ - Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence test:<file>` or `--evidence file:<file>` of c64-knowledge. These two refuse a file in `.c64-re-tools/local/`.
79
+ - Do not move a coverage map after `coverage` of c64-knowledge imported it. `--evidence coverage:<map>` names the map by its path at the import. The knowledge keeps the coverage and the SHA-256 of the map, so the evidence stays valid.
63
80
  - Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
64
81
  - A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
65
82
 
@@ -102,10 +119,10 @@ A multi-load program loads more parts from the disk while it runs, for example o
102
119
 
103
120
  ```json
104
121
  [
105
- { "runUntil": { "at": "$080d" }, "timeoutFrames": 6000 },
122
+ { "runUntil": { "target": { "kind": "address", "address": "$080d" }, "timeoutFrames": 6000 } },
106
123
  { "capture": "main" },
107
- { "type": " " },
108
- { "runUntil": { "at": "$c000" }, "timeoutFrames": 6000 },
124
+ { "type": { "mode": "text", "text": " " } },
125
+ { "runUntil": { "target": { "kind": "address", "address": "$c000" }, "timeoutFrames": 6000 } },
109
126
  { "capture": "level1" }
110
127
  ]
111
128
  ```
@@ -128,14 +145,14 @@ Do not record the unpacker in the project knowledge, unless the user asks to exa
128
145
 
129
146
  The packing evidence, the stages, or the unpacked program with its image, segments and entry state in a directory of the project.
130
147
 
131
- For `loads`: `captures`, with the name, the PC, the segments and the drive file of each capture. `keyWait: true` tells that the CPU was in the KERNAL wait for a key.
148
+ `trace`, `stages`, `capture` and `loads` give `result: "PASS"` or `result: "FAIL"`. A FAIL has `reason`. `capture` gives the written files in `files`, each as `{out, bytes, sha256}`.
132
149
 
133
- The exit status is 0 for a result. The exit status is 1 for `traced: false`, for `captured: false`, for `loaded: false` and for an error. The exit status is 2 for a wrong argument.
150
+ For `loads`: `captures`, with the name, the PC, the segments, the files and the drive file of each capture. `keyWait: true` tells that the CPU was in the KERNAL wait for a key.
134
151
 
135
152
  ## Failure and conflicts
136
153
 
137
- - `traced: false`: the emulator did not load the program into memory in the time limit. The reason gives the load address. Make sure that the file is a PRG that the C64 can load.
138
- - `captured: false`: the program did not get to the address in the time limit. The reason tells where it stopped. Find a better address with the c64-emulator skill, or give more frames. If it stopped in a key wait, give `--steps` with a key.
154
+ - `traced: false` or `followed: false`: the emulator did not load the program into memory in the time limit. The reason gives the load address. Make sure that the file is a PRG that the C64 can load.
155
+ - `captured: false`: the program did not get to the address in the time limit. The reason tells where it stopped. Find a better address with the c64-emulator skill, or give more frames. If it stopped in a key wait, give `--steps` with a `type` step.
139
156
  - A step did not reach its target: the reason names the step. Change the step or give it more frames.
140
157
  - `loaded: false` and no capture has a segment: the program did not load a part, or it did not start. `keyWait: true` on a capture tells that the C64 waited at READY. or at an INPUT statement. For a tape image, read `emulatorFlaw`. A known VICE flaw can stop a tape load. Tell the user to install the latest VICE.
141
158
  - Do not name a packer from weak evidence. The script never names one.
@@ -2,8 +2,7 @@
2
2
  // over and how compressed the bytes look. It never names a packer.
3
3
 
4
4
  import { basicLineText, findHandoffs, formatC64Address, instructionLength, parseTokenized } from "#src/c64.ts";
5
-
6
- export class PackingError extends Error {}
5
+ import { Refusal } from "#src/errors.ts";
7
6
 
8
7
  /** A 256-byte block above this many bits per byte looks compressed. */
9
8
  const HIGH_ENTROPY = 6.9;
@@ -112,7 +111,7 @@ function startSignals(program: Uint8Array, load: number, end: number, sys: numbe
112
111
  }
113
112
 
114
113
  export function inspect(program: Uint8Array): PackingEvidence {
115
- if (program.length < 3) throw new PackingError("the file is too short for a PRG");
114
+ if (program.length < 3) throw new Refusal("invalid-input", "the file is too short for a PRG");
116
115
  const load = program[0]! | (program[1]! << 8);
117
116
  const end = load + program.length - 3;
118
117
  const basic = basicStart(program);
@@ -175,7 +174,7 @@ export async function readMemoryMap(read: (start: number) => Promise<{ ranges: A
175
174
  for (let start: number | undefined = 0; start !== undefined; ) {
176
175
  const found: { ranges: AccessRange[]; next?: number } = await read(start);
177
176
  ranges.push(...found.ranges);
178
- if (found.next !== undefined && found.next <= start) throw new Error(`the memory map did not move on from ${start}`);
177
+ if (found.next !== undefined && found.next <= start) throw new Refusal("operation-failed", `the memory map did not move on from ${start}`);
179
178
  start = found.next;
180
179
  }
181
180
  return ranges;
@@ -4,16 +4,18 @@
4
4
  // image with its entry state, and with --drive the drive RAM as a PRG for
5
5
  // knowledge space drive8. No tool guesses where a load ends: the steps decide.
6
6
 
7
- import { COVERAGE, formatC64Address, parseC64Address } from "#src/c64.ts";
7
+ import { COVERAGE, formatC64Address } from "#src/c64.ts";
8
8
  import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
9
9
  import { readCoverage } from "#src/host-client/coverage.ts";
10
- import { ACTION_STEP_KEYS, ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
10
+ import { ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
11
+ import { knowledgeAt } from "#src/knowledge/names.ts";
11
12
  import { WireFailure } from "#src/host-client/tools.ts";
12
- import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
13
+ import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
13
14
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
14
15
  import { UsageError } from "#src/script.ts";
15
16
  import { entryState, json, machineState, readRam, vectorsOf } from "./machine.ts";
16
17
 
18
+
17
19
  export type LoadStep = ActionStep | { capture: string };
18
20
 
19
21
  /** A capture name: letters, digits, dots, underscores or hyphens. */
@@ -53,10 +55,8 @@ export function readLoadSteps(path: string): LoadStep[] {
53
55
  }
54
56
  const kinds = Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key));
55
57
  if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...ACTION_STEP_KINDS, "capture"].join(", ")}`);
56
- const kind = kinds[0]!;
57
- const allowed = ACTION_STEP_KEYS[kind] ?? [kind];
58
- for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
59
- checkActionStep(kind, step, at, fail, false);
58
+ // The step with its defaults filled in, as the shared schema of the action steps gives it.
59
+ (value as unknown[])[index] = checkActionStep(kinds[0] as ActionStepKind, step, at, fail);
60
60
  });
61
61
  if (names.size === 0) fail("the steps have no capture step");
62
62
  return value as LoadStep[];
@@ -134,9 +134,9 @@ export async function loads(program: string, options: { out: string; steps: Load
134
134
  const captures: Array<Record<string, unknown>> = [];
135
135
  for (const [index, step] of options.steps.entries()) {
136
136
  if (!("capture" in step)) {
137
- const notReached = await runActionStep(client, step, parseC64Address);
137
+ const notReached = await runActionStep(client, step, knowledgeAt());
138
138
  if (notReached !== undefined && "runUntil" in step) {
139
- const reason = `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
139
+ const reason = `Step ${index + 1} did not reach its target in ${step.runUntil.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
140
140
  return { output: { loaded: false, reason, captures }, failed: true };
141
141
  }
142
142
  continue;
@@ -157,7 +157,8 @@ export async function loads(program: string, options: { out: string; steps: Load
157
157
  const state = await machineState(client);
158
158
  const vectors = vectorsOf(state, memory);
159
159
  const directory = `${options.out}/${step.capture}`;
160
- const write = (name: string, bytes: Uint8Array) => writeProjectFile(`${directory}/${name}`, bytes, { what: "--out" });
160
+ const written: Written[] = [];
161
+ const write = (name: string, bytes: Uint8Array) => written.push(writeReported(`${directory}/${name}`, bytes, { what: "--out" }));
161
162
  write("image.bin", memory);
162
163
  const files = segments.map((segment) => {
163
164
  const name = `segment-${hex4(segment.start)}-${hex4(segment.end)}.prg`;
@@ -187,6 +188,7 @@ export async function loads(program: string, options: { out: string; steps: Load
187
188
  captures.push({
188
189
  capture: step.capture,
189
190
  out: directory,
191
+ files: written,
190
192
  pc: formatC64Address(registers.pc),
191
193
  ...(keyWait ? { keyWait: true } : {}),
192
194
  segments: segments.map(showRange),
@@ -11,8 +11,6 @@ const MAX_READ = 4096;
11
11
 
12
12
  /** The I/O areas of the entry state, read without side effects. */
13
13
  export const IO_AREAS = { vic: [0xd000, 0x2f], sid: [0xd400, 0x1d], cia1: [0xdc00, 0x10], cia2: [0xdd00, 0x10] } as const;
14
- /** Frames from a hard reset to the READY prompt, with margin. */
15
- export const READY_FRAMES = 200;
16
14
 
17
15
  /** What no register read of a CIA gives. */
18
16
  export interface CiaHidden {
@@ -8,15 +8,10 @@
8
8
  // to the stage, is the stage's own code too: a packer hands off by JMP,
9
9
  // RTS or RTI and never runs its own code again.
10
10
 
11
- import { COVERAGE } from "#src/c64.ts";
11
+ import { type AddressRange, COVERAGE } from "#src/c64.ts";
12
12
  import { writtenBlocks } from "#src/host-client/coverage.ts";
13
13
 
14
- export interface Range {
15
- start: number;
16
- end: number;
17
- }
18
-
19
- const contains = (range: Range, address: number) => address >= range.start && address <= range.end;
14
+ const contains = (range: AddressRange, address: number) => address >= range.start && address <= range.end;
20
15
 
21
16
  /** The flags of two maps from $0000 in one. */
22
17
  export function mergeMaps(into: Uint8Array, from: Uint8Array): void {
@@ -28,7 +23,7 @@ export function mergeMaps(into: Uint8Array, from: Uint8Array): void {
28
23
  * code that can start a stage: every executed address except own code that
29
24
  * the stage has not written.
30
25
  */
31
- export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: ReadonlySet<number>, minimum: number, called: readonly Range[] = []): Range[] {
26
+ export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: ReadonlySet<number>, minimum: number, called: readonly AddressRange[] = []): AddressRange[] {
32
27
  const combined = Uint8Array.from(stage);
33
28
  mergeMaps(combined, chunk);
34
29
  return writtenBlocks(combined, minimum).filter((block) => {
@@ -47,10 +42,10 @@ export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: Reado
47
42
  * them except own code that is not written yet, and write watches over that
48
43
  * own code.
49
44
  */
50
- export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>, written: (address: number) => boolean, called: readonly Range[] = []): { exec: Range[]; watch: Range[] } {
51
- const exec: Range[] = [];
52
- const watch: Range[] = [];
53
- const add = (list: Range[], address: number) => {
45
+ export function stagePoints(blocks: readonly AddressRange[], own: ReadonlySet<number>, written: (address: number) => boolean, called: readonly AddressRange[] = []): { exec: AddressRange[]; watch: AddressRange[] } {
46
+ const exec: AddressRange[] = [];
47
+ const watch: AddressRange[] = [];
48
+ const add = (list: AddressRange[], address: number) => {
54
49
  const last = list[list.length - 1];
55
50
  if (last !== undefined && last.end === address - 1) last.end = address;
56
51
  else list.push({ start: address, end: address });
@@ -66,7 +61,7 @@ export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>,
66
61
  }
67
62
 
68
63
  /** True when the address lies in one of the ranges. */
69
- export function inRanges(ranges: readonly Range[], address: number): boolean {
64
+ export function inRanges(ranges: readonly AddressRange[], address: number): boolean {
70
65
  return ranges.some((range) => contains(range, address));
71
66
  }
72
67
 
@@ -86,8 +81,8 @@ export function callReturn(stack: Uint8Array, sp: number, byteAt: (address: numb
86
81
  * The code that ran in a block: its executed opcode bytes with the two bytes
87
82
  * after each, which can be operands, joined and cut at the block's end.
88
83
  */
89
- export function executedIn(map: Uint8Array, block: Range): Range[] {
90
- const ranges: Range[] = [];
84
+ export function executedIn(map: Uint8Array, block: AddressRange): AddressRange[] {
85
+ const ranges: AddressRange[] = [];
91
86
  for (let address = block.start; address <= block.end; address++) {
92
87
  if ((map[address]! & COVERAGE.ramExecute) === 0) continue;
93
88
  const end = Math.min(block.end, address + 2);
@@ -109,16 +104,16 @@ export function executedIn(map: Uint8Array, block: Range): Range[] {
109
104
  * program with a leftover. `writtenBy` is the last stage that wrote into the
110
105
  * range.
111
106
  */
112
- export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: readonly Range[] }>, entry: number): Array<Range & { writtenBy: number }> {
107
+ export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: readonly AddressRange[] }>, entry: number): Array<AddressRange & { writtenBy: number }> {
113
108
  const last = stages.length - 1;
114
109
  const replaced = (from: number, address: number) => stages.slice(from).some((stage) => inRanges(stage.wrote, address));
115
- const leftover = (block: Range, writer: number) =>
110
+ const leftover = (block: AddressRange, writer: number) =>
116
111
  !contains(block, entry) && stages.some((stage, index) => index > writer && index < last && contains(block, stage.entry) && !replaced(index, stage.entry));
117
112
  const blocks = stages
118
113
  .flatMap((stage, index) => stage.wrote.map((range) => ({ start: range.start, end: range.end, writtenBy: index })))
119
114
  .filter((block) => !leftover(block, block.writtenBy))
120
115
  .sort((a, b) => a.start - b.start);
121
- const segments: Array<Range & { writtenBy: number }> = [];
116
+ const segments: Array<AddressRange & { writtenBy: number }> = [];
122
117
  for (const block of blocks) {
123
118
  const previous = segments[segments.length - 1];
124
119
  if (previous !== undefined && block.start <= previous.end + 1) {
@@ -130,7 +125,7 @@ export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: re
130
125
  }
131
126
 
132
127
  /** The segments without the bytes of `holes`: a segment that a hole cuts gives its parts, each with the fields of the segment. */
133
- export function withoutRanges<T extends Range>(segments: readonly T[], holes: readonly Range[]): T[] {
128
+ export function withoutRanges<T extends AddressRange>(segments: readonly T[], holes: readonly AddressRange[]): T[] {
134
129
  let pieces = [...segments];
135
130
  for (const hole of holes) {
136
131
  pieces = pieces.flatMap((piece) => {