@henols/c64-re-tools 2.0.0-rc.3 → 2.0.0-rc.5

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 (85) hide show
  1. package/README.md +17 -3
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +5 -2
  6. package/skills/c64-assembler/scripts/assemble.ts +8 -1
  7. package/skills/c64-assembler/scripts/compare.ts +84 -0
  8. package/skills/c64-basic/SKILL.md +4 -5
  9. package/skills/c64-disk/SKILL.md +20 -6
  10. package/skills/c64-disk/scripts/containers.ts +124 -0
  11. package/skills/c64-disk/scripts/disk.ts +59 -5
  12. package/skills/c64-emulator/SKILL.md +12 -5
  13. package/skills/c64-knowledge/SKILL.md +68 -4
  14. package/skills/c64-knowledge/scripts/knowledge.ts +262 -30
  15. package/skills/c64-memory-map/SKILL.md +4 -1
  16. package/skills/c64-memory-map/scripts/memmap.ts +14 -3
  17. package/skills/c64-provenance/SKILL.md +38 -9
  18. package/skills/c64-provenance/scripts/compare.ts +60 -29
  19. package/skills/c64-provenance/scripts/provenance.ts +250 -19
  20. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  21. package/skills/c64-provenance/scripts/separate.ts +132 -0
  22. package/skills/c64-reverse-engineering/SKILL.md +65 -11
  23. package/skills/c64-source/SKILL.md +119 -0
  24. package/skills/c64-source/scripts/generate.ts +445 -0
  25. package/skills/c64-source/scripts/loader.ts +324 -0
  26. package/skills/c64-source/scripts/opcodes.ts +61 -0
  27. package/skills/c64-source/scripts/source.ts +560 -0
  28. package/skills/c64-static-analysis/SKILL.md +28 -5
  29. package/skills/c64-static-analysis/scripts/analyze.ts +107 -14
  30. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  31. package/skills/c64-static-analysis/scripts/findings.ts +6 -4
  32. package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
  33. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  34. package/skills/c64-testing/SKILL.md +27 -3
  35. package/skills/c64-testing/references/scenario-format.md +43 -3
  36. package/skills/c64-testing/scripts/run.ts +189 -0
  37. package/skills/c64-testing/scripts/scenario.ts +153 -135
  38. package/skills/c64-testing/scripts/test.ts +16 -6
  39. package/skills/c64-unpacker/SKILL.md +69 -18
  40. package/skills/c64-unpacker/scripts/evidence.ts +113 -41
  41. package/skills/c64-unpacker/scripts/loads.ts +197 -0
  42. package/skills/c64-unpacker/scripts/machine.ts +68 -0
  43. package/skills/c64-unpacker/scripts/stages.ts +67 -0
  44. package/skills/c64-unpacker/scripts/unpack.ts +429 -55
  45. package/src/c64.ts +126 -10
  46. package/src/cli/main.ts +7 -4
  47. package/src/cli/status.ts +11 -7
  48. package/src/host/main.ts +8 -3
  49. package/src/host/server.ts +6 -5
  50. package/src/host/tools/petcat.ts +5 -1
  51. package/src/host/tools/status.ts +8 -2
  52. package/src/host/vice/adapter.ts +25 -10
  53. package/src/host/vice/process.ts +10 -5
  54. package/src/host/vice/session.ts +89 -32
  55. package/src/host-client/autostart.ts +45 -0
  56. package/src/host-client/connect.ts +5 -2
  57. package/src/host-client/coverage.ts +54 -0
  58. package/src/host-client/steps.ts +170 -0
  59. package/src/host-client/transfer.ts +29 -3
  60. package/src/host-client/vice-session.ts +79 -17
  61. package/src/knowledge/history.ts +48 -13
  62. package/src/knowledge/import.ts +104 -57
  63. package/src/knowledge/read.ts +203 -52
  64. package/src/knowledge/schema.ts +94 -12
  65. package/src/knowledge/write.ts +433 -123
  66. package/src/mcp/main.ts +43 -15
  67. package/src/mcp/output.ts +32 -0
  68. package/src/mcp/server.ts +13 -4
  69. package/src/mcp/tools/debug.ts +110 -24
  70. package/src/mcp/tools/execution.ts +3 -2
  71. package/src/mcp/tools/input.ts +7 -4
  72. package/src/mcp/tools/media.ts +26 -9
  73. package/src/mcp/tools/memory.ts +57 -10
  74. package/src/mcp/tools/video.ts +9 -3
  75. package/src/native/discover.ts +43 -6
  76. package/src/native/ghidra/index.ts +19 -3
  77. package/src/native/ghidra/scripts/C64Export.java +33 -1
  78. package/src/native/status.ts +15 -3
  79. package/src/native/types.ts +30 -1
  80. package/src/package.ts +21 -0
  81. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  82. package/src/protocol/messages.ts +17 -1
  83. package/src/protocol/tools.ts +7 -3
  84. package/src/protocol/vice.ts +86 -24
  85. package/src/script.ts +3 -2
