@henols/c64-re-tools 2.1.1 → 2.1.3

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 (67) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +6 -3
  3. package/skills/c64-assembler/scripts/assembler.ts +19 -11
  4. package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
  5. package/skills/c64-basic/SKILL.md +1 -0
  6. package/skills/c64-disk/SKILL.md +1 -0
  7. package/skills/c64-disk/scripts/disk.ts +2 -2
  8. package/skills/c64-disk/scripts/lib/containers.ts +4 -4
  9. package/skills/c64-docs/SKILL.md +2 -1
  10. package/skills/c64-docs/scripts/docs.ts +1 -1
  11. package/skills/c64-docs/scripts/lib/links.ts +2 -0
  12. package/skills/c64-emulator/SKILL.md +4 -1
  13. package/skills/c64-knowledge/SKILL.md +12 -6
  14. package/skills/c64-knowledge/scripts/knowledge.ts +41 -25
  15. package/skills/c64-knowledge/scripts/lib/listing.ts +16 -31
  16. package/skills/c64-memory-map/SKILL.md +3 -0
  17. package/skills/c64-memory-map/scripts/memory-map.ts +23 -4
  18. package/skills/c64-provenance/SKILL.md +5 -3
  19. package/skills/c64-provenance/scripts/lib/library.ts +28 -5
  20. package/skills/c64-provenance/scripts/lib/separate.ts +34 -6
  21. package/skills/c64-provenance/scripts/provenance.ts +22 -10
  22. package/skills/c64-source/SKILL.md +2 -1
  23. package/skills/c64-source/scripts/lib/source.ts +25 -33
  24. package/skills/c64-source/scripts/source.ts +13 -17
  25. package/skills/c64-static-analysis/SKILL.md +6 -3
  26. package/skills/c64-static-analysis/scripts/lib/findings.ts +23 -8
  27. package/skills/c64-static-analysis/scripts/lib/pointers.ts +38 -25
  28. package/skills/c64-static-analysis/scripts/static-analysis.ts +31 -19
  29. package/skills/c64-testing/SKILL.md +9 -5
  30. package/skills/c64-testing/references/scenario-format.md +5 -3
  31. package/skills/c64-testing/scripts/lib/diff-files.ts +41 -10
  32. package/skills/c64-testing/scripts/lib/recording.ts +3 -3
  33. package/skills/c64-testing/scripts/lib/replay.ts +1 -1
  34. package/skills/c64-testing/scripts/lib/runs.ts +45 -9
  35. package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
  36. package/skills/c64-testing/scripts/testing.ts +9 -6
  37. package/skills/c64-unpacker/SKILL.md +10 -7
  38. package/skills/c64-unpacker/scripts/lib/loads.ts +15 -6
  39. package/skills/c64-unpacker/scripts/lib/machine.ts +61 -3
  40. package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
  41. package/skills/c64-unpacker/scripts/unpacker.ts +21 -17
  42. package/src/host/vice/adapter.ts +4 -1
  43. package/src/host/vice/session.ts +144 -11
  44. package/src/host-client/image.ts +27 -13
  45. package/src/host-client/steps.ts +41 -5
  46. package/src/host-client/tools.ts +4 -3
  47. package/src/host-client/transfer.ts +22 -29
  48. package/src/knowledge/history.ts +55 -2
  49. package/src/knowledge/import.ts +79 -7
  50. package/src/knowledge/program.ts +31 -2
  51. package/src/knowledge/read.ts +7 -4
  52. package/src/knowledge/schema.ts +3 -2
  53. package/src/knowledge/text.ts +1 -0
  54. package/src/knowledge/write.ts +86 -42
  55. package/src/mcp/inputs.ts +6 -3
  56. package/src/mcp/output.ts +1 -1
  57. package/src/mcp/server.ts +9 -1
  58. package/src/mcp/tools/debug.ts +31 -4
  59. package/src/mcp/tools/machine.ts +3 -1
  60. package/src/mcp/tools/media.ts +5 -1
  61. package/src/mcp/tools/memory.ts +4 -3
  62. package/src/media.ts +57 -0
  63. package/src/opcodes.ts +81 -18
  64. package/src/platform.ts +14 -1
  65. package/src/project.ts +118 -3
  66. package/src/protocol/vice.ts +4 -1
  67. package/src/script.ts +118 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
5
5
  "license": "MIT",
6
6
  "author": "Henrik Olsson",
@@ -31,9 +31,11 @@ node <skill>/scripts/assembler.ts build src main.a --out build/game.prg
31
31
  | `--include lib` | An include directory, relative to the source root. Use it again for more directories. |
32
32
  | `--define DEBUG=1` | An assembler symbol: an integer, also as `$` hex, or `true` or `false`. Use it again for more symbols. |
33
33
  | `--set-pc '$0801'` | The start address when the source does not set one. |
34
- | `--compare` | Check the program byte by byte against the program in the knowledge, each segment. Put it in with `image` of the c64-knowledge skill. `--space drive8` or `--space bank0` compares with the program of that space. |
34
+ | `--compare` | Check the program byte by byte against the program in the knowledge, each segment. Put it in with `image` of the c64-knowledge skill. `--space drive8` or `--space bank0` compares with the program of that space. The script refuses `--space` without `--compare`. |
35
35
  | `--plain` | Write only the bytes of the program, without the 2-byte load address of a PRG. `loadRange` still tells where the bytes go. |
36
36
  | `--all-symbols` | List all symbols. Without it, the list does not include the constants that the source does not use. `unusedConstants` gives their count. |
37
+ | `--max-listed 200` | Show at most 200 symbols in the result. The default is 50. `symbols.count` gives the count of all symbols. |
38
+ | `--out-symbols build/game.json` | Write all symbols to this file as `{"symbols": [...]}`. Give this file to the c64-testing skill as the symbols of the rebuild. |
37
39
 
38
40
  The script copies the full source root. Put all files that the program includes under it. Version control directories stay out.
39
41
 
@@ -46,6 +48,7 @@ The target CPU is the 6510. The output is a C64 PRG, or the bytes only with `--p
46
48
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
47
49
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
48
50
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
51
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
49
52
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
50
53
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
51
54
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -64,7 +67,7 @@ The target CPU is the 6510. The output is a C64 PRG, or the bytes only with `--p
64
67
 
65
68
  1. Find the source root and the entry source. The entry source sets its own start address with `* = $0801` or similar.
66
69
  2. Run the script.
