@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11

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 (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,613 @@
1
+ // The c64-knowledge script: reads and writes .c64-re-tools/knowledge.db in the
2
+ // project directory (the working directory) and prints one compact JSON result.
3
+
4
+ import { createHash } from "node:crypto";
5
+ import { readFileSync } from "node:fs";
6
+ import type { DatabaseSync } from "node:sqlite";
7
+ import { parseArgs } from "node:util";
8
+
9
+ import { formatC64Address, parseC64Address } from "#src/c64.ts";
10
+ import { ImageError, readImage } from "#src/host-client/image.ts";
11
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
12
+ import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
13
+ import { operandText } from "#src/knowledge/expression.ts";
14
+ import { historyAt, historyNamed, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
15
+ import {
16
+ at,
17
+ currentRevision,
18
+ listComments,
19
+ listModules,
20
+ listOperands,
21
+ listSpaces,
22
+ listReferences,
23
+ listRegions,
24
+ listSuppressions,
25
+ listSymbols,
26
+ search,
27
+ type CommentRow,
28
+ type ModuleRow,
29
+ type OperandRow,
30
+ type ReferenceRow,
31
+ type RegionRow,
32
+ type SuppressionRow,
33
+ type SymbolRow,
34
+ } from "#src/knowledge/read.ts";
35
+ import {
36
+ COMMENT_PLACEMENTS,
37
+ DEFAULT_SPACE,
38
+ EVIDENCE_KINDS,
39
+ OPERAND_SELECTS,
40
+ ORIGINS,
41
+ REFERENCE_KINDS,
42
+ REGION_TYPES,
43
+ SPACE,
44
+ SYMBOL_KINDS,
45
+ type CommentPlacement,
46
+ type OperandSelect,
47
+ type Origin,
48
+ type ReferenceKind,
49
+ type RegionType,
50
+ type SymbolKind,
51
+ } from "#src/knowledge/schema.ts";
52
+ import {
53
+ addReference,
54
+ applyBatch,
55
+ classifyRegion,
56
+ removeComment,
57
+ removeModule,
58
+ removeOperand,
59
+ removeReference,
60
+ removeSymbol,
61
+ renameSymbol,
62
+ revert,
63
+ setComment,
64
+ setModule,
65
+ setOperand,
66
+ unclassifyRegion,
67
+ type BatchOperation,
68
+ type Evidence,
69
+ type OperandImage,
70
+ type WriteContext,
71
+ } from "#src/knowledge/write.ts";
72
+ import { platformNames } from "#src/platform.ts";
73
+ import { runScript, UsageError } from "#src/script.ts";
74
+
75
+ const USAGE = `knowledge.ts <command> [arguments] [options]
76
+
77
+ Read (no database needed):
78
+ at <address> everything known at one address
79
+ search <text> symbol names and comment texts
80
+ symbols [--start A --end A --kind K --origin O]
81
+ regions [--start A --end A --type T --origin O]
82
+ comments [--start A --end A --origin O]
83
+ references [--start A --end A --kind K --origin O]
84
+ suppressions [--start A --end A] places that removals keep free of analyzer facts
85
+ modules [--start A --end A --origin O] where the files of generated source start
86
+ operands [--start A --end A --origin O] operand expressions of instructions
87
+ spaces the address spaces that hold knowledge
88
+ history <address> | history --name <name>
89
+ revisions [--limit N --before N]
90
+ revision <id>
91
+ export --out <file> [--start A --end A --origin O] all current knowledge to a project file
92
+
93
+ Every command takes --space S: c64 (the default), drive8 for code in the 1541
94
+ drive, or bank0 to bank255 for cartridge banks. A write in a batch can name its
95
+ own space.
96
+
97
+ Read option --origin O: user, llm, dxa, ghidra, heuristic, semantic (user and
98
+ llm) or analyzer (dxa, ghidra and heuristic), or a comma list. search takes it too.
99
+
100
+ Write (one revision each; creates the database when needed):
101
+ rename <address> <name> [--kind K]
102
+ remove-symbol <address>
103
+ classify <start> <end> <type> [--element N]
104
+ unclassify <start> <end>
105
+ comment <address> <line|side> <text>
106
+ uncomment <address> <line|side>
107
+ reference <from> <to> <kind>
108
+ unreference <from> <to> <kind>
109
+ module <start> <file.asm> [--title T] a file of generated source starts here
110
+ unmodule <start>
111
+ operand <address> --lo|--hi|--value <symbol> [--offset N] --image <file>
112
+ operand <address> --expr <text> --image <file>
113
+ write the operand of the instruction at <address> as
114
+ an expression: the low or high byte of symbol + N, the
115
+ value symbol + N, or ACME expression text; refused when
116
+ it does not give the operand in the image (image.json
117
+ or PRG); give a negative offset as --offset=-1
118
+ unoperand <address>
119
+ revert <revision>
120
+ apply <file> many writes from a JSON file ('-' reads standard input),
121
+ all as one revision, or none when one is refused;
122
+ operand writes need --image <file>
123
+
124
+ Write options:
125
+ --reason <text> why, kept with the revision
126
+ --evidence <kind:ref> what the change rests on: coverage:<file>, test:<file>,
127
+ file:<file> or note:<text> (repeatable)
128
+ --expect-revision <N> refuse the write if knowledge changed after revision N
129
+ --origin user|llm who decided (default llm)
130
+
131
+ Addresses are $ and four hex digits; quote them in a shell: '$2100'.`;
132
+
133
+ function address(value: string | undefined, what = "address"): number {
134
+ if (value === undefined) throw new UsageError(`${what} is missing`);
135
+ try {
136
+ return parseC64Address(value);
137
+ } catch {
138
+ throw new UsageError(`${what} must be $ followed by four hex digits, for example '$2100', not ${value}`);
139
+ }
140
+ }
141
+
142
+ function oneOf<T extends string>(values: readonly T[], value: string | undefined, what: string): T {
143
+ if (value === undefined || !(values as readonly string[]).includes(value)) throw new UsageError(`${what} must be one of: ${values.join(", ")}`);
144
+ return value as T;
145
+ }
146
+
147
+ function integer(value: string | undefined, what: string): number {
148
+ if (value === undefined || !/^\d+$/.test(value)) throw new UsageError(`${what} must be a whole number`);
149
+ return Number(value);
150
+ }
151
+
152
+ // Output shapes: addresses as $xxxx, nothing internal.
153
+ /** The space of a row, shown only when it is not the C64 (D53). */
154
+ const spaceOf = (row: { space: string }) => (row.space === "c64" ? {} : { space: row.space });
155
+ const symbol = (row: SymbolRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), name: row.name, kind: row.kind, origin: row.origin, revision: row.revision });
156
+ const region = (row: RegionRow) => ({
157
+ ...spaceOf(row),
158
+ start: formatC64Address(row.start),
159
+ end: formatC64Address(row.end),
160
+ type: row.type,
161
+ ...(row.element === undefined ? {} : { element: row.element, count: (row.end - row.start + 1) / row.element }),
162
+ origin: row.origin,
163
+ revision: row.revision,
164
+ });
165
+ const comment = (row: CommentRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), placement: row.placement, text: row.text, origin: row.origin, revision: row.revision });
166
+ 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 });
167
+ const operand = (row: OperandRow) => ({
168
+ ...spaceOf(row),
169
+ address: formatC64Address(row.address),
170
+ select: row.select,
171
+ ...(row.symbol === undefined ? {} : { symbol: row.symbol }),
172
+ ...(row.offset === 0 ? {} : { offset: row.offset }),
173
+ ...(row.expression === undefined ? {} : { expression: row.expression }),
174
+ text: operandText(row),
175
+ origin: row.origin,
176
+ revision: row.revision,
177
+ });
178
+ const reference = (row: ReferenceRow) => ({ ...spaceOf(row), from: formatC64Address(row.from), to: formatC64Address(row.to), kind: row.kind, origin: row.origin, revision: row.revision });
179
+ function suppression(row: SuppressionRow) {
180
+ const place =
181
+ row.entity === "symbol"
182
+ ? { address: formatC64Address(row.start) }
183
+ : row.entity === "region"
184
+ ? { start: formatC64Address(row.start), end: formatC64Address(row.end) }
185
+ : { from: formatC64Address(row.start), to: formatC64Address(row.to!), kind: row.kind! };
186
+ return { ...spaceOf(row), suppresses: row.entity, ...place, origin: row.origin, revision: row.revision };
187
+ }
188
+
189
+ function historyEntry(entry: HistoryEntry) {
190
+ const shown =
191
+ entry.entity === "symbol"
192
+ ? symbol(entry.row)
193
+ : entry.entity === "region"
194
+ ? region(entry.row)
195
+ : entry.entity === "comment"
196
+ ? comment(entry.row)
197
+ : entry.entity === "reference"
198
+ ? reference(entry.row)
199
+ : entry.entity === "module"
200
+ ? module(entry.row)
201
+ : entry.entity === "operand"
202
+ ? operand(entry.row)
203
+ : suppression(entry.row);
204
+ const { revision: _revision, ...row } = shown;
205
+ return { entity: entry.entity, ...row, fromRevision: entry.from, ...(entry.to === undefined ? {} : { toRevision: entry.to }) };
206
+ }
207
+
208
+ /** The reason of each revision that wrote one of the rows. */
209
+ function reasonsOf(db: DatabaseSync | undefined, rows: SuppressionRow[]): Map<number, string> {
210
+ const reasons = new Map<number, string>();
211
+ const query = db?.prepare("SELECT description FROM revisions WHERE id = ?");
212
+ for (const { revision } of rows) {
213
+ if (query === undefined || reasons.has(revision)) continue;
214
+ const found = query.get(revision) as { description: string | null } | undefined;
215
+ if (found?.description != null) reasons.set(revision, found.description);
216
+ }
217
+ return reasons;
218
+ }
219
+
220
+ function run(argv: string[]): unknown {
221
+ const { values, positionals } = parseArgs({
222
+ args: argv,
223
+ allowPositionals: true,
224
+ strict: true,
225
+ options: {
226
+ start: { type: "string" },
227
+ end: { type: "string" },
228
+ kind: { type: "string" },
229
+ type: { type: "string" },
230
+ name: { type: "string" },
231
+ limit: { type: "string" },
232
+ before: { type: "string" },
233
+ reason: { type: "string" },
234
+ "expect-revision": { type: "string" },
235
+ origin: { type: "string" },
236
+ element: { type: "string" },
237
+ evidence: { type: "string", multiple: true },
238
+ out: { type: "string" },
239
+ title: { type: "string" },
240
+ space: { type: "string" },
241
+ lo: { type: "string" },
242
+ hi: { type: "string" },
243
+ value: { type: "string" },
244
+ expr: { type: "string" },
245
+ offset: { type: "string" },
246
+ image: { type: "string" },
247
+ help: { type: "boolean" },
248
+ },
249
+ });
250
+ const [command, ...args] = positionals;
251
+ if (values.help || command === undefined) throw new UsageError("");
252
+ const range = { start: values.start === undefined ? 0x0000 : address(values.start, "--start"), end: values.end === undefined ? 0xffff : address(values.end, "--end") };
253
+ if (values.space !== undefined && !SPACE.test(values.space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
254
+ const space = values.space ?? DEFAULT_SPACE;
255
+
256
+ const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "operands", "spaces", "history", "revisions", "revision", "export"]);
257
+ const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "operand", "unoperand", "revert", "apply"]);
258
+ // Refuse a mistyped command before anything opens, so no database appears by accident.
259
+ if (!read.has(command) && !write.has(command)) throw new UsageError(`unknown command: ${command}`);
260
+ if (read.has(command)) {
261
+ const origins = values.origin === undefined ? undefined : originFilter(values.origin);
262
+ if (command === "export" && values.out === undefined) throw new UsageError("export needs --out <file>");
263
+ const db = openForRead();
264
+ try {
265
+ switch (command) {
266
+ case "at": {
267
+ const known = at(db, address(args[0]), space);
268
+ return {
269
+ ...(space === DEFAULT_SPACE ? {} : { space }),
270
+ address: formatC64Address(known.address),
271
+ ...(known.symbol === undefined ? {} : { symbol: symbol(known.symbol) }),
272
+ ...(known.region === undefined ? {} : { region: region(known.region) }),
273
+ comments: known.comments.map(comment),
274
+ referencesFrom: known.referencesFrom.map(reference),
275
+ referencesTo: known.referencesTo.map(reference),
276
+ ...(known.operand === undefined ? {} : { operand: operand(known.operand) }),
277
+ ...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
278
+ revision: known.revision,
279
+ };
280
+ }
281
+ case "search": {
282
+ if (args[0] === undefined || args[0] === "") throw new UsageError("search needs text");
283
+ const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"), origins, space);
284
+ return { symbols: found.symbols.map(symbol), comments: found.comments.map(comment) };
285
+ }
286
+ case "symbols":
287
+ return { symbols: listSymbols(db, range, values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind"), origins, space).map(symbol) };
288
+ case "regions":
289
+ return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type"), origins, space).map(region) };
290
+ case "comments":
291
+ return { comments: listComments(db, range, origins, space).map(comment) };
292
+ case "references":
293
+ return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind"), origins, space).map(reference) };
294
+ case "export": {
295
+ const exported = {
296
+ revision: currentRevision(db),
297
+ ...(space === DEFAULT_SPACE ? {} : { space }),
298
+ symbols: listSymbols(db, range, undefined, origins, space).map(symbol),
299
+ regions: listRegions(db, range, undefined, origins, space).map(region),
300
+ comments: listComments(db, range, origins, space).map(comment),
301
+ references: listReferences(db, range, undefined, origins, space).map(reference),
302
+ suppressions: listSuppressions(db, range, space).map(suppression),
303
+ modules: listModules(db, range, origins, space).map(module),
304
+ operands: listOperands(db, range, origins, space).map(operand),
305
+ };
306
+ const bytes = Buffer.from(`${JSON.stringify(exported, null, 2)}\n`, "utf8");
307
+ writeProjectFile(values.out!, bytes, { what: "--out" });
308
+ return {
309
+ out: values.out,
310
+ bytes: bytes.length,
311
+ 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, operands: exported.operands.length },
312
+ revision: exported.revision,
313
+ };
314
+ }
315
+ case "suppressions": {
316
+ const rows = listSuppressions(db, range, space);
317
+ const reasons = reasonsOf(db, rows);
318
+ return { suppressions: rows.map((row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) })) };
319
+ }
320
+ case "modules":
321
+ return { modules: listModules(db, range, origins, space).map(module) };
322
+ case "operands":
323
+ return { operands: listOperands(db, range, origins, space).map(operand) };
324
+ case "spaces":
325
+ return { spaces: listSpaces(db) };
326
+ case "history":
327
+ // Each row with the reason and the evidence of the revision that wrote it (F-26).
328
+ return {
329
+ history: (values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
330
+ };
331
+ case "revisions": {
332
+ const options: { limit?: number; before?: number } = {};
333
+ if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
334
+ if (values.before !== undefined) options.before = integer(values.before, "--before");
335
+ return { revisions: revisions(db, options) };
336
+ }
337
+ default: {
338
+ const changes = revision(db, integer(args[0], "revision"));
339
+ return { revision: changes.revision, added: changes.added.map(historyEntry), closed: changes.closed.map(historyEntry) };
340
+ }
341
+ }
342
+ } finally {
343
+ db?.close();
344
+ }
345
+ }
346
+
347
+ const context: WriteContext = { origin: values.origin === undefined ? "llm" : oneOf(["user", "llm"] as const, values.origin, "--origin") };
348
+ if (values.reason !== undefined) context.description = values.reason;
349
+ if (values["expect-revision"] !== undefined) context.expectedRevision = integer(values["expect-revision"], "--expect-revision");
350
+ if (values.evidence !== undefined) context.evidence = values.evidence.map(evidenceOf);
351
+ // Every argument is checked before the database opens: a refused write creates no database.
352
+ if (values.title !== undefined && command !== "module") throw new UsageError("--title works only with module");
353
+ for (const option of ["lo", "hi", "value", "expr", "offset"] as const) {
354
+ if (values[option] !== undefined && command !== "operand") throw new UsageError(`--${option} works only with operand`);
355
+ }
356
+ if (values.image !== undefined && command !== "operand" && command !== "apply") throw new UsageError("--image works only with operand and apply");
357
+ const operation =
358
+ command === "apply"
359
+ ? applyOperation(args, context, space, values.image)
360
+ : command === "operand"
361
+ ? operandOperation(args, values, context, space)
362
+ : writeOperation(command, args, values.kind, values.element, context, values.title, space);
363
+ return withWrite(operation);
364
+ }
365
+
366
+ /** --origin for reads: one origin, semantic (user and llm), analyzer (dxa and ghidra), or a comma list. */
367
+ function originFilter(value: string): Origin[] {
368
+ const origins = new Set<Origin>();
369
+ for (const part of value.split(",")) {
370
+ if (part === "semantic") origins.add("user").add("llm");
371
+ else if (part === "analyzer") origins.add("dxa").add("ghidra").add("heuristic");
372
+ else origins.add(oneOf(ORIGINS, part, "--origin"));
373
+ }
374
+ return [...origins];
375
+ }
376
+
377
+ /** --evidence kind:ref. A file kind names a project file and keeps its SHA-256, so a later change of the file shows. */
378
+ function evidenceOf(value: string): Evidence {
379
+ const colon = value.indexOf(":");
380
+ const kind = oneOf(EVIDENCE_KINDS, colon < 0 ? value : value.slice(0, colon), "--evidence kind");
381
+ const ref = colon < 0 ? "" : value.slice(colon + 1);
382
+ if (ref === "") throw new UsageError(`--evidence needs ${kind}:<${kind === "note" ? "text" : "file"}>`);
383
+ if (kind === "note") return { kind, ref };
384
+ return { kind, ref, sha256: createHash("sha256").update(readProjectFile(ref).bytes).digest("hex") };
385
+ }
386
+
387
+ /**
388
+ * The image that operand writes are checked against (D65), with the platform
389
+ * names. A byte outside the segments of the image is no operand.
390
+ */
391
+ function operandImage(path: string | undefined, what: string): OperandImage {
392
+ if (path === undefined) throw new UsageError(`${what} checks each operand expression against the image: give --image <image.json|prg>`);
393
+ let image: ReturnType<typeof readImage>;
394
+ try {
395
+ image = readImage(path);
396
+ } catch (error) {
397
+ if (error instanceof ImageError) throw new UsageError(error.message);
398
+ throw error;
399
+ }
400
+ const inside = (at: number) => image.segments.some((segment) => at >= segment.start && at <= segment.end);
401
+ return { byteAt: (at) => (inside(at) ? image.byteAt(at) : undefined), platform: new Map(platformNames().map((entry) => [entry.name, entry.address])) };
402
+ }
403
+
404
+ /** An offset: a whole number in decimal or $ hex, with an optional minus sign. */
405
+ function offsetOf(value: unknown, what: string): number {
406
+ if (typeof value === "number" && Number.isInteger(value)) return value;
407
+ const match = typeof value === "string" ? /^(-?)(\$[0-9A-Fa-f]{1,4}|[0-9]{1,5})$/.exec(value) : null;
408
+ if (match === null) throw new UsageError(`${what} must be a whole number such as 1, -1 or '$1000'`);
409
+ const size = match[2]!.startsWith("$") ? Number.parseInt(match[2]!.slice(1), 16) : Number(match[2]);
410
+ return match[1] === "-" ? -size : size;
411
+ }
412
+
413
+ /** operand <address> with one of --lo, --hi, --value or --expr. */
414
+ function operandOperation(args: string[], values: Record<string, unknown>, context: WriteContext, space: string): WriteOperation {
415
+ const given = (["lo", "hi", "value", "expr"] as const).filter((option) => values[option] !== undefined);
416
+ if (given.length !== 1) throw new UsageError("operand needs exactly one of --lo, --hi, --value or --expr");
417
+ const select: OperandSelect = given[0]!;
418
+ const request: { space: string; address: number; select: OperandSelect; symbol?: string; offset?: number; expression?: string } = { space, address: address(args[0]), select };
419
+ if (select === "expr") {
420
+ if (values.offset !== undefined) throw new UsageError("--offset works with --lo, --hi and --value, not with --expr");
421
+ request.expression = values.expr as string;
422
+ } else {
423
+ request.symbol = values[select] as string;
424
+ if (values.offset !== undefined) request.offset = offsetOf(values.offset, "--offset");
425
+ }
426
+ const image = operandImage(values.image as string | undefined, "operand");
427
+ return (db) => {
428
+ const result = setOperand(db, { ...context, image }, request);
429
+ return { revision: result.revision, operand: operand(result.current), ...(result.previous === undefined ? {} : { previous: operand(result.previous) }) };
430
+ };
431
+ }
432
+
433
+ /** apply <file>: the batch as BatchOperation values, checked before the database opens. */
434
+ function applyOperation(args: string[], context: WriteContext, space: string, imagePath: string | undefined): WriteOperation {
435
+ if (args.length !== 1) throw new UsageError("apply needs one file, or - for standard input");
436
+ const text = args[0] === "-" ? readFileSync(0, "utf8") : readProjectFile(args[0]!).bytes.toString("utf8");
437
+ let parsed: unknown;
438
+ try {
439
+ parsed = JSON.parse(text);
440
+ } catch (error) {
441
+ throw new UsageError(`the batch is not JSON: ${(error as Error).message}`);
442
+ }
443
+ if (!Array.isArray(parsed)) throw new UsageError("the batch is a JSON list of writes, for example [{\"op\": \"rename\", \"address\": \"$2100\", \"name\": \"update_player\"}]");
444
+ const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
445
+ const withImage = operations.some((operation) => operation.op === "operand") ? { ...context, image: operandImage(imagePath, "apply with operand writes") } : context;
446
+ return (db) => {
447
+ const applied = applyBatch(db, withImage, operations);
448
+ // replaced: each write that replaced a row of another origin, with that row as previous gives it for one write (b2/07).
449
+ const replaced = applied.steps.flatMap((step, index) =>
450
+ step.previous === undefined
451
+ ? []
452
+ : [{ write: index + 1, op: operations[index]!.op, previous: Array.isArray(step.previous) ? step.previous.map(region) : "select" in step.previous ? operand(step.previous) : symbol(step.previous) }],
453
+ );
454
+ return { revision: applied.revision, writes: applied.steps.length, replaced };
455
+ };
456
+ }
457
+
458
+ const BATCH_FIELDS: Record<BatchOperation["op"], string[]> = {
459
+ rename: ["address", "name", "kind"],
460
+ "remove-symbol": ["address"],
461
+ classify: ["start", "end", "type", "element"],
462
+ unclassify: ["start", "end"],
463
+ comment: ["address", "placement", "text"],
464
+ uncomment: ["address", "placement"],
465
+ reference: ["from", "to", "kind"],
466
+ unreference: ["from", "to", "kind"],
467
+ module: ["start", "file", "title"],
468
+ unmodule: ["start"],
469
+ operand: ["address", "select", "symbol", "offset", "expression"],
470
+ unoperand: ["address"],
471
+ };
472
+
473
+ /** One write of a batch. A write names its own space, or takes the one of --space. */
474
+ function batchOperation(item: unknown, at: string, space: string): BatchOperation {
475
+ const operation = batchFields(item, at);
476
+ const own = (item as Record<string, unknown>).space;
477
+ if (own !== undefined && typeof own !== "string") throw new UsageError(`${at}: space must be a string`);
478
+ return { ...operation, space: own ?? space };
479
+ }
480
+
481
+ /** The fields of one write of a batch, with its addresses parsed. */
482
+ function batchFields(item: unknown, at: string): BatchOperation {
483
+ if (typeof item !== "object" || item === null || Array.isArray(item)) throw new UsageError(`${at}: a write is an object with op`);
484
+ const fields = item as Record<string, unknown>;
485
+ const op = oneOf(Object.keys(BATCH_FIELDS) as Array<BatchOperation["op"]>, String(fields.op), `${at}: op`);
486
+ const unknown = Object.keys(fields).filter((key) => key !== "op" && key !== "space" && !BATCH_FIELDS[op].includes(key));
487
+ if (unknown.length > 0) throw new UsageError(`${at}: ${op} takes ${BATCH_FIELDS[op].join(", ")}, not ${unknown.join(", ")}`);
488
+ const text = (key: string) => {
489
+ if (typeof fields[key] !== "string") throw new UsageError(`${at}: ${key} must be a string`);
490
+ return fields[key] as string;
491
+ };
492
+ const place = (key: string) => address(text(key), `${at}: ${key}`);
493
+ switch (op) {
494
+ case "rename":
495
+ return { op, address: place("address"), name: text("name"), ...(fields.kind === undefined ? {} : { kind: oneOf(SYMBOL_KINDS, text("kind"), `${at}: kind`) }) };
496
+ case "remove-symbol":
497
+ return { op, address: place("address") };
498
+ case "classify": {
499
+ if (fields.element !== undefined && typeof fields.element !== "number") throw new UsageError(`${at}: element must be a number`);
500
+ return { op, start: place("start"), end: place("end"), type: oneOf(REGION_TYPES, text("type"), `${at}: type`), ...(fields.element === undefined ? {} : { element: fields.element as number }) };
501
+ }
502
+ case "unclassify":
503
+ return { op, start: place("start"), end: place("end") };
504
+ case "comment":
505
+ return { op, address: place("address"), placement: oneOf(COMMENT_PLACEMENTS, text("placement"), `${at}: placement`), text: text("text") };
506
+ case "uncomment":
507
+ return { op, address: place("address"), placement: oneOf(COMMENT_PLACEMENTS, text("placement"), `${at}: placement`) };
508
+ case "module":
509
+ return { op, start: place("start"), file: text("file"), ...(fields.title === undefined ? {} : { title: text("title") }) };
510
+ case "unmodule":
511
+ return { op, start: place("start") };
512
+ case "operand": {
513
+ const select = oneOf(OPERAND_SELECTS, text("select"), `${at}: select`);
514
+ return {
515
+ op,
516
+ address: place("address"),
517
+ select,
518
+ ...(fields.symbol === undefined ? {} : { symbol: text("symbol") }),
519
+ ...(fields.offset === undefined ? {} : { offset: offsetOf(fields.offset, `${at}: offset`) }),
520
+ ...(fields.expression === undefined ? {} : { expression: text("expression") }),
521
+ };
522
+ }
523
+ case "unoperand":
524
+ return { op, address: place("address") };
525
+ case "reference":
526
+ case "unreference":
527
+ return { op, from: place("from"), to: place("to"), kind: oneOf(REFERENCE_KINDS, text("kind"), `${at}: kind`) };
528
+ }
529
+ }
530
+
531
+ type WriteOperation = (db: DatabaseSync) => unknown;
532
+
533
+ function writeOperation(command: string, args: string[], kind: string | undefined, element: string | undefined, context: WriteContext, title: string | undefined, space: string): WriteOperation {
534
+ switch (command) {
535
+ case "rename": {
536
+ if (args[1] === undefined) throw new UsageError("name is missing");
537
+ const request: { space: string; address: number; name: string; kind?: SymbolKind } = { space, address: address(args[0]), name: args[1] };
538
+ if (kind !== undefined) request.kind = oneOf(SYMBOL_KINDS, kind, "--kind");
539
+ return (db) => {
540
+ const result = renameSymbol(db, context, request);
541
+ return { revision: result.revision, symbol: symbol(result.current), ...(result.previous === undefined ? {} : { previous: symbol(result.previous) }) };
542
+ };
543
+ }
544
+ case "remove-symbol": {
545
+ const request = { space, address: address(args[0]) };
546
+ return (db) => ({ revision: removeSymbol(db, context, request).revision });
547
+ }
548
+ case "classify": {
549
+ const request = {
550
+ space,
551
+ start: address(args[0], "start"),
552
+ end: address(args[1], "end"),
553
+ type: oneOf(REGION_TYPES, args[2], "type") as RegionType,
554
+ ...(element === undefined ? {} : { element: integer(element, "--element") }),
555
+ };
556
+ return (db) => {
557
+ const result = classifyRegion(db, context, request);
558
+ return { revision: result.revision, regions: result.current.map(region), ...(result.previous === undefined ? {} : { previous: result.previous.map(region) }) };
559
+ };
560
+ }
561
+ case "unclassify": {
562
+ const request = { space, start: address(args[0], "start"), end: address(args[1], "end") };
563
+ return (db) => ({ revision: unclassifyRegion(db, context, request).revision });
564
+ }
565
+ case "comment": {
566
+ if (args.length < 3) throw new UsageError("text is missing");
567
+ const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") as CommentPlacement, text: args.slice(2).join(" ") };
568
+ return (db) => {
569
+ const result = setComment(db, context, request);
570
+ return { revision: result.revision, comment: comment(result.current) };
571
+ };
572
+ }
573
+ case "uncomment": {
574
+ const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") };
575
+ return (db) => ({ revision: removeComment(db, context, request).revision });
576
+ }
577
+ case "reference": {
578
+ const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") as ReferenceKind };
579
+ return (db) => {
580
+ const result = addReference(db, context, request);
581
+ return { revision: result.revision, reference: reference(result.current) };
582
+ };
583
+ }
584
+ case "unreference": {
585
+ const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") };
586
+ return (db) => ({ revision: removeReference(db, context, request).revision });
587
+ }
588
+ case "module": {
589
+ if (args[1] === undefined) throw new UsageError("file is missing");
590
+ const request = { space, start: address(args[0], "start"), file: args[1], ...(title === undefined ? {} : { title }) };
591
+ return (db) => {
592
+ const result = setModule(db, context, request);
593
+ return { revision: result.revision, module: module(result.current) };
594
+ };
595
+ }
596
+ case "unmodule": {
597
+ const request = { space, start: address(args[0], "start") };
598
+ return (db) => ({ revision: removeModule(db, context, request).revision });
599
+ }
600
+ case "unoperand": {
601
+ const request = { space, address: address(args[0]) };
602
+ return (db) => ({ revision: removeOperand(db, context, request).revision });
603
+ }
604
+ case "revert": {
605
+ const request = { revision: integer(args[0], "revision") };
606
+ return (db) => ({ revision: revert(db, context, request).revision });
607
+ }
608
+ default:
609
+ throw new UsageError(`unknown command: ${command}`);
610
+ }
611
+ }
612
+
613
+ await runScript(() => run(process.argv.slice(2)), { usage: USAGE, refusals: [KnowledgeError] });