@@ -35,33 +35,96 @@ The script prints one JSON object. An error prints `{"error": {"code": ..., "mes
35
35
  | Everything known at one address | `at '$2100'` |
36
36
  | Find names and comment text | `search player` |
37
37
  | Lists in a range | `symbols`, `regions`, `comments`, `references` with `--start` and `--end` |
38
+ | Only some origins | add `--origin semantic`, `--origin analyzer` (dxa, ghidra and heuristic), or `--origin user,llm` |
39
+ | All knowledge in one file | `export --out exports/knowledge.json` |
38
40
  | Name the symbol at an address | `rename '$2100' update_player --kind routine` |
39
41
  | Remove a symbol | `remove-symbol '$2100'` |
40
42
  | Classify memory | `classify '$3000' '$303f' sprite` |
41
- | Make memory unknown again | `unclassify '$3000' '$303f'` |
43
+ | Classify a table of records | `classify '$3000' '$303b' bytes --element 6` |
44
+ | Many writes as one revision | `apply batch.json`, or `apply -` with the JSON on standard input |
45
+ | Make memory unknown, and keep the analyzers out of it | `unclassify '$3000' '$303f'` |
42
46
  | Set or remove a comment | `comment '$2100' line Reads joystick 2.` / `uncomment '$2100' line` |
43
47
  | Add or remove a reference | `reference '$2000' '$2100' call` / `unreference ...` |
48
+ | Places that a removal blocks | `suppressions` with `--start` and `--end` |
49
+ | Start a file of generated source | `module '$2000' player.asm --title 'Player movement'` / `unmodule '$2000'` |
50
+ | Files of generated source | `modules` |
51
+ | The address spaces that hold knowledge | `spaces` |
44
52
  | History | `history '$2100'`, `history --name update_player`, `revisions`, `revision 12` |
45
53
  | Go back to an earlier state | `revert 12` |
46
54
 
47
- Symbol kinds are `label`, `routine`, `variable`, `data` and `vector`. Region types are `code`, `bytes`, `words`, `addresses`, `addresses-split`, `petscii`, `screen`, `bitmap` and `sprite`. Reference kinds are `call`, `jump`, `read`, `write` and `reference`.
55
+ 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`.
56
+
57
+ Symbol kinds are `label`, `routine`, `variable`, `data` and `vector`. Reference kinds are `call`, `jump`, `read`, `write` and `reference`.
58
+
59
+ Region types:
60
+
61
+ | Type | Meaning |
62
+ |---|---|
63
+ | `code` | Instructions. |
64
+ | `bytes`, `words`, `petscii`, `screen`, `bitmap`, `sprite` | Data of that form. |
65
+ | `addresses`, `addresses-split` | A table of addresses: one table of 16-bit values, or a low-byte table and then a high-byte table. |
66
+ | `rts-addresses`, `rts-addresses-split` | The same, with each value the target minus 1, for a PHA, PHA, RTS dispatch. |
67
+ | `jumps` | A table of `JMP abs` instructions. Each 3-byte entry is an entry point. It is code. |
68
+ | `workspace` | RAM that the program writes at run time. Its bytes in the image do not matter. |
69
+ | `unused` | Bytes that nothing executes or reads. |
70
+ | `dual-use` | Code at one time and data or a vector at another time. No analyzer finding conflicts with it. |
71
+
72
+ `--element` gives the bytes of one element of a table. The length of the region must be a multiple of it. The result shows `element` and `count`.
48
73
 
49
74
  ## Workflow
50
75
 
51
76
  Read:
52
77
 
53
78
  1. Use `at` for the addresses in the question before other work.
54
- 2. Use `history` when the current knowledge is surprising or disputed.
79
+ 2. Use `history` when the current knowledge is surprising or disputed. Each row gives the `reason` and the `evidence` of the revision that wrote it.
55
80
 
56
81
  Write:
57
82
 
58
83
  1. Read the current value with `at`. Note the `revision` in the result.
59
84
  2. Decide the change. Keep observed facts apart from your conclusions.
60
- 3. Write it with `--reason` and `--expect-revision` set to the revision you read.
85
+ 3. Write it with `--reason` and `--expect-revision` set to the revision you read. Add `--evidence` for each thing that the change rests on: `coverage:<file>`, `test:<file>`, `file:<file>` or `note:<text>`. The script keeps the SHA-256 of each file.
61
86
  4. Read the result. A `revision` of `null` means that nothing changed.
62
87
 
63
88
  Each write is one revision. A classification inside a region splits that region, so regions never overlap.
64
89
 