67
- 3. If `result` is `PASS`, the PRG is at `out`. `loadRange` tells where it loads. `symbols` gives each symbol:
70
+ 3. If `result` is `PASS`, the PRG is at `out`. `loadRange` tells where it loads. `symbols` is a list with `count` and `listed`. Each item in `listed` is one symbol:
68
71
  - Kind `address` has a value in `loadRange`. It gives the `address`.
69
72
  - Kind `constant` has a value outside `loadRange`. It gives the `value` as `$` and four or more hex digits. A label in a `!pseudopc` block can be a constant, because its value is the run address, not the load address.
70
73
  - `used` is `false` when the source does not use the symbol.
@@ -92,7 +95,7 @@ A symbol from the rebuilt source can give the address of a routine in the rebuil
92
95
  - `FAIL` with `assembled: false`: the source has errors. `diagnostics` gives them.
93
96
  - `FAIL` with `assembled: true`: `--compare` found differences. The PRG is at `out`.
94
97
 
95
- With `--compare`, `compare` gives `identical` and `segments`: one check for each segment of the program in the knowledge. The `result` of a check is `identical`, `different` with `firstDifference`, or `not covered` when the build does not hold the segment.
98
+ With `--compare`, `compare` gives `identical` and `segments`: one check for each segment of the program in the knowledge. The `result` of a check is `identical`, `different` with `firstDifference`, or `not covered` when the build does not hold the segment. `firstDifference` gives the `address`, and the byte of the build (`built`) and of the knowledge (`reference`), each as `$` and two hex digits.
96
99
 
97
100
  ## Failure and conflicts
98
101
 
@@ -11,8 +11,8 @@ import { knowledgeNames } from "#src/knowledge/names.ts";
11
11
  import { requireProgram, type ProgramImage } from "#src/knowledge/program.ts";
12
12
  import { assemble } from "#src/native/acme.ts";
13
13
  import { localToolContext } from "#src/native/local.ts";
14
- import type { AcmeParams } from "#src/native/types.ts";
15
- import { OPTIONS, runScript, UsageError } from "#src/script.ts";
14
+ import type { AcmeParams, AssembledSymbol } from "#src/native/types.ts";
15
+ import { listed as listedItems, OPTIONS, runScript, UsageError } from "#src/script.ts";
16
16
  import { compareBuild } from "./lib/compare.ts";
17
17
 
