@henols/c64-re-tools 2.1.0-rc.1 → 2.1.0

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 (103) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +24 -9
  3. package/skills/c64-assembler/scripts/assembler.ts +95 -0
  4. package/skills/c64-basic/SKILL.md +17 -9
  5. package/skills/c64-basic/scripts/basic.ts +26 -36
  6. package/skills/c64-disk/SKILL.md +17 -7
  7. package/skills/c64-disk/scripts/disk.ts +130 -104
  8. package/skills/c64-disk/scripts/lib/containers.ts +10 -12
  9. package/skills/c64-docs/SKILL.md +47 -28
  10. package/skills/c64-docs/references/style.md +6 -6
  11. package/skills/c64-docs/scripts/docs.ts +323 -0
  12. package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
  13. package/skills/c64-emulator/SKILL.md +11 -7
  14. package/skills/c64-knowledge/SKILL.md +34 -21
  15. package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
  16. package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
  17. package/skills/c64-memory-map/SKILL.md +18 -10
  18. package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
  19. package/skills/c64-provenance/SKILL.md +33 -24
  20. package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
  21. package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
  22. package/skills/c64-provenance/scripts/lib/library.ts +15 -25
  23. package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
  24. package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
  25. package/skills/c64-provenance/scripts/provenance.ts +177 -279
  26. package/skills/c64-reverse-engineering/SKILL.md +13 -12
  27. package/skills/c64-source/SKILL.md +28 -16
  28. package/skills/c64-source/scripts/lib/loader.ts +52 -77
  29. package/skills/c64-source/scripts/lib/source.ts +33 -42
  30. package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
  31. package/skills/c64-static-analysis/SKILL.md +24 -13
  32. package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
  33. package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
  34. package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
  35. package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
  36. package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
  37. package/skills/c64-testing/SKILL.md +48 -32
  38. package/skills/c64-testing/references/recording-format.md +48 -19
  39. package/skills/c64-testing/references/scenario-format.md +24 -19
  40. package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
  41. package/skills/c64-testing/scripts/lib/diff.ts +5 -3
  42. package/skills/c64-testing/scripts/lib/play.ts +244 -54
  43. package/skills/c64-testing/scripts/lib/recording.ts +153 -120
  44. package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
  45. package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
  46. package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
  47. package/skills/c64-testing/scripts/testing.ts +189 -0
  48. package/skills/c64-unpacker/SKILL.md +42 -25
  49. package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
  50. package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
  51. package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
  52. package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
  53. package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
  54. package/src/c64.ts +35 -20
  55. package/src/cli/main.ts +2 -2
  56. package/src/errors.ts +38 -0
  57. package/src/graphics.ts +3 -1
  58. package/src/host/server.ts +3 -2
  59. package/src/host/vice/session.ts +4 -4
  60. package/src/host-client/entry-state.ts +106 -0
  61. package/src/host-client/image.ts +21 -10
  62. package/src/host-client/steps.ts +285 -108
  63. package/src/host-client/transfer.ts +21 -15
  64. package/src/host-client/vice-session.ts +1 -1
  65. package/src/knowledge/database.ts +3 -23
  66. package/src/knowledge/expression.ts +8 -11
  67. package/src/knowledge/history.ts +2 -2
  68. package/src/knowledge/import.ts +13 -14
  69. package/src/knowledge/names.ts +50 -0
  70. package/src/knowledge/program.ts +11 -10
  71. package/src/knowledge/read.ts +16 -21
  72. package/src/knowledge/schema.ts +9 -3
  73. package/src/knowledge/text.ts +5 -5
  74. package/src/knowledge/write.ts +93 -137
  75. package/src/mcp/inputs.ts +105 -0
  76. package/src/mcp/output.ts +2 -13
  77. package/src/mcp/server.ts +12 -60
  78. package/src/mcp/tools/debug.ts +17 -12
  79. package/src/mcp/tools/execution.ts +5 -32
  80. package/src/mcp/tools/input.ts +7 -32
  81. package/src/mcp/tools/media.ts +6 -5
  82. package/src/mcp/tools/memory.ts +11 -38
  83. package/src/mcp/tools/video.ts +6 -5
  84. package/src/native/c64run.ts +1 -1
  85. package/src/native/dxa.ts +7 -6
  86. package/src/native/ghidra/analyze.ts +2 -1
  87. package/src/native/ghidra/index.ts +2 -1
  88. package/src/native/processes.ts +4 -3
  89. package/src/native/status.ts +2 -2
  90. package/src/native/types.ts +3 -7
  91. package/src/opcodes.ts +13 -13
  92. package/src/platform.ts +9 -9
  93. package/src/project.ts +16 -26
  94. package/src/protocol/messages.ts +3 -21
  95. package/src/protocol/vice.ts +7 -7
  96. package/src/script.ts +491 -49
  97. package/skills/c64-assembler/scripts/assemble.ts +0 -138
  98. package/skills/c64-docs/scripts/generate.ts +0 -109
  99. package/skills/c64-docs/scripts/link.ts +0 -239
  100. package/skills/c64-memory-map/scripts/memmap.ts +0 -92
  101. package/skills/c64-testing/scripts/checklist.ts +0 -51
  102. package/skills/c64-testing/scripts/record.ts +0 -96
  103. package/skills/c64-testing/scripts/test.ts +0 -65
