@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
@@ -3,27 +3,21 @@
3
3
  // knowledge; a target that knowledge does not name gets l_xxxx. Data is
4
4
  // written by its region type. The source assembles to the same bytes: the
5
5
  // generator forces operand sizes, and writes an opcode form that ACME
6
- // encodes differently as bytes (see the probe in generate.ts). An operand
6
+ // encodes differently as bytes (see the probe in source.ts). An operand
7
7
  // expression of the knowledge is written in place of its number. A
8
8
  // patch is written as !if SWITCH { code } else { original }, and a
9
9
  // macro use as one +name args line.
10
10
 
11
- import { COVERAGE, formatC64Address, POINTER_TABLE_TYPES, pointerTableEntries, type PointerTableType } from "#src/c64.ts";
11
+ import { type AddressRange, COVERAGE, formatC64Address, formatC64Byte, POINTER_TABLE_TYPES, pointerTableEntries, type PointerTableType } from "#src/c64.ts";
12
12
  import { WireFailure } from "#src/host-client/tools.ts";
13
13
  import { identifiers, operandText, replaceIdentifiers, type OperandExpression } from "#src/knowledge/expression.ts";
14
- import { ANALYZERS } from "#src/knowledge/import.ts";
15
- import { isGeneratedFile, type CommentPlacement, type Origin, type RegionType } from "#src/knowledge/schema.ts";
14
+ import { type CommentPlacement, isAnalyzer, isGeneratedFile, type Origin, type RegionType } from "#src/knowledge/schema.ts";
16
15
  import type { Area } from "#src/platform.ts";
17
16
  import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, USE_KINDS, useKind, type Mode, type UseKind } from "#src/opcodes.ts";
18
17
 