18
18
  function parseDefine(text: string): [string, number | boolean] {
@@ -25,6 +25,13 @@ function parseDefine(text: string): [string, number | boolean] {
25
25
  throw new UsageError(`--define ${match[1]} needs an integer or true/false, not ${value}`);
26
26
  }
27
27
 
28
+ /** A symbol of the build: an address as $ and four hex digits, a constant as $ and at least four hex digits. */
29
+ function showSymbol(symbol: AssembledSymbol) {
30
+ return symbol.kind === "address"
31
+ ? { name: symbol.name, kind: "address", address: formatC64Address(symbol.address), used: symbol.used }
32
+ : { name: symbol.name, kind: "constant", value: `$${symbol.value.toString(16).padStart(4, "0")}`, used: symbol.used };
33
+ }
34
+
28
35
  await runScript({
29
36
  name: "assembler",
30
37
  summary: "Assembles project source with ACME and writes the program into the project.",
@@ -38,14 +45,16 @@ await runScript({
38
45
  { name: "<entry>", help: "the main source file, relative to the source root" },
39
46
  ],
40
47
  options: {
41
- out: { ...OPTIONS.out, value: "<file.prg>", help: "where to write the program, relative to the project folder", required: true },
48
+ out: { ...OPTIONS.out, output: "file", value: "<file.prg>", help: "where to write the program, relative to the project folder", required: true },
42
49
  include: { kind: "text", value: "<dir>", multiple: true, help: "an include folder, relative to the source root" },
43
50
  define: { kind: "text", value: "NAME=VALUE", multiple: true, help: "an assembler symbol: an integer ($hex allowed) or true/false" },
44
51
  "set-pc": { kind: "address", value: "<address>", help: "the start address when the source sets none, for example '$0801'" },
45
52
  compare: { kind: "flag", help: "check the program byte by byte against the program in the knowledge, each segment" },
46
- space: { ...OPTIONS.space, help: "with --compare: the knowledge space of the program: c64 (the default), drive8, or bank0 to bank255" },
53
+ space: { kind: "space", value: "<space>", help: "with --compare: the knowledge space of the program: c64 (the default), drive8, or bank0 to bank255" },
47
54
  plain: { kind: "flag", help: "write the bytes only, without the 2-byte load address of a PRG" },
48
55
  "all-symbols": { kind: "flag", help: "list also the constants that the source does not use" },
56
+ "max-listed": { ...OPTIONS["max-listed"], help: "list at most n symbols (default 50). The count is always given" },
57
+ "out-symbols": { kind: "path", output: "file", value: "<file.json>", help: "write all symbols to this project file as {\"symbols\": [...]}: the names of the rebuild for c64-testing" },
49
58
  },
50
59
  notes: "Symbol values are $ and at least four hex digits. Put the program into the knowledge with knowledge.ts image of the c64-knowledge skill before --compare.",
51
60
  run: async (call) => {
@@ -53,6 +62,7 @@ await runScript({
53
62
  const out = call.string("out")!;
54
63
  // Read the program before ACME runs, so a project without it stops at once.
55
64
  let program: ProgramImage | undefined;
65
+ if (call.string("space") !== undefined && !call.flag("compare")) throw new UsageError("--space works only with --compare: it names the knowledge space of the program to compare with");
56
66
  if (call.flag("compare")) {
57
67
  const db = openForRead();
58
68
  try {
@@ -82,7 +92,8 @@ await runScript({
82
92
  const compared = program === undefined ? undefined : compareBuild(attachments![0]!, program);
83
93
  // An include file of platform names gives many constants that the program does not use.
84
94
  const all = call.flag("all-symbols");
85
- const listed = result.symbols!.filter((symbol) => all || symbol.kind === "address" || symbol.used);
95
+ const shown = result.symbols!.filter((symbol) => all || symbol.kind === "address" || symbol.used);
96
+ const symbolsPath = call.string("out-symbols");
86
97
  return {
87
98
  // A build that differs from its reference is a failed check.
88
99
  result: compared !== undefined && !compared.identical ? "FAIL" : "PASS",
@@ -90,12 +101,9 @@ await runScript({
90
101
  ...written,
91
102
  ...(plain ? { plain: true } : {}),
92
103
  loadRange: { start: formatC64Address(range.start), end: formatC64Address(range.end), bytes: range.bytes },
93
- symbols: listed.map((symbol) =>
94
- symbol.kind === "address"
95
- ? { name: symbol.name, kind: "address", address: formatC64Address(symbol.address), used: symbol.used }
96
- : { name: symbol.name, kind: "constant", value: `$${symbol.value.toString(16).padStart(4, "0")}`, used: symbol.used },
97
- ),
98
- ...(all ? {} : { unusedConstants: result.symbols!.length - listed.length }),
104
+ symbols: listedItems(shown, call.count("max-listed")!, showSymbol, "a larger --max-listed, or --out-symbols <file.json> for all symbols"),
105
+ ...(symbolsPath === undefined ? {} : { symbolsFile: writeReported(symbolsPath, `${JSON.stringify({ symbols: shown.map(showSymbol) }, null, 1)}\n`, { what: "--out-symbols" }) }),
106
+ ...(all ? {} : { unusedConstants: result.symbols!.length - shown.length }),
99
107
  diagnostics: result.diagnostics,
100
108
  ...(compared === undefined ? {} : { compare: compared }),
101
109
  };
@@ -1,7 +1,7 @@
1
1
  // The byte check of a build: the assembled program against the program
2
2
  // in the knowledge, segment by segment.
3
3
 
4
- import { formatC64Address, prgRange } from "#src/c64.ts";
4
+ import { formatC64Address, formatC64Byte, prgRange } from "#src/c64.ts";
5
5
 
6
6
  export interface SegmentCheck {
7
7
  start: string;
@@ -10,7 +10,8 @@ export interface SegmentCheck {
10
10
  /** Bytes of the segment that the build does not hold. */
11
11
  uncovered?: number;
12
12
  differences?: number;
13
- firstDifference?: { address: string; built: number; reference: number };
13
+ /** The first address that differs, with its byte in the build and in the knowledge, each as $ and two hex digits. */
14
+ firstDifference?: { address: string; built: string; reference: string };
14
15
  }
15
16
 
16
17
  /** The program that the build is checked against: its segments and its bytes in them. */
@@ -34,7 +35,7 @@ export function compareBuild(prg: Uint8Array, program: Reference): { identical:
34
35
  const referenceByte = program.byteAt(address);
35
36
  if (builtByte === referenceByte) continue;
36
37
  differences++;
37
- first ??= { address: formatC64Address(address), built: builtByte, reference: referenceByte };
38
+ first ??= { address: formatC64Address(address), built: formatC64Byte(builtByte), reference: formatC64Byte(referenceByte) };
38
39
  }
39
40
  const uncovered = segment.end - segment.start + 1 - (to - from + 1);
40
41
  return {
@@ -30,6 +30,7 @@ node <skill>/scripts/basic.ts list extracted/game.prg
30
30
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
31
31
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
32
32
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
33
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
33
34
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
34
35
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
35
36
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -47,6 +47,7 @@ File names show the characters that the C64 shows in a directory list. A byte th
47
47
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
48
48
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
49
49
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
50
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
50
51
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
51
52
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
52
53
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -123,7 +123,7 @@ await runScript({
123
123
  name: "read",
124
124
  summary: "Writes the bytes of one file into the project. It also reads a .t64 tape file.",
125
125
  subjects: [{ ...IMAGE, help: "a disk image or a .t64 tape file, relative to the project folder" }, NAME],
126
- options: { out: { ...OPTIONS.out, value: "<file>", help: "the file to write, relative to the project folder", required: true } },
126
+ options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the file to write, relative to the project folder", required: true } },
127
127
  run: async (call) => {
128
128
  const [image, name] = [call.subject(0), call.subject(1)];
129
129
  const out = call.string("out")!;
@@ -145,7 +145,7 @@ await runScript({
145
145
  name: "crt",
146
146
  summary: "Writes each CHIP packet of a cartridge as a PRG, and crt.json with the header and the packets.",
147
147
  subjects: [{ name: "<file.crt>", help: "a .crt cartridge file, relative to the project folder" }],
148
- options: { out: { ...OPTIONS.out, value: "<dir>", help: "the folder for the files, relative to the project folder", required: true } },
148
+ options: { out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "the folder for the files, relative to the project folder", required: true } },
149
149
  run: (call) => {
150
150
  const image = call.subject(0);
151
151
  kindFor("crt", image, ["crt"]);
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { formatC64Address, petsciiNameToText } from "#src/c64.ts";
7
7
  import { Refusal } from "#src/errors.ts";
8
+ import { hasSignature } from "#src/media.ts";
8
9
 
9
10
  /** The CRT hardware types this toolkit reads, by type number. */
10
11
  export const CRT_TYPES: Record<number, string> = { 0: "normal", 5: "Ocean", 19: "Magic Desk", 32: "EasyFlash" };
@@ -30,11 +31,10 @@ export interface Crt {
30
31
  chips: CrtChip[];
31
32
  }
32
33
 
33
- const SIGNATURE = "C64 CARTRIDGE ";
34
-
35
34
  export function parseCrt(bytes: Uint8Array): Crt {
36
35
  const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
37
- if (buffer.length < 0x40 || buffer.toString("latin1", 0, 16) !== SIGNATURE) throw new Refusal("media-error", "the file is not a CRT: it does not start with C64 CARTRIDGE");
36
+ if (!hasSignature("crt", buffer)) throw new Refusal("media-error", "the file is not a CRT: it does not start with C64 CARTRIDGE");
37
+ if (buffer.length < 0x40) throw new Refusal("media-error", `the CRT header is shorter than 64 bytes: the file has only ${buffer.length} bytes`);
38
38
  const headerLength = buffer.readUInt32BE(0x10);
39
39
  const type = buffer.readUInt16BE(0x16);
40
40
  const exrom = buffer[0x18]!;
@@ -85,7 +85,7 @@ export interface Tape {
85
85
 
86
86
  export function parseT64(bytes: Uint8Array): Tape {
87
87
  const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
88
- if (buffer.length < 0x40 || !buffer.toString("latin1", 0, 32).startsWith("C64")) throw new Refusal("media-error", "the file is not a T64: it does not start with C64");
88
+ if (buffer.length < 0x40 || !hasSignature("t64", buffer)) throw new Refusal("media-error", "the file is not a T64: it does not start with C64");
89
89
  const maxEntries = buffer.readUInt16LE(0x22);
90
90
  const name = buffer.toString("latin1", 0x28, 0x40).replace(/[\0 ]+$/, "");
91
91
  const raw: Array<Omit<TapeEntry, "bytes" | "end"> & { end: number }> = [];
@@ -49,6 +49,7 @@ node <skill>/scripts/docs.ts link README.md docs --source src --check
49
49
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
50
50
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
51
51
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
52
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
52
53
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
53
54
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
54
55
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -144,7 +145,7 @@ Do not store:
144
145
  - `invalid-input` that names an old revision: the knowledge changed after the source. Generate the source again, then run `docs.ts generate`.
145
146
  - `invalid-input` that names a page in the reference directory: the page is not a page of `docs.ts generate`. Move the page out of the directory. Do not give `--out` a directory with pages that you wrote.
146
147
  - `unknown-name`: the page uses a name that is not in the knowledge. Record the name with c64-knowledge, or correct the page.
147
- - `wrong-address`: the address after the name is not the address of the name, and it is not a place inside it. The message gives the routine that holds the address. Correct the name or the address. For a value, write `` `scroll_moves` = `$10` ``.
148
+ - `wrong-address`: the address after the name is not the address of the name, and it is not a place inside it. The message gives the routine that holds the address. Correct the name or the address. For a value, write `` `scroll_moves` = `$10` ``. The script does not link such a mention until it is correct.
148
149
  - `missing-file`, `missing-anchor` or `missing-line`: a link goes to nothing. Correct the link. An image of an asset must exist in the project.
149
150
  - `stale-reference`: the source or the reference is older than the knowledge. Do steps 2 and 3 of the workflow again.
150
151
  - `conflict`: the knowledge has conflicts from a git merge. Run `knowledge.ts merge` of c64-knowledge.
@@ -297,7 +297,7 @@ await runScript({
297
297
  summary: "Writes the reference pages: a page for each module, and pages for the names, the variables, the tables, the memory map and the graphics.",
298
298
  subjects: [{ name: "<source-dir>", help: SOURCE.help }],
299
299
  options: {
300
- out: { ...OPTIONS.out, value: "<dir>", help: "the folder for the reference pages, relative to the project folder, for example docs/reference. generate writes only there and refuses a page in it that it did not write", required: true },
300
+ out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "the folder for the reference pages, relative to the project folder, for example docs/reference. generate writes only there and refuses a page in it that it did not write", required: true },
301
301
  "max-listed": OPTIONS["max-listed"],
302
302
  },
303
303
  notes: "When the knowledge has the program of the source (knowledge.ts image of the c64-knowledge skill), the reference decodes the tables and shows the graphics.",
@@ -102,6 +102,8 @@ export function linkPage(text: string, context: LinkContext): { text: string; ch
102
102
  const expected = `$${(address + delta).toString(16).toUpperCase().padStart(4, "0")}`;
103
103
  const holder = context.placeOf(given!);
104
104
  report("wrong-address", `\`${name}${offset ?? ""}\` is ${expected} in the knowledge, and the page gives ${addressText}${holder === undefined || holder === name ? "" : `, a place in \`${holder}\``}. Correct the name or the address.${digits!.length <= 2 ? ` For a value, write \`${name}\` = \`${addressText}\`.` : ""}`);
105
+ // A mention with a problem gets no link: the text stays as the person wrote it until it is correct.
106
+ return whole;
105
107
  }
106
108
  const shownName = `${TICK}${name}${offset ?? ""}${TICK}`;
107
109
  const code = context.codeOf(name);
@@ -57,6 +57,7 @@ For a large result, give `out` with a path in the project. `c64_memory_read`, `c
57
57
 
58
58
  - Only the first byte of an instruction gets execute. Thus `read` without `out` gives a new range at each instruction of code. For code, use `summary`, or `read` with `out`.
59
59
  - A read on a byte right after executed code, or at a return address, can be a dummy read of the 6502. The CPU reads such a byte but does not use it.
60
+ - VICE has a known flaw: it uses only one bit of `$01` to find RAM or ROM. At `$A000-$BFFF` it gives `rom` when bit 0 is set, also when bit 1 is clear and the CPU used the RAM there. Many games set `$01` to `$35`. Then code that runs at `$A000-$BFFF` shows as ROM. `emulatorFlaw` in the result tells when this can occur. Every access at `$D000-$DFFF` shows as I/O.
60
61
 
61
62
  `c64_profile` gives the routines that used the most cycles since the session started, the last `clear` or the last `c64_window`. Use action `clear` before the part of the program that you want to measure. The profiler gives the cycles to the routine that a JSR called last. Code that the CPU enters with JMP or a branch counts for that routine. After `c64_window` or a snapshot restore, the profiler of VICE keeps the routines that ran before the restore. Then `c64_profile` can give the cycles of the program to such a routine, for example `$fd50`. Do not trust the routine addresses of `c64_profile` after a restore.
62
63
 
@@ -80,7 +81,7 @@ Write operations need a stopped CPU. Stop the CPU with `c64_execution` action `p
80
81
 
81
82
  A cartridge from `c64_autostart` stays in the C64 through every reset, as on a real C64. `c64_status` gives `cartridge: true` while the C64 has it, and `cartridge: false` when it does not. `c64_autostart` of a file that is not a cartridge removes the cartridge first. `c64_autostart` also sets both joysticks to the center first. `joysticksCentred` gives the ports that it changed.
82
83
 
83
- Step, next and until-return work only in space `c64`. The emulator cannot stop the disk drive CPU after one instruction. To find when the drive executes an address, use `c64_run_until` with an address target in space `drive8`, or a breakpoint in space `drive8`. The drive can execute a few more instructions before it stops. The computer completes its current instruction. The `pc` in the result is the drive address where it stopped. `space` in the result is `drive8`. A breakpoint or a watchpoint in space `drive8` that stops `advance-frames` gives the same `pc` and `space`.
84
+ Step, next and until-return work only in space `c64`. The emulator cannot stop the disk drive CPU after one instruction. To find when the drive executes an address, use `c64_run_until` with an address target in space `drive8`, or a breakpoint in space `drive8`. The drive can execute a few more instructions before it stops. The computer completes its current instruction. The `pc` in the result is the drive address where it stopped. `space` in the result is `drive8`. A breakpoint or a watchpoint in space `drive8` that stops `advance-frames` gives the same `pc` and `space`. `c64_status` also gives them. A memory target or a memory condition tests only space `c64`.
84
85
 
85
86
  `c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard matrix directly does not see them. Many games do this. No MCP tool presses a key of the keyboard matrix. For such a program:
86
87
 
@@ -134,6 +135,8 @@ An error is `{"error": {"code": "...", "message": "..."}}`. The message tells wh
134
135
  - `machine-unavailable` and the message says that the VICE window closed: the session starts the emulator again. Wait a few seconds. Then do the operation again.
135
136
  - `machine-state-lost` with a different message: the connection to the host runtime was lost, and the machine state is gone. Do not continue with results from before. The next call starts a new machine when the host runtime runs. Load the program again, and set the breakpoints and the watchpoints again. If the next call gives `machine-unavailable` with a message that the host runtime does not run, obey the rule for that message.
136
137
  - `machine-unavailable` and the message names a pause or a dialog in the emulator window: this occurs only while the window is open. Do not send more operations. Tell the user to resume the emulator (Pause, Alt+P) or to close the dialog in the VICE window. Then do the operation again. The machine state does not change.
138
+ - `machine-unavailable` and the message says that the emulator does not take commands now, with no window open: wait a few seconds. Then do the operation again one time. If you get the same error again, stop the task. Tell the user to restart the c64-re-tools MCP server, and wait for the user.
139
+ - `media-error`: the file is not of the type that its extension names. Give a file of that type.
137
140
  - `machine-running`: stop the CPU first, then try again.
138
141
  - `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script. Do not try to repair the installation.
139
142
  - `c64_run_until` with `stopReason` `timeout`: the target did not occur in the frame limit. This is evidence too. Report it.
@@ -27,7 +27,7 @@ One of these:
27
27
  node <skill>/scripts/knowledge.ts <command> [subjects] [options]
28
28
  ```
29
29
 
30
- A symbol is the name of an address from the user or the LLM. An analyzer name is a second name from DXA, Ghidra or an idiom, for example `FUN_17e2`. Each analyzer keeps its own name at an address. `at`, `search` and `export` show the analyzer names. The generated source uses only the symbols, unless c64-source gets `--analyzer-names`. To use an analyzer name in the source, `rename` the address with it.
30
+ A symbol is the name of an address from the user or the LLM. An analyzer name is a second name from DXA, Ghidra or an idiom, for example `FUN_17e2`. Each analyzer keeps its own name at an address. `at`, `search` and `export` show the analyzer names. The generated source uses only the symbols, unless c64-source gets `--analyzer-names`. To use an analyzer name in the source, `rename` the address with it. A symbol name is a label for ACME: a letter or underscore, then letters, digits or underscores. The knowledge refuses the name of a 6502 instruction, for example `lda`, in upper or lower case.
31
31
 
32
32
  | Need | Command |
33
33
  |---|---|
@@ -42,6 +42,7 @@ A symbol is the name of an address from the user or the LLM. An analyzer name is
42
42
  | What the knowledge holds of the program | `program` |
43
43
  | The code of a range with names, comments, access and references | `listing '$0400-$04ff'` |
44
44
  | Name the symbol at an address | `rename '$2100' update_player --kind routine` |
45
+ | The rows that one revision added and closed, also memory, entry and coverage rows | `revision 20261015T2145` |
45
46
  | Remove a symbol | `remove-symbol '$2100'` |
46
47
  | Classify memory | `classify '$3000-$303f' sprite` |
47
48
  | Classify a table of records | `classify '$3000-$303b' bytes --element 6` |
@@ -62,12 +63,12 @@ A symbol is the name of an address from the user or the LLM. An analyzer name is
62
63
  | Files of generated source | `modules` |
63
64
  | The address spaces that hold knowledge | `spaces` |
64
65
  | History | `history '$2100'`, `history --name update_player`, `history --macro inc16`, `revisions`, `revision 20261015T2145` |
65
- | Go back to an earlier state | `revert 20261015T2145` |
66
+ | Go back to an earlier state | `revert 20261015T2145` for all spaces, or `revert 20261015T2145 --space bank3` for one space |
66
67
  | Decide the conflicts after a git merge | `merge --reason 'merge of two computers'` |
67
68
 
68
69
  A revision has an id such as `20261015T214501.123-k3f9qz`. The id starts with the date and time. A command takes the full id, or a start of the id that only one revision has. The revision `0` is the knowledge before the first revision.
69
70
 
70
- Every command takes `--space`: `c64` (the default), `drive8` for code in the 1541 drive, or `bank0` to `bank255` for cartridge banks. Drive code and banks use the same addresses as the C64, so each space has its own knowledge. A name is unique inside one space. In a batch, a write can name its own `space`.
71
+ Every command takes `--space`: `c64` (the default), `drive8` for code in the 1541 drive, or `bank0` to `bank255` for cartridge banks. `revert` is different: without `--space`, it reverts all spaces. With `--space`, it reverts only that space. Drive code and banks use the same addresses as the C64, so each space has its own knowledge. A name is unique inside one space. In a batch, a write can name its own `space`.
71
72
 
72
73
  Symbol kinds are `label`, `routine`, `variable`, `data` and `vector`. Reference kinds are `call`, `jump`, `read`, `write`, `reference` and `pointer`. Only an operand expression writes a `pointer` reference.
73
74
 
@@ -94,6 +95,7 @@ Region types:
94
95
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
95
96
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
96
97
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
98
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
97
99
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
98
100
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
99
101
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -127,12 +129,16 @@ Write:
127
129
 
128
130
  Each write is one revision. A classification inside a region splits that region, so regions never overlap.
129
131
 
132
+ A write can give the same fact that an analyzer gave. Then the fact gets the origin of the write, and `previous` in the result shows the row of the analyzer. The next analysis does not remove the fact. This is true for a symbol, a region, a comment and a reference. A `--by user` write over the same fact of `llm` also changes the origin to `user`. A `--by llm` write over the same fact of `user` changes nothing.
133
+
134
+ A comment and a module are at the start of an instruction. In a code region, the script refuses an address that is an operand byte of an instruction. The error gives the start of that instruction. In data, each address is correct.
135
+
130
136
  ## The program and the coverage
131
137
 
132
138
  The knowledge holds the program, so a project needs no image or coverage files. The other skills read them from the knowledge and take no path to them.
133
139
 
134
140
  1. Capture a packed program with the c64-unpacker skill into `.c64-re-tools/local/`. Then put it in with `image .c64-re-tools/local/capture/image.json`. The script reads `image.bin`, `entry_state.json` and the stages next to it.
135
- 2. For a program without a packer, drive code or a cartridge bank, put the PRG in with `image <file.prg>`. Give `--space drive8` or `--space bank0` for drive code or a bank.
141
+ 2. For a program without a packer, drive code or a cartridge bank, put the PRG in with `image <file.prg>`. Give `--space drive8` or `--space bank0` for drive code or a bank. A PRG has the extension `.prg` or no extension. The script refuses a file with another extension.
136
142
  3. Write each coverage map of a run into `.c64-re-tools/local/`. Then add it with `coverage <map.bin> ...`.
137
143
  4. Read what the knowledge holds with `program`: the segments, the entry, and the coverage maps that it has.
138
144
 
@@ -172,7 +178,7 @@ The ops and their fields:
172
178
  | `unmacro` | `name` |
173
179
  | `use` | `start`, `end`, `macro`, `args` (a list or a comma list) |
174
180
 
175
- A `line` comment is a line of its own above the address. A `side` comment is at the end of the line of the address. The script checks an `operand`, `patch` or `use` write against the program of the knowledge in the space of `--space`. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
181
+ Each write can also have `space`. A `line` comment is a line of its own above the address. A `side` comment is at the end of the line of the address. The script checks an `operand`, `patch` or `use` write against the program of the knowledge in the space of `--space`. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
176
182
 
177
183
  The c64-provenance skill writes a batch with `match --out`. The batch holds the names of library routines at their addresses in this program. Read the `batch.leftOut` of its result first. Then apply the batch with the command in its `apply`.
178
184
 
@@ -233,7 +239,7 @@ A patch is ACME code that replaces a range of the program when the build defines
233
239
  - The code must fit in the range. NOP ($EA) fills the bytes after the code. `--nop` gives a range of NOP only.
234
240
  - `--code` gives the code as text. `--code-file` reads the code from a project file. The code can use the labels and the macros of the source, and local labels with a dot, for example `.skip`.
235
241
  - The braces in the code must be pairs, because the source puts the code inside `{ }`.
236
- - A patch cannot overlap another patch or a macro use.
242
+ - A patch cannot overlap another patch or a macro use. A patch with the same start and the same switch replaces the patch. A patch of another switch at the same start gives the error `conflict`.
237
243
  - Each byte of the range must be in the program of the knowledge.
238
244
  - A `patch` write without `--comment` keeps the comment that the patch has.
239
245
 
@@ -151,9 +151,18 @@ function suppression(row: SuppressionRow) {
151
151
  return { ...spaceOf(row), suppresses: row.entity, ...place, origin: row.origin, revision: row.revision };
152
152
  }
153
153
 
154
+ /** A row of the program in a revision: memory bytes, the entry, or a coverage range. */
155
+ function programRow(entry: Extract<HistoryEntry, { entity: "memory" | "entry" | "coverage" }>) {
156
+ if (entry.entity === "memory") return { ...spaceOf(entry.row), start: formatC64Address(entry.row.address), end: formatC64Address(entry.row.address + entry.row.bytes - 1), bytes: entry.row.bytes, origin: entry.row.origin, revision: entry.row.revision };
157
+ if (entry.entity === "entry") return { ...spaceOf(entry.row), address: formatC64Address(entry.row.address), state: entry.row.state, stages: entry.row.stages, origin: entry.row.origin, revision: entry.row.revision };
158
+ return { ...spaceOf(entry.row), kind: entry.row.kind, start: formatC64Address(entry.row.start), end: formatC64Address(entry.row.end), origin: entry.row.origin, revision: entry.row.revision };
159
+ }
160
+
154
161
  function historyEntry(entry: HistoryEntry) {
155
162
  const shown =
156
- entry.entity === "symbol" || entry.entity === "analyzer-name"
163
+ entry.entity === "memory" || entry.entity === "entry" || entry.entity === "coverage"
164
+ ? programRow(entry)
165
+ : entry.entity === "symbol" || entry.entity === "analyzer-name"
157
166
  ? symbol(entry.row)
158
167
  : entry.entity === "region"
159
168
  ? region(entry.row)
@@ -319,6 +328,9 @@ function shownPrevious(row: NonNullable<BatchStep["previous"]>) {
319
328
  if ("switch" in row) return patch(row);
320
329
  if ("body" in row) return macro(row);
321
330
  if ("macro" in row) return macroUse(row);
331
+ if ("placement" in row) return comment(row);
332
+ if ("from" in row) return reference(row);
333
+ if ("file" in row) return module(row);
322
334
  return symbol(row);
323
335
  }
324
336
 
@@ -420,7 +432,8 @@ function writeCommand(spec: Omit<CommandSpec, "run">, prepare: (call: Call) => W
420
432
  return {
421
433
  ...spec,
422
434
  notes: [spec.notes, "A write is one revision. It makes .c64-re-tools/knowledge/ when it is not there."].filter((note) => note !== undefined).join("\n"),
423
- options: { ...spec.options, ...WRITE },
435
+ // A command can give a WRITE option its own help or default, for example --space of revert.
436
+ options: Object.fromEntries(Object.entries({ ...spec.options, ...WRITE }).map(([name, option]) => [name, spec.options?.[name] ?? option])),
424
437
  run: (call) => withWrite(prepare(call)),
425
438
  };
426
439
  }
@@ -564,11 +577,11 @@ const READ_COMMANDS: CommandSpec[] = [
564
577
  return { revisions: list(call, rows, (row) => row, "a larger --max-listed, or --before <revision> with the oldest revision in listed") };
565
578
  },
566
579
  ),
567
- readCommand({ name: "revision", summary: "The rows that one revision added and closed.", subjects: [REVISION], options: LIST }, (db, call) => {
580
+ readCommand({ name: "revision", summary: "The rows that one revision added and closed: knowledge rows, and memory, entry and coverage rows of the program.", subjects: [REVISION], options: LIST }, (db, call) => {
568
581
  const changes = revision(db, call.subject(0));
569
582
  return { revision: changes.revision, added: list(call, changes.added, historyEntry, MORE), closed: list(call, changes.closed, historyEntry, MORE) };
570
583
  }),
571
- readCommand({ name: "export", summary: "All current knowledge to a project file as JSON.", options: { ...FILTER, out: { ...OPTIONS.out, value: "<file>", help: "the JSON file, relative to the project folder", required: true } } }, (db, call) => {
584
+ readCommand({ name: "export", summary: "All current knowledge to a project file as JSON.", options: { ...FILTER, out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the JSON file, relative to the project folder", required: true } } }, (db, call) => {
572
585
  const range = rangeOf(call);
573
586
  const origins = originsOf(call);
574
587
  const space = call.space;
@@ -595,7 +608,7 @@ const READ_COMMANDS: CommandSpec[] = [
595
608
  name: "listing",
596
609
  summary: "The program as ACME lines with names, operand expressions, comments, the access of each line in the coverage (x executed, r read, w written) and the references in and out.",
597
610
  subjects: [RANGE],
598
- options: { out: { ...OPTIONS.out, value: "<file>", help: "write the listing as text to this file in place of the lines in the result" }, ...LIST },
611
+ options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write the listing as text to this file in place of the lines in the result" }, ...LIST },
599
612
  },
600
613
  (db, call) => {
601
614
  const { start, end } = call.parseRange(call.subject(0), "<range>");
@@ -649,10 +662,7 @@ const WRITE_COMMANDS: CommandSpec[] = [
649
662
  (call) => {
650
663
  const context = contextOf(call);
651
664
  const request = { space: call.space, address: call.parseAddress(call.subject(0), "<address>"), placement: oneOf(COMMENT_PLACEMENTS, call.subject(1), "<placement>"), text: call.rest(2).join(" ") };
652
- return (db) => {
653
- const result = setComment(db, context, request);
654
- return { revision: result.revision, comment: comment(result.current) };
655
- };
665
+ return (db) => shown(setComment(db, context, request), "comment", comment);
656
666
  },
657
667
  ),
658
668
  writeCommand({ name: "uncomment", summary: "Removes a comment.", subjects: [ADDRESS, { name: "<placement>", help: `one of ${COMMENT_PLACEMENTS.join(", ")}` }] }, (call) => {
@@ -671,10 +681,7 @@ const WRITE_COMMANDS: CommandSpec[] = [
671
681
  const context = contextOf(call);
672
682
  const request = { space: call.space, from: call.parseAddress(call.subject(0), "<from>"), to: call.parseAddress(call.subject(1), "<to>"), kind: oneOf(REFERENCE_KINDS, call.subject(2), "<kind>") };
673
683
  return name === "reference"
674
- ? (db) => {
675
- const result = addReference(db, context, request);
676
- return { revision: result.revision, reference: reference(result.current) };
677
- }
684
+ ? (db) => shown(addReference(db, context, request), "reference", reference)
678
685
  : (db) => ({ revision: removeReference(db, context, request).revision });
679
686
  },
680
687
  ),
@@ -685,10 +692,7 @@ const WRITE_COMMANDS: CommandSpec[] = [
685
692
  const context = contextOf(call);
686
693
  const title = call.string("title");
687
694
  const request = { space: call.space, start: call.parseAddress(call.subject(0), "<start>"), file: call.subject(1), ...(title === undefined ? {} : { title }) };
688
- return (db) => {
689
- const result = setModule(db, context, request);
690
- return { revision: result.revision, module: module(result.current) };
691
- };
695
+ return (db) => shown(setModule(db, context, request), "module", module);
692
696
  },
693
697
  ),
694
698
  writeCommand({ name: "unmodule", summary: "Removes the module that starts at an address.", subjects: [START] }, (call) => {
@@ -837,11 +841,23 @@ const WRITE_COMMANDS: CommandSpec[] = [
837
841
  };
838
842
  },
839
843
  ),
840
- writeCommand({ name: "revert", summary: "Makes the knowledge the same as at one revision. The revert is a new revision: the history stays complete.", subjects: [REVISION] }, (call) => {
841
- const context = contextOf(call);
842
- const request = { revision: call.subject(0) };
843
- return (db) => ({ revision: revert(db, context, request).revision });
844
- }),
844
+ writeCommand(
845
+ {
846
+ name: "revert",
847
+ summary: "Makes the knowledge the same as at one revision: all spaces, or with --space only that space. The revert is a new revision: the history stays complete.",
848
+ subjects: [REVISION],
849
+ options: { space: { kind: "space", value: "<space>", help: "revert only this space: c64, drive8, or bank0 to bank255. The other spaces do not change. Without --space, every space reverts" } },
850
+ },
851
+ (call) => {
852
+ const context = contextOf(call);
853
+ const space = call.string("space");
854
+ const request = { revision: call.subject(0), ...(space === undefined ? {} : { space }) };
855
+ return (db) => {
856
+ const result = revert(db, context, request);
857
+ return { revision: result.revision, restores: result.current.restoredRevision, ...(space === undefined ? { spaces: "all" } : { space }) };
858
+ };
859
+ },
860
+ ),
845
861
  writeCommand({ name: "merge", summary: "Decides the conflicts that a git merge of two computers' work left: of the rows of one conflict, the newest stays. One revision for all." }, (call) => {
846
862
  const context = contextOf(call);
847
863
  const platform = new Map(platformNames().map((entry) => [entry.name, entry.address]));
@@ -885,7 +901,7 @@ const JOIN: CommandSpec = {
885
901
  name: "join",
886
902
  summary: "Joins the batches of agents that worked in parallel into one batch for apply. It writes no knowledge.",
887
903
  subjects: [{ name: "<batch>", help: 'a batch: a list of writes, or {"owner": {"start": "$1000", "end": "$1fff"}, "ops": [...]}', list: true }],
888
- options: { ...SPACE, out: { ...OPTIONS.out, value: "<file>", help: "the joined batch, relative to the project folder", required: true } },
904
+ options: { ...SPACE, out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the joined batch, relative to the project folder", required: true } },
889
905
  notes: "At an address in the owner range of a batch, its name, comment, operand and module win, and its regions come last. One name or one module file at two addresses writes nothing.",
890
906
  run: (call) => {
891
907
  if (call.subjects.length < 2) throw new UsageError("join needs two or more batch files");
@@ -940,7 +956,7 @@ function batchOperation(call: Call, item: unknown, at: string): BatchOperation {
940
956
  function batchFields(fields: Record<string, unknown>, at: string, address: (text: string, what: string) => number): BatchOperation {
941
957
  const op = oneOf(Object.keys(BATCH_FIELDS) as Array<BatchOperation["op"]>, String(fields.op), `${at}: op`);
942
958
  const unknown = Object.keys(fields).filter((key) => key !== "op" && key !== "space" && !BATCH_FIELDS[op].includes(key));
943
- if (unknown.length > 0) throw new Refusal("invalid-input", `${at}: ${op} takes ${BATCH_FIELDS[op].join(", ")}, not ${unknown.join(", ")}`);
959
+ if (unknown.length > 0) throw new Refusal("invalid-input", `${at}: ${op} takes ${[...BATCH_FIELDS[op], "space"].join(", ")}, not ${unknown.join(", ")}`);
944
960
  const text = (key: string) => {
945
961
  if (typeof fields[key] !== "string") throw new Refusal("invalid-input", `${at}: ${key} must be a string`);
946
962
  return fields[key] as string;
@@ -1006,7 +1022,7 @@ function batchFields(fields: Record<string, unknown>, at: string, address: (text
1006
1022
 
1007
1023
  await runScript({
1008
1024
  name: "knowledge",
1009
- summary: "Reads and writes the knowledge of the project: names, regions, comments, references, the program and its coverage, and the source knowledge. A read needs no knowledge folder. Every command takes --space: c64 (the default), drive8 for 1541 drive code, or bank0 to bank255 for cartridge banks.",
1025
+ summary: "Reads and writes the knowledge of the project: names, regions, comments, references, the program and its coverage, and the source knowledge. A read needs no knowledge folder. Every command takes --space: c64 (the default), drive8 for 1541 drive code, or bank0 to bank255 for cartridge banks. revert without --space reverts all spaces.",
1010
1026
  names: knowledgeNames,
1011
1027
  commands: [...READ_COMMANDS, ...WRITE_COMMANDS, JOIN],
1012
1028
  });
@@ -4,7 +4,7 @@
4
4
  // reads files: knowledge.ts gives the lookups.
5
5
 
6
6
  import { COVERAGE, formatC64Address, formatC64Byte } from "#src/c64.ts";
7
- import { MODE_LENGTH, OPCODES, type Mode } from "#src/opcodes.ts";
7
+ import { instructionWith, MODE_LENGTH, OPCODES, type Mode } from "#src/opcodes.ts";
8
8
 
9
9
  export interface ListingSource {
10
10
  /** The image byte at an address, or undefined outside the image. */
@@ -52,36 +52,13 @@ function accessOf(coverage: Uint8Array | undefined, start: number, length: numbe
52
52
  return text === "" ? "-" : text;
53
53
  }
54
54
 
55
- /** The operand of an instruction in ACME syntax, with the name of its target where one exists. */
55
+ /** The operand value of an instruction as an expression: the name of its target where one exists, else the number. Empty for no operand. */
56
56
  function operandOf(mode: Mode, address: number, low: number, high: number, nameOf: ListingSource["nameOf"]): string {
57
- const word = low | (high << 8);
58
57
  const named = (target: number, digits: 2 | 4) => nameOf(target) ?? (digits === 2 ? formatC64Byte(target) : formatC64Address(target));
59
- switch (mode) {
60
- case "imp":
61
- return "";
62
- case "imm":
63
- return `#${formatC64Byte(low)}`;
64
- case "zp":
65
- return named(low, 2);
66
- case "zpx":
67
- return `${named(low, 2)},x`;
68
- case "zpy":
69
- return `${named(low, 2)},y`;
70
- case "abs":
71
- return named(word, 4);
72
- case "abx":
73
- return `${named(word, 4)},x`;
74
- case "aby":
75
- return `${named(word, 4)},y`;
76
- case "ind":
77
- return `(${named(word, 4)})`;
78
- case "izx":
79
- return `(${named(low, 2)},x)`;
80
- case "izy":
81
- return `(${named(low, 2)}),y`;
82
- case "rel":
83
- return named((address + 2 + (low < 0x80 ? low : low - 0x100)) & 0xffff, 4);
84
- }
58
+ if (mode === "imp") return "";
59
+ if (mode === "imm") return formatC64Byte(low);
60
+ if (mode === "rel") return named((address + 2 + (low < 0x80 ? low : low - 0x100)) & 0xffff, 4);
61
+ return MODE_LENGTH[mode] === 3 ? named(low | (high << 8), 4) : named(low, 2);
85
62
  }
86
63
 
87
64
  /** The lines of start..end: instructions in code regions, decoded from the region start, and bytes elsewhere. */
@@ -91,7 +68,14 @@ export function listing(source: ListingSource, start: number, end: number): List
91
68
  const bytes = Array.from({ length }, (_, offset) => source.byteAt(address + offset)!.toString(16).padStart(2, "0")).join(" ");
92
69
  const label = source.nameOf(address);
93
70
  const second = source.secondNames(address).filter((name) => name !== label);
94
- const { line: lineComments, side } = source.comments(address);
71
+ const { line: lineComments, side: own } = source.comments(address);
72
+ // A comment inside the line (knowledge of an older version) shows with its address, as in the source of c64-source.
73
+ const inner: string[] = [];
74
+ for (let offset = 1; offset < length; offset++) {
75
+ const found = source.comments(address + offset);
76
+ for (const text of [...found.line, ...(found.side === undefined ? [] : [found.side])]) inner.push(`${formatC64Address(address + offset)}: ${text}`);
77
+ }
78
+ const side = own === undefined && inner.length === 0 ? undefined : [...(own === undefined ? [] : [own]), ...inner].join(" ");
95
79
  const references = source.references(address);
96
80
  const access = accessOf(source.coverage, address, length);
97
81
  return {
@@ -123,8 +107,9 @@ export function listing(source: ListingSource, start: number, end: number): List
123
107
  const length = MODE_LENGTH[opcode.mode];
124
108
  const bytes = Array.from({ length: length - 1 }, (_, offset) => source.byteAt(address + 1 + offset));
125
109
  if (address + length - 1 <= region.end && bytes.every((value) => value !== undefined)) {
110
+ // The operand expression of the knowledge in the syntax of the addressing mode, as c64-source writes it.
126
111
  const operand = source.operand(address) ?? operandOf(opcode.mode, address, bytes[0] ?? 0, bytes[1] ?? 0, source.nameOf);
127
- if (address >= start) lines.push(line(address, length, operand === "" ? opcode.mnemonic : `${opcode.mnemonic} ${operand}`));
112
+ if (address >= start) lines.push(line(address, length, instructionWith(byte, operand)));
128
113
  address += length;
129
114
  continue;
130
115
  }