@@ -8,15 +8,17 @@
8
8
  // routines that several programs share in a library folder and find them in the
9
9
  // program (D71).
10
10
 
11
- import { basename, posix, relative } from "node:path";
12
- import { parseArgs } from "node:util";
11
+ import { basename, relative } from "node:path";
13
12
 
14
- import { formatC64Address, parseC64Address } from "#src/c64.ts";
15
- import { checkActionSteps, type ActionStep } from "#src/host-client/steps.ts";
16
- import { MAX_EXECUTION_COUNT, WireFailure } from "#src/host-client/tools.ts";
17
- 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";
18
20
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
19
- import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
21
+ import { openForRead } from "#src/knowledge/database.ts";
20
22
  import { programCoverage, requireProgram } from "#src/knowledge/program.ts";
21
23
  import {
22
24
  commentsAt,
@@ -36,68 +38,17 @@ import {
36
38
  symbolNamed,
37
39
  symbolSuppressed,
38
40
  } from "#src/knowledge/read.ts";
39
- import { CODE_REGION_TYPES, type Origin } from "#src/knowledge/schema.ts";
41
+ import { CODE_REGION_TYPES, SEMANTIC_ORIGINS } from "#src/knowledge/schema.ts";
40
42
  import { platformNames } from "#src/platform.ts";
41
43
  import { projectRoot } from "#src/project.ts";
42
- import { runScript, UsageError } from "#src/script.ts";
43
- import { compareReleases, shiftSuggestion, suggestShifts, type Range, type Release } from "./lib/compare.ts";
44
- import { findRoutine, libraryFolder, LibraryError, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
45
- 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";
46
49
  import { findLeftovers } from "./lib/leftovers.ts";
47
50
  import { separate } from "./lib/separate.ts";
48
51
 
49
- const USAGE = `provenance.ts <release> <release> [<release> ...] [options]
50
- provenance.ts separate [--out <file>] [--max-listed <n>]
51
- provenance.ts leftovers [--out <file>] [--min-text <n>] [--max-listed <n>]
52
- provenance.ts routine --library <folder> (--module <address> | --start <address> --end <address>)
53
- [--name <name>] [--title <text>] [--replace]
54
- provenance.ts match --library <folder> [--out <file>] [--max-listed <n>]
55
-
56
- --shift <n>:<bytes> move release n (1 = the first) by a number of bytes, for example 2:-256
57
- --from <address> compare from this address
58
- --to <address> compare up to this address
59
- --run <frames> run each release in its own emulator for this many frames, and
60
- give each listed difference its first access and whether it changed
61
- --steps <file> with --run: action steps (JSON) to run before the frames
62
-
63
- separate gives evidence from the program in the knowledge: the unpack stage
64
- that wrote each range, its access in the coverage of the knowledge, whether call
65
- and jump references in knowledge reach each code region from the entry, and what
66
- the program reads or executes outside its segments. --out writes all ranges to a file;
67
- the result lists at most --max-listed of each list (default 50) and its count.
68
-
69
- leftovers lists what the program in the knowledge holds besides its working
70
- code: text in PETSCII, screen codes, ASCII or with bit 7 set (Apple II) and
71
- assembler source (at least --min-text characters, default 8), BASIC lines away
72
- from the BASIC start, fills, untouched bytes outside code, exact copies of
73
- earlier bytes, and code regions that no call or jump reaches and no run executed. Each finding has the stages and the accesses of
74
- its bytes. --out writes all findings to a file.
75
-
76
- routine saves a routine of the program in the knowledge to a library: a plain folder of routine
77
- files, beside the projects (--library, relative to the working directory, as
78
- --project). It takes a module of the knowledge, or --start and --end. Its
79
- code regions give the instructions; the user and LLM names, regions, comments,
80
- references and operand expressions go into the file at offsets. --name defaults
81
- to the user or LLM name at the start, --title to the module title.
82
-
83
- match finds each routine of the library in the program of the knowledge: the opcodes and other
84
- fixed bytes equal, and the addresses into the routine moved by one shift.
85
- Operands that address something outside the routine may differ; the result
86
- lists them. --out writes the names of the routines at the found addresses as a
87
- batch for knowledge.ts apply, and the result lists what it left out.
88
-
89
- A release to compare is a PRG, placed at its load address, or an image.json of
90
- the c64-unpacker skill, compared inside its segments. Paths are relative to the
91
- project folder. The result is one JSON object.`;
92
-
93
- function address(text: string, option: string): number {
94
- try {
95
- return parseC64Address(text);
96
- } catch {
97
- throw new UsageError(`${option} must be $ followed by four hex digits, not ${text}`);
98
- }
99
- }
100
-
101
52
  const hexBytes = (bytes: number[]) => Buffer.from(bytes).toString("hex").replace(/(..)(?!$)/g, "$1 ");
102
53
 
103
54
  interface LoadedRelease {
@@ -107,174 +58,66 @@ interface LoadedRelease {
107
58
  /** 64 KiB with the release bytes at their own addresses. */
108
59
  memory: Uint8Array;
109
60
  entry?: number;
110
- stages?: Array<{ entry: number; wrote: Range[] }>;
111
- }
112
-
113
- /** Reads entry_state.json of the c64-unpacker skill for a start by writes. */
114
- function entryState(path: string): ImageState | string {
115
- let value: Record<string, any>;
116
- try {
117
- value = JSON.parse(readProjectFile(path).bytes.toString("utf8")) as Record<string, any>;
118
- } catch (error) {
119
- if (error instanceof WireFailure && error.code === "not-found") return `It has no ${path}; capture it with c64-unpacker.`;
120
- if (error instanceof WireFailure) throw error;
121
- return `${path} is not JSON: ${(error as Error).message}`;
122
- }
123
- const registers = value.registers ?? {};
124
- const byte = (item: unknown) => Number.isInteger(item) && (item as number) >= 0 && (item as number) <= 0xff;
125
- const pages = ["$0000", "$0100", "$0200", "$0300"].map((page) => value.pages?.[page]);
126
- const ok =
127
- typeof registers.pc === "string" &&
128
- /^\$[0-9a-f]{4}$/i.test(registers.pc) &&
129
- ["a", "x", "y", "sp"].every((name) => byte(registers[name])) &&
130
- typeof registers.flags === "object" &&
131
- byte(value.cpuPort?.$00) &&
132
- byte(value.cpuPort?.$01) &&
133
- pages.every((page) => typeof page === "string" && /^[0-9a-f]{512}$/i.test(page));
134
- if (!ok) return `${path} is not an entry_state.json of the c64-unpacker skill.`;
135
- const flag = (name: string) => registers.flags[name] === true;
136
- return {
137
- pc: Number.parseInt(registers.pc.slice(1), 16),
138
- a: registers.a,
139
- x: registers.x,
140
- y: registers.y,
141
- sp: registers.sp,
142
- flags: { n: flag("n"), v: flag("v"), d: flag("d"), i: flag("i"), z: flag("z"), c: flag("c") },
143
- port: [value.cpuPort.$00, value.cpuPort.$01],
144
- pages: Buffer.from(pages.join(""), "hex"),
145
- };
61
+ stages?: Array<{ entry: number; wrote: AddressRange[] }>;
146
62
  }
147
63
 
148
- function readRelease(path: string, shift: number): LoadedRelease {
149
- const { bytes } = readProjectFile(path);
150
- const moved = (range: Range) => ({ start: range.start + shift, end: range.end + shift });
151
- const inside = (range: Range) => range.start >= 0 && range.end <= 0xffff;
152
- if (!path.endsWith(".json")) {
153
- if (bytes.length < 3) throw new UsageError(`${path} is too short for a PRG`);
154
- const load = bytes.readUInt16LE(0);
155
- const segment = moved({ start: load, end: load + bytes.length - 3 });
156
- if (!inside(segment)) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
157
- const memory = new Uint8Array(0x10000);
158
- memory.set(bytes.subarray(2), load);
159
- return { release: { name: path, segments: [segment], byteAt: (at) => bytes[2 + at - segment.start]! }, shift, start: { kind: "program", path }, memory };
160
- }
161
- const fail = (what: string): never => {
162
- throw new UsageError(`${path} is not an image.json of the c64-unpacker skill: ${what}`);
163
- };
164
- let manifest: Record<string, unknown>;
165
- try {
166
- manifest = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
167
- } catch (error) {
168
- 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.`;
169
86
  }
170
- 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"));
171
- if (typeof manifest.image !== "string" || !Array.isArray(manifest.segments) || manifest.segments.length === 0) fail("it needs image and segments");
172
- 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);
173
- const directory = posix.dirname(path);
174
- const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
175
- if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
176
- const segments = own.map(moved);
177
- if (!segments.every(inside)) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
178
- const state = entryState(posix.join(directory, "entry_state.json"));
179
- const stages = Array.isArray(manifest.stages)
180
- ? (manifest.stages as Array<Record<string, unknown>>).map((stage) => ({
181
- entry: parse(stage.entry),
182
- wrote: (Array.isArray(stage.wrote) ? (stage.wrote as Array<Record<string, unknown>>) : []).map((range) => ({ start: parse(range.start), end: parse(range.end) })),
183
- }))
184
- : 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);
185
93
  return {
186
- release: { name: path, segments, byteAt: (at) => memory[at - shift]! },
94
+ release: { name, segments, byteAt: (at) => memory[at - shift]! },
187
95
  shift,
188
- 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 },
189
97
  memory,
190
- ...(manifest.entry === undefined ? {} : { entry: parse(manifest.entry) }),
98
+ ...(image.entry === undefined ? {} : { entry: image.entry }),
191
99
  ...(stages === undefined ? {} : { stages }),
192
100
  };
193
101
  }
194
102
 
195
- async function run(argv: string[]): Promise<unknown> {
196
- const { values, positionals } = parseArgs({
197
- args: argv,
198
- strict: true,
199
- allowPositionals: true,
200
- options: {
201
- shift: { type: "string", multiple: true },
202
- from: { type: "string" },
203
- to: { type: "string" },
204
- run: { type: "string" },
205
- steps: { type: "string" },
206
- out: { type: "string" },
207
- "max-listed": { type: "string" },
208
- "min-text": { type: "string" },
209
- library: { type: "string" },
210
- module: { type: "string" },
211
- start: { type: "string" },
212
- end: { type: "string" },
213
- name: { type: "string" },
214
- title: { type: "string" },
215
- replace: { type: "boolean" },
216
- help: { type: "boolean" },
217
- },
218
- });
219
- if (values.help) throw new UsageError("");
220
- /** Refuses the options that `command` does not take. */
221
- const only = (command: string, allowed: readonly string[]) => {
222
- const used = Object.keys(values).filter((option) => option !== "help" && !allowed.includes(option));
223
- if (used.length > 0) throw new UsageError(`${command} does not take --${used.join(", --")}`);
224
- };
225
- if (positionals[0] === "separate") {
226
- only("separate", ["out", "max-listed"]);
227
- if (positionals.length !== 1) throw new UsageError("separate takes no release: it reads the program from the knowledge");
228
- return separateCommand(values.out, values["max-listed"]);
229
- }
230
- if (positionals[0] === "leftovers") {
231
- only("leftovers", ["out", "min-text", "max-listed"]);
232
- if (positionals.length !== 1) throw new UsageError("leftovers takes no release: it reads the program from the knowledge");
233
- return leftoversCommand(values.out, values["min-text"], values["max-listed"]);
234
- }
235
- if (positionals[0] === "routine") {
236
- only("routine", ["library", "module", "start", "end", "name", "title", "replace"]);
237
- if (positionals.length !== 1) throw new UsageError("routine takes no image: it reads the program from the knowledge");
238
- return routineCommand(values);
239
- }
240
- if (positionals[0] === "match") {
241
- only("match", ["library", "out", "max-listed"]);
242
- if (positionals.length !== 1) throw new UsageError("match takes no image: it reads the program from the knowledge");
243
- return matchCommand(values.library, values.out, values["max-listed"]);
244
- }
245
- only("a comparison", ["shift", "from", "to", "run", "steps"]);
246
- 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");
247
107
  const shifts = new Map<number, number>();
248
- for (const text of values.shift ?? []) {
108
+ for (const text of call.strings("shift")) {
249
109
  const match = /^(\d+):([+-]?\d+)$/.exec(text);
250
110
  if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
251
111
  shifts.set(Number(match[1]), Number(match[2]));
252
112
  }
253
- if (values.steps !== undefined && values.run === undefined) throw new UsageError("--steps works only with --run");
254
- let frames: number | undefined;
255
- if (values.run !== undefined) {
256
- 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}`);
257
- frames = Number(values.run);
258
- }
259
- let steps: ActionStep[] = [];
260
- if (values.steps !== undefined) {
261
- let parsed: unknown;
262
- try {
263
- parsed = JSON.parse(readProjectFile(values.steps).bytes.toString("utf8"));
264
- } catch (error) {
265
- if (error instanceof WireFailure) throw error;
266
- throw new UsageError(`the steps file ${values.steps} is not JSON: ${(error as Error).message}`);
267
- }
268
- steps = checkActionSteps(parsed, (message) => {
269
- throw new UsageError(`steps file ${values.steps}: ${message}`);
270
- });
271
- }
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");
272
117
  const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
273
118
  const releases = loaded.map((item) => item.release);
274
- const comparison = compareReleases(releases, {
275
- ...(values.from === undefined ? {} : { from: address(values.from, "--from") }),
276
- ...(values.to === undefined ? {} : { to: address(values.to, "--to") }),
277
- });
119
+ const range = call.range("range");
120
+ const comparison = compareReleases(releases, range === undefined ? {} : { from: range.start, to: range.end });
278
121
  const compared = comparison.identicalBytes + comparison.differingBytes;
279
122
  const identicalShare = compared === 0 ? 0 : comparison.identicalBytes / compared;
280
123
  // A poor match at the load addresses can mean that the releases are placed differently.
@@ -294,7 +137,7 @@ async function run(argv: string[]): Promise<unknown> {
294
137
  const notSuggested = suggestions.filter((suggestion) => "reason" in suggestion);
295
138
 
296
139
  // The runtime evidence: one emulator for each release, the differences at the release's own addresses.
297
- const runtime = new Map<string, Array<Range & { first: string; changed: boolean }>>();
140
+ const runtime = new Map<string, Array<AddressRange & { first: string; changed: boolean }>>();
298
141
  const runs: Record<string, unknown> = {};
299
142
  if (frames !== undefined && comparison.differences.length > 0) {
300
143
  for (const item of loaded) {
@@ -350,12 +193,13 @@ async function run(argv: string[]): Promise<unknown> {
350
193
  };
351
194
  }
352
195
 
353
- /** The stages of the capture in the knowledge as ranges; a stage that is not a capture stage is refused. */
354
- 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 {
355
198
  if (stages === undefined) return undefined;
356
199
  const parse = (value: unknown) => {
357
- if (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value)) return Number.parseInt(value.slice(1), 16);
358
- 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`);
359
203
  };
360
204
  return stages.map((stage) => ({
361
205
  entry: parse(stage.entry),
@@ -394,17 +238,18 @@ function programEvidence() {
394
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.";
395
239
 
396
240
  /** separate: evidence from the program in the knowledge, with all ranges in --out and the first ones in the result. */
397
- function separateCommand(out: string | undefined, maxListed: string | undefined): unknown {
398
- const max = maxListed ?? "50";
399
- if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
241
+ function separateCommand(call: Call): unknown {
242
+ const out = call.string("out");
243
+ const max = call.count("max-listed")!;
400
244
  const { program, stages, coverage, found } = programEvidence();
401
245
  const loaded = { entry: program.entry, stages };
402
- const show = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
246
+ const show = (range: AddressRange) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
403
247
  const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
404
248
  const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
405
249
  const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
406
- const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
407
- 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);
408
253
  const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
409
254
  const lastStage = loaded.stages === undefined ? undefined : loaded.stages.length - 1;
410
255
  return {
@@ -416,14 +261,14 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
416
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)]]),
417
262
  }),
418
263
  ...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
419
- ranges: listed(ranges),
264
+ ranges: list(ranges),
420
265
  // total counts every code region; unreachable is a list like the others.
421
266
  codeRegions: {
422
267
  total: codeRegions.length,
423
- ...(loaded.entry === undefined ? {} : { unreachable: listed(codeRegions.filter((region) => region.reachable === false)) }),
268
+ ...(loaded.entry === undefined ? {} : { unreachable: list(codeRegions.filter((region) => region.reachable === false)) }),
424
269
  },
425
- ...(coverage === undefined ? {} : { outside: { read: listed(outside.read), executed: listed(outside.executed), generated: listed(outside.generated) } }),
426
- ...(out === undefined ? {} : { out }),
270
+ ...(coverage === undefined ? {} : { outside: { read: list(outside.read), executed: list(outside.executed), generated: list(outside.generated) } }),
271
+ ...(written === undefined ? {} : written),
427
272
  };
428
273
  }
429
274
 
@@ -431,20 +276,19 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
431
276
  const SHOWN_TEXT = 120;
432
277
 
433
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. */
434
- function leftoversCommand(out: string | undefined, minText: string | undefined, maxListed: string | undefined): unknown {
435
- const max = maxListed ?? "50";
436
- if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
437
- const min = minText ?? "8";
438
- if (!/^\d{1,3}$/.test(min) || Number(min) < 4) throw new UsageError(`--min-text must be a whole number of 4 or more, not ${min}`);
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")!;
439
283
  const { program, stages, coverage, found } = programEvidence();
440
284
  const leftovers = findLeftovers({
441
285
  segments: program.segments,
442
286
  memory: program.memory,
443
287
  separation: found,
444
288
  ...(program.segments.some((segment) => segment.start === 0x0801) ? { basicStart: 0x0801 } : {}),
445
- minText: Number(min),
289
+ minText: min,
446
290
  });
447
- const show = <T extends Range & { stages?: number[]; access?: string[] }>({ start, end, stages: wrote, access, ...rest }: T) => ({
291
+ const show = <T extends AddressRange & { stages?: number[]; access?: string[] }>({ start, end, stages: wrote, access, ...rest }: T) => ({
448
292
  start: formatC64Address(start),
449
293
  end: formatC64Address(end),
450
294
  ...rest,
@@ -459,56 +303,50 @@ function leftoversCommand(out: string | undefined, minText: string | undefined,
459
303
  copies: leftovers.copies.map(({ copyOf, ...rest }) => ({ ...show(rest), copyOf: formatC64Address(copyOf) })),
460
304
  deadCode: leftovers.deadCode.map(show),
461
305
  };
462
- const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
463
- if (out !== undefined) writeProjectFile(out, Buffer.from(`${JSON.stringify(all, null, 1)}\n`), { what: "--out" });
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>");
464
308
  return {
465
309
  ...(program.entry === undefined ? { noEntry: "The program in the knowledge has no entry, so deadCode is empty." } : { entry: formatC64Address(program.entry) }),
466
310
  ...(stages === undefined ? { noStages: NO_STAGES } : { stages: stages.length }),
467
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." } : {}),
468
- text: listed(all.text.map((item) => (item.text.length > SHOWN_TEXT ? { ...item, text: `${item.text.slice(0, SHOWN_TEXT)} ...`, length: item.text.length } : item))),
469
- basic: listed(all.basic),
470
- fill: listed(all.fill),
471
- residue: listed(all.residue),
472
- copies: listed(all.copies),
473
- deadCode: listed(all.deadCode),
474
- ...(out === undefined ? {} : { out }),
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),
475
319
  };
476
320
  }
477
321
 
478
- const SEMANTIC: readonly Origin[] = ["user", "llm"];
479
-
480
322
  const platform = () => new Map(platformNames().map((entry) => [entry.name, entry.address]));
481
323
 
482
324
  /** routine: saves a module or a range of the program in the knowledge, with its knowledge, as a routine of the library (D71). */
483
- function routineCommand(values: { library?: string; module?: string; start?: string; end?: string; name?: string; title?: string; replace?: boolean }): unknown {
484
- if (values.library === undefined) throw new UsageError("routine needs --library <folder>");
485
- if (values.module !== undefined && (values.start !== undefined || values.end !== undefined)) throw new UsageError("routine takes --module, or --start and --end, not both");
486
- if (values.module === undefined && values.start === undefined && values.end === undefined) throw new UsageError("routine needs --module <address>, or --start and --end");
487
- 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")!;
488
328
  const db = openForRead();
489
329
  try {
490
330
  const image = requireProgram(db);
491
331
  const segmentOf = (at: number) => image.segments.find((segment) => at >= segment.start && at <= segment.end);
492
332
  let start: number;
493
333
  let end: number;
494
- let title = values.title;
495
- if (values.module !== undefined) {
496
- start = address(values.module, "--module");
334
+ let title = call.string("title");
335
+ if (!given.includes("-")) {
336
+ start = call.parseAddress(given, "<module>");
497
337
  const modules = listModules(db);
498
338
  const module = modules.find((item) => item.start === start);
499
- 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>.`);
500
340
  const segment = segmentOf(start);
501
341
  if (segment === undefined) throw new UsageError(`The module at ${formatC64Address(start)} is outside the segments of the program.`);
502
342
  end = (modules.find((item) => item.start > start && item.start <= segment.end)?.start ?? segment.end + 1) - 1;
503
343
  title ??= module.title;
504
344
  } else {
505
- start = address(values.start!, "--start");
506
- end = address(values.end!, "--end");
507
- if (end < start) throw new UsageError("--end is before --start");
345
+ ({ start, end } = call.parseRange(given, "<range>"));
508
346
  if (segmentOf(start) === undefined || segmentOf(start) !== segmentOf(end)) throw new UsageError(`${formatC64Address(start)}-${formatC64Address(end)} is not inside one segment of the program.`);
509
347
  }
510
348
  const range = { start, end };
511
- 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;
512
350
  if (name === undefined) throw new UsageError(`The knowledge has no user or LLM name at ${formatC64Address(start)}: give --name.`);
513
351
  const { routine, leftOut } = routineFrom({
514
352
  name,
@@ -519,17 +357,17 @@ function routineCommand(values: { library?: string; module?: string; start?: str
519
357
  end,
520
358
  byteAt: image.byteAt,
521
359
  analyzed: listRegions(db, range),
522
- symbols: listSymbols(db, range, undefined, SEMANTIC),
523
- regions: listRegions(db, range, undefined, SEMANTIC),
524
- comments: listComments(db, range, SEMANTIC),
525
- references: listReferences(db, range, undefined, SEMANTIC),
526
- 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),
527
365
  platform: platform(),
528
366
  });
529
- 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"));
530
368
  return {
531
369
  routine: routine.name,
532
- file: relative(process.cwd(), file),
370
+ file: relative(projectRoot(), file).split("\\").join("/"),
533
371
  start: formatC64Address(start),
534
372
  end: formatC64Address(end),
535
373
  length: routine.bytes.length,
@@ -545,13 +383,13 @@ function routineCommand(values: { library?: string; module?: string; start?: str
545
383
  }
546
384
 
547
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). */
548
- function matchCommand(library: string | undefined, out: string | undefined, maxListed: string | undefined): unknown {
549
- if (library === undefined) throw new UsageError("match needs --library <folder>");
550
- const max = maxListed ?? "50";
551
- if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
552
- const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
553
- const routines = readLibrary(libraryFolder(library, { create: false }));
554
- 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.`);
555
393
  const programDb = openForRead();
556
394
  let image: ReturnType<typeof requireProgram>;
557
395
  try {
@@ -572,7 +410,7 @@ function matchCommand(library: string | undefined, out: string | undefined, maxL
572
410
  })),
573
411
  )
574
412
  .sort((a, b) => a.start.localeCompare(b.start));
575
- 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) };
576
414
  if (out === undefined) return result;
577
415
  const db = openForRead();
578
416
  let suggestions: ReturnType<typeof suggest>;
@@ -597,12 +435,72 @@ function matchCommand(library: string | undefined, out: string | undefined, maxL
597
435
  db?.close();
598
436
  }
599
437
  const { batch, known, leftOut } = suggestions;
600
- 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" });
601
439
  return {
602
440
  ...result,
603
- batch: { out, writes: batch.length, known, leftOut: listed(leftOut) },
441
+ batch: { ...written, writes: batch.length, known, leftOut: list(leftOut) },
604
442
  ...(batch.length === 0 ? {} : { apply: `knowledge.ts apply ${out} --reason "names of library routines"` }),
605
443
  };
606
444
  }
607
445
 
608
- 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
+ });