@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
@@ -3,19 +3,22 @@
3
3
  // release is a PRG or an unpacker image.json. With --run, each release
4
4
  // runs in its own emulator, and each listed difference gets its first access
5
5
  // and whether the run changed it. The separate command gives evidence from
6
- // the program in the knowledge. The routine and match commands keep
6
+ // the program in the knowledge, and the leftovers command lists what its
7
+ // unused bytes hold: text, BASIC lines, fills and copies. The routine and match commands keep
7
8
  // routines that several programs share in a library folder and find them in the
8
9
  // program (D71).
9
10
 
10
- import { basename, posix, relative } from "node:path";
11
- import { parseArgs } from "node:util";
11
+ import { basename, relative } from "node:path";
12
12
 
13
- import { formatC64Address, parseC64Address } from "#src/c64.ts";
14
- import { checkActionSteps, type ActionStep } from "#src/host-client/steps.ts";
15
- import { MAX_EXECUTION_COUNT, WireFailure } from "#src/host-client/tools.ts";
16
- import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
13
+ import { type AddressRange, c64AddressOf, formatC64Address } from "#src/c64.ts";
14
+ import { Refusal } from "#src/errors.ts";
15
+ import { parseEntryState, type EntryState } from "#src/host-client/entry-state.ts";
16
+ import { readImage } from "#src/host-client/image.ts";
17
+ import { readActionSteps, type ActionStep } from "#src/host-client/steps.ts";
18
+ import { MAX_EXECUTION_COUNT } from "#src/host-client/tools.ts";
19
+ import { writeReported } from "#src/host-client/transfer.ts";
17
20
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
18
- import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
21
+ import { openForRead } from "#src/knowledge/database.ts";
19
22
  import { programCoverage, requireProgram } from "#src/knowledge/program.ts";