19
- export interface Range {
20
- start: number;
21
- end: number;
22
- }
23
-
24
18
  export interface SourceInput {
25
19
  /** In address order, without overlap. */
26
- segments: Range[];
20
+ segments: AddressRange[];
27
21
  /** The image byte at an address inside a segment. */
28
22
  byteAt: (address: number) => number;
29
23
  entry?: number;
@@ -144,7 +138,7 @@ function decodedStores(pieces: readonly Piece[], input: SourceInput): Array<{ ad
144
138
  if (input.segments.some((segment) => target >= segment.start && target <= segment.end)) continue;
145
139
  try {
146
140
  const text = decodedText(input.decode(target, input.byteAt(load.address + 1)));
147
- if (text !== "") found.push({ address: store.address, placement: "side", text: `${hex4(target)}: ${text}` });
141
+ if (text !== "") found.push({ address: store.address, placement: "side", text: `${formatC64Address(target)}: ${text}` });
148
142
  } catch {
149
143
  // No decoder for this register.
150
144
  }
@@ -212,10 +206,8 @@ export interface Layout {
212
206
  macrosAsInstructions: MacroAsInstructions[];
213
207
  }
214
208
 
215
- const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
216
- const hex4 = (value: number) => formatC64Address(value);
217
209
  /** ACME reads leading zeros as a 16-bit value, so a zero-page name gets two digits. */
218
- const constantValue = (address: number) => (address < 0x100 ? hex2(address) : hex4(address));
210
+ const constantValue = (address: number) => (address < 0x100 ? formatC64Byte(address) : formatC64Address(address));
219
211
  const signed = (byte: number) => (byte < 0x80 ? byte : byte - 0x100);
220
212
 
221
213
  function operandValue(address: number, opcode: number, byteAt: (address: number) => number): number | undefined {
@@ -240,9 +232,9 @@ export function entriesInData<R extends { start: number; end: number; type: Regi
240
232
  }
241
233
 
242
234
  /** Why an image without its entry fails: the image does not hold the program. Undefined when the entry is in a segment, or the image has no entry. */
243
- export function entryOutsideImage(entry: number | undefined, segments: readonly Range[]): string | undefined {
235
+ export function entryOutsideImage(entry: number | undefined, segments: readonly AddressRange[]): string | undefined {
244
236
  if (entry === undefined || segments.some((segment) => entry >= segment.start && entry <= segment.end)) return undefined;
245
- return `The entry ${hex4(entry)} is in no segment of the image: the image does not hold the program. Capture it again with c64-unpacker loads or capture.`;
237
+ return `The entry ${formatC64Address(entry)} is in no segment of the image: the image does not hold the program. Capture it again with c64-unpacker loads or capture.`;
246
238
  }
247
239
 
248
240
  /** Splits the segments into pieces at module starts and lays out instructions and data. */
@@ -255,7 +247,7 @@ export function layout(input: SourceInput): Layout {
255
247
  };
256
248
 
257
249
  for (const module of input.modules) {
258
- if (isGeneratedFile(module.file)) throw new WireFailure("invalid-input", `The module at ${hex4(module.start)} has the file ${module.file}, which the generator writes itself. Give the module another file.`);
250
+ if (isGeneratedFile(module.file)) throw new WireFailure("invalid-input", `The module at ${formatC64Address(module.start)} has the file ${module.file}, which the generator writes itself. Give the module another file.`);
259
251
  }
260
252
  const modules = [...input.modules].sort((a, b) => a.start - b.start);
261
253
  const unusedModules = modules.filter((module) => !inImage(module.start)).map(({ start, file }) => ({ start, file }));
@@ -274,14 +266,14 @@ export function layout(input: SourceInput): Layout {
274
266
  const end = bounds[index + 1]! - 1;
275
267
  const module = modules.find((row) => row.start === start);
276
268
  const file = module?.file ?? `segment_${start.toString(16).padStart(4, "0")}.asm`;
277
- const title = module?.title?.replace(/\s*\r?\n\s*/g, " ") ?? `Segment ${hex4(segment.start)}-${hex4(segment.end)}`;
269
+ const title = module?.title?.replace(/\s*\r?\n\s*/g, " ") ?? `Segment ${formatC64Address(segment.start)}-${formatC64Address(segment.end)}`;
278
270
  pieces.push({ start, end, file, title, segmentStart: index === 0, items: [] });
279
271
  }
280
272
  }
281
273
  const files = new Map<string, number>();
282
274
  for (const piece of pieces) {
283
275
  const other = files.get(piece.file);
284
- if (other !== undefined) throw new WireFailure("invalid-input", `The file ${piece.file} is used at ${hex4(other)} and at ${hex4(piece.start)}. Give the module another file.`);
276
+ if (other !== undefined) throw new WireFailure("invalid-input", `The file ${piece.file} is used at ${formatC64Address(other)} and at ${formatC64Address(piece.start)}. Give the module another file.`);
285
277
  files.set(piece.file, piece.start);
286
278
  }
287
279
 
@@ -329,7 +321,6 @@ export function layout(input: SourceInput): Layout {
329
321
  const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
330
322
  const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && !MNEMONICS.has(name.toLowerCase())).map(({ address, name }) => [address, name]));
331
323
  // An analyzer name (DXA's ld020) gives way to the platform name (EXTCOL); a user or LLM name wins.
332
- const isAnalyzer = (origin: Origin | undefined) => (ANALYZERS as readonly (Origin | undefined)[]).includes(origin);
333
324
  const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
334
325
  /** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry. */
335
326
  const insideEntry = new Map<number, number>();
@@ -444,7 +435,7 @@ export function layout(input: SourceInput): Layout {
444
435
  if (inside === undefined) continue;
445
436
  accepted.delete(candidate);
446
437
  changed = true;
447
- macrosAsInstructions.push({ start: candidate.use.start, end: candidate.use.end, macro: candidate.use.macro, reason: `The source has a label at ${hex4(inside)}, inside the use.` });
438
+ macrosAsInstructions.push({ start: candidate.use.start, end: candidate.use.end, macro: candidate.use.macro, reason: `The source has a label at ${formatC64Address(inside)}, inside the use.` });
448
439
  for (const item of candidate.items) {
449
440
  inUse.delete(item.address);
450
441
  if (takesAddress(OPCODES[item.opcode]!.mode) && !written.has(item.address)) use(operandValue(item.address, item.opcode, input.byteAt)!, useKind(item.opcode));
@@ -459,7 +450,7 @@ export function layout(input: SourceInput): Layout {
459
450
  for (const address of [...wanted, ...outside].sort((a, b) => a - b)) {
460
451
  const known = symbols.get(address);
461
452
  if (known !== undefined) {
462
- if (MNEMONICS.has(known.toLowerCase())) throw new WireFailure("invalid-input", `The symbol ${known} at ${hex4(address)} has the name of an instruction, which ACME cannot read as a label. Rename the symbol with c64-knowledge.`);
453
+ if (MNEMONICS.has(known.toLowerCase())) throw new WireFailure("invalid-input", `The symbol ${known} at ${formatC64Address(address)} has the name of an instruction, which ACME cannot read as a label. Rename the symbol with c64-knowledge.`);
463
454
  names.set(address, known);
464
455
  if (inImage(address)) namedLabels++;
465
456
  continue;
@@ -488,14 +479,14 @@ export function layout(input: SourceInput): Layout {
488
479
  continue;
489
480
  }
490
481
  if (patch.end > piece.end) {
491
- refuse(`The range runs past the end of the file ${piece.file} at ${hex4(piece.end)}: a patch is in one file.`);
482
+ refuse(`The range runs past the end of the file ${piece.file} at ${formatC64Address(piece.end)}: a patch is in one file.`);
492
483
  continue;
493
484
  }
494
485
  const starts = new Set(piece.items.map((item) => (item.kind === "instruction" ? item.address : item.start)));
495
486
  const ends = new Set(piece.items.map((item) => (item.kind === "instruction" ? item.address + item.length - 1 : item.end)));
496
487
  if (!starts.has(patch.start) || !ends.has(patch.end)) {
497
488
  const cut = piece.items.find((item) => item.kind === "instruction" && ((patch.start > item.address && patch.start < item.address + item.length) || (patch.end >= item.address && patch.end < item.address + item.length - 1)));
498
- refuse(cut === undefined ? "The range does not start and end at an item of the source." : `The range cuts the instruction at ${hex4((cut as InstructionItem).address)}.`);
489
+ refuse(cut === undefined ? "The range does not start and end at an item of the source." : `The range cuts the instruction at ${formatC64Address((cut as InstructionItem).address)}.`);
499
490
  continue;
500
491
  }
501
492
  if (sourceNames.has(patch.switch)) {
@@ -607,7 +598,7 @@ export function instructionText(opcode: number, suffix: string, expression: stri
607
598
  /** The plain number of an operand: what the probe assembles, and what a comment shows. */
608
599
  export function literalOperand(opcode: number, value: number): string {
609
600
  const { mode } = OPCODES[opcode]!;
610
- return MODE_LENGTH[mode] === 3 || mode === "rel" ? hex4(value) : hex2(value);
601
+ return MODE_LENGTH[mode] === 3 || mode === "rel" ? formatC64Address(value) : formatC64Byte(value);
611
602
  }
612
603
 
613
604
  /** One instruction of each form, for the probe: the form, and the instruction bytes. */
@@ -659,9 +650,9 @@ function cellsOf(item: DataItem, layout: Layout, breaks: Set<number>): Cell[] {
659
650
  const { byteAt } = layout;
660
651
  const cells: Cell[] = [];
661
652
  const name = (address: number) => layout.names.get(address);
662
- const expression = (target: number) => name(target) ?? hex4(target);
663
- const rtsExpression = (target: number) => (name(target) === undefined ? hex4((target - 1) & 0xffff) : `${name(target)}-1`);
664
- const byteCell = (address: number, text = hex2(byteAt(address))): Cell => ({ address, length: 1, directive: "!byte", text });
653
+ const expression = (target: number) => name(target) ?? formatC64Address(target);
654
+ const rtsExpression = (target: number) => (name(target) === undefined ? formatC64Address((target - 1) & 0xffff) : `${name(target)}-1`);
655
+ const byteCell = (address: number, text = formatC64Byte(byteAt(address))): Cell => ({ address, length: 1, directive: "!byte", text });
665
656
 
666
657
  if (item.region !== undefined && POINTER_TYPES.includes(item.type)) {
667
658
  const rts = item.type === "rts-addresses" || item.type === "rts-addresses-split";
@@ -696,7 +687,7 @@ function cellsOf(item: DataItem, layout: Layout, breaks: Set<number>): Cell[] {
696
687
  const offset = address - (item.region?.start ?? item.start);
697
688
  const last = odd && address === item.region!.end;
698
689
  if (offset % 2 === 0 && !last && address + 1 <= item.end && !breaks.has(address + 1)) {
699
- cells.push({ address, length: 2, directive: "!word", text: hex4(byteAt(address) | (byteAt(address + 1) << 8)) });
690
+ cells.push({ address, length: 2, directive: "!word", text: formatC64Address(byteAt(address) | (byteAt(address + 1) << 8)) });
700
691
  address++;
701
692
  } else cells.push(byteCell(address));
702
693
  }
@@ -721,12 +712,12 @@ function cellsOf(item: DataItem, layout: Layout, breaks: Set<number>): Cell[] {
721
712
  let end = address;
722
713
  while (end + 1 <= item.end && byteAt(end + 1) === value && !breaks.has(end + 1)) end++;
723
714
  if (end - address + 1 >= MIN_FILL) {
724
- cells.push({ address, length: end - address + 1, directive: "!fill", text: `${end - address + 1}, ${hex2(value)}` });
715
+ cells.push({ address, length: end - address + 1, directive: "!fill", text: `${end - address + 1}, ${formatC64Byte(value)}` });
725
716
  address = end;
726
717
  continue;
727
718
  }
728
719
  }
729
- cells.push(byteCell(address, binary ? `%${value.toString(2).padStart(8, "0")}` : hex2(value)));
720
+ cells.push(byteCell(address, binary ? `%${value.toString(2).padStart(8, "0")}` : formatC64Byte(value)));
730
721
  }
731
722
  return cells;
732
723
  }
@@ -779,7 +770,7 @@ function describeConstant(layout: Layout, address: number): string | undefined {
779
770
  const mirrorOf = layout.describe?.(address).mirrorOf;
780
771
  if (mirrorOf === undefined) return undefined;
781
772
  const register = layout.platform.find((row) => row.address === mirrorOf);
782
- return `Mirror of ${hex4(mirrorOf)}${register === undefined ? "" : ` (${register.name})`}.`;
773
+ return `Mirror of ${formatC64Address(mirrorOf)}${register === undefined ? "" : ` (${register.name})`}.`;
783
774
  }
784
775
 
785
776
  /** How the source uses a constant, for example "Used: read 4, written 1." */
@@ -801,7 +792,7 @@ function switchLines(layout: Layout): string[] {
801
792
  const width = Math.max(...[...switches.keys()].map((name) => name.length));
802
793
  const lines = ["; Switches: each is 0 unless the build defines it. Assemble with -DNAME=1", "; (c64-assembler --define NAME=1) to put in the patches of NAME."];
803
794
  for (const [name, patches] of switches) {
804
- const ranges = patches.map((patch) => `${hex4(patch.start)}-${hex4(patch.end)}`).join(", ");
795
+ const ranges = patches.map((patch) => `${formatC64Address(patch.start)}-${formatC64Address(patch.end)}`).join(", ");
805
796
  lines.push(`!ifndef ${name} { ${name} = 0 }${" ".repeat(width - name.length)} ; ${patches.length} patch${patches.length === 1 ? "" : "es"}: ${ranges}`);
806
797
  }
807
798
  return [...lines, ""];
@@ -829,7 +820,7 @@ function constantsText(layout: Layout): { text: string; labels: SourceLabel[] }
829
820
  const next = describe?.(row.address).area;
830
821
  if (next !== undefined && next.start !== area?.start) {
831
822
  area = next;
832
- out.push("", `; ${area.name[0]!.toUpperCase()}${area.name.slice(1)} ${hex4(area.start)}-${hex4(area.end)}`);
823
+ out.push("", `; ${area.name[0]!.toUpperCase()}${area.name.slice(1)} ${formatC64Address(area.start)}-${formatC64Address(area.end)}`);
833
824
  if (area.meaning !== undefined) out.push(`; ${area.meaning}`);
834
825
  }
835
826
  const line = `${row.name.padEnd(nameColumn)} = ${row.value.padEnd(valueColumn)}`;
@@ -865,7 +856,7 @@ export interface Rendered {
865
856
  /** Instructions written as bytes, because ACME writes their form with other bytes. */
866
857
  asBytes: Array<{ address: number; text: string }>;
867
858
  /** With coverage: runs of instructions that no run executed. */
868
- notExecuted?: { instructions: number; ranges: Range[] };
859
+ notExecuted?: { instructions: number; ranges: AddressRange[] };
869
860
  /** Each instruction written with an operand expression: its file and line (1 is the first line of the text). */
870
861
  expressions: Array<{ address: number; length: number; file: string; line: number }>;
871
862
  /** Operand expressions written as numbers, in address order. */
@@ -900,7 +891,7 @@ function macrosText(layout: Layout): { text: string; lines: Map<string, { first:
900
891
  /** The lines that open a patch: its zone, the switch, the code, and the guard that stops a patch longer than its range. */
901
892
  function patchOpening(patch: PatchBlock): string[] {
902
893
  const bytes = patch.end - patch.start + 1;
903
- const range = `${hex4(patch.start)}-${hex4(patch.end)}`;
894
+ const range = `${formatC64Address(patch.start)}-${formatC64Address(patch.end)}`;
904
895
  const comment = patch.comment === undefined ? [] : patch.comment.split(/\r?\n/).map((line) => `${INDENT}; ${line}`.trimEnd());
905
896
  return [
906
897
  `!zone patch_${patch.switch}_${patch.start.toString(16).padStart(4, "0")} {`,
@@ -909,7 +900,7 @@ function patchOpening(patch: PatchBlock): string[] {
909
900
  `${INDENT}; ${patch.switch} on: ${bytes} byte${bytes === 1 ? "" : "s"} at ${range}, NOP after the code`,
910
901
  ...comment,
911
902
  ...(patch.code === "" ? [] : patch.code.split(/\r?\n/)),
912
- `${INDENT}!if * - .patch_start > ${bytes} { !error "The patch ${patch.switch} at ${hex4(patch.start)} has ", * - .patch_start, " bytes; the range has ${bytes}." } else { !fill ${bytes} - (* - .patch_start), $ea }`,
903
+ `${INDENT}!if * - .patch_start > ${bytes} { !error "The patch ${patch.switch} at ${formatC64Address(patch.start)} has ", * - .patch_start, " bytes; the range has ${bytes}." } else { !fill ${bytes} - (* - .patch_start), $ea }`,
913
904
  "} else {",
914
905
  ];
915
906
  }
@@ -957,10 +948,10 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
957
948
  result.macrosAsInstructions.sort((a, b) => a.start - b.start);
958
949
  const unwritten = new Map(layout.operandsAsNumbers.map((row) => [row.address, row.expression]));
959
950
  const { coverage } = layout;
960
- const notExecuted = { instructions: 0, ranges: [] as Range[] };
951
+ const notExecuted = { instructions: 0, ranges: [] as AddressRange[] };
961
952
 
962
953
  for (const piece of layout.pieces) {
963
- const out: string[] = [`; ${piece.title}`, `; ${hex4(piece.start)}-${hex4(piece.end)}`, ""];
954
+ const out: string[] = [`; ${piece.title}`, `; ${formatC64Address(piece.start)}-${formatC64Address(piece.end)}`, ""];
964
955
  /** True while the instructions before have not executed: they form one range. */
965
956
  let running = false;
966
957
  /** The line of the mark of the open range of instructions that did not execute. */
@@ -981,7 +972,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
981
972
  }
982
973
  // The mark gives the range, so a reader finds it in the full list.
983
974
  const range = notExecuted.ranges.at(-1)!;
984
- out[markLine] = `; not executed ${hex4(range.start)}-${hex4(range.end)}`;
975
+ out[markLine] = `; not executed ${formatC64Address(range.start)}-${formatC64Address(range.end)}`;
985
976
  notExecuted.instructions += count;
986
977
  }
987
978
  running = !executed;
@@ -1067,7 +1058,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
1067
1058
  if (written !== undefined && numbers.has(item.address)) asNumber(written, numbers.get(item.address)!);
1068
1059
  if (unwritten.has(item.address)) sides.unshift(`= ${unwritten.get(item.address)!}`);
1069
1060
  if (asBytes.has(form.key)) {
1070
- const bytes = Array.from({ length: item.length }, (_, offset) => hex2(layout.byteAt(item.address + offset)));
1061
+ const bytes = Array.from({ length: item.length }, (_, offset) => formatC64Byte(layout.byteAt(item.address + offset)));
1071
1062
  out.push(withSide(`${INDENT}!byte ${bytes.join(",")}`, [literal, ...sides].join(" ")));
1072
1063
  result.asBytes.push({ address: item.address, text: literal });
1073
1064
  } else if (written !== undefined) {
@@ -1111,7 +1102,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
1111
1102
  }
1112
1103
  const main = [...header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', ...(layout.macros.length > 0 ? ['!source "macros.asm"'] : [])];
1113
1104
  for (const piece of layout.pieces) {
1114
- if (piece.segmentStart) main.push("", `* = ${hex4(piece.start)}`);
1105
+ if (piece.segmentStart) main.push("", `* = ${formatC64Address(piece.start)}`);
1115
1106
  main.push(`!source "${piece.file}"`);
1116
1107
  }
1117
1108
  result.files.push({ name: "main.asm", text: `${main.join("\n")}\n` });
@@ -4,55 +4,38 @@
4
4
  // also writes a loader that recreates the entry state, simulates it, and
5
5
  // checks it in the emulator. Each switch of the patches is built on
6
6
  // alone and checked. A macro use that does not give the bytes of the
7
- // image is written as its instructions. Exit status: 0 when every check
8
- // passes, 1 when one fails, 2 for a bad call.
7
+ // image is written as its instructions. result is PASS when every check
8
+ // passes, else FAIL.
9
9
 
10
10
  import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
11
11
  import { join } from "node:path";
12
- import { parseArgs } from "node:util";
13
12
 
14
- import { COVERAGE, coverageSeedKinds, formatC64Address, parseC64Address, textToPetscii } from "#src/c64.ts";
13
+ import { Refusal } from "#src/errors.ts";
14
+ import { type AddressRange, COVERAGE, coverageSeedKinds, formatC64Address, textToPetscii } from "#src/c64.ts";
15
15
  import { autostartLoaded, RUN_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
16
16
  import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
17
- import { writeProjectFile } from "#src/host-client/transfer.ts";
17
+ import { writeReported, type Written } from "#src/host-client/transfer.ts";
18
18
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
19
19
  import { conflicts } from "#src/knowledge/conflicts.ts";
20
- import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
20
+ import { openForRead } from "#src/knowledge/database.ts";
21
21
  import { identifiers } from "#src/knowledge/expression.ts";
22
22
  import { analyzedBytes, programCoverage, requireProgram, type ProgramImage } from "#src/knowledge/program.ts";
23
23
  import { ANALYZER_NAME_ORDER, currentRevision, listAnalyzerNames, listComments, listMacros, listMacroUses, listModules, listOperands, listPatches, listReferences, listRegions, listSymbols, type SymbolRow } from "#src/knowledge/read.ts";
24
- import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowledge/schema.ts";
24
+ import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
25
25
  import { assemble } from "#src/native/acme.ts";
26
26
  import { analyze } from "#src/native/ghidra/analyze.ts";
27
27
  import { localToolContext } from "#src/native/local.ts";
28
28
  import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
29
29
  import { decode as decodeRegister, describeAddress, platformNamesFor, romUseOf, romUseOfPort, type RomUse } from "#src/platform.ts";
30
30
  import { resolveProjectPath } from "#src/project.ts";
31
- import { runScript, UsageError } from "#src/script.ts";
32
- import { COPIER_SOURCE, LOADER_JUMP, loaderLimits, planLoader, rasterLine, readEntryState, simulate, stopCause, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./lib/loader.ts";
31
+ import { listed, OPTIONS, runScript, UsageError, type Call } from "#src/script.ts";
32
+ import { knowledgeNames } from "#src/knowledge/names.ts";
33
+ import { COPIER_SOURCE, LOADER_JUMP, loaderLimits, planLoader, rasterLine, loaderState, simulate, stopCause, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./lib/loader.ts";
33
34
  import { OPCODES } from "#src/opcodes.ts";
34
- import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Range, type Rendered } from "./lib/source.ts";
35
+ import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Rendered } from "./lib/source.ts";
35
36
 
36
- const USAGE = `generate.ts --out <dir> [options]
37
-
38
- The program, its entry state and the coverage come from the knowledge:
39
- put them in with knowledge.ts image and coverage of the c64-knowledge skill.
40
- Code that no imported run executed is marked and listed; <dir>/not_executed.json
41
- has all of its ranges.
42
-
43
- --out <dir> the directory for the source files, relative to the project
44
- --entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
45
- --space <space> the knowledge space of the program: c64 (the default), drive8,
46
- or bank0 to bank255; the coverage and --loader are for c64 only
47
- --loader also write loader.asm and loader.prg: the loader copies the
48
- segments into place, sets the entry state of the knowledge,
49
- and jumps to the entry; it is checked in the emulator
50
- --analyzer-names name an address that has no name of the user or the LLM, and
51
- no platform name, with its analyzer name
52
-
53
- The result is one JSON object. Addresses are $ and four hex digits.`;
54
-
55
- const MAX_LISTED = 20;
37
+ /** At most this many diagnostics or differences of one check. */
38
+ const MAX_DIAGNOSTICS = 20;
56
39
  /** Rounds of assembly that move operand expressions to numbers and macro uses to instructions. */
57
40
  const ROUNDS = 6;
58
41
  const PROBE_BASE = 0x1000;
@@ -66,7 +49,7 @@ function readKnowledge(space: Space, analyzerNames = false) {
66
49
  try {
67
50
  // With conflicts from a git merge, the source would depend on which of two rows comes first.
68
51
  const found = conflicts(db).length;
69
- if (found > 0) throw new KnowledgeError("conflict", `The knowledge has ${found} conflict${found === 1 ? "" : "s"} from a git merge. Nothing was generated. Run c64-knowledge's knowledge.ts merge, then generate again.`);
52
+ if (found > 0) throw new Refusal("conflict", `The knowledge has ${found} conflict${found === 1 ? "" : "s"} from a git merge. Nothing was generated. Run c64-knowledge's knowledge.ts merge, then generate again.`);
70
53
  const symbols = listSymbols(db, undefined, undefined, undefined, space);
71
54
  const second = listAnalyzerNames(db, undefined, undefined, undefined, space);
72
55
  return {
@@ -392,7 +375,7 @@ async function checkSwitch(name: string, patches: PatchBlock[], files: Array<{ n
392
375
  return found === null ? [] : [{ start: found[1]!, message: found[0] }];
393
376
  });
394
377
  if (overflow.length > 0) return { ...shown, result: "overflow", overflow };
395
- return { ...shown, result: "does not assemble", diagnostics: built.diagnostics.slice(0, MAX_LISTED) };
378
+ return { ...shown, result: "does not assemble", diagnostics: built.diagnostics.slice(0, MAX_DIAGNOSTICS) };
396
379
  }
397
380
  const inside = (address: number) => patches.some((patch) => address >= patch.start && address <= patch.end);
398
381
  let changed = 0;
@@ -499,7 +482,7 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
499
482
  }
500
483
  plan.cia.masks.forEach((mask, index) => differ(`CIA ${index + 1} interrupt mask`, chips[index]!.interruptMask, mask));
501
484
  }
502
- return { verified: differences.length === 0, differences: differences.slice(0, MAX_LISTED) };
485
+ return { verified: differences.length === 0, differences: differences.slice(0, MAX_DIAGNOSTICS) };
503
486
  } finally {
504
487
  await client.close();
505
488
  }
@@ -548,46 +531,21 @@ function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
548
531
  }
549
532
  }
550
533
 
551
- async function run(argv: string[]): Promise<{ output: Record<string, unknown>; failed: boolean }> {
552
- const { values, positionals } = parseArgs({
553
- args: argv,
554
- strict: true,
555
- allowPositionals: true,
556
- options: {
557
- out: { type: "string" },
558
- entry: { type: "string", multiple: true },
559
- space: { type: "string" },
560
- loader: { type: "boolean" },
561
- "analyzer-names": { type: "boolean" },
562
- help: { type: "boolean" },
563
- },
564
- });
565
- if (values.help) throw new UsageError("");
566
- if (positionals.length !== 0) throw new UsageError("generate takes no image: it reads the program from the knowledge (knowledge.ts image of the c64-knowledge skill puts it in)");
567
- if (values.out === undefined) throw new UsageError("generate needs --out <dir>");
568
- try {
569
- resolveProjectPath(values.out);
570
- } catch {
571
- throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
572
- }
573
- const entries = (values.entry ?? []).map((value) => {
574
- try {
575
- return parseC64Address(value);
576
- } catch {
577
- throw new UsageError(`--entry must be $ followed by four hex digits, not ${value}`);
578
- }
579
- });
580
- const space = values.space ?? DEFAULT_SPACE;
581
- if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
582
- if (space !== DEFAULT_SPACE && values.loader) throw new UsageError("--loader works only in space c64");
583
- const knowledge = readKnowledge(space, values["analyzer-names"] === true);
534
+ async function generate(call: Call): Promise<{ output: Record<string, unknown>; failed: boolean }> {
535
+ const out = call.string("out")!;
536
+ const entries = call.addresses("entry");
537
+ const max = call.count("max-listed")!;
538
+ const space = call.space;
539
+ const loaderWanted = call.flag("loader");
540
+ if (space !== DEFAULT_SPACE && loaderWanted) throw new UsageError("--loader works only in space c64");
541
+ const knowledge = readKnowledge(space, call.flag("analyzer-names"));
584
542
  const image = knowledge.program;
585
543
  const coverage = knowledge.coverage;
586
- if (values.loader && (image.state === undefined || image.entry === undefined)) {
587
- throw new UsageError("--loader needs the entry and the entry state of a c64-unpacker capture in the knowledge: put the capture in with knowledge.ts image <image.json>");
544
+ if (loaderWanted && (image.state === undefined || image.entry === undefined)) {
545
+ throw new Refusal("not-found", "--loader needs the entry and the entry state of a c64-unpacker capture in the knowledge: put the capture in with knowledge.ts image <image.json>");
588
546
  }
589
547
  // Read the entry state before the long steps, so a bad state stops the run at once.
590
- const state = values.loader ? readEntryState(JSON.stringify(image.state), "the entry state of the knowledge") : undefined;
548
+ const state = loaderWanted ? loaderState(image.state, "the entry state of the knowledge") : undefined;
591
549
  const romUse = romsInUse(image, coverage);
592
550
  const instructions = await decode(image, knowledge, entries, coverage);
593
551
  const laid = layout({
@@ -646,10 +604,10 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
646
604
  files = marked([...rendered.files, ...loaderFiles]);
647
605
  built = await assembleFiles(files, "main.asm");
648
606
  }
649
- removeOldFiles(values.out, new Set(files.map((file) => file.name)));
650
- for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
651
- writeProjectFile(`${values.out}/${SOURCE_MAP_FILE}`, Buffer.from(sourceMapText(rendered, space, knowledge.revision, files), "utf8"), { what: "--out" });
652
- const showRange = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
607
+ removeOldFiles(out, new Set(files.map((file) => file.name)));
608
+ const written: Written[] = files.map((file) => writeReported(`${out}/${file.name}`, file.text, { what: "--out" }));
609
+ written.push(writeReported(`${out}/${SOURCE_MAP_FILE}`, sourceMapText(rendered, space, knowledge.revision, files), { what: "--out" }));
610
+ const showRange = (range: AddressRange) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
653
611
  const inData = entriesInData(entries, knowledge.regions);
654
612
  if (rendered.notExecuted !== undefined) {
655
613
  // Each range with the file of the source that holds it and the routine before it, as a report groups them.
@@ -662,13 +620,13 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
662
620
  const files: Record<string, number> = {};
663
621
  for (const range of placed) if (range.file !== undefined) files[range.file] = (files[range.file] ?? 0) + 1;
664
622
  const all = { revision: knowledge.revision, instructions: rendered.notExecuted.instructions, files, ranges: placed };
665
- writeProjectFile(`${values.out}/${NOT_EXECUTED_FILE}`, Buffer.from(`${JSON.stringify(all, null, 2)}\n`, "utf8"), { what: "--out" });
623
+ written.push(writeReported(`${out}/${NOT_EXECUTED_FILE}`, `${JSON.stringify(all, null, 2)}\n`, { what: "--out" }));
666
624
  }
667
625
 
668
626
  const output: Record<string, unknown> = {
669
- out: values.out,
670
- files: files.map((file) => file.name),
671
- sourceMap: `${values.out}/${SOURCE_MAP_FILE}`,
627
+ out,
628
+ files: written,
629
+ sourceMap: `${out}/${SOURCE_MAP_FILE}`,
672
630
  revision: knowledge.revision,
673
631
  instructions: rendered.instructions,
674
632
  dataBytes: rendered.dataBytes,
@@ -683,51 +641,37 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
683
641
  : { entriesInData: inData.map(({ entry, region }) => ({ entry: formatC64Address(entry), region: { ...showRange(region), type: region.type }, origin: region.origin })) }),
684
642
  ...(rendered.asBytes.length === 0
685
643
  ? {}
686
- : { asBytes: { count: rendered.asBytes.length, listed: rendered.asBytes.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), instruction: item.text })) } }),
644
+ : { asBytes: listed(rendered.asBytes, max, (item) => ({ address: formatC64Address(item.address), instruction: item.text })) }),
687
645
  ...(rendered.notExecuted === undefined
688
646
  ? {}
689
647
  : {
690
- notExecuted: {
691
- instructions: rendered.notExecuted.instructions,
692
- ranges: rendered.notExecuted.ranges.length,
693
- listed: rendered.notExecuted.ranges.slice(0, MAX_LISTED).map(showRange),
694
- all: `${values.out}/${NOT_EXECUTED_FILE}`,
695
- },
648
+ notExecuted: { instructions: rendered.notExecuted.instructions, ...listed(rendered.notExecuted.ranges, max, showRange, `${out}/${NOT_EXECUTED_FILE}`) },
696
649
  }),
697
650
  ...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
698
651
  ...(rendered.expressions.length === 0 && rendered.operandsAsNumbers.length === 0 ? {} : { operandExpressions: rendered.expressions.length }),
699
652
  ...(rendered.operandsAsNumbers.length === 0
700
653
  ? {}
701
654
  : {
702
- operandsAsNumbers: {
703
- count: rendered.operandsAsNumbers.length,
704
- listed: rendered.operandsAsNumbers.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
705
- },
655
+ operandsAsNumbers: listed(rendered.operandsAsNumbers, max, (item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
706
656
  }),
707
657
  ...(rendered.uses.length === 0 && rendered.macrosAsInstructions.length === 0 ? {} : { macroUses: rendered.uses.length }),
708
658
  // A macro use that the source writes as its instructions does not fail the run: the source stays identical.
709
659
  ...(rendered.macrosAsInstructions.length === 0
710
660
  ? {}
711
661
  : {
712
- macrosAsInstructions: {
713
- count: rendered.macrosAsInstructions.length,
714
- listed: rendered.macrosAsInstructions.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), macro: item.macro, reason: item.reason })),
715
- },
662
+ macrosAsInstructions: listed(rendered.macrosAsInstructions, max, (item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), macro: item.macro, reason: item.reason })),
716
663
  }),
717
664
  // A patch that the source cannot write fails the run: the switch would miss a part.
718
665
  ...(laid.patchesNotWritten.length === 0
719
666
  ? {}
720
667
  : {
721
- patchesNotWritten: {
722
- count: laid.patchesNotWritten.length,
723
- listed: laid.patchesNotWritten.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), switch: item.switch, reason: item.reason })),
724
- },
668
+ patchesNotWritten: listed(laid.patchesNotWritten, max, (item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), switch: item.switch, reason: item.reason })),
725
669
  }),
726
670
  };
