@henols/c64-re-tools 2.0.0-rc.11 → 2.0.0-rc.13
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.
- package/README.md +7 -5
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +11 -2
- package/skills/c64-assembler/scripts/assemble.ts +4 -4
- package/skills/c64-basic/SKILL.md +8 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +10 -3
- package/skills/c64-disk/scripts/disk.ts +4 -4
- package/skills/c64-emulator/SKILL.md +4 -0
- package/skills/c64-knowledge/SKILL.md +62 -12
- package/skills/c64-knowledge/scripts/knowledge.ts +352 -102
- package/skills/c64-memory-map/SKILL.md +7 -0
- package/skills/c64-memory-map/scripts/memmap.ts +1 -1
- package/skills/c64-provenance/SKILL.md +56 -3
- package/skills/c64-provenance/scripts/library.ts +584 -0
- package/skills/c64-provenance/scripts/provenance.ts +201 -9
- package/skills/c64-reverse-engineering/SKILL.md +16 -2
- package/skills/c64-source/SKILL.md +38 -7
- package/skills/c64-source/scripts/generate.ts +213 -29
- package/skills/c64-source/scripts/loader.ts +3 -1
- package/skills/c64-source/scripts/opcodes.ts +19 -0
- package/skills/c64-source/scripts/source.ts +387 -35
- package/skills/c64-static-analysis/SKILL.md +11 -2
- package/skills/c64-static-analysis/scripts/analyze.ts +95 -8
- package/skills/c64-static-analysis/scripts/macros.ts +233 -0
- package/skills/c64-testing/SKILL.md +8 -1
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/checklist.ts +2 -2
- package/skills/c64-testing/scripts/run.ts +4 -4
- package/skills/c64-testing/scripts/scenario.ts +1 -1
- package/skills/c64-testing/scripts/test.ts +4 -4
- package/skills/c64-unpacker/SKILL.md +11 -4
- package/skills/c64-unpacker/scripts/unpack.ts +4 -4
- package/src/cli/main.ts +5 -3
- package/src/host-client/image.ts +1 -1
- package/src/host-client/transfer.ts +34 -10
- package/src/host-client/vice-session.ts +9 -9
- package/src/knowledge/conflicts.ts +157 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/history.ts +54 -20
- package/src/knowledge/import.ts +3 -2
- package/src/knowledge/read.ts +169 -20
- package/src/knowledge/schema.ts +115 -72
- package/src/knowledge/text.ts +391 -0
- package/src/knowledge/write.ts +508 -51
- package/src/mcp/output.ts +14 -7
- package/src/mcp/tools/debug.ts +8 -6
- package/src/mcp/tools/media.ts +17 -13
- package/src/mcp/tools/memory.ts +9 -5
- package/src/mcp/tools/video.ts +6 -5
- package/src/platform.ts +28 -5
- package/src/project.ts +166 -3
- package/src/protocol/tools.ts +1 -1
- package/src/script.ts +46 -7
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// The c64-knowledge script: reads and writes .c64-re-tools/knowledge
|
|
2
|
-
// project
|
|
1
|
+
// The c64-knowledge script: reads and writes .c64-re-tools/knowledge/ in the
|
|
2
|
+
// project folder (--project, or the one found from the working directory) and
|
|
3
|
+
// prints one compact JSON result.
|
|
3
4
|
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
5
6
|
import { readFileSync } from "node:fs";
|
|
@@ -9,15 +10,19 @@ import { parseArgs } from "node:util";
|
|
|
9
10
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
10
11
|
import { ImageError, readImage } from "#src/host-client/image.ts";
|
|
11
12
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
13
|
+
import { CONFLICT_RULES, conflicts, type Conflict } from "#src/knowledge/conflicts.ts";
|
|
12
14
|
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
13
15
|
import { operandText } from "#src/knowledge/expression.ts";
|
|
14
|
-
import { historyAt, historyNamed, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
16
|
+
import { historyAt, historyNamed, historyOfMacro, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
15
17
|
import {
|
|
16
18
|
at,
|
|
17
19
|
currentRevision,
|
|
18
20
|
listComments,
|
|
21
|
+
listMacros,
|
|
22
|
+
listMacroUses,
|
|
19
23
|
listModules,
|
|
20
24
|
listOperands,
|
|
25
|
+
listPatches,
|
|
21
26
|
listSpaces,
|
|
22
27
|
listReferences,
|
|
23
28
|
listRegions,
|
|
@@ -25,8 +30,11 @@ import {
|
|
|
25
30
|
listSymbols,
|
|
26
31
|
search,
|
|
27
32
|
type CommentRow,
|
|
33
|
+
type MacroRow,
|
|
34
|
+
type MacroUseRow,
|
|
28
35
|
type ModuleRow,
|
|
29
36
|
type OperandRow,
|
|
37
|
+
type PatchRow,
|
|
30
38
|
type ReferenceRow,
|
|
31
39
|
type RegionRow,
|
|
32
40
|
type SuppressionRow,
|
|
@@ -53,19 +61,28 @@ import {
|
|
|
53
61
|
addReference,
|
|
54
62
|
applyBatch,
|
|
55
63
|
classifyRegion,
|
|
64
|
+
mergeConflicts,
|
|
56
65
|
removeComment,
|
|
66
|
+
removeMacro,
|
|
67
|
+
removeMacroUse,
|
|
57
68
|
removeModule,
|
|
58
69
|
removeOperand,
|
|
70
|
+
removePatch,
|
|
59
71
|
removeReference,
|
|
60
72
|
removeSymbol,
|
|
61
73
|
renameSymbol,
|
|
62
74
|
revert,
|
|
63
75
|
setComment,
|
|
76
|
+
setMacro,
|
|
77
|
+
setMacroUse,
|
|
64
78
|
setModule,
|
|
65
79
|
setOperand,
|
|
80
|
+
setPatch,
|
|
66
81
|
unclassifyRegion,
|
|
67
82
|
type BatchOperation,
|
|
83
|
+
type BatchStep,
|
|
68
84
|
type Evidence,
|
|
85
|
+
type MergeChoice,
|
|
69
86
|
type OperandImage,
|
|
70
87
|
type WriteContext,
|
|
71
88
|
} from "#src/knowledge/write.ts";
|
|
@@ -84,10 +101,14 @@ Read (no database needed):
|
|
|
84
101
|
suppressions [--start A --end A] places that removals keep free of analyzer facts
|
|
85
102
|
modules [--start A --end A --origin O] where the files of generated source start
|
|
86
103
|
operands [--start A --end A --origin O] operand expressions of instructions
|
|
104
|
+
patches [--start A --end A --switch S --origin O] code that a switch puts in
|
|
105
|
+
switches each switch with the ranges of its patches
|
|
106
|
+
macros [--origin O] the macros of the generated source
|
|
107
|
+
uses [--start A --end A --macro M --origin O] instructions written as a macro
|
|
87
108
|
spaces the address spaces that hold knowledge
|
|
88
|
-
history <address> | history --name <name>
|
|
89
|
-
revisions [--limit N --before
|
|
90
|
-
revision <
|
|
109
|
+
history <address> | history --name <name> | history --macro <name>
|
|
110
|
+
revisions [--limit N --before R]
|
|
111
|
+
revision <revision>
|
|
91
112
|
export --out <file> [--start A --end A --origin O] all current knowledge to a project file
|
|
92
113
|
|
|
93
114
|
Every command takes --space S: c64 (the default), drive8 for code in the 1541
|
|
@@ -97,7 +118,7 @@ own space.
|
|
|
97
118
|
Read option --origin O: user, llm, dxa, ghidra, heuristic, semantic (user and
|
|
98
119
|
llm) or analyzer (dxa, ghidra and heuristic), or a comma list. search takes it too.
|
|
99
120
|
|
|
100
|
-
Write (one revision each; creates
|
|
121
|
+
Write (one revision each; creates .c64-re-tools/knowledge/ when needed):
|
|
101
122
|
rename <address> <name> [--kind K]
|
|
102
123
|
remove-symbol <address>
|
|
103
124
|
classify <start> <end> <type> [--element N]
|
|
@@ -116,19 +137,36 @@ Write (one revision each; creates the database when needed):
|
|
|
116
137
|
it does not give the operand in the image (image.json
|
|
117
138
|
or PRG); give a negative offset as --offset=-1
|
|
118
139
|
unoperand <address>
|
|
140
|
+
patch <start> <end> <SWITCH> --code <text>|--code-file <file>|--nop [--comment T] --image <file>
|
|
141
|
+
ACME code that replaces <start>-<end> when the build
|
|
142
|
+
defines SWITCH as 1 (c64-assembler --define SWITCH=1);
|
|
143
|
+
NOP fills the rest of the range; --nop puts only NOP
|
|
144
|
+
unpatch <start>
|
|
145
|
+
macro <name> [--params a,b] --body <text>|--body-file <file> [--comment T]
|
|
146
|
+
an ACME macro; the body names a param with a dot: .a
|
|
147
|
+
unmacro <name>
|
|
148
|
+
use <start> <end> <macro> [--args "x, y"] --image <file>
|
|
149
|
+
write the instructions <start>-<end> as one use of
|
|
150
|
+
the macro, with one expression for each param
|
|
151
|
+
unuse <start>
|
|
119
152
|
revert <revision>
|
|
153
|
+
merge decide the conflicts that a git merge of two
|
|
154
|
+
computers' work left: of the rows of one conflict,
|
|
155
|
+
the newest stays; one revision for all
|
|
120
156
|
apply <file> many writes from a JSON file ('-' reads standard input),
|
|
121
157
|
all as one revision, or none when one is refused;
|
|
122
|
-
operand writes need --image <file>
|
|
158
|
+
operand, patch and use writes need --image <file>
|
|
123
159
|
|
|
124
160
|
Write options:
|
|
125
161
|
--reason <text> why, kept with the revision
|
|
126
162
|
--evidence <kind:ref> what the change rests on: coverage:<file>, test:<file>,
|
|
127
163
|
file:<file> or note:<text> (repeatable)
|
|
128
|
-
--expect-revision <
|
|
164
|
+
--expect-revision <R> refuse the write if knowledge changed after revision R
|
|
129
165
|
--origin user|llm who decided (default llm)
|
|
130
166
|
|
|
131
|
-
Addresses are $ and four hex digits; quote them in a shell: '$2100'
|
|
167
|
+
Addresses are $ and four hex digits; quote them in a shell: '$2100'.
|
|
168
|
+
A revision is its id, for example 20261015T214501.123-k3f9qz, or a start of
|
|
169
|
+
the id that only one revision has; 0 is the knowledge before every revision.`;
|
|
132
170
|
|
|
133
171
|
function address(value: string | undefined, what = "address"): number {
|
|
134
172
|
if (value === undefined) throw new UsageError(`${what} is missing`);
|
|
@@ -175,6 +213,28 @@ const operand = (row: OperandRow) => ({
|
|
|
175
213
|
origin: row.origin,
|
|
176
214
|
revision: row.revision,
|
|
177
215
|
});
|
|
216
|
+
const patch = (row: PatchRow) => ({
|
|
217
|
+
...spaceOf(row),
|
|
218
|
+
start: formatC64Address(row.start),
|
|
219
|
+
end: formatC64Address(row.end),
|
|
220
|
+
bytes: row.end - row.start + 1,
|
|
221
|
+
switch: row.switch,
|
|
222
|
+
code: row.code,
|
|
223
|
+
...(row.comment === undefined ? {} : { comment: row.comment }),
|
|
224
|
+
origin: row.origin,
|
|
225
|
+
revision: row.revision,
|
|
226
|
+
});
|
|
227
|
+
const macro = (row: MacroRow) => ({ ...spaceOf(row), name: row.name, params: row.params, body: row.body, ...(row.comment === undefined ? {} : { comment: row.comment }), origin: row.origin, revision: row.revision });
|
|
228
|
+
const macroUse = (row: MacroUseRow) => ({
|
|
229
|
+
...spaceOf(row),
|
|
230
|
+
start: formatC64Address(row.start),
|
|
231
|
+
end: formatC64Address(row.end),
|
|
232
|
+
macro: row.macro,
|
|
233
|
+
args: row.args,
|
|
234
|
+
text: `+${row.macro}${row.args.length === 0 ? "" : ` ${row.args.join(", ")}`}`,
|
|
235
|
+
origin: row.origin,
|
|
236
|
+
revision: row.revision,
|
|
237
|
+
});
|
|
178
238
|
const reference = (row: ReferenceRow) => ({ ...spaceOf(row), from: formatC64Address(row.from), to: formatC64Address(row.to), kind: row.kind, origin: row.origin, revision: row.revision });
|
|
179
239
|
function suppression(row: SuppressionRow) {
|
|
180
240
|
const place =
|
|
@@ -200,14 +260,30 @@ function historyEntry(entry: HistoryEntry) {
|
|
|
200
260
|
? module(entry.row)
|
|
201
261
|
: entry.entity === "operand"
|
|
202
262
|
? operand(entry.row)
|
|
203
|
-
:
|
|
263
|
+
: entry.entity === "patch"
|
|
264
|
+
? patch(entry.row)
|
|
265
|
+
: entry.entity === "macro"
|
|
266
|
+
? macro(entry.row)
|
|
267
|
+
: entry.entity === "macro-use"
|
|
268
|
+
? macroUse(entry.row)
|
|
269
|
+
: suppression(entry.row);
|
|
204
270
|
const { revision: _revision, ...row } = shown;
|
|
205
271
|
return { entity: entry.entity, ...row, fromRevision: entry.from, ...(entry.to === undefined ? {} : { toRevision: entry.to }) };
|
|
206
272
|
}
|
|
207
273
|
|
|
274
|
+
/** A conflict of a git merge (D69): what is wrong, and the rows, oldest first. */
|
|
275
|
+
function showConflict(conflict: Conflict) {
|
|
276
|
+
return { conflict: CONFLICT_RULES[conflict.rule], ...spaceOf(conflict), rows: conflict.rows.map(historyEntry) };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** What merge decided for one conflict: the row that stays and the rows it closed. */
|
|
280
|
+
function showChoice(choice: MergeChoice) {
|
|
281
|
+
return { conflict: CONFLICT_RULES[choice.rule], ...spaceOf(choice), kept: historyEntry(choice.kept), closed: choice.closed.map(historyEntry) };
|
|
282
|
+
}
|
|
283
|
+
|
|
208
284
|
/** The reason of each revision that wrote one of the rows. */
|
|
209
|
-
function reasonsOf(db: DatabaseSync | undefined, rows: SuppressionRow[]): Map<
|
|
210
|
-
const reasons = new Map<
|
|
285
|
+
function reasonsOf(db: DatabaseSync | undefined, rows: SuppressionRow[]): Map<string, string> {
|
|
286
|
+
const reasons = new Map<string, string>();
|
|
211
287
|
const query = db?.prepare("SELECT description FROM revisions WHERE id = ?");
|
|
212
288
|
for (const { revision } of rows) {
|
|
213
289
|
if (query === undefined || reasons.has(revision)) continue;
|
|
@@ -244,6 +320,16 @@ function run(argv: string[]): unknown {
|
|
|
244
320
|
expr: { type: "string" },
|
|
245
321
|
offset: { type: "string" },
|
|
246
322
|
image: { type: "string" },
|
|
323
|
+
code: { type: "string" },
|
|
324
|
+
"code-file": { type: "string" },
|
|
325
|
+
nop: { type: "boolean" },
|
|
326
|
+
comment: { type: "string" },
|
|
327
|
+
params: { type: "string" },
|
|
328
|
+
body: { type: "string" },
|
|
329
|
+
"body-file": { type: "string" },
|
|
330
|
+
args: { type: "string" },
|
|
331
|
+
switch: { type: "string" },
|
|
332
|
+
macro: { type: "string" },
|
|
247
333
|
help: { type: "boolean" },
|
|
248
334
|
},
|
|
249
335
|
});
|
|
@@ -253,8 +339,8 @@ function run(argv: string[]): unknown {
|
|
|
253
339
|
if (values.space !== undefined && !SPACE.test(values.space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
254
340
|
const space = values.space ?? DEFAULT_SPACE;
|
|
255
341
|
|
|
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"]);
|
|
342
|
+
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "operands", "patches", "switches", "macros", "uses", "spaces", "history", "revisions", "revision", "export"]);
|
|
343
|
+
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "operand", "unoperand", "patch", "unpatch", "macro", "unmacro", "use", "unuse", "revert", "apply", "merge"]);
|
|
258
344
|
// Refuse a mistyped command before anything opens, so no database appears by accident.
|
|
259
345
|
if (!read.has(command) && !write.has(command)) throw new UsageError(`unknown command: ${command}`);
|
|
260
346
|
if (read.has(command)) {
|
|
@@ -262,83 +348,10 @@ function run(argv: string[]): unknown {
|
|
|
262
348
|
if (command === "export" && values.out === undefined) throw new UsageError("export needs --out <file>");
|
|
263
349
|
const db = openForRead();
|
|
264
350
|
try {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
}
|
|
351
|
+
const answer = readAnswer(db, command, args, values, range, origins, space);
|
|
352
|
+
const found = conflicts(db);
|
|
353
|
+
// A git merge can leave conflicts: every read shows them until merge decides them (D69).
|
|
354
|
+
return found.length === 0 ? answer : { ...answer, conflicts: found.map(showConflict) };
|
|
342
355
|
} finally {
|
|
343
356
|
db?.close();
|
|
344
357
|
}
|
|
@@ -346,23 +359,149 @@ function run(argv: string[]): unknown {
|
|
|
346
359
|
|
|
347
360
|
const context: WriteContext = { origin: values.origin === undefined ? "llm" : oneOf(["user", "llm"] as const, values.origin, "--origin") };
|
|
348
361
|
if (values.reason !== undefined) context.description = values.reason;
|
|
349
|
-
if (values["expect-revision"] !== undefined) context.expectedRevision =
|
|
362
|
+
if (values["expect-revision"] !== undefined) context.expectedRevision = values["expect-revision"];
|
|
350
363
|
if (values.evidence !== undefined) context.evidence = values.evidence.map(evidenceOf);
|
|
351
364
|
// Every argument is checked before the database opens: a refused write creates no database.
|
|
352
365
|
if (values.title !== undefined && command !== "module") throw new UsageError("--title works only with module");
|
|
353
366
|
for (const option of ["lo", "hi", "value", "expr", "offset"] as const) {
|
|
354
367
|
if (values[option] !== undefined && command !== "operand") throw new UsageError(`--${option} works only with operand`);
|
|
355
368
|
}
|
|
356
|
-
|
|
369
|
+
const only: Array<[keyof typeof values, readonly string[]]> = [
|
|
370
|
+
["image", ["operand", "patch", "use", "apply"]],
|
|
371
|
+
["code", ["patch"]],
|
|
372
|
+
["code-file", ["patch"]],
|
|
373
|
+
["nop", ["patch"]],
|
|
374
|
+
["comment", ["patch", "macro"]],
|
|
375
|
+
["params", ["macro"]],
|
|
376
|
+
["body", ["macro"]],
|
|
377
|
+
["body-file", ["macro"]],
|
|
378
|
+
["args", ["use"]],
|
|
379
|
+
];
|
|
380
|
+
for (const [option, commands] of only) {
|
|
381
|
+
if (values[option] !== undefined && !commands.includes(command)) throw new UsageError(`--${option} works only with ${commands.join(", ")}`);
|
|
382
|
+
}
|
|
357
383
|
const operation =
|
|
358
384
|
command === "apply"
|
|
359
385
|
? applyOperation(args, context, space, values.image)
|
|
360
386
|
: command === "operand"
|
|
361
387
|
? operandOperation(args, values, context, space)
|
|
362
|
-
:
|
|
388
|
+
: command === "patch" || command === "macro" || command === "use"
|
|
389
|
+
? sourceOperation(command, args, values, context, space)
|
|
390
|
+
: writeOperation(command, args, values.kind, values.element, context, values.title, space);
|
|
363
391
|
return withWrite(operation);
|
|
364
392
|
}
|
|
365
393
|
|
|
394
|
+
type ReadValues = {
|
|
395
|
+
kind?: string | undefined;
|
|
396
|
+
type?: string | undefined;
|
|
397
|
+
name?: string | undefined;
|
|
398
|
+
limit?: string | undefined;
|
|
399
|
+
before?: string | undefined;
|
|
400
|
+
out?: string | undefined;
|
|
401
|
+
switch?: string | undefined;
|
|
402
|
+
macro?: string | undefined;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/** The answer of one read command. */
|
|
406
|
+
function readAnswer(db: DatabaseSync | undefined, command: string, args: string[], values: ReadValues, range: { start: number; end: number }, origins: Origin[] | undefined, space: string): Record<string, unknown> {
|
|
407
|
+
switch (command) {
|
|
408
|
+
case "at": {
|
|
409
|
+
const known = at(db, address(args[0]), space);
|
|
410
|
+
return {
|
|
411
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
412
|
+
address: formatC64Address(known.address),
|
|
413
|
+
...(known.symbol === undefined ? {} : { symbol: symbol(known.symbol) }),
|
|
414
|
+
...(known.region === undefined ? {} : { region: region(known.region) }),
|
|
415
|
+
comments: known.comments.map(comment),
|
|
416
|
+
referencesFrom: known.referencesFrom.map(reference),
|
|
417
|
+
referencesTo: known.referencesTo.map(reference),
|
|
418
|
+
...(known.operand === undefined ? {} : { operand: operand(known.operand) }),
|
|
419
|
+
...(known.patch === undefined ? {} : { patch: patch(known.patch) }),
|
|
420
|
+
...(known.macroUse === undefined ? {} : { macroUse: macroUse(known.macroUse) }),
|
|
421
|
+
...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
|
|
422
|
+
revision: known.revision,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
case "search": {
|
|
426
|
+
if (args[0] === undefined || args[0] === "") throw new UsageError("search needs text");
|
|
427
|
+
const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"), origins, space);
|
|
428
|
+
return { symbols: found.symbols.map(symbol), comments: found.comments.map(comment) };
|
|
429
|
+
}
|
|
430
|
+
case "symbols":
|
|
431
|
+
return { symbols: listSymbols(db, range, values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind"), origins, space).map(symbol) };
|
|
432
|
+
case "regions":
|
|
433
|
+
return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type"), origins, space).map(region) };
|
|
434
|
+
case "comments":
|
|
435
|
+
return { comments: listComments(db, range, origins, space).map(comment) };
|
|
436
|
+
case "references":
|
|
437
|
+
return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind"), origins, space).map(reference) };
|
|
438
|
+
case "export": {
|
|
439
|
+
const exported = {
|
|
440
|
+
revision: currentRevision(db),
|
|
441
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
442
|
+
symbols: listSymbols(db, range, undefined, origins, space).map(symbol),
|
|
443
|
+
regions: listRegions(db, range, undefined, origins, space).map(region),
|
|
444
|
+
comments: listComments(db, range, origins, space).map(comment),
|
|
445
|
+
references: listReferences(db, range, undefined, origins, space).map(reference),
|
|
446
|
+
suppressions: listSuppressions(db, range, space).map(suppression),
|
|
447
|
+
modules: listModules(db, range, origins, space).map(module),
|
|
448
|
+
operands: listOperands(db, range, origins, space).map(operand),
|
|
449
|
+
patches: listPatches(db, range, undefined, origins, space).map(patch),
|
|
450
|
+
macros: listMacros(db, origins, space).map(macro),
|
|
451
|
+
macroUses: listMacroUses(db, range, undefined, origins, space).map(macroUse),
|
|
452
|
+
};
|
|
453
|
+
const bytes = Buffer.from(`${JSON.stringify(exported, null, 2)}\n`, "utf8");
|
|
454
|
+
writeProjectFile(values.out!, bytes, { what: "--out" });
|
|
455
|
+
return {
|
|
456
|
+
out: values.out,
|
|
457
|
+
bytes: bytes.length,
|
|
458
|
+
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, patches: exported.patches.length, macros: exported.macros.length, macroUses: exported.macroUses.length },
|
|
459
|
+
revision: exported.revision,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
case "suppressions": {
|
|
463
|
+
const rows = listSuppressions(db, range, space);
|
|
464
|
+
const reasons = reasonsOf(db, rows);
|
|
465
|
+
return { suppressions: rows.map((row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) })) };
|
|
466
|
+
}
|
|
467
|
+
case "modules":
|
|
468
|
+
return { modules: listModules(db, range, origins, space).map(module) };
|
|
469
|
+
case "operands":
|
|
470
|
+
return { operands: listOperands(db, range, origins, space).map(operand) };
|
|
471
|
+
case "patches":
|
|
472
|
+
return { patches: listPatches(db, range, values.switch, origins, space).map(patch) };
|
|
473
|
+
case "switches": {
|
|
474
|
+
const switches = new Map<string, PatchRow[]>();
|
|
475
|
+
for (const row of listPatches(db, undefined, undefined, undefined, space)) switches.set(row.switch, [...(switches.get(row.switch) ?? []), row]);
|
|
476
|
+
return {
|
|
477
|
+
switches: [...switches].sort(([a], [b]) => (a < b ? -1 : 1)).map(([name, rows]) => ({ name, define: `--define ${name}=1`, patches: rows.map((row) => ({ start: formatC64Address(row.start), end: formatC64Address(row.end) })) })),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
case "macros":
|
|
481
|
+
return { macros: listMacros(db, origins, space).map(macro) };
|
|
482
|
+
case "uses":
|
|
483
|
+
return { uses: listMacroUses(db, range, values.macro, origins, space).map(macroUse) };
|
|
484
|
+
case "spaces":
|
|
485
|
+
return { spaces: listSpaces(db) };
|
|
486
|
+
case "history":
|
|
487
|
+
// Each row with the reason and the evidence of the revision that wrote it (F-26).
|
|
488
|
+
return {
|
|
489
|
+
history: (values.macro !== undefined ? historyOfMacro(db, values.macro, space) : values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
|
|
490
|
+
};
|
|
491
|
+
case "revisions": {
|
|
492
|
+
const options: { limit?: number; before?: string } = {};
|
|
493
|
+
if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
|
|
494
|
+
if (values.before !== undefined) options.before = values.before;
|
|
495
|
+
return { revisions: revisions(db, options) };
|
|
496
|
+
}
|
|
497
|
+
default: {
|
|
498
|
+
if (args[0] === undefined) throw new UsageError("revision is missing");
|
|
499
|
+
const changes = revision(db, args[0]);
|
|
500
|
+
return { revision: changes.revision, added: changes.added.map(historyEntry), closed: changes.closed.map(historyEntry) };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
366
505
|
/** --origin for reads: one origin, semantic (user and llm), analyzer (dxa and ghidra), or a comma list. */
|
|
367
506
|
function originFilter(value: string): Origin[] {
|
|
368
507
|
const origins = new Set<Origin>();
|
|
@@ -385,11 +524,12 @@ function evidenceOf(value: string): Evidence {
|
|
|
385
524
|
}
|
|
386
525
|
|
|
387
526
|
/**
|
|
388
|
-
* The image that operand writes are checked against (D65
|
|
389
|
-
* names. A byte outside the segments of the image
|
|
527
|
+
* The image that operand, patch and macro use writes are checked against (D65,
|
|
528
|
+
* D72, D73), with the platform names. A byte outside the segments of the image
|
|
529
|
+
* is not in the image.
|
|
390
530
|
*/
|
|
391
531
|
function operandImage(path: string | undefined, what: string): OperandImage {
|
|
392
|
-
if (path === undefined) throw new UsageError(`${what} checks
|
|
532
|
+
if (path === undefined) throw new UsageError(`${what} checks the write against the image: give --image <image.json|prg>`);
|
|
393
533
|
let image: ReturnType<typeof readImage>;
|
|
394
534
|
try {
|
|
395
535
|
image = readImage(path);
|
|
@@ -430,6 +570,57 @@ function operandOperation(args: string[], values: Record<string, unknown>, conte
|
|
|
430
570
|
};
|
|
431
571
|
}
|
|
432
572
|
|
|
573
|
+
/** A comma list of params or args; an empty text is an empty list. */
|
|
574
|
+
const commaList = (value: string) => (value.trim() === "" ? [] : value.split(",").map((item) => item.trim()));
|
|
575
|
+
|
|
576
|
+
/** The text of an option, or of the project file that a --*-file option names; exactly one of them. */
|
|
577
|
+
function textOrFile(values: Record<string, unknown>, option: string, what: string): string | undefined {
|
|
578
|
+
const inline = values[option] as string | undefined;
|
|
579
|
+
const file = values[`${option}-file`] as string | undefined;
|
|
580
|
+
if (inline !== undefined && file !== undefined) throw new UsageError(`give --${option} or --${option}-file, not both`);
|
|
581
|
+
if (file !== undefined) return readProjectFile(file).bytes.toString("utf8");
|
|
582
|
+
if (inline === undefined) return undefined;
|
|
583
|
+
if (inline === "") throw new UsageError(`${what} is empty`);
|
|
584
|
+
return inline;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** patch, macro and use: the writes of the generated source (D72, D73). */
|
|
588
|
+
function sourceOperation(command: "patch" | "macro" | "use", args: string[], values: Record<string, unknown>, context: WriteContext, space: string): WriteOperation {
|
|
589
|
+
const comment = values.comment as string | undefined;
|
|
590
|
+
switch (command) {
|
|
591
|
+
case "patch": {
|
|
592
|
+
if (args[2] === undefined) throw new UsageError("the switch name is missing");
|
|
593
|
+
const code = textOrFile(values, "code", "--code");
|
|
594
|
+
if ((code === undefined) === (values.nop !== true)) throw new UsageError("patch needs exactly one of --code, --code-file or --nop");
|
|
595
|
+
const request = { space, start: address(args[0], "start"), end: address(args[1], "end"), switch: args[2], code: code ?? "", ...(comment === undefined ? {} : { comment }) };
|
|
596
|
+
const image = operandImage(values.image as string | undefined, "patch");
|
|
597
|
+
return (db) => {
|
|
598
|
+
const result = setPatch(db, { ...context, image }, request);
|
|
599
|
+
return { revision: result.revision, patch: patch(result.current), ...(result.previous === undefined ? {} : { previous: patch(result.previous) }) };
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
case "macro": {
|
|
603
|
+
if (args[0] === undefined) throw new UsageError("the macro name is missing");
|
|
604
|
+
const body = textOrFile(values, "body", "--body");
|
|
605
|
+
if (body === undefined) throw new UsageError("macro needs --body or --body-file");
|
|
606
|
+
const request = { space, name: args[0], params: commaList((values.params as string | undefined) ?? ""), body, ...(comment === undefined ? {} : { comment }) };
|
|
607
|
+
return (db) => {
|
|
608
|
+
const result = setMacro(db, context, request);
|
|
609
|
+
return { revision: result.revision, macro: macro(result.current), ...(result.previous === undefined ? {} : { previous: macro(result.previous) }) };
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
case "use": {
|
|
613
|
+
if (args[2] === undefined) throw new UsageError("the macro name is missing");
|
|
614
|
+
const request = { space, start: address(args[0], "start"), end: address(args[1], "end"), macro: args[2], args: commaList((values.args as string | undefined) ?? "") };
|
|
615
|
+
const image = operandImage(values.image as string | undefined, "use");
|
|
616
|
+
return (db) => {
|
|
617
|
+
const result = setMacroUse(db, { ...context, image }, request);
|
|
618
|
+
return { revision: result.revision, use: macroUse(result.current), ...(result.previous === undefined ? {} : { previous: macroUse(result.previous) }) };
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
433
624
|
/** apply <file>: the batch as BatchOperation values, checked before the database opens. */
|
|
434
625
|
function applyOperation(args: string[], context: WriteContext, space: string, imagePath: string | undefined): WriteOperation {
|
|
435
626
|
if (args.length !== 1) throw new UsageError("apply needs one file, or - for standard input");
|
|
@@ -442,19 +633,30 @@ function applyOperation(args: string[], context: WriteContext, space: string, im
|
|
|
442
633
|
}
|
|
443
634
|
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
635
|
const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
|
|
445
|
-
const
|
|
636
|
+
const checked = operations.some((operation) => operation.op === "operand" || operation.op === "patch" || operation.op === "use");
|
|
637
|
+
const withImage = checked ? { ...context, image: operandImage(imagePath, "apply with operand, patch or use writes") } : context;
|
|
446
638
|
return (db) => {
|
|
447
639
|
const applied = applyBatch(db, withImage, operations);
|
|
448
640
|
// replaced: each write that replaced a row of another origin, with that row as previous gives it for one write (b2/07).
|
|
449
641
|
const replaced = applied.steps.flatMap((step, index) =>
|
|
450
642
|
step.previous === undefined
|
|
451
643
|
? []
|
|
452
|
-
: [{ write: index + 1, op: operations[index]!.op, previous:
|
|
644
|
+
: [{ write: index + 1, op: operations[index]!.op, previous: shownPrevious(step.previous) }],
|
|
453
645
|
);
|
|
454
646
|
return { revision: applied.revision, writes: applied.steps.length, replaced };
|
|
455
647
|
};
|
|
456
648
|
}
|
|
457
649
|
|
|
650
|
+
/** A row that a write of a batch replaced, in its output shape. */
|
|
651
|
+
function shownPrevious(row: NonNullable<BatchStep["previous"]>) {
|
|
652
|
+
if (Array.isArray(row)) return row.map(region);
|
|
653
|
+
if ("select" in row) return operand(row);
|
|
654
|
+
if ("switch" in row) return patch(row);
|
|
655
|
+
if ("body" in row) return macro(row);
|
|
656
|
+
if ("macro" in row) return macroUse(row);
|
|
657
|
+
return symbol(row);
|
|
658
|
+
}
|
|
659
|
+
|
|
458
660
|
const BATCH_FIELDS: Record<BatchOperation["op"], string[]> = {
|
|
459
661
|
rename: ["address", "name", "kind"],
|
|
460
662
|
"remove-symbol": ["address"],
|
|
@@ -468,6 +670,12 @@ const BATCH_FIELDS: Record<BatchOperation["op"], string[]> = {
|
|
|
468
670
|
unmodule: ["start"],
|
|
469
671
|
operand: ["address", "select", "symbol", "offset", "expression"],
|
|
470
672
|
unoperand: ["address"],
|
|
673
|
+
patch: ["start", "end", "switch", "code", "comment"],
|
|
674
|
+
unpatch: ["start"],
|
|
675
|
+
macro: ["name", "params", "body", "comment"],
|
|
676
|
+
unmacro: ["name"],
|
|
677
|
+
use: ["start", "end", "macro", "args"],
|
|
678
|
+
unuse: ["start"],
|
|
471
679
|
};
|
|
472
680
|
|
|
473
681
|
/** One write of a batch. A write names its own space, or takes the one of --space. */
|
|
@@ -490,6 +698,15 @@ function batchFields(item: unknown, at: string): BatchOperation {
|
|
|
490
698
|
return fields[key] as string;
|
|
491
699
|
};
|
|
492
700
|
const place = (key: string) => address(text(key), `${at}: ${key}`);
|
|
701
|
+
// A list is a JSON list of strings or a comma list.
|
|
702
|
+
const list = (key: string) => {
|
|
703
|
+
const value = fields[key];
|
|
704
|
+
if (value === undefined) return [];
|
|
705
|
+
if (typeof value === "string") return commaList(value);
|
|
706
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new UsageError(`${at}: ${key} must be a list of strings or a comma list`);
|
|
707
|
+
return (value as string[]).map((item) => item.trim());
|
|
708
|
+
};
|
|
709
|
+
const optional = (key: string) => (fields[key] === undefined ? {} : { [key]: text(key) });
|
|
493
710
|
switch (op) {
|
|
494
711
|
case "rename":
|
|
495
712
|
return { op, address: place("address"), name: text("name"), ...(fields.kind === undefined ? {} : { kind: oneOf(SYMBOL_KINDS, text("kind"), `${at}: kind`) }) };
|
|
@@ -522,6 +739,17 @@ function batchFields(item: unknown, at: string): BatchOperation {
|
|
|
522
739
|
}
|
|
523
740
|
case "unoperand":
|
|
524
741
|
return { op, address: place("address") };
|
|
742
|
+
case "patch":
|
|
743
|
+
return { op, start: place("start"), end: place("end"), switch: text("switch"), code: fields.code === undefined ? "" : text("code"), ...optional("comment") };
|
|
744
|
+
case "macro":
|
|
745
|
+
return { op, name: text("name"), params: list("params"), body: text("body"), ...optional("comment") };
|
|
746
|
+
case "unmacro":
|
|
747
|
+
return { op, name: text("name") };
|
|
748
|
+
case "use":
|
|
749
|
+
return { op, start: place("start"), end: place("end"), macro: text("macro"), args: list("args") };
|
|
750
|
+
case "unpatch":
|
|
751
|
+
case "unuse":
|
|
752
|
+
return { op, start: place("start") };
|
|
525
753
|
case "reference":
|
|
526
754
|
case "unreference":
|
|
527
755
|
return { op, from: place("from"), to: place("to"), kind: oneOf(REFERENCE_KINDS, text("kind"), `${at}: kind`) };
|
|
@@ -601,8 +829,30 @@ function writeOperation(command: string, args: string[], kind: string | undefine
|
|
|
601
829
|
const request = { space, address: address(args[0]) };
|
|
602
830
|
return (db) => ({ revision: removeOperand(db, context, request).revision });
|
|
603
831
|
}
|
|
832
|
+
case "unpatch": {
|
|
833
|
+
const request = { space, start: address(args[0], "start") };
|
|
834
|
+
return (db) => ({ revision: removePatch(db, context, request).revision });
|
|
835
|
+
}
|
|
836
|
+
case "unmacro": {
|
|
837
|
+
if (args[0] === undefined) throw new UsageError("the macro name is missing");
|
|
838
|
+
const request = { space, name: args[0] };
|
|
839
|
+
return (db) => ({ revision: removeMacro(db, context, request).revision });
|
|
840
|
+
}
|
|
841
|
+
case "unuse": {
|
|
842
|
+
const request = { space, start: address(args[0], "start") };
|
|
843
|
+
return (db) => ({ revision: removeMacroUse(db, context, request).revision });
|
|
844
|
+
}
|
|
845
|
+
case "merge": {
|
|
846
|
+
if (args.length > 0) throw new UsageError("merge takes no arguments");
|
|
847
|
+
const platform = new Map(platformNames().map((entry) => [entry.name, entry.address]));
|
|
848
|
+
return (db) => {
|
|
849
|
+
const merged = mergeConflicts(db, context, platform);
|
|
850
|
+
return { revision: merged.revision, choices: merged.current.map(showChoice) };
|
|
851
|
+
};
|
|
852
|
+
}
|
|
604
853
|
case "revert": {
|
|
605
|
-
|
|
854
|
+
if (args[0] === undefined) throw new UsageError("revision is missing");
|
|
855
|
+
const request = { revision: args[0] };
|
|
606
856
|
return (db) => ({ revision: revert(db, context, request).revision });
|
|
607
857
|
}
|
|
608
858
|
default:
|
|
@@ -610,4 +860,4 @@ function writeOperation(command: string, args: string[], kind: string | undefine
|
|
|
610
860
|
}
|
|
611
861
|
}
|
|
612
862
|
|
|
613
|
-
await runScript(() => run(
|
|
863
|
+
await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError] });
|