90
+ For more than a few writes, put them in one JSON file and use `apply`. The file is a list of writes. Each write has `op` and the arguments of the command with the same name, for example:
91
+
92
+ ```json
93
+ [
94
+ { "op": "rename", "address": "$2100", "name": "update_player", "kind": "routine" },
95
+ { "op": "classify", "start": "$3000", "end": "$303b", "type": "bytes", "element": 6 },
96
+ { "op": "comment", "address": "$2100", "placement": "line", "text": "Reads joystick 2." }
97
+ ]
98
+ ```
99
+
100
+ The ops and their fields:
101
+
102
+ | op | Fields |
103
+ |---|---|
104
+ | `rename` | `address`, `name`, `kind` (optional) |
105
+ | `remove-symbol` | `address` |
106
+ | `classify` | `start`, `end`, `type`, `element` (optional) |
107
+ | `unclassify` | `start`, `end` |
108
+ | `comment` | `address`, `placement` (`line` or `side`), `text` |
109
+ | `uncomment` | `address`, `placement` |
110
+ | `reference`, `unreference` | `from`, `to`, `kind` |
111
+ | `module` | `start`, `file`, `title` (optional) |
112
+ | `unmodule` | `start` |
113
+
114
+ 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. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
115
+
116
+ ## Modules
117
+
118
+ A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The title becomes the first line of the file. A `module` write without a title keeps the title that the module has. Modules have a history, and `revert` restores them.
119
+
120
+ ## Removals
121
+
122
+ `remove-symbol`, `unclassify` and `unreference` block the place for DXA and Ghidra. An analysis does not add a symbol at that address, a region in that range or that reference again. This applies to every removed fact, also to a fact from the LLM.
123
+
124
+ - `suppressions` lists the blocked places with the reason of each removal. `at` and `history` also show them.
125
+ - A `rename`, `classify` or `reference` at a blocked place ends the block there, because your own fact is at the place then.
126
+ - A `revert` to a revision before the removal also ends the block.
127
+
65
128
  ## Knowledge
66
129
 
67
130
  Write after interpretation:
@@ -100,3 +163,4 @@ A revert makes a new revision. The history stays complete.
100
163
 
101
164
  - Runtime evidence → c64-emulator
102
165
  - Static evidence → c64-static-analysis
166
+ - Source from the knowledge → c64-source
@@ -1,44 +1,63 @@
1
1
  // The c64-knowledge script: reads and writes .c64-re-tools/knowledge.db in the
2
2
  // project directory (the working directory) and prints one compact JSON result.
3
3
 
4
+ import { createHash } from "node:crypto";
5
+ import { readFileSync } from "node:fs";
4
6
  import type { DatabaseSync } from "node:sqlite";
5
7
  import { parseArgs } from "node:util";
6
8
 
7
9
  import { formatC64Address, parseC64Address } from "#src/c64.ts";
10
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
8
11
  import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