20
23
  import {
21
24
  commentsAt,
@@ -35,59 +38,17 @@ import {
35
38
  symbolNamed,
36
39
  symbolSuppressed,
37
40
  } from "#src/knowledge/read.ts";
38
- import { CODE_REGION_TYPES, type Origin } from "#src/knowledge/schema.ts";
41
+ import { CODE_REGION_TYPES, SEMANTIC_ORIGINS } from "#src/knowledge/schema.ts";
39
42
  import { platformNames } from "#src/platform.ts";
40
43
  import { projectRoot } from "#src/project.ts";
41
- import { runScript, UsageError } from "#src/script.ts";
42
- import { compareReleases, shiftSuggestion, suggestShifts, type Range, type Release } from "./lib/compare.ts";
43
- import { findRoutine, libraryFolder, LibraryError, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
44
- import { watchRelease, type ImageState, type Start } from "./lib/runtime.ts";
44
+ import { knowledgeNames } from "#src/knowledge/names.ts";
45
+ import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec } from "#src/script.ts";
46
+ import { compareReleases, shiftSuggestion, suggestShifts, type Release } from "./lib/compare.ts";
47
+ import { findRoutine, libraryFolder, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
48
+ import { watchRelease, type Start } from "./lib/runtime.ts";
49
+ import { findLeftovers } from "./lib/leftovers.ts";
45
50
  import { separate } from "./lib/separate.ts";
46
51
 
47
- const USAGE = `provenance.ts <release> <release> [<release> ...] [options]
48
- provenance.ts separate [--out <file>] [--max-listed <n>]
49
- provenance.ts routine --library <folder> (--module <address> | --start <address> --end <address>)
50
- [--name <name>] [--title <text>] [--replace]
51
- provenance.ts match --library <folder> [--out <file>] [--max-listed <n>]
52
-
53
- --shift <n>:<bytes> move release n (1 = the first) by a number of bytes, for example 2:-256
54
- --from <address> compare from this address
55
- --to <address> compare up to this address
56
- --run <frames> run each release in its own emulator for this many frames, and
57
- give each listed difference its first access and whether it changed
58
- --steps <file> with --run: action steps (JSON) to run before the frames
59
-
60
- separate gives evidence from the program in the knowledge: the unpack stage
61
- that wrote each range, its access in the coverage of the knowledge, whether call
62
- and jump references in knowledge reach each code region from the entry, and what
63
- the program reads or executes outside its segments. --out writes all ranges to a file;
64
- the result lists at most --max-listed of each list (default 50) and its count.
65
-
66
- routine saves a routine of the program in the knowledge to a library: a plain folder of routine
67
- files, beside the projects (--library, relative to the working directory, as
68
- --project). It takes a module of the knowledge, or --start and --end. Its
69
- code regions give the instructions; the user and LLM names, regions, comments,
70
- references and operand expressions go into the file at offsets. --name defaults
71
- to the user or LLM name at the start, --title to the module title.
72
-
73
- match finds each routine of the library in the program of the knowledge: the opcodes and other
74
- fixed bytes equal, and the addresses into the routine moved by one shift.
75
- Operands that address something outside the routine may differ; the result
76
- lists them. --out writes the names of the routines at the found addresses as a
77
- batch for knowledge.ts apply, and the result lists what it left out.
78
-
79
- A release to compare is a PRG, placed at its load address, or an image.json of
80
- the c64-unpacker skill, compared inside its segments. Paths are relative to the
81
- project folder. The result is one JSON object.`;
82
-
83
- function address(text: string, option: string): number {
84
- try {
85
- return parseC64Address(text);
86
- } catch {
87
- throw new UsageError(`${option} must be $ followed by four hex digits, not ${text}`);
88
- }
89
- }
90
-
91
52
  const hexBytes = (bytes: number[]) => Buffer.from(bytes).toString("hex").replace(/(..)(?!$)/g, "$1 ");
92
53
 
93
54
  interface LoadedRelease {
@@ -97,168 +58,66 @@ interface LoadedRelease {
97
58
  /** 64 KiB with the release bytes at their own addresses. */
98
59
  memory: Uint8Array;
99
60
  entry?: number;
100
- stages?: Array<{ entry: number; wrote: Range[] }>;
101
- }
102
-
103
- /** Reads entry_state.json of the c64-unpacker skill for a start by writes. */
104
- function entryState(path: string): ImageState | string {
105
- let value: Record<string, any>;
106
- try {
107
- value = JSON.parse(readProjectFile(path).bytes.toString("utf8")) as Record<string, any>;
108
- } catch (error) {
109
- if (error instanceof WireFailure && error.code === "not-found") return `It has no ${path}; capture it with c64-unpacker.`;
110
- if (error instanceof WireFailure) throw error;
111
- return `${path} is not JSON: ${(error as Error).message}`;
112
- }
113
- const registers = value.registers ?? {};
114
- const byte = (item: unknown) => Number.isInteger(item) && (item as number) >= 0 && (item as number) <= 0xff;
115
- const pages = ["$0000", "$0100", "$0200", "$0300"].map((page) => value.pages?.[page]);
116
- const ok =
117
- typeof registers.pc === "string" &&
118
- /^\$[0-9a-f]{4}$/i.test(registers.pc) &&
119
- ["a", "x", "y", "sp"].every((name) => byte(registers[name])) &&
120
- typeof registers.flags === "object" &&
121
- byte(value.cpuPort?.$00) &&
122
- byte(value.cpuPort?.$01) &&
123
- pages.every((page) => typeof page === "string" && /^[0-9a-f]{512}$/i.test(page));
124
- if (!ok) return `${path} is not an entry_state.json of the c64-unpacker skill.`;
125
- const flag = (name: string) => registers.flags[name] === true;
126
- return {
127
- pc: Number.parseInt(registers.pc.slice(1), 16),
128
- a: registers.a,
129
- x: registers.x,
130
- y: registers.y,
131
- sp: registers.sp,
132
- flags: { n: flag("n"), v: flag("v"), d: flag("d"), i: flag("i"), z: flag("z"), c: flag("c") },
133
- port: [value.cpuPort.$00, value.cpuPort.$01],
134
- pages: Buffer.from(pages.join(""), "hex"),
135
- };
61
+ stages?: Array<{ entry: number; wrote: AddressRange[] }>;
136
62
  }
137
63
 
138
- function readRelease(path: string, shift: number): LoadedRelease {
139
- const { bytes } = readProjectFile(path);
140
- const moved = (range: Range) => ({ start: range.start + shift, end: range.end + shift });
141
- const inside = (range: Range) => range.start >= 0 && range.end <= 0xffff;
142
- if (!path.endsWith(".json")) {
143
- if (bytes.length < 3) throw new UsageError(`${path} is too short for a PRG`);
144
- const load = bytes.readUInt16LE(0);
145
- const segment = moved({ start: load, end: load + bytes.length - 3 });
146
- if (!inside(segment)) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
147
- const memory = new Uint8Array(0x10000);
148
- memory.set(bytes.subarray(2), load);
149
- return { release: { name: path, segments: [segment], byteAt: (at) => bytes[2 + at - segment.start]! }, shift, start: { kind: "program", path }, memory };
150
- }
151
- const fail = (what: string): never => {
152
- throw new UsageError(`${path} is not an image.json of the c64-unpacker skill: ${what}`);
153
- };
154
- let manifest: Record<string, unknown>;
155
- try {
156
- manifest = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
157
- } catch (error) {
158
- return fail((error as Error).message);
64
+ /**
65
+ * A release to compare: "knowledge" (the program of the knowledge, with its
66
+ * entry state and stages), a PRG at its load address, or an image.json of the
67
+ * c64-unpacker skill with the entry_state.json beside it.
68
+ */
69
+ function readRelease(name: string, shift: number): LoadedRelease {
70
+ const moved = (range: AddressRange) => ({ start: range.start + shift, end: range.end + shift });
71
+ const inside = (range: AddressRange) => range.start >= 0 && range.end <= 0xffff;
72
+ let image: { segments: AddressRange[]; byteAt: (address: number) => number; memory?: Uint8Array; entry?: number; stages?: Array<Record<string, unknown>>; state?: EntryState };
73
+ let noState: string;
74
+ if (name === "knowledge") {
75
+ const db = openForRead();
76
+ try {
77
+ const program = requireProgram(db);
78
+ image = { ...program, ...(program.state === undefined ? {} : { state: parseEntryState(program.state, "the entry state of the knowledge") }) } as typeof image;
79
+ } finally {
80
+ db?.close();
81
+ }
82
+ noState = "The knowledge has no entry state: put a c64-unpacker capture in with knowledge.ts image <image.json>.";
83
+ } else {
84
+ image = readImage(name);
85
+ noState = `It has no entry_state.json beside it; capture it with c64-unpacker.`;
159
86
  }
160
- const parse = (value: unknown) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : fail("a segment needs start and end as $ addresses"));
161
- if (typeof manifest.image !== "string" || !Array.isArray(manifest.segments) || manifest.segments.length === 0) fail("it needs image and segments");
162
- const own = (manifest.segments as Array<Record<string, unknown>>).map((segment) => ({ start: parse(segment.start), end: parse(segment.end) })).sort((a, b) => a.start - b.start);
163
- const directory = posix.dirname(path);
164
- const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
165
- if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
166
- const segments = own.map(moved);
167
- if (!segments.every(inside)) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
168
- const state = entryState(posix.join(directory, "entry_state.json"));
169
- const stages = Array.isArray(manifest.stages)
170
- ? (manifest.stages as Array<Record<string, unknown>>).map((stage) => ({
171
- entry: parse(stage.entry),
172
- wrote: (Array.isArray(stage.wrote) ? (stage.wrote as Array<Record<string, unknown>>) : []).map((range) => ({ start: parse(range.start), end: parse(range.end) })),
173
- }))
174
- : undefined;
87
+ const segments = image.segments.map(moved);
88
+ if (!segments.every(inside)) throw new UsageError(`the shift moves ${name} outside $0000-$ffff`);
89
+ const memory = image.memory ?? new Uint8Array(0x10000);
90
+ if (image.memory === undefined) for (const segment of image.segments) for (let at = segment.start; at <= segment.end; at++) memory[at] = image.byteAt(at);
91
+ const prg = name !== "knowledge" && !name.endsWith(".json");
92
+ const stages = stagesOf(image.stages, name);
175
93
  return {
176
- release: { name: path, segments, byteAt: (at) => memory[at - shift]! },
94
+ release: { name, segments, byteAt: (at) => memory[at - shift]! },
177
95
  shift,
178
- start: typeof state === "string" ? { kind: "none", reason: state } : { kind: "image", segments: own, byteAt: (at) => memory[at]!, state },
96
+ start: prg ? { kind: "program", path: name } : image.state === undefined ? { kind: "none", reason: noState } : { kind: "image", segments: image.segments, byteAt: (at) => memory[at]!, state: image.state },
179
97
  memory,
180
- ...(manifest.entry === undefined ? {} : { entry: parse(manifest.entry) }),
98
+ ...(image.entry === undefined ? {} : { entry: image.entry }),
181
99
  ...(stages === undefined ? {} : { stages }),
182
100
  };
183
101
  }
184
102
 
185
- async function run(argv: string[]): Promise<unknown> {
186
- const { values, positionals } = parseArgs({
187
- args: argv,
188
- strict: true,
189
- allowPositionals: true,
190
- options: {
191
- shift: { type: "string", multiple: true },
192
- from: { type: "string" },
193
- to: { type: "string" },
194
- run: { type: "string" },
195
- steps: { type: "string" },
196
- out: { type: "string" },
197
- "max-listed": { type: "string" },
198
- library: { type: "string" },
199
- module: { type: "string" },
200
- start: { type: "string" },
201
- end: { type: "string" },
202
- name: { type: "string" },
203
- title: { type: "string" },
204
- replace: { type: "boolean" },
205
- help: { type: "boolean" },
206
- },
207
- });
208
- if (values.help) throw new UsageError("");
209
- /** Refuses the options that `command` does not take. */
210
- const only = (command: string, allowed: readonly string[]) => {
211
- const used = Object.keys(values).filter((option) => option !== "help" && !allowed.includes(option));
212
- if (used.length > 0) throw new UsageError(`${command} does not take --${used.join(", --")}`);
213
- };
214
- if (positionals[0] === "separate") {
215
- only("separate", ["out", "max-listed"]);
216
- if (positionals.length !== 1) throw new UsageError("separate takes no release: it reads the program from the knowledge");
217
- return separateCommand(values.out, values["max-listed"]);
218
- }
219
- if (positionals[0] === "routine") {
220
- only("routine", ["library", "module", "start", "end", "name", "title", "replace"]);
221
- if (positionals.length !== 1) throw new UsageError("routine takes no image: it reads the program from the knowledge");
222
- return routineCommand(values);
223
- }
224
- if (positionals[0] === "match") {
225
- only("match", ["library", "out", "max-listed"]);
226
- if (positionals.length !== 1) throw new UsageError("match takes no image: it reads the program from the knowledge");
227
- return matchCommand(values.library, values.out, values["max-listed"]);
228
- }
229
- only("a comparison", ["shift", "from", "to", "run", "steps"]);
230
- if (positionals.length < 2) throw new UsageError("give two or more releases");
103
+ /** compare: the releases byte by byte, and with --frames the first access of each difference in each release. */
104
+ async function compare(call: Call): Promise<unknown> {
105
+ const positionals = call.subjects;
106
+ if (positionals.length < 2) throw new UsageError("compare needs two or more releases");
231
107
  const shifts = new Map<number, number>();
232
- for (const text of values.shift ?? []) {
108
+ for (const text of call.strings("shift")) {
233
109
  const match = /^(\d+):([+-]?\d+)$/.exec(text);
234
110
  if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
235
111
  shifts.set(Number(match[1]), Number(match[2]));
236
112
  }
237
- if (values.steps !== undefined && values.run === undefined) throw new UsageError("--steps works only with --run");
238
- let frames: number | undefined;
239
- if (values.run !== undefined) {
240
- if (!/^\d{1,6}$/.test(values.run) || Number(values.run) < 1 || Number(values.run) > MAX_EXECUTION_COUNT) throw new UsageError(`--run needs a frame count from 1 to ${MAX_EXECUTION_COUNT}, not ${values.run}`);
241
- frames = Number(values.run);
242
- }
243
- let steps: ActionStep[] = [];
244
- if (values.steps !== undefined) {
245
- let parsed: unknown;
246
- try {
247
- parsed = JSON.parse(readProjectFile(values.steps).bytes.toString("utf8"));
248
- } catch (error) {
249
- if (error instanceof WireFailure) throw error;
250
- throw new UsageError(`the steps file ${values.steps} is not JSON: ${(error as Error).message}`);
251
- }
252
- steps = checkActionSteps(parsed, (message) => {
253
- throw new UsageError(`steps file ${values.steps}: ${message}`);
254
- });
255
- }
113
+ const frames = call.count("frames");
114
+ const stepsFile = call.string("steps");
115
+ if (stepsFile !== undefined && frames === undefined) throw new UsageError("--steps works only with --frames");
116
+ const steps: ActionStep[] = stepsFile === undefined ? [] : readActionSteps(stepsFile, "--steps");
256
117
  const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
257
118
  const releases = loaded.map((item) => item.release);
258
- const comparison = compareReleases(releases, {
259
- ...(values.from === undefined ? {} : { from: address(values.from, "--from") }),
260
- ...(values.to === undefined ? {} : { to: address(values.to, "--to") }),
261
- });
119
+ const range = call.range("range");
120
+ const comparison = compareReleases(releases, range === undefined ? {} : { from: range.start, to: range.end });
262
121
  const compared = comparison.identicalBytes + comparison.differingBytes;
263
122
  const identicalShare = compared === 0 ? 0 : comparison.identicalBytes / compared;
264
123
  // A poor match at the load addresses can mean that the releases are placed differently.
@@ -278,7 +137,7 @@ async function run(argv: string[]): Promise<unknown> {
278
137
  const notSuggested = suggestions.filter((suggestion) => "reason" in suggestion);
279
138
 
280
139
  // The runtime evidence: one emulator for each release, the differences at the release's own addresses.
281
- const runtime = new Map<string, Array<Range & { first: string; changed: boolean }>>();
140
+ const runtime = new Map<string, Array<AddressRange & { first: string; changed: boolean }>>();
282
141
  const runs: Record<string, unknown> = {};
283
142
  if (frames !== undefined && comparison.differences.length > 0) {
284
143
  for (const item of loaded) {
@@ -334,12 +193,13 @@ async function run(argv: string[]): Promise<unknown> {
334
193
  };
335
194
  }
336
195
 
337
- /** The stages of the capture in the knowledge as ranges; a stage that is not a capture stage is refused. */
338
- function stagesOf(stages: Array<Record<string, unknown>> | undefined): Array<{ entry: number; wrote: Range[] }> | undefined {
196
+ /** The stages of a capture as ranges; a stage that is not a capture stage is refused. `where` names the capture. */
197
+ function stagesOf(stages: Array<Record<string, unknown>> | undefined, where = "the program in the knowledge"): Array<{ entry: number; wrote: AddressRange[] }> | undefined {
339
198
  if (stages === undefined) return undefined;
340
199
  const parse = (value: unknown) => {
341
- if (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value)) return Number.parseInt(value.slice(1), 16);
342
- throw new UsageError("the stages of the program in the knowledge are not the stages of a c64-unpacker capture; put the capture in again with knowledge.ts image");
200
+ const at = typeof value === "string" ? c64AddressOf(value) : undefined;
201
+ if (at !== undefined) return at;
202
+ throw new Refusal("invalid-input", `the stages of ${where} are not the stages of a c64-unpacker capture; capture it again with c64-unpacker`);
343
203
  };
344
204
  return stages.map((stage) => ({
345
205
  entry: parse(stage.entry),
@@ -347,10 +207,8 @@ function stagesOf(stages: Array<Record<string, unknown>> | undefined): Array<{ e
347
207
  }));
348
208
  }
349
209
 
350
- /** separate: evidence from the program in the knowledge, with all ranges in --out and the first ones in the result. */
351
- function separateCommand(out: string | undefined, maxListed: string | undefined): unknown {
352
- const max = maxListed ?? "50";
353
- if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
210
+ /** The program in the knowledge with its stages and coverage, and the separation of its bytes. */
211
+ function programEvidence() {
354
212
  const db = openForRead();
355
213
  let program: ReturnType<typeof requireProgram>;
356
214
  let coverage: Uint8Array | undefined;
@@ -365,7 +223,6 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
365
223
  db?.close();
366
224
  }
367
225
  const stages = stagesOf(program.stages);
368
- const loaded = { entry: program.entry, stages };
369
226
  const found = separate({
370
227
  segments: program.segments,
371
228
  memory: program.memory,
@@ -375,68 +232,121 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
375
232
  codeRegions: regions,
376
233
  references,
377
234
  });
378
- const show = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
235
+ return { program, stages, coverage, found };
236
+ }
237
+
238
+ const NO_STAGES = "The program in the knowledge has no stage log. Only a capture of c64-unpacker gives one. A capture of loads holds one part of a program and has none.";
239
+
240
+ /** separate: evidence from the program in the knowledge, with all ranges in --out and the first ones in the result. */
241
+ function separateCommand(call: Call): unknown {
242
+ const out = call.string("out");
243
+ const max = call.count("max-listed")!;
244
+ const { program, stages, coverage, found } = programEvidence();
245
+ const loaded = { entry: program.entry, stages };
246
+ const show = (range: AddressRange) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
379
247
  const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
380
248
  const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
381
249
  const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
382
- const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
383
- if (out !== undefined) writeProjectFile(out, Buffer.from(`${JSON.stringify({ ranges, codeRegions, outside }, null, 1)}\n`), { what: "--out" });
250
+ const written = out === undefined ? undefined : writeReported(out, `${JSON.stringify({ ranges, codeRegions, outside }, null, 1)}\n`, { what: "--out" });
251
+ const rest = out === undefined ? undefined : out;
252
+ const list = <T>(items: T[]) => listed(items, max, undefined, rest);
384
253
  const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
385
254
  const lastStage = loaded.stages === undefined ? undefined : loaded.stages.length - 1;
386
255
  return {
387
256
  ...(loaded.entry === undefined ? {} : { entry: formatC64Address(loaded.entry) }),
388
257
  ...(lastStage === undefined
389
- ? { noStages: "The program in the knowledge has no stage log. Only a capture of c64-unpacker gives one. A capture of loads holds one part of a program and has none." }
258
+ ? { noStages: NO_STAGES }
390
259
  : {
391
260
  stages: lastStage + 1,
392
261
  bytesByStage: Object.fromEntries([...[...Array(lastStage + 1).keys()].map((stage): [string, number] => [String(stage), bytes((range) => range.stage === stage)]), ["none", bytes((range) => range.stage === undefined)]]),
393
262
  }),
394
263
  ...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
395
- ranges: listed(ranges),
264
+ ranges: list(ranges),
396
265
  // total counts every code region; unreachable is a list like the others.
397
266
  codeRegions: {
398
267
  total: codeRegions.length,
399
- ...(loaded.entry === undefined ? {} : { unreachable: listed(codeRegions.filter((region) => region.reachable === false)) }),
268
+ ...(loaded.entry === undefined ? {} : { unreachable: list(codeRegions.filter((region) => region.reachable === false)) }),
400
269
  },
401
- ...(coverage === undefined ? {} : { outside: { read: listed(outside.read), executed: listed(outside.executed), generated: listed(outside.generated) } }),
402
- ...(out === undefined ? {} : { out }),
270
+ ...(coverage === undefined ? {} : { outside: { read: list(outside.read), executed: list(outside.executed), generated: list(outside.generated) } }),
271
+ ...(written === undefined ? {} : written),
403
272
  };
404
273
  }
405
274
 
406
- const SEMANTIC: readonly Origin[] = ["user", "llm"];
275
+ /** The longest text that the result shows; --out has all of it. */
276
+ const SHOWN_TEXT = 120;
277
+
278
+ /** leftovers: what the program in the knowledge holds besides its working code, with all findings in --out and the first ones in the result. */
279
+ function leftoversCommand(call: Call): unknown {
280
+ const out = call.string("out");
281
+ const max = call.count("max-listed")!;
282
+ const min = call.count("min-text")!;
283
+ const { program, stages, coverage, found } = programEvidence();
284
+ const leftovers = findLeftovers({
285
+ segments: program.segments,
286
+ memory: program.memory,
287
+ separation: found,
288
+ ...(program.segments.some((segment) => segment.start === 0x0801) ? { basicStart: 0x0801 } : {}),
289
+ minText: min,
290
+ });
291
+ const show = <T extends AddressRange & { stages?: number[]; access?: string[] }>({ start, end, stages: wrote, access, ...rest }: T) => ({
292
+ start: formatC64Address(start),
293
+ end: formatC64Address(end),
294
+ ...rest,
295
+ ...(wrote === undefined ? {} : { stages: wrote }),
296
+ ...(access === undefined ? {} : { access }),
297
+ });
298
+ const all = {
299
+ text: leftovers.text.map(show),
300
+ basic: leftovers.basic.map(({ linkedAt, ...rest }) => ({ ...show(rest), linkedAt: formatC64Address(linkedAt) })),
301
+ fill: leftovers.fill.map(({ pattern, ...rest }) => ({ ...show(rest), pattern: hexBytes(pattern) })),
302
+ residue: leftovers.residue.map((range) => ({ ...show(range), bytes: hexBytes([...program.memory.subarray(range.start, Math.min(range.end, range.start + 15) + 1)]) + (range.end - range.start >= 16 ? " ..." : "") })),
303
+ copies: leftovers.copies.map(({ copyOf, ...rest }) => ({ ...show(rest), copyOf: formatC64Address(copyOf) })),
304
+ deadCode: leftovers.deadCode.map(show),
305
+ };
306
+ const written = out === undefined ? undefined : writeReported(out, `${JSON.stringify(all, null, 1)}\n`, { what: "--out" });
307
+ const list = <T>(items: T[]) => listed(items, max, undefined, out ?? "a larger --max-listed, or --out <file>");
308
+ return {
309
+ ...(program.entry === undefined ? { noEntry: "The program in the knowledge has no entry, so deadCode is empty." } : { entry: formatC64Address(program.entry) }),
310
+ ...(stages === undefined ? { noStages: NO_STAGES } : { stages: stages.length }),
311
+ ...(coverage === undefined ? { noCoverage: "The knowledge has no coverage, so residue is empty and no finding has access. Add coverage with coverage of the c64-knowledge skill." } : {}),
312
+ text: list(all.text.map((item) => (item.text.length > SHOWN_TEXT ? { ...item, text: `${item.text.slice(0, SHOWN_TEXT)} ...`, length: item.text.length } : item))),
313
+ basic: list(all.basic),
314
+ fill: list(all.fill),
315
+ residue: list(all.residue),
316
+ copies: list(all.copies),
317
+ deadCode: list(all.deadCode),
318
+ ...(written === undefined ? {} : written),
319
+ };
320
+ }
407
321
 
408
322
  const platform = () => new Map(platformNames().map((entry) => [entry.name, entry.address]));
409
323
 
410
324
  /** routine: saves a module or a range of the program in the knowledge, with its knowledge, as a routine of the library (D71). */
411
- function routineCommand(values: { library?: string; module?: string; start?: string; end?: string; name?: string; title?: string; replace?: boolean }): unknown {
412
- if (values.library === undefined) throw new UsageError("routine needs --library <folder>");
413
- if (values.module !== undefined && (values.start !== undefined || values.end !== undefined)) throw new UsageError("routine takes --module, or --start and --end, not both");
414
- if (values.module === undefined && values.start === undefined && values.end === undefined) throw new UsageError("routine needs --module <address>, or --start and --end");
415
- if (values.module === undefined && (values.start === undefined || values.end === undefined)) throw new UsageError("routine needs both --start and --end");
325
+ function routineCommand(call: Call): unknown {
326
+ const given = call.subject(0);
327
+ const library = call.string("library")!;
416
328
  const db = openForRead();
417
329
  try {
418
330
  const image = requireProgram(db);
419
331
  const segmentOf = (at: number) => image.segments.find((segment) => at >= segment.start && at <= segment.end);
420
332
  let start: number;
421
333
  let end: number;
422
- let title = values.title;
423
- if (values.module !== undefined) {
424
- start = address(values.module, "--module");
334
+ let title = call.string("title");
335
+ if (!given.includes("-")) {
336
+ start = call.parseAddress(given, "<module>");
425
337
  const modules = listModules(db);
426
338
  const module = modules.find((item) => item.start === start);
427
- if (module === undefined) throw new LibraryError("not-found", `No module starts at ${formatC64Address(start)}. List the modules with knowledge.ts modules, or give --start and --end.`);
339
+ if (module === undefined) throw new Refusal("not-found", `No module starts at ${formatC64Address(start)}. List the modules with knowledge.ts modules, or give a range <start>-<end>.`);
428
340
  const segment = segmentOf(start);
429
341
  if (segment === undefined) throw new UsageError(`The module at ${formatC64Address(start)} is outside the segments of the program.`);
430
342
  end = (modules.find((item) => item.start > start && item.start <= segment.end)?.start ?? segment.end + 1) - 1;
431
343
  title ??= module.title;
432
344
  } else {
433
- start = address(values.start!, "--start");
434
- end = address(values.end!, "--end");
435
- if (end < start) throw new UsageError("--end is before --start");
345
+ ({ start, end } = call.parseRange(given, "<range>"));
436
346
  if (segmentOf(start) === undefined || segmentOf(start) !== segmentOf(end)) throw new UsageError(`${formatC64Address(start)}-${formatC64Address(end)} is not inside one segment of the program.`);
437
347
  }
438
348
  const range = { start, end };
439
- const name = values.name ?? listSymbols(db, { start, end: start }, undefined, SEMANTIC)[0]?.name;
349
+ const name = call.string("name") ?? listSymbols(db, { start, end: start }, undefined, SEMANTIC_ORIGINS)[0]?.name;
440
350
  if (name === undefined) throw new UsageError(`The knowledge has no user or LLM name at ${formatC64Address(start)}: give --name.`);
441
351
  const { routine, leftOut } = routineFrom({
442
352
  name,
@@ -447,17 +357,17 @@ function routineCommand(values: { library?: string; module?: string; start?: str
447
357
  end,
448
358
  byteAt: image.byteAt,
449
359
  analyzed: listRegions(db, range),
450
- symbols: listSymbols(db, range, undefined, SEMANTIC),
451
- regions: listRegions(db, range, undefined, SEMANTIC),
452
- comments: listComments(db, range, SEMANTIC),
453
- references: listReferences(db, range, undefined, SEMANTIC),
454
- operands: listOperands(db, range, SEMANTIC),
360
+ symbols: listSymbols(db, range, undefined, SEMANTIC_ORIGINS),
361
+ regions: listRegions(db, range, undefined, SEMANTIC_ORIGINS),
362
+ comments: listComments(db, range, SEMANTIC_ORIGINS),
363
+ references: listReferences(db, range, undefined, SEMANTIC_ORIGINS),
364
+ operands: listOperands(db, range, SEMANTIC_ORIGINS),
455
365
  platform: platform(),
456
366
  });
457
- const file = saveRoutine(libraryFolder(values.library, { create: true }), routine, values.replace === true);
367
+ const file = saveRoutine(libraryFolder(library, { create: true, cwd: projectRoot() }), routine, call.flag("replace"));
458
368
  return {
459
369
  routine: routine.name,
460
- file: relative(process.cwd(), file),
370
+ file: relative(projectRoot(), file).split("\\").join("/"),
461
371
  start: formatC64Address(start),
462
372
  end: formatC64Address(end),
463
373
  length: routine.bytes.length,
@@ -473,13 +383,13 @@ function routineCommand(values: { library?: string; module?: string; start?: str
473
383
  }
474
384
 
475
385
  /** match: finds the routines of the library in the program of the knowledge, and with --out writes their names there as an apply batch (D71). */
476
- function matchCommand(library: string | undefined, out: string | undefined, maxListed: string | undefined): unknown {
477
- if (library === undefined) throw new UsageError("match needs --library <folder>");
478
- const max = maxListed ?? "50";
479
- if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
480
- const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
481
- const routines = readLibrary(libraryFolder(library, { create: false }));
482
- if (routines.length === 0) throw new LibraryError("not-found", `The library ${library} has no routine files. Save a routine with routine first.`);
386
+ function matchCommand(call: Call): unknown {
387
+ const library = call.string("library")!;
388
+ const out = call.string("out");
389
+ const max = call.count("max-listed")!;
390
+ const list = <T>(items: T[]) => listed(items, max);
391
+ const routines = readLibrary(libraryFolder(library, { create: false, cwd: projectRoot() }));
392
+ if (routines.length === 0) throw new Refusal("not-found", `The library ${library} has no routine files. Save a routine with routine first.`);
483
393
  const programDb = openForRead();
484
394
  let image: ReturnType<typeof requireProgram>;
485
395
  try {
@@ -500,7 +410,7 @@ function matchCommand(library: string | undefined, out: string | undefined, maxL
500
410
  })),
501
411
  )
502
412
  .sort((a, b) => a.start.localeCompare(b.start));
503
- const result: Record<string, unknown> = { library, routines: routines.length, matches: listed(matches) };
413
+ const result: Record<string, unknown> = { library, routines: routines.length, matches: list(matches) };
504
414
  if (out === undefined) return result;
505
415
  const db = openForRead();
506
416
  let suggestions: ReturnType<typeof suggest>;
@@ -525,12 +435,72 @@ function matchCommand(library: string | undefined, out: string | undefined, maxL
525
435
  db?.close();
526
436
  }
527
437
  const { batch, known, leftOut } = suggestions;
528
- writeProjectFile(out, Buffer.from(batch.length === 0 ? "[]\n" : `[\n${batch.map((write) => ` ${JSON.stringify(write)}`).join(",\n")}\n]\n`), { what: "--out" });
438
+ const written = writeReported(out, batch.length === 0 ? "[]\n" : `[\n${batch.map((write) => ` ${JSON.stringify(write)}`).join(",\n")}\n]\n`, { what: "--out" });
529
439
  return {
530
440
  ...result,
531
- batch: { out, writes: batch.length, known, leftOut: listed(leftOut) },
441
+ batch: { ...written, writes: batch.length, known, leftOut: list(leftOut) },
532
442
  ...(batch.length === 0 ? {} : { apply: `knowledge.ts apply ${out} --reason "names of library routines"` }),
533
443
  };
534
444
  }
535
445
 
536
- await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError, LibraryError] });
446
+ const LIBRARY = { kind: "text", value: "<folder>", required: true, help: "the library: a plain folder of routine files beside the projects, relative to the project folder, for example ../library" } as const;
447
+
448
+ const COMMANDS: CommandSpec[] = [
449
+ {
450
+ name: "compare",
451
+ summary: "Compares releases byte by byte, in their common ranges, and suggests shifts when they are placed differently.",
452
+ subjects: [{ name: "<release>", help: "knowledge (the program in the knowledge), a PRG at its load address, or an image.json of the c64-unpacker skill with the entry_state.json beside it. Two or more", list: true }],
453
+ options: {
454
+ shift: { kind: "text", value: "<n>:<bytes>", multiple: true, help: "move release n (1 is the first) by a number of bytes, for example 2:-256" },
455
+ range: { ...OPTIONS.range, help: "compare only the addresses from start to end" },
456
+ frames: { ...OPTIONS.frames, max: MAX_EXECUTION_COUNT, help: "run each release in its own emulator for n frames, and give each listed difference its first access and whether it changed" },
457
+ steps: { ...OPTIONS.steps, help: "with --frames: action steps (JSON) to run before the frames" },
458
+ },
459
+ run: compare,
460
+ },
461
+ {
462
+ name: "separate",
463
+ summary: "Evidence from the program in the knowledge: the unpack stage that wrote each range, its access in the coverage, whether call and jump references reach each code region from the entry, and what the program reads or executes outside its segments.",
464
+ options: { out: { ...OPTIONS.out, value: "<file>", help: "write all ranges to this file" }, "max-listed": OPTIONS["max-listed"] },
465
+ run: separateCommand,
466
+ },
467
+ {
468
+ name: "leftovers",
469
+ summary: "What the program in the knowledge holds besides its working code: texts in PETSCII, screen codes, ASCII or with bit 7 set (Apple II), assembler source, BASIC lines away from the BASIC start, fills, untouched bytes outside code, copies of earlier bytes, and code regions that nothing reaches and no run executed.",
470
+ options: {
471
+ out: { ...OPTIONS.out, value: "<file>", help: "write all findings to this file" },
472
+ "min-text": { kind: "count", value: "<n>", min: 4, max: 999, default: 8, help: "the shortest text to find (default 8)" },
473
+ "max-listed": OPTIONS["max-listed"],
474
+ },
475
+ run: leftoversCommand,
476
+ },
477
+ {
478
+ name: "routine",
479
+ summary: "Saves a routine of the program in the knowledge to a library, with its user and LLM names, regions, comments, references and operand expressions at offsets.",
480
+ subjects: [{ name: "<module|range>", help: "the start address of a module of the knowledge, or a range <start>-<end>" }],
481
+ options: {
482
+ library: LIBRARY,
483
+ name: { kind: "text", value: "<name>", help: "the routine name. The default is the user or LLM name at the start" },
484
+ title: { kind: "text", value: "<text>", help: "the title. The default is the module title" },
485
+ replace: { kind: "flag", help: "replace a routine file of the same name" },
486
+ },
487
+ run: routineCommand,
488
+ },
489
+ {
490
+ name: "match",
491
+ summary: "Finds each routine of the library in the program of the knowledge: the opcodes and other fixed bytes equal, and the addresses into the routine moved by one shift.",
492
+ options: {
493
+ library: { ...LIBRARY, help: "the library folder, relative to the project folder, for example ../library" },
494
+ out: { ...OPTIONS.out, value: "<file>", help: "write the names of the routines at the found addresses as a batch for knowledge.ts apply" },
495
+ "max-listed": OPTIONS["max-listed"],
496
+ },
497
+ run: matchCommand,
498
+ },
499
+ ];
500
+
501
+ await runScript({
502
+ name: "provenance",
503
+ summary: "Where the parts of a program come from: a comparison of releases, the separation of the program in the knowledge, its leftovers, and a library of known routines.",
504
+ names: knowledgeNames,
505
+ commands: COMMANDS,
506
+ });