727
671
  // The loader includes the source, so it is built only from a source that is identical to the image.
728
672
  const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
729
673
  if (built.program === undefined) {
730
- return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_LISTED) }, ...notBuilt("The source does not assemble, and the loader includes it.") }, failed: true };
674
+ return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_DIAGNOSTICS) }, ...notBuilt("The source does not assemble, and the loader includes it.") }, failed: true };
731
675
  }
732
676
  const checked = check(built.program, built.start!, image);
733
677
  output.check = checked;
@@ -741,7 +685,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
741
685
  if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined || sourceFailed };
742
686
 
743
687
  const loaderBuilt = await assembleFiles(files, "loader.asm");
744
- if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
688
+ if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0, MAX_DIAGNOSTICS) } }, failed: true };
745
689
  const program = loaderBuilt.program;
746
690
  // The simulation: the loader in memory as it loads, then every copy in its order, byte by byte.
747
691
  const memory = new Int16Array(0x10000).fill(-1);
@@ -759,9 +703,8 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
759
703
  const jump = jumpSymbol.kind === "address" ? jumpSymbol.address : jumpSymbol.value;
760
704
  const wrong = simulate(memory, loader.copies, expected);
761
705
  if (wrong !== undefined) throw new WireFailure("operation-failed", `The simulated loader writes a wrong byte at ${formatC64Address(wrong)}. This is a defect of the c64-source script.`);
