@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21

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 (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +138 -0
  14. package/skills/c64-docs/references/page-types.md +141 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +207 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +90 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -61
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +36 -0
  81. package/src/host-client/image.ts +63 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +202 -31
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +133 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -22
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. package/skills/c64-unpacker/scripts/evidence.ts +0 -135
@@ -1,21 +1,84 @@
1
1
  // The c64-provenance script: compares two or more releases byte by byte at
2
- // their addresses and reports where they differ and which releases agree.
2
+ // their addresses and reports where they differ and which releases agree. A
3
+ // release is a PRG or an unpacker image.json (D50). With --run, each release
4
+ // runs in its own emulator, and each listed difference gets its first access
5
+ // and whether the run changed it. The separate command gives evidence from
6
+ // the program in the knowledge (D51, D83). The routine and match commands keep
7
+ // routines that several programs share in a library folder and find them in the
8
+ // program (D71).
3
9
 
10
+ import { basename, posix, relative } from "node:path";
4
11
  import { parseArgs } from "node:util";
5
12
 
6
13
  import { formatC64Address, parseC64Address } from "#src/c64.ts";
7
- import { readProjectFile } from "#src/host-client/transfer.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";
17
+ import { ViceSessionClient } from "#src/host-client/vice-session.ts";
18
+ import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
19
+ import { programCoverage, requireProgram } from "#src/knowledge/program.ts";
20
+ import {
21
+ commentsAt,
22
+ currentRevision,
23
+ listComments,
24
+ listModules,
25
+ listOperands,
26
+ listReferences,
27
+ listRegions,
28
+ listSymbols,
29
+ operandAt,
30
+ referencesFrom,
31
+ referenceSuppressed,
32
+ regionsOverlapping,
33
+ suppressionsOverlapping,
34
+ symbolAt,
35
+ symbolNamed,
36
+ symbolSuppressed,
37
+ } from "#src/knowledge/read.ts";
38
+ import { CODE_REGION_TYPES, type Origin } from "#src/knowledge/schema.ts";
39
+ import { platformNames } from "#src/platform.ts";
40
+ import { projectRoot } from "#src/project.ts";
8
41
  import { runScript, UsageError } from "#src/script.ts";
9
- import { compareReleases, suggestShift, type Release } from "./compare.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";
45
+ import { separate } from "./lib/separate.ts";
10
46
 
11
- const USAGE = `provenance.ts <release.prg> <release.prg> [<release.prg> ...] [options]
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>]
12
52
 
13
53
  --shift <n>:<bytes> move release n (1 = the first) by a number of bytes, for example 2:-256
14
54
  --from <address> compare from this address
15
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
16
59
 
17
- Each release is a PRG file relative to the project directory, placed at its
18
- load address. The result is one JSON object.`;
60
+ separate gives evidence from the program in the knowledge (D83): 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 (D48) 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.`;
19
82
 
20
83
  function address(text: string, option: string): number {
21
84
  try {
@@ -27,14 +90,143 @@ function address(text: string, option: string): number {
27
90
 
28
91
  const hexBytes = (bytes: number[]) => Buffer.from(bytes).toString("hex").replace(/(..)(?!$)/g, "$1 ");
29
92
 
30
- function run(argv: string[]): unknown {
93
+ interface LoadedRelease {
94
+ release: Release;
95
+ shift: number;
96
+ start: Start | { kind: "none"; reason: string };
97
+ /** 64 KiB with the release bytes at their own addresses. */
98
+ memory: Uint8Array;
99
+ 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
+ };
136
+ }
137
+
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);
159
+ }
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;
175
+ return {
176
+ release: { name: path, segments, byteAt: (at) => memory[at - shift]! },
177
+ shift,
178
+ start: typeof state === "string" ? { kind: "none", reason: state } : { kind: "image", segments: own, byteAt: (at) => memory[at]!, state },
179
+ memory,
180
+ ...(manifest.entry === undefined ? {} : { entry: parse(manifest.entry) }),
181
+ ...(stages === undefined ? {} : { stages }),
182
+ };
183
+ }
184
+
185
+ async function run(argv: string[]): Promise<unknown> {
31
186
  const { values, positionals } = parseArgs({
32
187
  args: argv,
33
188
  strict: true,
34
189
  allowPositionals: true,
35
- options: { shift: { type: "string", multiple: true }, from: { type: "string" }, to: { type: "string" }, help: { type: "boolean" } },
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
+ },
36
207
  });
37
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"]);
38
230
  if (positionals.length < 2) throw new UsageError("give two or more releases");
39
231
  const shifts = new Map<number, number>();
40
232
  for (const text of values.shift ?? []) {
@@ -42,13 +234,27 @@ function run(argv: string[]): unknown {
42
234
  if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
43
235
  shifts.set(Number(match[1]), Number(match[2]));
44
236
  }
45
- const releases: Release[] = positionals.map((path, index) => {
46
- const { bytes } = readProjectFile(path);
47
- if (bytes.length < 3) throw new UsageError(`${path} is too short for a PRG`);
48
- const start = bytes.readUInt16LE(0) + (shifts.get(index + 1) ?? 0);
49
- if (start < 0 || start + bytes.length - 3 > 0xffff) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
50
- return { name: path, start, bytes: bytes.subarray(2) };
51
- });
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
+ }
256
+ const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
257
+ const releases = loaded.map((item) => item.release);
52
258
  const comparison = compareReleases(releases, {
53
259
  ...(values.from === undefined ? {} : { from: address(values.from, "--from") }),
54
260
  ...(values.to === undefined ? {} : { to: address(values.to, "--to") }),
@@ -56,28 +262,275 @@ function run(argv: string[]): unknown {
56
262
  const compared = comparison.identicalBytes + comparison.differingBytes;
57
263
  const identicalShare = compared === 0 ? 0 : comparison.identicalBytes / compared;
58
264
  // A poor match at the load addresses can mean that the releases are placed differently.
265
+ // Each release gets its best shifts: data that stayed can outvote code that moved, so one is not enough (b2/04).
266
+ const applied = loaded.map((item) => item.shift);
59
267
  const suggestions =
60
268
  identicalShare < 0.5
61
- ? releases.slice(1).flatMap((release, index) => {
62
- const best = suggestShift(releases[0]!, release);
63
- return best === undefined || best.shift === 0 ? [] : [{ release: index + 2, shift: best.shift, matchingSequences: best.matches }];
64
- })
269
+ ? releases.slice(1).flatMap((release, index) =>
270
+ suggestShifts(releases[0]!, release).map((found) => {
271
+ const suggestion = shiftSuggestion(releases, applied, index + 1, found);
272
+ // The shift that the run already has: its votes tell how good the present alignment is.
273
+ return "shift" in suggestion && suggestion.shift === applied[suggestion.release - 1] ? { ...suggestion, current: true } : suggestion;
274
+ }),
275
+ )
65
276
  : [];
277
+ const suggested = suggestions.filter((suggestion) => "shift" in suggestion);
278
+ const notSuggested = suggestions.filter((suggestion) => "reason" in suggestion);
279
+
280
+ // 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 }>>();
282
+ const runs: Record<string, unknown> = {};
283
+ if (frames !== undefined && comparison.differences.length > 0) {
284
+ for (const item of loaded) {
285
+ const name = item.release.name;
286
+ if (item.start.kind === "none") {
287
+ runs[name] = { started: false, reason: item.start.reason };
288
+ continue;
289
+ }
290
+ const ranges = comparison.differences.map((difference) => ({ start: difference.start - item.shift, end: difference.end - item.shift }));
291
+ const client = await ViceSessionClient.open({ videoStandard: "pal" });
292
+ try {
293
+ const watched = await watchRelease(client, item.start, ranges, (at) => item.release.byteAt(at + item.shift), { frames, steps });
294
+ if (!watched.started) runs[name] = { started: false, reason: watched.reason };
295
+ else {
296
+ runs[name] = { started: true };
297
+ runtime.set(name, watched.accesses.map((access, index) => ({ ...ranges[index]!, ...access })));
298
+ }
299
+ } finally {
300
+ await client.close();
301
+ }
302
+ }
303
+ }
304
+
66
305
  return {
67
- releases: releases.map((release) => ({ name: release.name, start: formatC64Address(release.start), end: formatC64Address(release.start + release.bytes.length - 1) })),
68
- common: comparison.common === null ? null : { start: formatC64Address(comparison.common.start), end: formatC64Address(comparison.common.end) },
306
+ releases: loaded.map((item) => ({ name: item.release.name, shift: item.shift, segments: item.release.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })) })),
307
+ common: comparison.common.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
69
308
  identicalBytes: comparison.identicalBytes,
70
309
  differingBytes: comparison.differingBytes,
71
310
  identicalShare: Number(identicalShare.toFixed(3)),
72
- differences: comparison.differences.map((difference) => ({
311
+ differences: comparison.differences.map((difference, index) => ({
73
312
  start: formatC64Address(difference.start),
74
313
  end: formatC64Address(difference.end),
75
314
  values: Object.fromEntries(Object.entries(difference.values).map(([name, bytes]) => [name, hexBytes(bytes.slice(0, 32)) + (bytes.length > 32 ? " ..." : "")])),
76
315
  groups: difference.groups,
316
+ // runtime gives the release's own addresses: with a shift, they are not the addresses of the difference (F-24).
317
+ ...(runtime.size === 0
318
+ ? {}
319
+ : {
320
+ runtime: Object.fromEntries(
321
+ [...runtime].map(([name, accesses]) => {
322
+ const { start, end, first, changed } = accesses[index]!;
323
+ return [name, { start: formatC64Address(start), end: formatC64Address(end), first, changed }];
324
+ }),
325
+ ),
326
+ }),
77
327
  })),
78
328
  ...(comparison.moreDifferences ? { moreDifferences: true } : {}),
79
- ...(suggestions.length > 0 ? { suggestedShifts: suggestions } : {}),
329
+ ...(suggested.length > 0 ? { suggestedShifts: suggested } : {}),
330
+ ...(notSuggested.length > 0 ? { shiftsNotSuggested: notSuggested } : {}),
331
+ ...(frames === undefined
332
+ ? {}
333
+ : { run: { frames, steps: steps.length, releases: runs, ...(comparison.differences.length === 0 ? { skipped: "The releases have no differences, so no release ran." } : {}) } }),
334
+ };
335
+ }
336
+
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 {
339
+ if (stages === undefined) return undefined;
340
+ 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");
343
+ };
344
+ return stages.map((stage) => ({
345
+ entry: parse(stage.entry),
346
+ wrote: (Array.isArray(stage.wrote) ? (stage.wrote as Array<Record<string, unknown>>) : []).map((range) => ({ start: parse(range.start), end: parse(range.end) })),
347
+ }));
348
+ }
349
+
350
+ /** separate: evidence from the program in the knowledge (D51, D83), 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}`);
354
+ const db = openForRead();
355
+ let program: ReturnType<typeof requireProgram>;
356
+ let coverage: Uint8Array | undefined;
357
+ let regions: ReturnType<typeof listRegions>;
358
+ let references: ReturnType<typeof listReferences>;
359
+ try {
360
+ program = requireProgram(db);
361
+ coverage = programCoverage(db)?.map;
362
+ regions = listRegions(db).filter((region) => CODE_REGION_TYPES.includes(region.type));
363
+ references = listReferences(db);
364
+ } finally {
365
+ db?.close();
366
+ }
367
+ const stages = stagesOf(program.stages);
368
+ const loaded = { entry: program.entry, stages };
369
+ const found = separate({
370
+ segments: program.segments,
371
+ memory: program.memory,
372
+ ...(program.entry === undefined ? {} : { entry: program.entry }),
373
+ ...(stages === undefined ? {} : { stages }),
374
+ ...(coverage === undefined ? {} : { coverage }),
375
+ codeRegions: regions,
376
+ references,
377
+ });
378
+ const show = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
379
+ const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
380
+ const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
381
+ 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" });
384
+ const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
385
+ const lastStage = loaded.stages === undefined ? undefined : loaded.stages.length - 1;
386
+ return {
387
+ ...(loaded.entry === undefined ? {} : { entry: formatC64Address(loaded.entry) }),
388
+ ...(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." }
390
+ : {
391
+ stages: lastStage + 1,
392
+ 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
+ }),
394
+ ...(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),
396
+ // total counts every code region; unreachable is a list like the others (b2/08).
397
+ codeRegions: {
398
+ total: codeRegions.length,
399
+ ...(loaded.entry === undefined ? {} : { unreachable: listed(codeRegions.filter((region) => region.reachable === false)) }),
400
+ },
401
+ ...(coverage === undefined ? {} : { outside: { read: listed(outside.read), executed: listed(outside.executed), generated: listed(outside.generated) } }),
402
+ ...(out === undefined ? {} : { out }),
403
+ };
404
+ }
405
+
406
+ const SEMANTIC: readonly Origin[] = ["user", "llm"];
407
+
408
+ const platform = () => new Map(platformNames().map((entry) => [entry.name, entry.address]));
409
+
410
+ /** 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");
416
+ const db = openForRead();
417
+ try {
418
+ const image = requireProgram(db);
419
+ const segmentOf = (at: number) => image.segments.find((segment) => at >= segment.start && at <= segment.end);
420
+ let start: number;
421
+ let end: number;
422
+ let title = values.title;
423
+ if (values.module !== undefined) {
424
+ start = address(values.module, "--module");
425
+ const modules = listModules(db);
426
+ 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.`);
428
+ const segment = segmentOf(start);
429
+ if (segment === undefined) throw new UsageError(`The module at ${formatC64Address(start)} is outside the segments of the program.`);
430
+ end = (modules.find((item) => item.start > start && item.start <= segment.end)?.start ?? segment.end + 1) - 1;
431
+ title ??= module.title;
432
+ } else {
433
+ start = address(values.start!, "--start");
434
+ end = address(values.end!, "--end");
435
+ if (end < start) throw new UsageError("--end is before --start");
436
+ if (segmentOf(start) === undefined || segmentOf(start) !== segmentOf(end)) throw new UsageError(`${formatC64Address(start)}-${formatC64Address(end)} is not inside one segment of the program.`);
437
+ }
438
+ const range = { start, end };
439
+ const name = values.name ?? listSymbols(db, { start, end: start }, undefined, SEMANTIC)[0]?.name;
440
+ if (name === undefined) throw new UsageError(`The knowledge has no user or LLM name at ${formatC64Address(start)}: give --name.`);
441
+ const { routine, leftOut } = routineFrom({
442
+ name,
443
+ ...(title === undefined ? {} : { title }),
444
+ program: basename(projectRoot()),
445
+ image: `knowledge revision ${currentRevision(db)}`,
446
+ start,
447
+ end,
448
+ byteAt: image.byteAt,
449
+ 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),
455
+ platform: platform(),
456
+ });
457
+ const file = saveRoutine(libraryFolder(values.library, { create: true }), routine, values.replace === true);
458
+ return {
459
+ routine: routine.name,
460
+ file: relative(process.cwd(), file),
461
+ start: formatC64Address(start),
462
+ end: formatC64Address(end),
463
+ length: routine.bytes.length,
464
+ instructions: routine.instructions.length,
465
+ relocations: routine.relocations.length,
466
+ variable: routine.variable.length,
467
+ counts: { symbols: routine.symbols.length, regions: routine.regions.length, comments: routine.comments.length, references: routine.references.length, operands: routine.operands.length },
468
+ ...(leftOut.length === 0 ? {} : { leftOut }),
469
+ };
470
+ } finally {
471
+ db?.close();
472
+ }
473
+ }
474
+
475
+ /** 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.`);
483
+ const programDb = openForRead();
484
+ let image: ReturnType<typeof requireProgram>;
485
+ try {
486
+ image = requireProgram(programDb);
487
+ } finally {
488
+ programDb?.close();
489
+ }
490
+ const found = routines.map((routine) => ({ routine, matches: findRoutine(routine, image) }));
491
+ const show = (value: number, size: 1 | 2) => `$${value.toString(16).padStart(2 * size, "0")}`;
492
+ const matches = found
493
+ .flatMap(({ routine, matches: each }) =>
494
+ each.map((match) => ({
495
+ routine: match.routine,
496
+ start: formatC64Address(match.start),
497
+ end: formatC64Address(match.start + routine.bytes.length - 1),
498
+ shift: match.shift,
499
+ differing: match.differing.map((operand) => ({ address: formatC64Address(operand.address), library: show(operand.library, operand.size), program: show(operand.program, operand.size) })),
500
+ })),
501
+ )
502
+ .sort((a, b) => a.start.localeCompare(b.start));
503
+ const result: Record<string, unknown> = { library, routines: routines.length, matches: listed(matches) };
504
+ if (out === undefined) return result;
505
+ const db = openForRead();
506
+ let suggestions: ReturnType<typeof suggest>;
507
+ try {
508
+ suggestions = suggest(
509
+ found,
510
+ {
511
+ symbolAt: (at) => symbolAt(db, at),
512
+ symbolNamed: (name) => symbolNamed(db, name),
513
+ regionsOverlapping: (start, end) => regionsOverlapping(db, start, end),
514
+ commentsAt: (at) => commentsAt(db, at),
515
+ referencesFrom: (at) => referencesFrom(db, at),
516
+ operandAt: (at) => operandAt(db, at),
517
+ symbolSuppressed: (at) => symbolSuppressed(db, at),
518
+ regionSuppressed: (start, end) => suppressionsOverlapping(db, "region", start, end).length > 0,
519
+ referenceSuppressed: (reference) => referenceSuppressed(db, reference),
520
+ },
521
+ image,
522
+ platform(),
523
+ );
524
+ } finally {
525
+ db?.close();
526
+ }
527
+ 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" });
529
+ return {
530
+ ...result,
531
+ batch: { out, writes: batch.length, known, leftOut: listed(leftOut) },
532
+ ...(batch.length === 0 ? {} : { apply: `knowledge.ts apply ${out} --reason "names of library routines"` }),
80
533
  };
81
534
  }
82
535
 
83
- await runScript(() => run(process.argv.slice(2)), { usage: USAGE });
536
+ await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError, LibraryError] });