9
- import { historyAt, historyNamed, revision, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
12
+ import { historyAt, historyNamed, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
10
13
  import {
11
14
  at,
15
+ currentRevision,
12
16
  listComments,
17
+ listModules,
18
+ listSpaces,
13
19
  listReferences,
14
20
  listRegions,
21
+ listSuppressions,
15
22
  listSymbols,
16
23
  search,
17
24
  type CommentRow,
25
+ type ModuleRow,
18
26
  type ReferenceRow,
19
27
  type RegionRow,
28
+ type SuppressionRow,
20
29
  type SymbolRow,
21
30
  } from "#src/knowledge/read.ts";
22
31
  import {
23
32
  COMMENT_PLACEMENTS,
33
+ DEFAULT_SPACE,
34
+ EVIDENCE_KINDS,
35
+ ORIGINS,
24
36
  REFERENCE_KINDS,
25
37
  REGION_TYPES,
38
+ SPACE,
26
39
  SYMBOL_KINDS,
27
40
  type CommentPlacement,
41
+ type Origin,
28
42
  type ReferenceKind,
29
43
  type RegionType,
30
44
  type SymbolKind,
31
45
  } from "#src/knowledge/schema.ts";
32
46
  import {
33
47
  addReference,
48
+ applyBatch,
34
49
  classifyRegion,
35
50
  removeComment,
51
+ removeModule,
36
52
  removeReference,
37
53
  removeSymbol,
38
54
  renameSymbol,
39
55
  revert,
40
56
  setComment,
57
+ setModule,
41
58
  unclassifyRegion,
59
+ type BatchOperation,
60
+ type Evidence,
42
61
  type WriteContext,
43
62
  } from "#src/knowledge/write.ts";
44
63
  import { runScript, UsageError } from "#src/script.ts";
@@ -48,27 +67,44 @@ const USAGE = `knowledge.ts <command> [arguments] [options]
48
67
  Read (no database needed):
49
68
  at <address> everything known at one address
50
69
  search <text> symbol names and comment texts
51
- symbols [--start A --end A --kind K]
52
- regions [--start A --end A --type T]
53
- comments [--start A --end A]
54
- references [--start A --end A --kind K]
70
+ symbols [--start A --end A --kind K --origin O]
71
+ regions [--start A --end A --type T --origin O]
72
+ comments [--start A --end A --origin O]
73
+ references [--start A --end A --kind K --origin O]
74
+ suppressions [--start A --end A] places that removals keep free of analyzer facts
75
+ modules [--start A --end A --origin O] where the files of generated source start
76
+ spaces the address spaces that hold knowledge
55
77
  history <address> | history --name <name>
56
78
  revisions [--limit N --before N]
57
79
  revision <id>
80
+ export --out <file> [--start A --end A --origin O] all current knowledge to a project file
81
+
82
+ Every command takes --space S: c64 (the default), drive8 for code in the 1541
83
+ drive, or bank0 to bank255 for cartridge banks. A write in a batch can name its
84
+ own space.
85
+
86
+ Read option --origin O: user, llm, dxa, ghidra, heuristic, semantic (user and
87
+ llm) or analyzer (dxa, ghidra and heuristic), or a comma list. search takes it too.
58
88
 
59
89
  Write (one revision each; creates the database when needed):
60
90
  rename <address> <name> [--kind K]
61
91
  remove-symbol <address>
62
- classify <start> <end> <type>
92
+ classify <start> <end> <type> [--element N]
63
93
  unclassify <start> <end>
64
94
  comment <address> <line|side> <text>
65
95
  uncomment <address> <line|side>
66
96
  reference <from> <to> <kind>
67
97
  unreference <from> <to> <kind>
98
+ module <start> <file.asm> [--title T] a file of generated source starts here
99
+ unmodule <start>
68
100
  revert <revision>
101
+ apply <file> many writes from a JSON file ('-' reads standard input),
102
+ all as one revision, or none when one is refused
69
103
 
70
104
  Write options:
71
105
  --reason <text> why, kept with the revision
106
+ --evidence <kind:ref> what the change rests on: coverage:<file>, test:<file>,
107
+ file:<file> or note:<text> (repeatable)
72
108
  --expect-revision <N> refuse the write if knowledge changed after revision N
73
109
  --origin user|llm who decided (default llm)
74
110
 
@@ -94,10 +130,30 @@ function integer(value: string | undefined, what: string): number {
94
130
  }
95
131
 
96
132
  // Output shapes: addresses as $xxxx, nothing internal.
97
- const symbol = (row: SymbolRow) => ({ address: formatC64Address(row.address), name: row.name, kind: row.kind, origin: row.origin, revision: row.revision });
98
- const region = (row: RegionRow) => ({ start: formatC64Address(row.start), end: formatC64Address(row.end), type: row.type, origin: row.origin, revision: row.revision });
99
- const comment = (row: CommentRow) => ({ address: formatC64Address(row.address), placement: row.placement, text: row.text, origin: row.origin, revision: row.revision });
100
- const reference = (row: ReferenceRow) => ({ from: formatC64Address(row.from), to: formatC64Address(row.to), kind: row.kind, origin: row.origin, revision: row.revision });
133
+ /** The space of a row, shown only when it is not the C64 (D53). */
134
+ const spaceOf = (row: { space: string }) => (row.space === "c64" ? {} : { space: row.space });
135
+ const symbol = (row: SymbolRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), name: row.name, kind: row.kind, origin: row.origin, revision: row.revision });
136
+ const region = (row: RegionRow) => ({
137
+ ...spaceOf(row),
138
+ start: formatC64Address(row.start),
139
+ end: formatC64Address(row.end),
140
+ type: row.type,
141
+ ...(row.element === undefined ? {} : { element: row.element, count: (row.end - row.start + 1) / row.element }),
142
+ origin: row.origin,
143
+ revision: row.revision,
144
+ });
145
+ const comment = (row: CommentRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), placement: row.placement, text: row.text, origin: row.origin, revision: row.revision });
146
+ const module = (row: ModuleRow) => ({ ...spaceOf(row), start: formatC64Address(row.start), file: row.file, ...(row.title === undefined ? {} : { title: row.title }), origin: row.origin, revision: row.revision });
147
+ const reference = (row: ReferenceRow) => ({ ...spaceOf(row), from: formatC64Address(row.from), to: formatC64Address(row.to), kind: row.kind, origin: row.origin, revision: row.revision });
148
+ function suppression(row: SuppressionRow) {
149
+ const place =
150
+ row.entity === "symbol"
151
+ ? { address: formatC64Address(row.start) }
152
+ : row.entity === "region"
153
+ ? { start: formatC64Address(row.start), end: formatC64Address(row.end) }
154
+ : { from: formatC64Address(row.start), to: formatC64Address(row.to!), kind: row.kind! };
155
+ return { ...spaceOf(row), suppresses: row.entity, ...place, origin: row.origin, revision: row.revision };
156
+ }
101
157
 
102
158
  function historyEntry(entry: HistoryEntry) {
103
159
  const shown =
@@ -107,11 +163,27 @@ function historyEntry(entry: HistoryEntry) {
107
163
  ? region(entry.row)
108
164
  : entry.entity === "comment"
109
165
  ? comment(entry.row)
110
- : reference(entry.row);
166
+ : entry.entity === "reference"
167
+ ? reference(entry.row)
168
+ : entry.entity === "module"
169
+ ? module(entry.row)
170
+ : suppression(entry.row);
111
171
  const { revision: _revision, ...row } = shown;
112
172
  return { entity: entry.entity, ...row, fromRevision: entry.from, ...(entry.to === undefined ? {} : { toRevision: entry.to }) };
113
173
  }
114
174
 
175
+ /** The reason of each revision that wrote one of the rows. */
176
+ function reasonsOf(db: DatabaseSync | undefined, rows: SuppressionRow[]): Map<number, string> {
177
+ const reasons = new Map<number, string>();
178
+ const query = db?.prepare("SELECT description FROM revisions WHERE id = ?");
179
+ for (const { revision } of rows) {
180
+ if (query === undefined || reasons.has(revision)) continue;
181
+ const found = query.get(revision) as { description: string | null } | undefined;
182
+ if (found?.description != null) reasons.set(revision, found.description);
183
+ }
184
+ return reasons;
185
+ }
186
+
115
187
  function run(argv: string[]): unknown {
116
188
  const { values, positionals } = parseArgs({
117
189
  args: argv,
@@ -128,48 +200,91 @@ function run(argv: string[]): unknown {
128
200
  reason: { type: "string" },
129
201
  "expect-revision": { type: "string" },
130
202
  origin: { type: "string" },
203
+ element: { type: "string" },
204
+ evidence: { type: "string", multiple: true },
205
+ out: { type: "string" },
206
+ title: { type: "string" },
207
+ space: { type: "string" },
131
208
  help: { type: "boolean" },
132
209
  },
133
210
  });
134
211
  const [command, ...args] = positionals;
135
212
  if (values.help || command === undefined) throw new UsageError("");
136
213
  const range = { start: values.start === undefined ? 0x0000 : address(values.start, "--start"), end: values.end === undefined ? 0xffff : address(values.end, "--end") };
214
+ if (values.space !== undefined && !SPACE.test(values.space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
215
+ const space = values.space ?? DEFAULT_SPACE;
137
216
 
138
- const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "history", "revisions", "revision"]);
139
- const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "revert"]);
217
+ const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "spaces", "history", "revisions", "revision", "export"]);
218
+ const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "revert", "apply"]);
140
219
  // Refuse a mistyped command before anything opens, so no database appears by accident.
141
220
  if (!read.has(command) && !write.has(command)) throw new UsageError(`unknown command: ${command}`);
142
221
  if (read.has(command)) {
222
+ const origins = values.origin === undefined ? undefined : originFilter(values.origin);
223
+ if (command === "export" && values.out === undefined) throw new UsageError("export needs --out <file>");
143
224
  const db = openForRead();
144
225
  try {
145
226
  switch (command) {
146
227
  case "at": {
147
- const known = at(db, address(args[0]));
228
+ const known = at(db, address(args[0]), space);
148
229
  return {
230
+ ...(space === DEFAULT_SPACE ? {} : { space }),
149
231
  address: formatC64Address(known.address),
150
232
  ...(known.symbol === undefined ? {} : { symbol: symbol(known.symbol) }),
151
233
  ...(known.region === undefined ? {} : { region: region(known.region) }),
152
234
  comments: known.comments.map(comment),
153
235
  referencesFrom: known.referencesFrom.map(reference),
154
236
  referencesTo: known.referencesTo.map(reference),
237
+ ...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
155
238
  revision: known.revision,
156
239
  };
157
240
  }
158
241
  case "search": {
159
242
  if (args[0] === undefined || args[0] === "") throw new UsageError("search needs text");
160
- const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"));
243
+ const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"), origins, space);
161
244
  return { symbols: found.symbols.map(symbol), comments: found.comments.map(comment) };
162
245
  }
163
246
  case "symbols":
164
- return { symbols: listSymbols(db, range, values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind")).map(symbol) };
247
+ return { symbols: listSymbols(db, range, values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind"), origins, space).map(symbol) };
165
248
  case "regions":
166
- return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type")).map(region) };
249
+ return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type"), origins, space).map(region) };
167
250
  case "comments":
168
- return { comments: listComments(db, range).map(comment) };
251
+ return { comments: listComments(db, range, origins, space).map(comment) };
169
252
  case "references":
170
- return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind")).map(reference) };
253
+ return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind"), origins, space).map(reference) };
254
+ case "export": {
255
+ const exported = {
256
+ revision: currentRevision(db),
257
+ ...(space === DEFAULT_SPACE ? {} : { space }),
258
+ symbols: listSymbols(db, range, undefined, origins, space).map(symbol),
259
+ regions: listRegions(db, range, undefined, origins, space).map(region),
260
+ comments: listComments(db, range, origins, space).map(comment),
261
+ references: listReferences(db, range, undefined, origins, space).map(reference),
262
+ suppressions: listSuppressions(db, range, space).map(suppression),
263
+ modules: listModules(db, range, origins, space).map(module),
264
+ };
265
+ const bytes = Buffer.from(`${JSON.stringify(exported, null, 2)}\n`, "utf8");
266
+ writeProjectFile(values.out!, bytes, { what: "--out" });
267
+ return {
268
+ out: values.out,
269
+ bytes: bytes.length,
270
+ counts: { symbols: exported.symbols.length, regions: exported.regions.length, comments: exported.comments.length, references: exported.references.length, suppressions: exported.suppressions.length, modules: exported.modules.length },
271
+ revision: exported.revision,
272
+ };
273
+ }
274
+ case "suppressions": {
275
+ const rows = listSuppressions(db, range, space);
276
+ const reasons = reasonsOf(db, rows);
277
+ return { suppressions: rows.map((row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) })) };
278
+ }
279
+ case "modules":
280
+ return { modules: listModules(db, range, origins, space).map(module) };
281
+ case "spaces":
282
+ return { spaces: listSpaces(db) };
171
283
  case "history":
172
- return { history: (values.name === undefined ? historyAt(db, address(args[0])) : historyNamed(db, values.name)).map(historyEntry) };
284
+ // Each row with the reason and the evidence of the revision that wrote it (F-26).
285
+ return {
286
+ history: (values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
287
+ };
173
288
  case "revisions": {
174
289
  const options: { limit?: number; before?: number } = {};
175
290
  if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
@@ -189,18 +304,117 @@ function run(argv: string[]): unknown {
189
304
  const context: WriteContext = { origin: values.origin === undefined ? "llm" : oneOf(["user", "llm"] as const, values.origin, "--origin") };
190
305
  if (values.reason !== undefined) context.description = values.reason;
191
306
  if (values["expect-revision"] !== undefined) context.expectedRevision = integer(values["expect-revision"], "--expect-revision");
307
+ if (values.evidence !== undefined) context.evidence = values.evidence.map(evidenceOf);
192
308
  // Every argument is checked before the database opens: a refused write creates no database.
193
- const operation = writeOperation(command, args, values.kind, context);
309
+ if (values.title !== undefined && command !== "module") throw new UsageError("--title works only with module");
310
+ const operation = command === "apply" ? applyOperation(args, context, space) : writeOperation(command, args, values.kind, values.element, context, values.title, space);
194
311
  return withWrite(operation);
195
312
  }
196
313
 
314
+ /** --origin for reads: one origin, semantic (user and llm), analyzer (dxa and ghidra), or a comma list. */
315
+ function originFilter(value: string): Origin[] {
316
+ const origins = new Set<Origin>();
317
+ for (const part of value.split(",")) {
318
+ if (part === "semantic") origins.add("user").add("llm");
319
+ else if (part === "analyzer") origins.add("dxa").add("ghidra").add("heuristic");
320
+ else origins.add(oneOf(ORIGINS, part, "--origin"));
321
+ }
322
+ return [...origins];
323
+ }
324
+
325
+ /** --evidence kind:ref. A file kind names a project file and keeps its SHA-256, so a later change of the file shows. */
326
+ function evidenceOf(value: string): Evidence {
327
+ const colon = value.indexOf(":");
328
+ const kind = oneOf(EVIDENCE_KINDS, colon < 0 ? value : value.slice(0, colon), "--evidence kind");
329
+ const ref = colon < 0 ? "" : value.slice(colon + 1);
330
+ if (ref === "") throw new UsageError(`--evidence needs ${kind}:<${kind === "note" ? "text" : "file"}>`);
331
+ if (kind === "note") return { kind, ref };
332
+ return { kind, ref, sha256: createHash("sha256").update(readProjectFile(ref).bytes).digest("hex") };
333
+ }
334
+
335
+ /** apply <file>: the batch as BatchOperation values, checked before the database opens. */
336
+ function applyOperation(args: string[], context: WriteContext, space: string): WriteOperation {
337
+ if (args.length !== 1) throw new UsageError("apply needs one file, or - for standard input");
338
+ const text = args[0] === "-" ? readFileSync(0, "utf8") : readProjectFile(args[0]!).bytes.toString("utf8");
339
+ let parsed: unknown;
340
+ try {
341
+ parsed = JSON.parse(text);
342
+ } catch (error) {
343
+ throw new UsageError(`the batch is not JSON: ${(error as Error).message}`);
344
+ }
345
+ if (!Array.isArray(parsed)) throw new UsageError("the batch is a JSON list of writes, for example [{\"op\": \"rename\", \"address\": \"$2100\", \"name\": \"update_player\"}]");
346
+ const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
347
+ return (db) => {
348
+ const applied = applyBatch(db, context, operations);
349
+ // replaced: writes that replaced a row of another origin, as previous does for one write.
350
+ return { revision: applied.revision, writes: applied.steps.length, replaced: applied.steps.filter((step) => step.previous !== undefined).length };
351
+ };
352
+ }
353
+
354
+ const BATCH_FIELDS: Record<BatchOperation["op"], string[]> = {
355
+ rename: ["address", "name", "kind"],
356
+ "remove-symbol": ["address"],
357
+ classify: ["start", "end", "type", "element"],
358
+ unclassify: ["start", "end"],
359
+ comment: ["address", "placement", "text"],
360
+ uncomment: ["address", "placement"],
361
+ reference: ["from", "to", "kind"],
362
+ unreference: ["from", "to", "kind"],
363
+ module: ["start", "file", "title"],
364
+ unmodule: ["start"],
365
+ };
366
+
367
+ /** One write of a batch. A write names its own space, or takes the one of --space. */
368
+ function batchOperation(item: unknown, at: string, space: string): BatchOperation {
369
+ const operation = batchFields(item, at);
370
+ const own = (item as Record<string, unknown>).space;
371
+ if (own !== undefined && typeof own !== "string") throw new UsageError(`${at}: space must be a string`);
372
+ return { ...operation, space: own ?? space };
373
+ }
374
+
375
+ /** The fields of one write of a batch, with its addresses parsed. */
376
+ function batchFields(item: unknown, at: string): BatchOperation {
377
+ if (typeof item !== "object" || item === null || Array.isArray(item)) throw new UsageError(`${at}: a write is an object with op`);
378
+ const fields = item as Record<string, unknown>;
379
+ const op = oneOf(Object.keys(BATCH_FIELDS) as Array<BatchOperation["op"]>, String(fields.op), `${at}: op`);
380
+ const unknown = Object.keys(fields).filter((key) => key !== "op" && key !== "space" && !BATCH_FIELDS[op].includes(key));
381
+ if (unknown.length > 0) throw new UsageError(`${at}: ${op} takes ${BATCH_FIELDS[op].join(", ")}, not ${unknown.join(", ")}`);
382
+ const text = (key: string) => {
383
+ if (typeof fields[key] !== "string") throw new UsageError(`${at}: ${key} must be a string`);
384
+ return fields[key] as string;
385
+ };
386
+ const place = (key: string) => address(text(key), `${at}: ${key}`);
387
+ switch (op) {
388
+ case "rename":
389
+ return { op, address: place("address"), name: text("name"), ...(fields.kind === undefined ? {} : { kind: oneOf(SYMBOL_KINDS, text("kind"), `${at}: kind`) }) };
390
+ case "remove-symbol":
391
+ return { op, address: place("address") };
392
+ case "classify": {
393
+ if (fields.element !== undefined && typeof fields.element !== "number") throw new UsageError(`${at}: element must be a number`);
394
+ return { op, start: place("start"), end: place("end"), type: oneOf(REGION_TYPES, text("type"), `${at}: type`), ...(fields.element === undefined ? {} : { element: fields.element as number }) };
395
+ }
396
+ case "unclassify":
397
+ return { op, start: place("start"), end: place("end") };
398
+ case "comment":
399
+ return { op, address: place("address"), placement: oneOf(COMMENT_PLACEMENTS, text("placement"), `${at}: placement`), text: text("text") };
400
+ case "uncomment":
401
+ return { op, address: place("address"), placement: oneOf(COMMENT_PLACEMENTS, text("placement"), `${at}: placement`) };
402
+ case "module":
403
+ return { op, start: place("start"), file: text("file"), ...(fields.title === undefined ? {} : { title: text("title") }) };
404
+ case "unmodule":
405
+ return { op, start: place("start") };
406
+ default:
407
+ return { op, from: place("from"), to: place("to"), kind: oneOf(REFERENCE_KINDS, text("kind"), `${at}: kind`) };
408
+ }
409
+ }
410
+
197
411
  type WriteOperation = (db: DatabaseSync) => unknown;
198
412
 
199
- function writeOperation(command: string, args: string[], kind: string | undefined, context: WriteContext): WriteOperation {
413
+ function writeOperation(command: string, args: string[], kind: string | undefined, element: string | undefined, context: WriteContext, title: string | undefined, space: string): WriteOperation {
200
414
  switch (command) {
201
415
  case "rename": {
202
416
  if (args[1] === undefined) throw new UsageError("name is missing");
203
- const request: { address: number; name: string; kind?: SymbolKind } = { address: address(args[0]), name: args[1] };
417
+ const request: { space: string; address: number; name: string; kind?: SymbolKind } = { space, address: address(args[0]), name: args[1] };
204
418
  if (kind !== undefined) request.kind = oneOf(SYMBOL_KINDS, kind, "--kind");
205
419
  return (db) => {
206
420
  const result = renameSymbol(db, context, request);
@@ -208,43 +422,61 @@ function writeOperation(command: string, args: string[], kind: string | undefine
208
422
  };
209
423
  }
210
424
  case "remove-symbol": {
211
- const request = { address: address(args[0]) };
425
+ const request = { space, address: address(args[0]) };
212
426
  return (db) => ({ revision: removeSymbol(db, context, request).revision });
213
427
  }
214
428
  case "classify": {
215
- const request = { start: address(args[0], "start"), end: address(args[1], "end"), type: oneOf(REGION_TYPES, args[2], "type") as RegionType };
429
+ const request = {
430
+ space,
431
+ start: address(args[0], "start"),
432
+ end: address(args[1], "end"),
433
+ type: oneOf(REGION_TYPES, args[2], "type") as RegionType,
434
+ ...(element === undefined ? {} : { element: integer(element, "--element") }),
435
+ };
216
436
  return (db) => {
217
437
  const result = classifyRegion(db, context, request);
218
438
  return { revision: result.revision, regions: result.current.map(region), ...(result.previous === undefined ? {} : { previous: result.previous.map(region) }) };
219
439
  };
220
440
  }
221
441
  case "unclassify": {
222
- const request = { start: address(args[0], "start"), end: address(args[1], "end") };
442
+ const request = { space, start: address(args[0], "start"), end: address(args[1], "end") };
223
443
  return (db) => ({ revision: unclassifyRegion(db, context, request).revision });
224
444
  }
225
445
  case "comment": {
226
446
  if (args.length < 3) throw new UsageError("text is missing");
227
- const request = { address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") as CommentPlacement, text: args.slice(2).join(" ") };
447
+ const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") as CommentPlacement, text: args.slice(2).join(" ") };
228
448
  return (db) => {
229
449
  const result = setComment(db, context, request);
230
450
  return { revision: result.revision, comment: comment(result.current) };
231
451
  };
232
452
  }
233
453
  case "uncomment": {
234
- const request = { address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") };
454
+ const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") };
235
455
  return (db) => ({ revision: removeComment(db, context, request).revision });
236
456
  }
237
457
  case "reference": {
238
- const request = { from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") as ReferenceKind };
458
+ const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") as ReferenceKind };
239
459
  return (db) => {
240
460
  const result = addReference(db, context, request);
241
461
  return { revision: result.revision, reference: reference(result.current) };
242
462
  };
243
463
  }
244
464
  case "unreference": {
245
- const request = { from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") };
465
+ const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") };
246
466
  return (db) => ({ revision: removeReference(db, context, request).revision });
247
467
  }
468
+ case "module": {
469
+ if (args[1] === undefined) throw new UsageError("file is missing");
470
+ const request = { space, start: address(args[0], "start"), file: args[1], ...(title === undefined ? {} : { title }) };
471
+ return (db) => {
472
+ const result = setModule(db, context, request);
473
+ return { revision: result.revision, module: module(result.current) };
474
+ };
475
+ }
476
+ case "unmodule": {
477
+ const request = { space, start: address(args[0], "start") };
478
+ return (db) => ({ revision: removeModule(db, context, request).revision });
479
+ }
248
480
  case "revert": {
249
481
  const request = { revision: integer(args[0], "revision") };
250
482
  return (db) => ({ revision: revert(db, context, request).revision });
@@ -24,6 +24,7 @@ The script looks up local reference data. It does not use the host runtime.
24
24
  node <skill>/scripts/memmap.ts at '$d020' '$0314' '$ffd2'
25
25
  node <skill>/scripts/memmap.ts decode '$d018' '$15' --bank '$97'
26
26
  node <skill>/scripts/memmap.ts list vic
27
+ node <skill>/scripts/memmap.ts export --out src/hw.asm
27
28
  ```
28
29
 
29
30
  | Command | Result |
@@ -31,6 +32,7 @@ node <skill>/scripts/memmap.ts list vic
31
32
  | `at <address> ...` | For each address: `kind` (`platform` or `application`), the area, the name and the meaning. |
32
33
  | `decode <register> <value>` | The meaning of the value. Use `--bank` with the value of `$DD00` to get the addresses for `$D018`. |
33
34
  | `list <vic\|sid\|cia1\|cia2\|kernal\|system>` | All entries of that group. |
35
+ | `export --out <file>` | Writes all platform names to a project file as an ACME include: one `NAME = $xxxx` line for each name, with its meaning as a comment. |
34
36
 
35
37
  Write an address or a value as `$` and hex digits, as a decimal number, or as `%` and binary digits. Put a `$` value in single quotes in a shell.
36
38
 
@@ -45,6 +47,7 @@ The script prints one JSON object.
45
47
  3. For an I/O register, `mirrorOf` shows the register that a mirror address repeats. `visible` tells that an address in `$D000`-`$DFFF` is visible only when `$01` shows I/O at `$D000`. This area holds the chip registers, the color RAM and the cartridge I/O areas.
46
48
  4. For a byte in a multi-byte entry, for example the high byte of a vector, `entryStart` gives the first address of the entry.
47
49
  5. To understand a value in a register, run `decode`. For the memory configuration, decode the value of `$01`.
50
+ 6. For source that you write by hand, run `export` and include the file with `!source`. The c64-source skill uses the same names without the file.
48
51
 
49
52
  The decoders are for `$01`, `$D010`, `$D011`, `$D015` to `$D01F` (the sprite and interrupt bits), `$D016`, `$D018`, `$DC0D`, `$DD00` and `$DD0D`.
50
53
 
@@ -59,7 +62,7 @@ A conclusion about the application can be project knowledge. For example: "the p
59
62
 
60
63
  ## Result
61
64
 
62
- The platform meaning of each address, or the decoded meaning of a register value.
65
+ The platform meaning of each address, or the decoded meaning of a register value. For `export`: the file, the count of names and the bytes.
63
66
 
64
67
  The exit status is 0 for a result. The exit status is 2 for a wrong argument.
65
68