@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
@@ -7,10 +7,11 @@
7
7
  import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { join, resolve } from "node:path";
9
9
 
10
+ import { Refusal } from "#src/errors.ts";
10
11
  import { formatC64Address, instructionLength } from "#src/c64.ts";
11
- import { evaluate, ExpressionError, identifiers, operandText } from "#src/knowledge/expression.ts";
12
+ import { evaluate, identifiers, operandText } from "#src/knowledge/expression.ts";
12
13
  import type { CommentRow, OperandRow, ReferenceRow, RegionRow, SymbolRow } from "#src/knowledge/read.ts";
13
- import { COMMENT_PLACEMENTS, OPERAND_SELECTS, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type OperandSelect, type ReferenceKind, type RegionType, type SymbolKind } from "#src/knowledge/schema.ts";
14
+ import { COMMENT_PLACEMENTS, type CommentPlacement, isSemantic, OPERAND_SELECTS, type OperandSelect, REFERENCE_KINDS, type ReferenceKind, REGION_TYPES, type RegionType, SYMBOL_KINDS, type SymbolKind } from "#src/knowledge/schema.ts";
14
15
  import { PROJECT_DIRECTORY } from "#src/project.ts";
15
16
 
16
17
  /** A routine shorter than this is not saved and not searched: its bytes occur by chance. */
@@ -22,17 +23,6 @@ const ROUTINE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
22
23
  /** Bytes on each line of a routine file, so that git shows a small diff. */
23
24
  const BYTES_PER_LINE = 16;
24
25
 
25
- /** A refusal of the library rules, with a stable code. */
26
- export class LibraryError extends Error {
27
- override name = "LibraryError";
28
- readonly code: "invalid-input" | "not-found" | "conflict";
29
-
30
- constructor(code: "invalid-input" | "not-found" | "conflict", message: string) {
31
- super(message);
32
- this.code = code;
33
- }
34
- }
35
-
36
26
  /**
37
27
  * Bytes that move with the routine: a word, or the low or high byte, of the
38
28
  * address at `value` (an offset from the routine start; -1 for the RTS entry
@@ -110,9 +100,9 @@ export interface LeftOut {
110
100
  export function routineFrom(source: RoutineSource): { routine: Routine; leftOut: LeftOut[] } {
111
101
  const { start, end } = source;
112
102
  const where = (address: number) => formatC64Address(address);
113
- if (!ROUTINE_NAME.test(source.name)) throw new LibraryError("invalid-input", `A routine name is a letter or underscore, then letters, digits or underscores (at most 64), not ${source.name}.`);
103
+ if (!ROUTINE_NAME.test(source.name)) throw new Refusal("invalid-input", `A routine name is a letter or underscore, then letters, digits or underscores (at most 64), not ${source.name}.`);
114
104
  const length = end - start + 1;
115
- if (length < MIN_ROUTINE) throw new LibraryError("invalid-input", `${where(start)}-${where(end)} has ${length} bytes. A routine has at least ${MIN_ROUTINE}, because shorter byte sequences occur by chance.`);
105
+ if (length < MIN_ROUTINE) throw new Refusal("invalid-input", `${where(start)}-${where(end)} has ${length} bytes. A routine has at least ${MIN_ROUTINE}, because shorter byte sequences occur by chance.`);
116
106
  const inside = (address: number) => address >= start && address <= end;
117
107
  const bytes = Uint8Array.from({ length }, (_, index) => source.byteAt(start + index));
118
108
  const leftOut: LeftOut[] = [];
@@ -125,11 +115,11 @@ export function routineFrom(source: RoutineSource): { routine: Routine; leftOut:
125
115
  for (const region of codeRegions) {
126
116
  for (let at = Math.max(region.start, next); at <= Math.min(region.end, end); at = next) {
127
117
  next = at + instructionLength(bytes[at - start]!);
128
- if (next - 1 > end) throw new LibraryError("invalid-input", `The instruction at ${where(at)} runs past the end ${where(end)} of the routine. Give an end after it.`);
118
+ if (next - 1 > end) throw new Refusal("invalid-input", `The instruction at ${where(at)} runs past the end ${where(end)} of the routine. Give an end after it.`);
129
119
  instructions.push(at - start);
130
120
  }
131
121
  }
132
- if (instructions.length === 0) throw new LibraryError("invalid-input", `The knowledge has no code region in ${where(start)}-${where(end)}. Run c64-static-analysis first, or classify the code.`);
122
+ if (instructions.length === 0) throw new Refusal("invalid-input", `The knowledge has no code region in ${where(start)}-${where(end)}. Run c64-static-analysis first, or classify the code.`);
133
123
 
134
124
  // The operands: an address inside the routine moves with it; one outside may differ.
135
125
  const relocations = new Map<number, Relocation>();
@@ -246,7 +236,7 @@ export function routineText(routine: Routine): string {
246
236
  /** Reads the text of a routine file. Throws LibraryError for a file that is no routine. */
