@henols/c64-re-tools 2.1.1 → 2.1.2

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 (57) 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 +3 -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 +2 -1
  13. package/skills/c64-knowledge/SKILL.md +6 -4
  14. package/skills/c64-knowledge/scripts/knowledge.ts +35 -13
  15. package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
  16. package/skills/c64-memory-map/SKILL.md +1 -0
  17. package/skills/c64-memory-map/scripts/memory-map.ts +1 -1
  18. package/skills/c64-provenance/SKILL.md +1 -0
  19. package/skills/c64-provenance/scripts/provenance.ts +9 -6
  20. package/skills/c64-source/SKILL.md +1 -0
  21. package/skills/c64-source/scripts/lib/source.ts +7 -29
  22. package/skills/c64-source/scripts/source.ts +4 -4
  23. package/skills/c64-static-analysis/SKILL.md +2 -0
  24. package/skills/c64-static-analysis/scripts/static-analysis.ts +14 -5
  25. package/skills/c64-testing/SKILL.md +3 -2
  26. package/skills/c64-testing/scripts/lib/diff-files.ts +14 -2
  27. package/skills/c64-testing/scripts/lib/recording.ts +3 -3
  28. package/skills/c64-testing/scripts/lib/replay.ts +1 -1
  29. package/skills/c64-testing/scripts/lib/runs.ts +27 -7
  30. package/skills/c64-testing/scripts/testing.ts +5 -5
  31. package/skills/c64-unpacker/SKILL.md +8 -5
  32. package/skills/c64-unpacker/scripts/lib/loads.ts +12 -6
  33. package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
  34. package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
  35. package/skills/c64-unpacker/scripts/unpacker.ts +18 -17
  36. package/src/host/vice/adapter.ts +4 -1
  37. package/src/host/vice/session.ts +49 -3
  38. package/src/host-client/image.ts +10 -10
  39. package/src/host-client/steps.ts +39 -3
  40. package/src/host-client/transfer.ts +14 -26
  41. package/src/knowledge/history.ts +55 -2
  42. package/src/knowledge/import.ts +79 -7
  43. package/src/knowledge/program.ts +31 -2
  44. package/src/knowledge/read.ts +7 -4
  45. package/src/knowledge/schema.ts +3 -2
  46. package/src/knowledge/text.ts +1 -0
  47. package/src/knowledge/write.ts +22 -14
  48. package/src/mcp/inputs.ts +6 -3
  49. package/src/mcp/server.ts +2 -1
  50. package/src/mcp/tools/debug.ts +29 -3
  51. package/src/mcp/tools/machine.ts +3 -1
  52. package/src/mcp/tools/memory.ts +4 -3
  53. package/src/media.ts +42 -0
  54. package/src/opcodes.ts +51 -18
  55. package/src/project.ts +110 -2
  56. package/src/protocol/vice.ts +4 -1
  57. package/src/script.ts +55 -3
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.2",
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,9 @@ 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 (buffer.length < 0x40 || !hasSignature("crt", buffer)) throw new Refusal("media-error", "the file is not a CRT: it does not start with C64 CARTRIDGE");
38
37
  const headerLength = buffer.readUInt32BE(0x10);
39
38
  const type = buffer.readUInt16BE(0x16);
40
39
  const exrom = buffer[0x18]!;
@@ -85,7 +84,7 @@ export interface Tape {
85
84
 
86
85
  export function parseT64(bytes: Uint8Array): Tape {
87
86
  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");
87
+ 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
88
  const maxEntries = buffer.readUInt16LE(0x22);
90
89
  const name = buffer.toString("latin1", 0x28, 0x40).replace(/[\0 ]+$/, "");
91
90
  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
 
@@ -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.
@@ -172,7 +174,7 @@ The ops and their fields:
172
174
  | `unmacro` | `name` |
173
175
  | `use` | `start`, `end`, `macro`, `args` (a list or a comma list) |
174
176
 
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.
177
+ 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
178
 
177
179
  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
180
 
@@ -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)
@@ -420,7 +429,8 @@ function writeCommand(spec: Omit<CommandSpec, "run">, prepare: (call: Call) => W
420
429
  return {
421
430
  ...spec,
422
431
  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 },
432
+ // A command can give a WRITE option its own help or default, for example --space of revert.
433
+ options: Object.fromEntries(Object.entries({ ...spec.options, ...WRITE }).map(([name, option]) => [name, spec.options?.[name] ?? option])),
424
434
  run: (call) => withWrite(prepare(call)),
425
435
  };
426
436
  }
@@ -564,11 +574,11 @@ const READ_COMMANDS: CommandSpec[] = [
564
574
  return { revisions: list(call, rows, (row) => row, "a larger --max-listed, or --before <revision> with the oldest revision in listed") };
565
575
  },
566
576
  ),
