@henols/c64-re-tools 2.1.0-rc.1 → 2.1.0
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/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +33 -24
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +177 -279
- package/skills/c64-reverse-engineering/SKILL.md +13 -12
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +48 -32
- package/skills/c64-testing/references/recording-format.md +48 -19
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +244 -54
- package/skills/c64-testing/scripts/lib/recording.ts +153 -120
- package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +1 -1
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +2 -2
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- package/skills/c64-testing/scripts/record.ts +0 -96
- package/skills/c64-testing/scripts/test.ts +0 -65
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
// routines that several programs share in a library folder and find them in the
|
|
9
9
|
// program (D71).
|
|
10
10
|
|
|
11
|
-
import { basename,
|
|
12
|
-
import { parseArgs } from "node:util";
|
|
11
|
+
import { basename, relative } from "node:path";
|
|
13
12
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
13
|
+
import { type AddressRange, c64AddressOf, formatC64Address } from "#src/c64.ts";
|
|
14
|
+
import { Refusal } from "#src/errors.ts";
|
|
15
|
+
import { parseEntryState, type EntryState } from "#src/host-client/entry-state.ts";
|
|
16
|
+
import { readImage } from "#src/host-client/image.ts";
|
|
17
|
+
import { readActionSteps, type ActionStep } from "#src/host-client/steps.ts";
|
|
18
|
+
import { MAX_EXECUTION_COUNT } from "#src/host-client/tools.ts";
|
|
19
|
+
import { writeReported } from "#src/host-client/transfer.ts";
|
|
18
20
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
19
|
-
import {
|
|
21
|
+
import { openForRead } from "#src/knowledge/database.ts";
|
|
20
22
|
import { programCoverage, requireProgram } from "#src/knowledge/program.ts";
|
|
21
23
|
import {
|
|
22
24
|
commentsAt,
|
|
@@ -36,68 +38,17 @@ import {
|
|
|
36
38
|
symbolNamed,
|
|
37
39
|
symbolSuppressed,
|
|
38
40
|
} from "#src/knowledge/read.ts";
|
|
39
|
-
import { CODE_REGION_TYPES,
|
|
41
|
+
import { CODE_REGION_TYPES, SEMANTIC_ORIGINS } from "#src/knowledge/schema.ts";
|
|
40
42
|
import { platformNames } from "#src/platform.ts";
|
|
41
43
|
import { projectRoot } from "#src/project.ts";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
44
|
+
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
45
|
+
import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec } from "#src/script.ts";
|
|
46
|
+
import { compareReleases, shiftSuggestion, suggestShifts, type Release } from "./lib/compare.ts";
|
|
47
|
+
import { findRoutine, libraryFolder, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
|
|
48
|
+
import { watchRelease, type Start } from "./lib/runtime.ts";
|
|
46
49
|
import { findLeftovers } from "./lib/leftovers.ts";
|
|
47
50
|
import { separate } from "./lib/separate.ts";
|
|
48
51
|
|
|
49
|
-
const USAGE = `provenance.ts <release> <release> [<release> ...] [options]
|
|
50
|
-
provenance.ts separate [--out <file>] [--max-listed <n>]
|
|
51
|
-
provenance.ts leftovers [--out <file>] [--min-text <n>] [--max-listed <n>]
|
|
52
|
-
provenance.ts routine --library <folder> (--module <address> | --start <address> --end <address>)
|
|
53
|
-
[--name <name>] [--title <text>] [--replace]
|
|
54
|
-
provenance.ts match --library <folder> [--out <file>] [--max-listed <n>]
|
|
55
|
-
|
|
56
|
-
--shift <n>:<bytes> move release n (1 = the first) by a number of bytes, for example 2:-256
|
|
57
|
-
--from <address> compare from this address
|
|
58
|
-
--to <address> compare up to this address
|
|
59
|
-
--run <frames> run each release in its own emulator for this many frames, and
|
|
60
|
-
give each listed difference its first access and whether it changed
|
|
61
|
-
--steps <file> with --run: action steps (JSON) to run before the frames
|
|
62
|
-
|
|
63
|
-
separate gives evidence from the program in the knowledge: the unpack stage
|
|
64
|
-
that wrote each range, its access in the coverage of the knowledge, whether call
|
|
65
|
-
and jump references in knowledge reach each code region from the entry, and what
|
|
66
|
-
the program reads or executes outside its segments. --out writes all ranges to a file;
|
|
67
|
-
the result lists at most --max-listed of each list (default 50) and its count.
|
|
68
|
-
|
|
69
|
-
leftovers lists what the program in the knowledge holds besides its working
|
|
70
|
-
code: text in PETSCII, screen codes, ASCII or with bit 7 set (Apple II) and
|
|
71
|
-
assembler source (at least --min-text characters, default 8), BASIC lines away
|
|
72
|
-
from the BASIC start, fills, untouched bytes outside code, exact copies of
|
|
73
|
-
earlier bytes, and code regions that no call or jump reaches and no run executed. Each finding has the stages and the accesses of
|
|
74
|
-
its bytes. --out writes all findings to a file.
|
|
75
|
-
|
|
76
|
-
routine saves a routine of the program in the knowledge to a library: a plain folder of routine
|
|
77
|
-
files, beside the projects (--library, relative to the working directory, as
|
|
78
|
-
--project). It takes a module of the knowledge, or --start and --end. Its
|
|
79
|
-
code regions give the instructions; the user and LLM names, regions, comments,
|
|
80
|
-
references and operand expressions go into the file at offsets. --name defaults
|
|
81
|
-
to the user or LLM name at the start, --title to the module title.
|
|
82
|
-
|
|
83
|
-
match finds each routine of the library in the program of the knowledge: the opcodes and other
|
|
84
|
-
fixed bytes equal, and the addresses into the routine moved by one shift.
|
|
85
|
-
Operands that address something outside the routine may differ; the result
|
|
86
|
-
lists them. --out writes the names of the routines at the found addresses as a
|
|
87
|
-
batch for knowledge.ts apply, and the result lists what it left out.
|
|
88
|
-
|
|
89
|
-
A release to compare is a PRG, placed at its load address, or an image.json of
|
|
90
|
-
the c64-unpacker skill, compared inside its segments. Paths are relative to the
|
|
91
|
-
project folder. The result is one JSON object.`;
|
|
92
|
-
|
|
93
|
-
function address(text: string, option: string): number {
|
|
94
|
-
try {
|
|
95
|
-
return parseC64Address(text);
|
|
96
|
-
} catch {
|
|
97
|
-
throw new UsageError(`${option} must be $ followed by four hex digits, not ${text}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
52
|
const hexBytes = (bytes: number[]) => Buffer.from(bytes).toString("hex").replace(/(..)(?!$)/g, "$1 ");
|
|
102
53
|
|
|
103
54
|
interface LoadedRelease {
|
|
@@ -107,174 +58,66 @@ interface LoadedRelease {
|
|
|
107
58
|
/** 64 KiB with the release bytes at their own addresses. */
|
|
108
59
|
memory: Uint8Array;
|
|
109
60
|
entry?: number;
|
|
110
|
-
stages?: Array<{ entry: number; wrote:
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Reads entry_state.json of the c64-unpacker skill for a start by writes. */
|
|
114
|
-
function entryState(path: string): ImageState | string {
|
|
115
|
-
let value: Record<string, any>;
|
|
116
|
-
try {
|
|
117
|
-
value = JSON.parse(readProjectFile(path).bytes.toString("utf8")) as Record<string, any>;
|
|
118
|
-
} catch (error) {
|
|
119
|
-
if (error instanceof WireFailure && error.code === "not-found") return `It has no ${path}; capture it with c64-unpacker.`;
|
|
120
|
-
if (error instanceof WireFailure) throw error;
|
|
121
|
-
return `${path} is not JSON: ${(error as Error).message}`;
|
|
122
|
-
}
|
|
123
|
-
const registers = value.registers ?? {};
|
|
124
|
-
const byte = (item: unknown) => Number.isInteger(item) && (item as number) >= 0 && (item as number) <= 0xff;
|
|
125
|
-
const pages = ["$0000", "$0100", "$0200", "$0300"].map((page) => value.pages?.[page]);
|
|
126
|
-
const ok =
|
|
127
|
-
typeof registers.pc === "string" &&
|
|
128
|
-
/^\$[0-9a-f]{4}$/i.test(registers.pc) &&
|
|
129
|
-
["a", "x", "y", "sp"].every((name) => byte(registers[name])) &&
|
|
130
|
-
typeof registers.flags === "object" &&
|
|
131
|
-
byte(value.cpuPort?.$00) &&
|
|
132
|
-
byte(value.cpuPort?.$01) &&
|
|
133
|
-
pages.every((page) => typeof page === "string" && /^[0-9a-f]{512}$/i.test(page));
|
|
134
|
-
if (!ok) return `${path} is not an entry_state.json of the c64-unpacker skill.`;
|
|
135
|
-
const flag = (name: string) => registers.flags[name] === true;
|
|
136
|
-
return {
|
|
137
|
-
pc: Number.parseInt(registers.pc.slice(1), 16),
|
|
138
|
-
a: registers.a,
|
|
139
|
-
x: registers.x,
|
|
140
|
-
y: registers.y,
|
|
141
|
-
sp: registers.sp,
|
|
142
|
-
flags: { n: flag("n"), v: flag("v"), d: flag("d"), i: flag("i"), z: flag("z"), c: flag("c") },
|
|
143
|
-
port: [value.cpuPort.$00, value.cpuPort.$01],
|
|
144
|
-
pages: Buffer.from(pages.join(""), "hex"),
|
|
145
|
-
};
|
|
61
|
+
stages?: Array<{ entry: number; wrote: AddressRange[] }>;
|
|
146
62
|
}
|
|
147
63
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
64
|
+
/**
|
|
65
|
+
* A release to compare: "knowledge" (the program of the knowledge, with its
|
|
66
|
+
* entry state and stages), a PRG at its load address, or an image.json of the
|
|
67
|
+
* c64-unpacker skill with the entry_state.json beside it.
|
|
68
|
+
*/
|
|
69
|
+
function readRelease(name: string, shift: number): LoadedRelease {
|
|
70
|
+
const moved = (range: AddressRange) => ({ start: range.start + shift, end: range.end + shift });
|
|
71
|
+
const inside = (range: AddressRange) => range.start >= 0 && range.end <= 0xffff;
|
|
72
|
+
let image: { segments: AddressRange[]; byteAt: (address: number) => number; memory?: Uint8Array; entry?: number; stages?: Array<Record<string, unknown>>; state?: EntryState };
|
|
73
|
+
let noState: string;
|
|
74
|
+
if (name === "knowledge") {
|
|
75
|
+
const db = openForRead();
|
|
76
|
+
try {
|
|
77
|
+
const program = requireProgram(db);
|
|
78
|
+
image = { ...program, ...(program.state === undefined ? {} : { state: parseEntryState(program.state, "the entry state of the knowledge") }) } as typeof image;
|
|
79
|
+
} finally {
|
|
80
|
+
db?.close();
|
|
81
|
+
}
|
|
82
|
+
noState = "The knowledge has no entry state: put a c64-unpacker capture in with knowledge.ts image <image.json>.";
|
|
83
|
+
} else {
|
|
84
|
+
image = readImage(name);
|
|
85
|
+
noState = `It has no entry_state.json beside it; capture it with c64-unpacker.`;
|
|
169
86
|
}
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
const segments = own.map(moved);
|
|
177
|
-
if (!segments.every(inside)) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
|
|
178
|
-
const state = entryState(posix.join(directory, "entry_state.json"));
|
|
179
|
-
const stages = Array.isArray(manifest.stages)
|
|
180
|
-
? (manifest.stages as Array<Record<string, unknown>>).map((stage) => ({
|
|
181
|
-
entry: parse(stage.entry),
|
|
182
|
-
wrote: (Array.isArray(stage.wrote) ? (stage.wrote as Array<Record<string, unknown>>) : []).map((range) => ({ start: parse(range.start), end: parse(range.end) })),
|
|
183
|
-
}))
|
|
184
|
-
: undefined;
|
|
87
|
+
const segments = image.segments.map(moved);
|
|
88
|
+
if (!segments.every(inside)) throw new UsageError(`the shift moves ${name} outside $0000-$ffff`);
|
|
89
|
+
const memory = image.memory ?? new Uint8Array(0x10000);
|
|
90
|
+
if (image.memory === undefined) for (const segment of image.segments) for (let at = segment.start; at <= segment.end; at++) memory[at] = image.byteAt(at);
|
|
91
|
+
const prg = name !== "knowledge" && !name.endsWith(".json");
|
|
92
|
+
const stages = stagesOf(image.stages, name);
|
|
185
93
|
return {
|
|
186
|
-
release: { name
|
|
94
|
+
release: { name, segments, byteAt: (at) => memory[at - shift]! },
|
|
187
95
|
shift,
|
|
188
|
-
start:
|
|
96
|
+
start: prg ? { kind: "program", path: name } : image.state === undefined ? { kind: "none", reason: noState } : { kind: "image", segments: image.segments, byteAt: (at) => memory[at]!, state: image.state },
|
|
189
97
|
memory,
|
|
190
|
-
...(
|
|
98
|
+
...(image.entry === undefined ? {} : { entry: image.entry }),
|
|
191
99
|
...(stages === undefined ? {} : { stages }),
|
|
192
100
|
};
|
|
193
101
|
}
|
|
194
102
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
allowPositionals: true,
|
|
200
|
-
options: {
|
|
201
|
-
shift: { type: "string", multiple: true },
|
|
202
|
-
from: { type: "string" },
|
|
203
|
-
to: { type: "string" },
|
|
204
|
-
run: { type: "string" },
|
|
205
|
-
steps: { type: "string" },
|
|
206
|
-
out: { type: "string" },
|
|
207
|
-
"max-listed": { type: "string" },
|
|
208
|
-
"min-text": { type: "string" },
|
|
209
|
-
library: { type: "string" },
|
|
210
|
-
module: { type: "string" },
|
|
211
|
-
start: { type: "string" },
|
|
212
|
-
end: { type: "string" },
|
|
213
|
-
name: { type: "string" },
|
|
214
|
-
title: { type: "string" },
|
|
215
|
-
replace: { type: "boolean" },
|
|
216
|
-
help: { type: "boolean" },
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
if (values.help) throw new UsageError("");
|
|
220
|
-
/** Refuses the options that `command` does not take. */
|
|
221
|
-
const only = (command: string, allowed: readonly string[]) => {
|
|
222
|
-
const used = Object.keys(values).filter((option) => option !== "help" && !allowed.includes(option));
|
|
223
|
-
if (used.length > 0) throw new UsageError(`${command} does not take --${used.join(", --")}`);
|
|
224
|
-
};
|
|
225
|
-
if (positionals[0] === "separate") {
|
|
226
|
-
only("separate", ["out", "max-listed"]);
|
|
227
|
-
if (positionals.length !== 1) throw new UsageError("separate takes no release: it reads the program from the knowledge");
|
|
228
|
-
return separateCommand(values.out, values["max-listed"]);
|
|
229
|
-
}
|
|
230
|
-
if (positionals[0] === "leftovers") {
|
|
231
|
-
only("leftovers", ["out", "min-text", "max-listed"]);
|
|
232
|
-
if (positionals.length !== 1) throw new UsageError("leftovers takes no release: it reads the program from the knowledge");
|
|
233
|
-
return leftoversCommand(values.out, values["min-text"], values["max-listed"]);
|
|
234
|
-
}
|
|
235
|
-
if (positionals[0] === "routine") {
|
|
236
|
-
only("routine", ["library", "module", "start", "end", "name", "title", "replace"]);
|
|
237
|
-
if (positionals.length !== 1) throw new UsageError("routine takes no image: it reads the program from the knowledge");
|
|
238
|
-
return routineCommand(values);
|
|
239
|
-
}
|
|
240
|
-
if (positionals[0] === "match") {
|
|
241
|
-
only("match", ["library", "out", "max-listed"]);
|
|
242
|
-
if (positionals.length !== 1) throw new UsageError("match takes no image: it reads the program from the knowledge");
|
|
243
|
-
return matchCommand(values.library, values.out, values["max-listed"]);
|
|
244
|
-
}
|
|
245
|
-
only("a comparison", ["shift", "from", "to", "run", "steps"]);
|
|
246
|
-
if (positionals.length < 2) throw new UsageError("give two or more releases");
|
|
103
|
+
/** compare: the releases byte by byte, and with --frames the first access of each difference in each release. */
|
|
104
|
+
async function compare(call: Call): Promise<unknown> {
|
|
105
|
+
const positionals = call.subjects;
|
|
106
|
+
if (positionals.length < 2) throw new UsageError("compare needs two or more releases");
|
|
247
107
|
const shifts = new Map<number, number>();
|
|
248
|
-
for (const text of
|
|
108
|
+
for (const text of call.strings("shift")) {
|
|
249
109
|
const match = /^(\d+):([+-]?\d+)$/.exec(text);
|
|
250
110
|
if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
|
|
251
111
|
shifts.set(Number(match[1]), Number(match[2]));
|
|
252
112
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
frames = Number(values.run);
|
|
258
|
-
}
|
|
259
|
-
let steps: ActionStep[] = [];
|
|
260
|
-
if (values.steps !== undefined) {
|
|
261
|
-
let parsed: unknown;
|
|
262
|
-
try {
|
|
263
|
-
parsed = JSON.parse(readProjectFile(values.steps).bytes.toString("utf8"));
|
|
264
|
-
} catch (error) {
|
|
265
|
-
if (error instanceof WireFailure) throw error;
|
|
266
|
-
throw new UsageError(`the steps file ${values.steps} is not JSON: ${(error as Error).message}`);
|
|
267
|
-
}
|
|
268
|
-
steps = checkActionSteps(parsed, (message) => {
|
|
269
|
-
throw new UsageError(`steps file ${values.steps}: ${message}`);
|
|
270
|
-
});
|
|
271
|
-
}
|
|
113
|
+
const frames = call.count("frames");
|
|
114
|
+
const stepsFile = call.string("steps");
|
|
115
|
+
if (stepsFile !== undefined && frames === undefined) throw new UsageError("--steps works only with --frames");
|
|
116
|
+
const steps: ActionStep[] = stepsFile === undefined ? [] : readActionSteps(stepsFile, "--steps");
|
|
272
117
|
const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
|
|
273
118
|
const releases = loaded.map((item) => item.release);
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
...(values.to === undefined ? {} : { to: address(values.to, "--to") }),
|
|
277
|
-
});
|
|
119
|
+
const range = call.range("range");
|
|
120
|
+
const comparison = compareReleases(releases, range === undefined ? {} : { from: range.start, to: range.end });
|
|
278
121
|
const compared = comparison.identicalBytes + comparison.differingBytes;
|
|
279
122
|
const identicalShare = compared === 0 ? 0 : comparison.identicalBytes / compared;
|
|
280
123
|
// A poor match at the load addresses can mean that the releases are placed differently.
|
|
@@ -294,7 +137,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
294
137
|
const notSuggested = suggestions.filter((suggestion) => "reason" in suggestion);
|
|
295
138
|
|
|
296
139
|
// The runtime evidence: one emulator for each release, the differences at the release's own addresses.
|
|
297
|
-
const runtime = new Map<string, Array<
|
|
140
|
+
const runtime = new Map<string, Array<AddressRange & { first: string; changed: boolean }>>();
|
|
298
141
|
const runs: Record<string, unknown> = {};
|
|
299
142
|
if (frames !== undefined && comparison.differences.length > 0) {
|
|
300
143
|
for (const item of loaded) {
|
|
@@ -350,12 +193,13 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
350
193
|
};
|
|
351
194
|
}
|
|
352
195
|
|
|
353
|
-
/** The stages of
|
|
354
|
-
function stagesOf(stages: Array<Record<string, unknown>> | undefined): Array<{ entry: number; wrote:
|
|
196
|
+
/** The stages of a capture as ranges; a stage that is not a capture stage is refused. `where` names the capture. */
|
|
197
|
+
function stagesOf(stages: Array<Record<string, unknown>> | undefined, where = "the program in the knowledge"): Array<{ entry: number; wrote: AddressRange[] }> | undefined {
|
|
355
198
|
if (stages === undefined) return undefined;
|
|
356
199
|
const parse = (value: unknown) => {
|
|
357
|
-
|
|
358
|
-
|
|
200
|
+
const at = typeof value === "string" ? c64AddressOf(value) : undefined;
|
|
201
|
+
if (at !== undefined) return at;
|
|
202
|
+
throw new Refusal("invalid-input", `the stages of ${where} are not the stages of a c64-unpacker capture; capture it again with c64-unpacker`);
|
|
359
203
|
};
|
|
360
204
|
return stages.map((stage) => ({
|
|
361
205
|
entry: parse(stage.entry),
|
|
@@ -394,17 +238,18 @@ function programEvidence() {
|
|
|
394
238
|
const NO_STAGES = "The program in the knowledge has no stage log. Only a capture of c64-unpacker gives one. A capture of loads holds one part of a program and has none.";
|
|
395
239
|
|
|
396
240
|
/** separate: evidence from the program in the knowledge, with all ranges in --out and the first ones in the result. */
|
|
397
|
-
function separateCommand(
|
|
398
|
-
const
|
|
399
|
-
|
|
241
|
+
function separateCommand(call: Call): unknown {
|
|
242
|
+
const out = call.string("out");
|
|
243
|
+
const max = call.count("max-listed")!;
|
|
400
244
|
const { program, stages, coverage, found } = programEvidence();
|
|
401
245
|
const loaded = { entry: program.entry, stages };
|
|
402
|
-
const show = (range:
|
|
246
|
+
const show = (range: AddressRange) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
403
247
|
const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
|
|
404
248
|
const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
|
|
405
249
|
const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
|
|
406
|
-
const
|
|
407
|
-
|
|
250
|
+
const written = out === undefined ? undefined : writeReported(out, `${JSON.stringify({ ranges, codeRegions, outside }, null, 1)}\n`, { what: "--out" });
|
|
251
|
+
const rest = out === undefined ? undefined : out;
|
|
252
|
+
const list = <T>(items: T[]) => listed(items, max, undefined, rest);
|
|
408
253
|
const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
|
|
409
254
|
const lastStage = loaded.stages === undefined ? undefined : loaded.stages.length - 1;
|
|
410
255
|
return {
|
|
@@ -416,14 +261,14 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
|
|
|
416
261
|
bytesByStage: Object.fromEntries([...[...Array(lastStage + 1).keys()].map((stage): [string, number] => [String(stage), bytes((range) => range.stage === stage)]), ["none", bytes((range) => range.stage === undefined)]]),
|
|
417
262
|
}),
|
|
418
263
|
...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
|
|
419
|
-
ranges:
|
|
264
|
+
ranges: list(ranges),
|
|
420
265
|
// total counts every code region; unreachable is a list like the others.
|
|
421
266
|
codeRegions: {
|
|
422
267
|
total: codeRegions.length,
|
|
423
|
-
...(loaded.entry === undefined ? {} : { unreachable:
|
|
268
|
+
...(loaded.entry === undefined ? {} : { unreachable: list(codeRegions.filter((region) => region.reachable === false)) }),
|
|
424
269
|
},
|
|
425
|
-
...(coverage === undefined ? {} : { outside: { read:
|
|
426
|
-
...(
|
|
270
|
+
...(coverage === undefined ? {} : { outside: { read: list(outside.read), executed: list(outside.executed), generated: list(outside.generated) } }),
|
|
271
|
+
...(written === undefined ? {} : written),
|
|
427
272
|
};
|
|
428
273
|
}
|
|
429
274
|
|
|
@@ -431,20 +276,19 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
|
|
|
431
276
|
const SHOWN_TEXT = 120;
|
|
432
277
|
|
|
433
278
|
/** leftovers: what the program in the knowledge holds besides its working code, with all findings in --out and the first ones in the result. */
|
|
434
|
-
function leftoversCommand(
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
const min =
|
|
438
|
-
if (!/^\d{1,3}$/.test(min) || Number(min) < 4) throw new UsageError(`--min-text must be a whole number of 4 or more, not ${min}`);
|
|
279
|
+
function leftoversCommand(call: Call): unknown {
|
|
280
|
+
const out = call.string("out");
|
|
281
|
+
const max = call.count("max-listed")!;
|
|
282
|
+
const min = call.count("min-text")!;
|
|
439
283
|
const { program, stages, coverage, found } = programEvidence();
|
|
440
284
|
const leftovers = findLeftovers({
|
|
441
285
|
segments: program.segments,
|
|
442
286
|
memory: program.memory,
|
|
443
287
|
separation: found,
|
|
444
288
|
...(program.segments.some((segment) => segment.start === 0x0801) ? { basicStart: 0x0801 } : {}),
|
|
445
|
-
minText:
|
|
289
|
+
minText: min,
|
|
446
290
|
});
|
|
447
|
-
const show = <T extends
|
|
291
|
+
const show = <T extends AddressRange & { stages?: number[]; access?: string[] }>({ start, end, stages: wrote, access, ...rest }: T) => ({
|
|
448
292
|
start: formatC64Address(start),
|
|
449
293
|
end: formatC64Address(end),
|
|
450
294
|
...rest,
|
|
@@ -459,56 +303,50 @@ function leftoversCommand(out: string | undefined, minText: string | undefined,
|
|
|
459
303
|
copies: leftovers.copies.map(({ copyOf, ...rest }) => ({ ...show(rest), copyOf: formatC64Address(copyOf) })),
|
|
460
304
|
deadCode: leftovers.deadCode.map(show),
|
|
461
305
|
};
|
|
462
|
-
const
|
|
463
|
-
|
|
306
|
+
const written = out === undefined ? undefined : writeReported(out, `${JSON.stringify(all, null, 1)}\n`, { what: "--out" });
|
|
307
|
+
const list = <T>(items: T[]) => listed(items, max, undefined, out ?? "a larger --max-listed, or --out <file>");
|
|
464
308
|
return {
|
|
465
309
|
...(program.entry === undefined ? { noEntry: "The program in the knowledge has no entry, so deadCode is empty." } : { entry: formatC64Address(program.entry) }),
|
|
466
310
|
...(stages === undefined ? { noStages: NO_STAGES } : { stages: stages.length }),
|
|
467
311
|
...(coverage === undefined ? { noCoverage: "The knowledge has no coverage, so residue is empty and no finding has access. Add coverage with coverage of the c64-knowledge skill." } : {}),
|
|
468
|
-
text:
|
|
469
|
-
basic:
|
|
470
|
-
fill:
|
|
471
|
-
residue:
|
|
472
|
-
copies:
|
|
473
|
-
deadCode:
|
|
474
|
-
...(
|
|
312
|
+
text: list(all.text.map((item) => (item.text.length > SHOWN_TEXT ? { ...item, text: `${item.text.slice(0, SHOWN_TEXT)} ...`, length: item.text.length } : item))),
|
|
313
|
+
basic: list(all.basic),
|
|
314
|
+
fill: list(all.fill),
|
|
315
|
+
residue: list(all.residue),
|
|
316
|
+
copies: list(all.copies),
|
|
317
|
+
deadCode: list(all.deadCode),
|
|
318
|
+
...(written === undefined ? {} : written),
|
|
475
319
|
};
|
|
476
320
|
}
|
|
477
321
|
|
|
478
|
-
const SEMANTIC: readonly Origin[] = ["user", "llm"];
|
|
479
|
-
|
|
480
322
|
const platform = () => new Map(platformNames().map((entry) => [entry.name, entry.address]));
|
|
481
323
|
|
|
482
324
|
/** routine: saves a module or a range of the program in the knowledge, with its knowledge, as a routine of the library (D71). */
|
|
483
|
-
function routineCommand(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (values.module === undefined && values.start === undefined && values.end === undefined) throw new UsageError("routine needs --module <address>, or --start and --end");
|
|
487
|
-
if (values.module === undefined && (values.start === undefined || values.end === undefined)) throw new UsageError("routine needs both --start and --end");
|
|
325
|
+
function routineCommand(call: Call): unknown {
|
|
326
|
+
const given = call.subject(0);
|
|
327
|
+
const library = call.string("library")!;
|
|
488
328
|
const db = openForRead();
|
|
489
329
|
try {
|
|
490
330
|
const image = requireProgram(db);
|
|
491
331
|
const segmentOf = (at: number) => image.segments.find((segment) => at >= segment.start && at <= segment.end);
|
|
492
332
|
let start: number;
|
|
493
333
|
let end: number;
|
|
494
|
-
let title =
|
|
495
|
-
if (
|
|
496
|
-
start =
|
|
334
|
+
let title = call.string("title");
|
|
335
|
+
if (!given.includes("-")) {
|
|
336
|
+
start = call.parseAddress(given, "<module>");
|
|
497
337
|
const modules = listModules(db);
|
|
498
338
|
const module = modules.find((item) => item.start === start);
|
|
499
|
-
if (module === undefined) throw new
|
|
339
|
+
if (module === undefined) throw new Refusal("not-found", `No module starts at ${formatC64Address(start)}. List the modules with knowledge.ts modules, or give a range <start>-<end>.`);
|
|
500
340
|
const segment = segmentOf(start);
|
|
501
341
|
if (segment === undefined) throw new UsageError(`The module at ${formatC64Address(start)} is outside the segments of the program.`);
|
|
502
342
|
end = (modules.find((item) => item.start > start && item.start <= segment.end)?.start ?? segment.end + 1) - 1;
|
|
503
343
|
title ??= module.title;
|
|
504
344
|
} else {
|
|
505
|
-
start =
|
|
506
|
-
end = address(values.end!, "--end");
|
|
507
|
-
if (end < start) throw new UsageError("--end is before --start");
|
|
345
|
+
({ start, end } = call.parseRange(given, "<range>"));
|
|
508
346
|
if (segmentOf(start) === undefined || segmentOf(start) !== segmentOf(end)) throw new UsageError(`${formatC64Address(start)}-${formatC64Address(end)} is not inside one segment of the program.`);
|
|
509
347
|
}
|
|
510
348
|
const range = { start, end };
|
|
511
|
-
const name =
|
|
349
|
+
const name = call.string("name") ?? listSymbols(db, { start, end: start }, undefined, SEMANTIC_ORIGINS)[0]?.name;
|
|
512
350
|
if (name === undefined) throw new UsageError(`The knowledge has no user or LLM name at ${formatC64Address(start)}: give --name.`);
|
|
513
351
|
const { routine, leftOut } = routineFrom({
|
|
514
352
|
name,
|
|
@@ -519,17 +357,17 @@ function routineCommand(values: { library?: string; module?: string; start?: str
|
|
|
519
357
|
end,
|
|
520
358
|
byteAt: image.byteAt,
|
|
521
359
|
analyzed: listRegions(db, range),
|
|
522
|
-
symbols: listSymbols(db, range, undefined,
|
|
523
|
-
regions: listRegions(db, range, undefined,
|
|
524
|
-
comments: listComments(db, range,
|
|
525
|
-
references: listReferences(db, range, undefined,
|
|
526
|
-
operands: listOperands(db, range,
|
|
360
|
+
symbols: listSymbols(db, range, undefined, SEMANTIC_ORIGINS),
|
|
361
|
+
regions: listRegions(db, range, undefined, SEMANTIC_ORIGINS),
|
|
362
|
+
comments: listComments(db, range, SEMANTIC_ORIGINS),
|
|
363
|
+
references: listReferences(db, range, undefined, SEMANTIC_ORIGINS),
|
|
364
|
+
operands: listOperands(db, range, SEMANTIC_ORIGINS),
|
|
527
365
|
platform: platform(),
|
|
528
366
|
});
|
|
529
|
-
const file = saveRoutine(libraryFolder(
|
|
367
|
+
const file = saveRoutine(libraryFolder(library, { create: true, cwd: projectRoot() }), routine, call.flag("replace"));
|
|
530
368
|
return {
|
|
531
369
|
routine: routine.name,
|
|
532
|
-
file: relative(
|
|
370
|
+
file: relative(projectRoot(), file).split("\\").join("/"),
|
|
533
371
|
start: formatC64Address(start),
|
|
534
372
|
end: formatC64Address(end),
|
|
535
373
|
length: routine.bytes.length,
|
|
@@ -545,13 +383,13 @@ function routineCommand(values: { library?: string; module?: string; start?: str
|
|
|
545
383
|
}
|
|
546
384
|
|
|
547
385
|
/** match: finds the routines of the library in the program of the knowledge, and with --out writes their names there as an apply batch (D71). */
|
|
548
|
-
function matchCommand(
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
const routines = readLibrary(libraryFolder(library, { create: false }));
|
|
554
|
-
if (routines.length === 0) throw new
|
|
386
|
+
function matchCommand(call: Call): unknown {
|
|
387
|
+
const library = call.string("library")!;
|
|
388
|
+
const out = call.string("out");
|
|
389
|
+
const max = call.count("max-listed")!;
|
|
390
|
+
const list = <T>(items: T[]) => listed(items, max);
|
|
391
|
+
const routines = readLibrary(libraryFolder(library, { create: false, cwd: projectRoot() }));
|
|
392
|
+
if (routines.length === 0) throw new Refusal("not-found", `The library ${library} has no routine files. Save a routine with routine first.`);
|
|
555
393
|
const programDb = openForRead();
|
|
556
394
|
let image: ReturnType<typeof requireProgram>;
|
|
557
395
|
try {
|
|
@@ -572,7 +410,7 @@ function matchCommand(library: string | undefined, out: string | undefined, maxL
|
|
|
572
410
|
})),
|
|
573
411
|
)
|
|
574
412
|
.sort((a, b) => a.start.localeCompare(b.start));
|
|
575
|
-
const result: Record<string, unknown> = { library, routines: routines.length, matches:
|
|
413
|
+
const result: Record<string, unknown> = { library, routines: routines.length, matches: list(matches) };
|
|
576
414
|
if (out === undefined) return result;
|
|
577
415
|
const db = openForRead();
|
|
578
416
|
let suggestions: ReturnType<typeof suggest>;
|
|
@@ -597,12 +435,72 @@ function matchCommand(library: string | undefined, out: string | undefined, maxL
|
|
|
597
435
|
db?.close();
|
|
598
436
|
}
|
|
599
437
|
const { batch, known, leftOut } = suggestions;
|
|
600
|
-
|
|
438
|
+
const written = writeReported(out, batch.length === 0 ? "[]\n" : `[\n${batch.map((write) => ` ${JSON.stringify(write)}`).join(",\n")}\n]\n`, { what: "--out" });
|
|
601
439
|
return {
|
|
602
440
|
...result,
|
|
603
|
-
batch: {
|
|
441
|
+
batch: { ...written, writes: batch.length, known, leftOut: list(leftOut) },
|
|
604
442
|
...(batch.length === 0 ? {} : { apply: `knowledge.ts apply ${out} --reason "names of library routines"` }),
|
|
605
443
|
};
|
|
606
444
|
}
|
|
607
445
|
|
|
608
|
-
|
|
446
|
+
const LIBRARY = { kind: "text", value: "<folder>", required: true, help: "the library: a plain folder of routine files beside the projects, relative to the project folder, for example ../library" } as const;
|
|
447
|
+
|
|
448
|
+
const COMMANDS: CommandSpec[] = [
|
|
449
|
+
{
|
|
450
|
+
name: "compare",
|
|
451
|
+
summary: "Compares releases byte by byte, in their common ranges, and suggests shifts when they are placed differently.",
|
|
452
|
+
subjects: [{ name: "<release>", help: "knowledge (the program in the knowledge), a PRG at its load address, or an image.json of the c64-unpacker skill with the entry_state.json beside it. Two or more", list: true }],
|
|
453
|
+
options: {
|
|
454
|
+
shift: { kind: "text", value: "<n>:<bytes>", multiple: true, help: "move release n (1 is the first) by a number of bytes, for example 2:-256" },
|
|
455
|
+
range: { ...OPTIONS.range, help: "compare only the addresses from start to end" },
|
|
456
|
+
frames: { ...OPTIONS.frames, max: MAX_EXECUTION_COUNT, help: "run each release in its own emulator for n frames, and give each listed difference its first access and whether it changed" },
|
|
457
|
+
steps: { ...OPTIONS.steps, help: "with --frames: action steps (JSON) to run before the frames" },
|
|
458
|
+
},
|
|
459
|
+
run: compare,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: "separate",
|
|
463
|
+
summary: "Evidence from the program in the knowledge: the unpack stage that wrote each range, its access in the coverage, whether call and jump references reach each code region from the entry, and what the program reads or executes outside its segments.",
|
|
464
|
+
options: { out: { ...OPTIONS.out, value: "<file>", help: "write all ranges to this file" }, "max-listed": OPTIONS["max-listed"] },
|
|
465
|
+
run: separateCommand,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: "leftovers",
|
|
469
|
+
summary: "What the program in the knowledge holds besides its working code: texts in PETSCII, screen codes, ASCII or with bit 7 set (Apple II), assembler source, BASIC lines away from the BASIC start, fills, untouched bytes outside code, copies of earlier bytes, and code regions that nothing reaches and no run executed.",
|
|
470
|
+
options: {
|
|
471
|
+
out: { ...OPTIONS.out, value: "<file>", help: "write all findings to this file" },
|
|
472
|
+
"min-text": { kind: "count", value: "<n>", min: 4, max: 999, default: 8, help: "the shortest text to find (default 8)" },
|
|
473
|
+
"max-listed": OPTIONS["max-listed"],
|
|
474
|
+
},
|
|
475
|
+
run: leftoversCommand,
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: "routine",
|
|
479
|
+
summary: "Saves a routine of the program in the knowledge to a library, with its user and LLM names, regions, comments, references and operand expressions at offsets.",
|
|
480
|
+
subjects: [{ name: "<module|range>", help: "the start address of a module of the knowledge, or a range <start>-<end>" }],
|
|
481
|
+
options: {
|
|
482
|
+
library: LIBRARY,
|
|
483
|
+
name: { kind: "text", value: "<name>", help: "the routine name. The default is the user or LLM name at the start" },
|
|
484
|
+
title: { kind: "text", value: "<text>", help: "the title. The default is the module title" },
|
|
485
|
+
replace: { kind: "flag", help: "replace a routine file of the same name" },
|
|
486
|
+
},
|
|
487
|
+
run: routineCommand,
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: "match",
|
|
491
|
+
summary: "Finds each routine of the library in the program of the knowledge: the opcodes and other fixed bytes equal, and the addresses into the routine moved by one shift.",
|
|
492
|
+
options: {
|
|
493
|
+
library: { ...LIBRARY, help: "the library folder, relative to the project folder, for example ../library" },
|
|
494
|
+
out: { ...OPTIONS.out, value: "<file>", help: "write the names of the routines at the found addresses as a batch for knowledge.ts apply" },
|
|
495
|
+
"max-listed": OPTIONS["max-listed"],
|
|
496
|
+
},
|
|
497
|
+
run: matchCommand,
|
|
498
|
+
},
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
await runScript({
|
|
502
|
+
name: "provenance",
|
|
503
|
+
summary: "Where the parts of a program come from: a comparison of releases, the separation of the program in the knowledge, its leftovers, and a library of known routines.",
|
|
504
|
+
names: knowledgeNames,
|
|
505
|
+
commands: COMMANDS,
|
|
506
|
+
});
|