247
237
  export function parseRoutine(text: string, file: string): Routine {
248
238
  const fail = (what: string): never => {
249
- throw new LibraryError("invalid-input", `${file} is not a routine file of the library: ${what}.`);
239
+ throw new Refusal("invalid-input", `${file} is not a routine file of the library: ${what}.`);
250
240
  };
251
241
  let value: Record<string, unknown>;
252
242
  try {
@@ -305,15 +295,15 @@ export function parseRoutine(text: string, file: string): Routine {
305
295
  * `create`, a missing folder is made.
306
296
  */
307
297
  export function libraryFolder(folder: string, options: { create: boolean; cwd?: string }): string {
308
- if (folder.length === 0) throw new LibraryError("invalid-input", "The library folder must not be empty.");
298
+ if (folder.length === 0) throw new Refusal("invalid-input", "The library folder must not be empty.");
309
299
  const path = resolve(options.cwd ?? process.cwd(), folder);
310
300
  const stat = statSync(path, { throwIfNoEntry: false });
311
301
  if (stat === undefined) {
312
- if (!options.create) throw new LibraryError("not-found", `There is no library folder ${folder}. Save a routine to it first with routine.`);
302
+ if (!options.create) throw new Refusal("not-found", `There is no library folder ${folder}. Save a routine to it first with routine.`);
313
303
  mkdirSync(path, { recursive: true });
314
- } else if (!stat.isDirectory()) throw new LibraryError("invalid-input", `${folder} is not a folder.`);
304
+ } else if (!stat.isDirectory()) throw new Refusal("invalid-input", `${folder} is not a folder.`);
315
305
  if (statSync(join(path, PROJECT_DIRECTORY), { throwIfNoEntry: false }) !== undefined) {
316
- throw new LibraryError("invalid-input", `${folder} is a project folder. A library is a plain folder beside the projects, for example library/ at the workspace root.`);
306
+ throw new Refusal("invalid-input", `${folder} is a project folder. A library is a plain folder beside the projects, for example library/ at the workspace root.`);
317
307
  }
318
308
  return path;
319
309
  }
@@ -331,7 +321,7 @@ export function readLibrary(folder: string): Routine[] {
331
321
  export function saveRoutine(folder: string, routine: Routine, replace: boolean): string {
332
322
  const file = join(folder, `${routine.name}.json`);
333
323
  if (!replace && statSync(file, { throwIfNoEntry: false }) !== undefined) {
334
- throw new LibraryError("conflict", `The library already has the routine ${routine.name}. Give another --name, or give --replace to save over it.`);
324
+ throw new Refusal("conflict", `The library already has the routine ${routine.name}. Give another --name, or give --replace to save over it.`);
335
325
  }
336
326
  const temporary = `${file}.${process.pid}.tmp`;
337
327
  writeFileSync(temporary, routineText(routine));
@@ -449,7 +439,7 @@ export interface Suggestions {
449
439
  leftOut: Skipped[];
450
440
  }
451
441
 
452
- const semantic = (row: { origin: string }) => row.origin === "user" || row.origin === "llm";
442
+ const semantic = (row: { origin: string }) => isSemantic(row.origin);
453
443
 
454
444
  /**
455
445
  * The names, regions, comments, references and operand expressions of each
@@ -573,7 +563,7 @@ function operandProblem(address: number, text: string, image: Searched, resolveN
573
563
  try {
574
564
  value = evaluate(text, resolveName);
575
565
  } catch (error) {
576
- if (error instanceof ExpressionError) return error.message;
566
+ if (error instanceof Refusal) return error.message;
577
567
  throw error;
578
568
  }
579
569
  const range = size === 2 ? 0x100 : 0x10000;
@@ -4,14 +4,15 @@
4
4
  // Watchpoints and range breakpoints stop the run at an access; the coverage
5
5
  // map then tells which range and which access it was.
6
6
 
7
- import { COVERAGE, formatC64Address, instructionLength } from "#src/c64.ts";
7
+ import { type AddressRange, COVERAGE, formatC64Address, instructionLength } from "#src/c64.ts";
8
8
  import { runProgram } from "#src/host-client/autostart.ts";
9
9
  import { readCoverage } from "#src/host-client/coverage.ts";
10
+ import { startAtEntry, type EntryState } from "#src/host-client/entry-state.ts";
10
11
  import { runActionStep, type ActionStep } from "#src/host-client/steps.ts";
11
- import { MAX_MEMORY_READ, MAX_MEMORY_WRITE } from "#src/host-client/tools.ts";
12
+ import { knowledgeAt } from "#src/knowledge/names.ts";
13
+ import { MAX_MEMORY_READ } from "#src/host-client/tools.ts";
12
14
  import { readProjectFile } from "#src/host-client/transfer.ts";
13
15
  import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
14
- import type { Range } from "./compare.ts";
15
16
 
16
17
  export type FirstAccess = "read" | "write" | "execute" | "none";
17
18
 
@@ -21,26 +22,12 @@ export interface RangeAccess {
21
22
  changed: boolean;
22
23
  }
23
24
 
24
- /** How a release starts in the emulator. */
25
+ /** How a release starts in the emulator: autostart of a PRG, or the captured program at its entry. */
25
26
  export type Start =
26
27
  | { kind: "program"; path: string }
27
- | { kind: "image"; segments: Range[]; byteAt: (address: number) => number; state: ImageState };
28
-
29
- export interface ImageState {
30
- pc: number;
31
- a: number;
32
- x: number;
33
- y: number;
34
- sp: number;
35
- flags: { n: boolean; v: boolean; d: boolean; i: boolean; z: boolean; c: boolean };
36
- port: [number, number];
37
- /** $0000-$03ff at the entry. */
38
- pages: Uint8Array;
39
- }
28
+ | { kind: "image"; segments: AddressRange[]; byteAt: (address: number) => number; state: EntryState };
40
29
 
41
30
  const MAX_WATCH = 256;
42
- /** Frames until the KERNAL is at the READY prompt, before an image is written. */
43
- const READY_FRAMES = 150;
44
31
 
45
32
  const READ = COVERAGE.ramRead | COVERAGE.romRead | COVERAGE.ioRead;
46
33
  const WRITE = COVERAGE.ramWrite | COVERAGE.ioWrite;
@@ -62,20 +49,7 @@ async function start(client: ViceSessionClient, how: Start): Promise<string | un
62
49
  if (failed === "not-started") return "The program did not start after RUN; it needs a BASIC start line.";
63
50
  return undefined;
64
51
  }
65
- await client.reset({ mode: "hard", run: true });
66
- await client.execution({ action: "advance-frames", count: READY_FRAMES, space: "c64" });
67
- for (const segment of how.segments) {
68
- for (let at = segment.start; at <= segment.end; at += MAX_MEMORY_WRITE) {
69
- const last = Math.min(segment.end, at + MAX_MEMORY_WRITE - 1);
70
- const bytes = Buffer.from(Array.from({ length: last - at + 1 }, (_, offset) => how.byteAt(at + offset)));
71
- await client.memoryWrite({ address: at, data: bytes.toString("hex"), space: "c64", view: "ram" });
72
- }
73
- }
74
- const { state } = how;
75
- // Pages 0 to 3 from $0002, then the CPU port: the port bits switch what the CPU sees.
76
- await client.memoryWrite({ address: 0x0002, data: Buffer.from(state.pages.subarray(2)).toString("hex"), space: "c64", view: "ram" });
77
- await client.memoryWrite({ address: 0x0000, data: Buffer.from(state.port).toString("hex"), space: "c64", view: "cpu" });
78
- await client.registersSet("c64", { pc: state.pc, a: state.a, x: state.x, y: state.y, sp: state.sp, flags: state.flags });
52
+ await startAtEntry(client, how, how.state);
79
53
  return undefined;
80
54
  }
81
55
 
@@ -86,7 +60,7 @@ async function start(client: ViceSessionClient, how: Start): Promise<string | un
86
60
  export async function watchRelease(
87
61
  client: ViceSessionClient,
88
62
  how: Start,
89
- ranges: Range[],
63
+ ranges: AddressRange[],
90
64
  release: (address: number) => number,
91
65
  run: { frames: number; steps: ActionStep[] },
92
66
  ): Promise<{ started: true; accesses: RangeAccess[] } | { started: false; reason: string }> {
@@ -151,6 +125,7 @@ export async function watchRelease(
151
125
  if (left > 0 && !(await settle())) left--;
152
126
  }
153
127
  };
128
+ const at = knowledgeAt();
154
129
  for (const step of run.steps) {
155
130
  if ("frames" in step) {
156
131
  await runFrames(step.frames);
@@ -159,13 +134,13 @@ export async function watchRelease(
159
134
  if ("runUntil" in step) {
160
135
  // A stop at a point is not the target: settle and run to the target again.
161
136
  while (guard-- > 0) {
162
- const reached = await runActionStep(client, step, (location) => Number.parseInt(location.slice(1), 16));
137
+ const reached = await runActionStep(client, step, at);
163
138
  if (reached === undefined || reached.stopReason !== "watchpoint" && reached.stopReason !== "breakpoint") break;
164
139
  await settle();
165
140
  }
166
141
  continue;
167
142
  }
168
- await runActionStep(client, step, (location) => Number.parseInt(location.slice(1), 16));
143
+ await runActionStep(client, step, at);
169
144
  }
170
145
  await runFrames(run.frames);
171
146
  if (points.size > 0) await settle();
@@ -188,4 +163,4 @@ export async function watchRelease(
188
163
  return { started: true, accesses };
189
164
  }
190
165
 
191
- export const showRange = (range: Range) => `${formatC64Address(range.start)}-${formatC64Address(range.end)}`;
166
+ export const showRange = (range: AddressRange) => `${formatC64Address(range.start)}-${formatC64Address(range.end)}`;
@@ -4,31 +4,30 @@
4
4
  // entry; and what the program reads or executes outside the image. Evidence
5
5
  // only: nothing here decides what is original.
6
6
 
7
- import { COVERAGE, executedRamCode } from "#src/c64.ts";
8
- import type { Range } from "./compare.ts";
7
+ import { type AddressRange, COVERAGE, executedRamCode } from "#src/c64.ts";
9
8
 
10
9
  export type Access = "executed" | "read" | "written" | "read-written" | "untouched";
11
10
 
12
11
  export interface SeparationInput {
13
- segments: Range[];
12
+ segments: AddressRange[];
14
13
  /** 64 KiB with the image bytes at their addresses. */
15
14
  memory: Uint8Array;
16
15
  entry?: number;
17
16
  /** The stages of an unpacker capture, in order; the last one is the program. */
18
- stages?: Array<{ entry: number; wrote: Range[] }>;
17
+ stages?: Array<{ entry: number; wrote: AddressRange[] }>;
19
18
  /** Joined coverage files. */
20
19
  coverage?: Uint8Array;
21
- codeRegions: Array<Range & { type: string }>;
20
+ codeRegions: Array<AddressRange & { type: string }>;
22
21
  references: Array<{ from: number; to: number; kind: string }>;
23
22
  }
24
23
 
25
- export interface EvidenceRange extends Range {
24
+ export interface EvidenceRange extends AddressRange {
26
25
  /** The last stage that wrote the range; absent when no stage wrote it. */
27
26
  stage?: number;
28
27
  access?: Access;
29
28
  }
30
29
 
31
- export interface CodeRegionEvidence extends Range {
30
+ export interface CodeRegionEvidence extends AddressRange {
32
31
  type: string;
33
32
  /** Absent when the image has no entry. */
34
33
  reachable?: boolean;
@@ -45,14 +44,14 @@ export interface Separation {
45
44
  * those bytes there), and `generated` it wrote and then executed, in
46
45
  * pieces of code with gaps of up to two operand bytes.
47
46
  */
48
- outside: { read: Range[]; executed: Range[]; generated: Range[] };
47
+ outside: { read: AddressRange[]; executed: AddressRange[]; generated: AddressRange[] };
49
48
  }
50
49
 
51
50
  const SYSTEM_PAGES_END = 0x03ff;
52
51
 
53
52
  /** Joins addresses in order into ranges; `gap` is the most bytes between two addresses in one range. */
54
- function merge(addresses: number[], gap = 0, joins: (from: number, to: number) => boolean = () => true): Range[] {
55
- const ranges: Range[] = [];
53
+ function merge(addresses: number[], gap = 0, joins: (from: number, to: number) => boolean = () => true): AddressRange[] {
54
+ const ranges: AddressRange[] = [];
56
55
  for (const address of addresses) {
57
56
  const last = ranges.at(-1);
58
57
  if (last !== undefined && address - last.end - 1 <= gap && joins(last.end, address)) last.end = address;