567
- readCommand({ name: "revision", summary: "The rows that one revision added and closed.", subjects: [REVISION], options: LIST }, (db, call) => {
577
+ 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
578
  const changes = revision(db, call.subject(0));
569
579
  return { revision: changes.revision, added: list(call, changes.added, historyEntry, MORE), closed: list(call, changes.closed, historyEntry, MORE) };
570
580
  }),
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) => {
581
+ 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
582
  const range = rangeOf(call);
573
583
  const origins = originsOf(call);
574
584
  const space = call.space;
@@ -595,7 +605,7 @@ const READ_COMMANDS: CommandSpec[] = [
595
605
  name: "listing",
596
606
  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
607
  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 },
608
+ 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
609
  },
600
610
  (db, call) => {
601
611
  const { start, end } = call.parseRange(call.subject(0), "<range>");
@@ -837,11 +847,23 @@ const WRITE_COMMANDS: CommandSpec[] = [
837
847
  };
838
848
  },
839
849
  ),
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
- }),
850
+ writeCommand(
851
+ {
852
+ name: "revert",
853
+ 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.",
854
+ subjects: [REVISION],
855
+ 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" } },
856
+ },
857
+ (call) => {
858
+ const context = contextOf(call);
859
+ const space = call.string("space");
860
+ const request = { revision: call.subject(0), ...(space === undefined ? {} : { space }) };
861
+ return (db) => {
862
+ const result = revert(db, context, request);
863
+ return { revision: result.revision, restores: result.current.restoredRevision, ...(space === undefined ? { spaces: "all" } : { space }) };
864
+ };
865
+ },
866
+ ),
845
867
  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
868
  const context = contextOf(call);
847
869
  const platform = new Map(platformNames().map((entry) => [entry.name, entry.address]));
@@ -885,7 +907,7 @@ const JOIN: CommandSpec = {
885
907
  name: "join",
886
908
  summary: "Joins the batches of agents that worked in parallel into one batch for apply. It writes no knowledge.",
887
909
  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 } },
910
+ options: { ...SPACE, out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the joined batch, relative to the project folder", required: true } },
889
911
  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
912
  run: (call) => {
891
913
  if (call.subjects.length < 2) throw new UsageError("join needs two or more batch files");
@@ -940,7 +962,7 @@ function batchOperation(call: Call, item: unknown, at: string): BatchOperation {
940
962
  function batchFields(fields: Record<string, unknown>, at: string, address: (text: string, what: string) => number): BatchOperation {
941
963
  const op = oneOf(Object.keys(BATCH_FIELDS) as Array<BatchOperation["op"]>, String(fields.op), `${at}: op`);
942
964
  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(", ")}`);
965
+ if (unknown.length > 0) throw new Refusal("invalid-input", `${at}: ${op} takes ${[...BATCH_FIELDS[op], "space"].join(", ")}, not ${unknown.join(", ")}`);
944
966
  const text = (key: string) => {
945
967
  if (typeof fields[key] !== "string") throw new Refusal("invalid-input", `${at}: ${key} must be a string`);
946
968
  return fields[key] as string;
@@ -1006,7 +1028,7 @@ function batchFields(fields: Record<string, unknown>, at: string, address: (text
1006
1028
 
1007
1029
  await runScript({
1008
1030
  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.",
1031
+ 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
1032
  names: knowledgeNames,
1011
1033
  commands: [...READ_COMMANDS, ...WRITE_COMMANDS, JOIN],
1012
1034
  });
@@ -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. */
@@ -123,8 +100,9 @@ export function listing(source: ListingSource, start: number, end: number): List
123
100
  const length = MODE_LENGTH[opcode.mode];
124
101
  const bytes = Array.from({ length: length - 1 }, (_, offset) => source.byteAt(address + 1 + offset));
125
102
  if (address + length - 1 <= region.end && bytes.every((value) => value !== undefined)) {
103
+ // The operand expression of the knowledge in the syntax of the addressing mode, as c64-source writes it.
126
104
  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}`));
105
+ if (address >= start) lines.push(line(address, length, instructionWith(byte, operand)));
128
106
  address += length;
129
107
  continue;
130
108
  }
@@ -43,6 +43,7 @@ Write a value for `decode` and `--bank` as `$` and hex digits, as a decimal numb
43
43
  - 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.
44
44
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
45
45
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
46
+ - 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.
46
47
  - 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.