762
- const loaderPath = `${values.out}/loader.prg`;
763
- writeProjectFile(loaderPath, program, { what: "--out" });
764
- (output.files as string[]).push("loader.prg");
706
+ const loaderPath = `${out}/loader.prg`;
707
+ written.push(writeReported(loaderPath, program, { what: "--out" }));
765
708
  const copierSymbol = loaderBuilt.symbols?.find((symbol) => symbol.name === COPIER_SOURCE);
766
709
  if (copierSymbol === undefined) throw new WireFailure("operation-failed", `The assembled loader has no ${COPIER_SOURCE} label. This is a defect of the c64-source script.`);
767
710
  const copierSource = copierSymbol.kind === "address" ? copierSymbol.address : copierSymbol.value;
@@ -778,11 +721,27 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
778
721
  return { output, failed: !ran.verified || sourceFailed };
779
722
  }
780
723
 
781
- await runScript(
782
- async (argv) => {
783
- const { output, failed } = await run(argv);
784
- if (failed) process.exitCode = 1;
785
- return output;
786
- },
787
- { usage: USAGE, refusals: [KnowledgeError] },
788
- );
724
+ await runScript({
725
+ name: "source",
726
+ summary: "Generates ACME source from the program and the knowledge, and checks with ACME that it assembles to the program byte by byte.",
727
+ names: knowledgeNames,
728
+ commands: [
729
+ {
730
+ name: "generate",
731
+ summary: "Writes the source files, source-map.json and not_executed.json, and checks them. result is PASS when every check passes, else FAIL.",
732
+ options: {
733
+ out: { ...OPTIONS.out, value: "<dir>", help: "the folder for the source files, relative to the project folder", required: true },
734
+ entry: { ...OPTIONS.entry, multiple: true, help: "an entry point for Ghidra" },
735
+ space: { ...OPTIONS.space, help: "the knowledge space of the program: c64 (the default), drive8, or bank0 to bank255. The coverage and --loader are for c64 only" },
736
+ loader: { kind: "flag", help: "also write loader.asm and loader.prg: the loader copies the segments into place, sets the entry state of the knowledge, and jumps to the entry. It is checked in the emulator" },
737
+ "analyzer-names": { kind: "flag", help: "name an address that has no name of the user or the LLM, and no platform name, with its analyzer name" },
738
+ "max-listed": OPTIONS["max-listed"],
739
+ },
740
+ notes: "The program, its entry state and the coverage come from the knowledge: put them in with knowledge.ts image and coverage of the c64-knowledge skill. Code that no imported run executed is marked and listed, and <dir>/not_executed.json has all of its ranges. Without --analyzer-names, an address without a name gets a name of the source, for example l_17e2.",
741
+ run: async (call) => {
742
+ const { output, failed } = await generate(call);
743
+ return { result: failed ? "FAIL" : "PASS", ...output };
744
+ },
745
+ },
746
+ ],
747
+ });