47
48
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
48
49
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -71,7 +71,7 @@ await runScript({
71
71
  {
72
72
  name: "export",
73
73
  summary: "Writes the platform names as an ACME include: NAME = $xxxx.",
74
- options: { out: { ...OPTIONS.out, value: "<file>", help: "the include file, relative to the project folder", required: true } },
74
+ options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the include file, relative to the project folder", required: true } },
75
75
  run: (call) => {
76
76
  const names = platformNames();
77
77
  const lines = ["; C64 platform names from the c64-memory-map skill, for ACME.", ...names.map((item) => `${item.name} = ${item.address < 0x100 ? formatC64Byte(item.address) : formatC64Address(item.address)}`.padEnd(24) + `; ${item.meaning}`)];
@@ -56,6 +56,7 @@ With `--frames`, a PRG starts with autostart and RUN. An image starts from its b
56
56
  - 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.
57
57
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
58
58
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
59
+ - 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.
59
60
  - 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.
60
61
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
61
62
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -41,7 +41,7 @@ import {
41
41
  import { CODE_REGION_TYPES, SEMANTIC_ORIGINS } from "#src/knowledge/schema.ts";
42
42
  import { platformNames } from "#src/platform.ts";
43
43
  import { projectRoot } from "#src/project.ts";
44
- import { knowledgeNames } from "#src/knowledge/names.ts";
44
+ import { knowledgeAt, knowledgeNames } from "#src/knowledge/names.ts";
45
45
  import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec } from "#src/script.ts";
46
46
  import { compareReleases, shiftSuggestion, suggestShifts, type Release } from "./lib/compare.ts";
47
47
  import { findRoutine, libraryFolder, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
@@ -107,14 +107,17 @@ async function compare(call: Call): Promise<unknown> {
107
107
  const shifts = new Map<number, number>();
108
108
  for (const text of call.strings("shift")) {
109
109
  const match = /^(\d+):([+-]?\d+)$/.exec(text);
110
- if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
110
+ if (match === null) throw new UsageError(`--shift must be <release number>:<bytes>, for example 2:-256, not ${text}`);
111
+ if (Number(match[1]) < 1 || Number(match[1]) > positionals.length) {
112
+ throw new UsageError(`--shift ${text}: there is no release ${match[1]}. This compare has ${positionals.length} releases: give a release number from 1 to ${positionals.length}`);
113
+ }
111
114
  if (shifts.has(Number(match[1]))) throw new UsageError(`--shift gives release ${match[1]} more than one shift (${shifts.get(Number(match[1]))} and ${match[2]}). Give one --shift for each release`);
112
115
  shifts.set(Number(match[1]), Number(match[2]));
113
116
  }
114
117
  const frames = call.count("frames");
115
118
  const stepsFile = call.string("steps");
116
119
  if (stepsFile !== undefined && frames === undefined) throw new UsageError("--steps works only with --frames");
117
- const steps: ActionStep[] = stepsFile === undefined ? [] : readActionSteps(stepsFile, "--steps");
120
+ const steps: ActionStep[] = stepsFile === undefined ? [] : readActionSteps(stepsFile, "--steps", knowledgeAt());
118
121
  const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
119
122
  const releases = loaded.map((item) => item.release);
120
123
  const range = call.range("range");
@@ -462,14 +465,14 @@ const COMMANDS: CommandSpec[] = [
462
465
  {
463
466
  name: "separate",
464
467
  summary: "Evidence from the program in the knowledge: the unpack stage that wrote each range, its access in the coverage, whether call and jump references, or the run on from the code before it, reach each code region from the entry, and what the program reads or executes outside its segments.",
465
- options: { out: { ...OPTIONS.out, value: "<file>", help: "write all ranges to this file" }, "max-listed": OPTIONS["max-listed"] },
468
+ options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write all ranges to this file" }, "max-listed": OPTIONS["max-listed"] },
466
469
  run: separateCommand,
467
470
  },
468
471
  {
469
472
  name: "leftovers",
470
473
  summary: "What the program in the knowledge holds besides its working code: texts in PETSCII, screen codes, ASCII or with bit 7 set (Apple II), assembler source, BASIC lines away from the BASIC start, fills, untouched bytes outside code, copies of earlier bytes, and code regions that nothing reaches and no run executed.",
471
474
  options: {
472
- out: { ...OPTIONS.out, value: "<file>", help: "write all findings to this file" },
475
+ out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write all findings to this file" },
473
476
  "min-text": { kind: "count", value: "<n>", min: 4, max: 999, default: 8, help: "the shortest text to find (default 8)" },
474
477
  "max-listed": OPTIONS["max-listed"],
475
478
  },
@@ -492,7 +495,7 @@ const COMMANDS: CommandSpec[] = [
492
495
  summary: "Finds each routine of the library in the program of the knowledge: the opcodes and other fixed bytes equal, and the addresses into the routine moved by one shift.",
493
496
  options: {
494
497
  library: { ...LIBRARY, help: "the library folder, relative to the project folder, for example ../library" },
495
- out: { ...OPTIONS.out, value: "<file>", help: "write the names of the routines at the found addresses as a batch for knowledge.ts apply" },
498
+ out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write the names of the routines at the found addresses as a batch for knowledge.ts apply" },
496
499
  "max-listed": OPTIONS["max-listed"],
497
500
  },
498
501
  run: matchCommand,