@henols/c64-re-tools 2.0.1 → 2.1.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- 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 +82 -26
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +442 -0
- 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 +222 -252
- package/skills/c64-reverse-engineering/SKILL.md +18 -15
- 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 +94 -19
- package/skills/c64-testing/references/recording-format.md +125 -0
- 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 +334 -0
- package/skills/c64-testing/scripts/lib/recording.ts +662 -0
- package/skills/c64-testing/scripts/lib/replay.ts +105 -0
- 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 +162 -0
- package/src/native/discover.ts +8 -0
- 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 +25 -4
- 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/test.ts +0 -65
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
// release is a PRG or an unpacker image.json. With --run, each release
|
|
4
4
|
// runs in its own emulator, and each listed difference gets its first access
|
|
5
5
|
// and whether the run changed it. The separate command gives evidence from
|
|
6
|
-
// the program in the knowledge
|
|
6
|
+
// the program in the knowledge, and the leftovers command lists what its
|
|
7
|
+
// unused bytes hold: text, BASIC lines, fills and copies. The routine and match commands keep
|
|
7
8
|
// routines that several programs share in a library folder and find them in the
|
|
8
9
|
// program (D71).
|
|
9
10
|
|
|
10
|
-
import { basename,
|
|
11
|
-
import { parseArgs } from "node:util";
|
|
11
|
+
import { basename, relative } from "node:path";
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
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";
|
|
17
20
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
18
|
-
import {
|
|
21
|
+
import { openForRead } from "#src/knowledge/database.ts";
|
|
19
22
|
import { programCoverage, requireProgram } from "#src/knowledge/program.ts";
|
|
20
23
|
import {
|
|
21
24
|
commentsAt,
|
|
@@ -35,59 +38,17 @@ import {
|
|
|
35
38
|
symbolNamed,
|
|
36
39
|
symbolSuppressed,
|
|
37
40
|
} from "#src/knowledge/read.ts";
|
|
38
|
-
import { CODE_REGION_TYPES,
|
|
41
|
+
import { CODE_REGION_TYPES, SEMANTIC_ORIGINS } from "#src/knowledge/schema.ts";
|
|
39
42
|
import { platformNames } from "#src/platform.ts";
|
|
40
43
|
import { projectRoot } from "#src/project.ts";
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
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";
|
|
49
|
+
import { findLeftovers } from "./lib/leftovers.ts";
|
|
45
50
|
import { separate } from "./lib/separate.ts";
|
|
46
51
|
|
|
47
|
-
const USAGE = `provenance.ts <release> <release> [<release> ...] [options]
|
|
48
|
-
provenance.ts separate [--out <file>] [--max-listed <n>]
|
|
49
|
-
provenance.ts routine --library <folder> (--module <address> | --start <address> --end <address>)
|
|
50
|
-
[--name <name>] [--title <text>] [--replace]
|
|
51
|
-
provenance.ts match --library <folder> [--out <file>] [--max-listed <n>]
|
|
52
|
-
|
|
53
|
-
--shift <n>:<bytes> move release n (1 = the first) by a number of bytes, for example 2:-256
|
|
54
|
-
--from <address> compare from this address
|
|
55
|
-
--to <address> compare up to this address
|
|
56
|
-
--run <frames> run each release in its own emulator for this many frames, and
|
|
57
|
-
give each listed difference its first access and whether it changed
|
|
58
|
-
--steps <file> with --run: action steps (JSON) to run before the frames
|
|
59
|
-
|
|
60
|
-
separate gives evidence from the program in the knowledge: the unpack stage
|
|
61
|
-
that wrote each range, its access in the coverage of the knowledge, whether call
|
|
62
|
-
and jump references in knowledge reach each code region from the entry, and what
|
|
63
|
-
the program reads or executes outside its segments. --out writes all ranges to a file;
|
|
64
|
-
the result lists at most --max-listed of each list (default 50) and its count.
|
|
65
|
-
|
|
66
|
-
routine saves a routine of the program in the knowledge to a library: a plain folder of routine
|
|
67
|
-
files, beside the projects (--library, relative to the working directory, as
|
|
68
|
-
--project). It takes a module of the knowledge, or --start and --end. Its
|
|
69
|
-
code regions give the instructions; the user and LLM names, regions, comments,
|
|
70
|
-
references and operand expressions go into the file at offsets. --name defaults
|
|
71
|
-
to the user or LLM name at the start, --title to the module title.
|
|
72
|
-
|
|
73
|
-
match finds each routine of the library in the program of the knowledge: the opcodes and other
|
|
74
|
-
fixed bytes equal, and the addresses into the routine moved by one shift.
|
|
75
|
-
Operands that address something outside the routine may differ; the result
|
|
76
|
-
lists them. --out writes the names of the routines at the found addresses as a
|
|
77
|
-
batch for knowledge.ts apply, and the result lists what it left out.
|
|
78
|
-
|
|
79
|
-
A release to compare is a PRG, placed at its load address, or an image.json of
|
|
80
|
-
the c64-unpacker skill, compared inside its segments. Paths are relative to the
|
|
81
|
-
project folder. The result is one JSON object.`;
|
|
82
|
-
|
|
83
|
-
function address(text: string, option: string): number {
|
|
84
|
-
try {
|
|
85
|
-
return parseC64Address(text);
|
|
86
|
-
} catch {
|
|
87
|
-
throw new UsageError(`${option} must be $ followed by four hex digits, not ${text}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
52
|
const hexBytes = (bytes: number[]) => Buffer.from(bytes).toString("hex").replace(/(..)(?!$)/g, "$1 ");
|
|
92
53
|
|
|
93
54
|
interface LoadedRelease {
|
|
@@ -97,168 +58,66 @@ interface LoadedRelease {
|
|
|
97
58
|
/** 64 KiB with the release bytes at their own addresses. */
|
|
98
59
|
memory: Uint8Array;
|
|
99
60
|
entry?: number;
|
|
100
|
-
stages?: Array<{ entry: number; wrote:
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Reads entry_state.json of the c64-unpacker skill for a start by writes. */
|
|
104
|
-
function entryState(path: string): ImageState | string {
|
|
105
|
-
let value: Record<string, any>;
|
|
106
|
-
try {
|
|
107
|
-
value = JSON.parse(readProjectFile(path).bytes.toString("utf8")) as Record<string, any>;
|
|
108
|
-
} catch (error) {
|
|
109
|
-
if (error instanceof WireFailure && error.code === "not-found") return `It has no ${path}; capture it with c64-unpacker.`;
|
|
110
|
-
if (error instanceof WireFailure) throw error;
|
|
111
|
-
return `${path} is not JSON: ${(error as Error).message}`;
|
|
112
|
-
}
|
|
113
|
-
const registers = value.registers ?? {};
|
|
114
|
-
const byte = (item: unknown) => Number.isInteger(item) && (item as number) >= 0 && (item as number) <= 0xff;
|
|
115
|
-
const pages = ["$0000", "$0100", "$0200", "$0300"].map((page) => value.pages?.[page]);
|
|
116
|
-
const ok =
|
|
117
|
-
typeof registers.pc === "string" &&
|
|
118
|
-
/^\$[0-9a-f]{4}$/i.test(registers.pc) &&
|
|
119
|
-
["a", "x", "y", "sp"].every((name) => byte(registers[name])) &&
|
|
120
|
-
typeof registers.flags === "object" &&
|
|
121
|
-
byte(value.cpuPort?.$00) &&
|
|
122
|
-
byte(value.cpuPort?.$01) &&
|
|
123
|
-
pages.every((page) => typeof page === "string" && /^[0-9a-f]{512}$/i.test(page));
|
|
124
|
-
if (!ok) return `${path} is not an entry_state.json of the c64-unpacker skill.`;
|
|
125
|
-
const flag = (name: string) => registers.flags[name] === true;
|
|
126
|
-
return {
|
|
127
|
-
pc: Number.parseInt(registers.pc.slice(1), 16),
|
|
128
|
-
a: registers.a,
|
|
129
|
-
x: registers.x,
|
|
130
|
-
y: registers.y,
|
|
131
|
-
sp: registers.sp,
|
|
132
|
-
flags: { n: flag("n"), v: flag("v"), d: flag("d"), i: flag("i"), z: flag("z"), c: flag("c") },
|
|
133
|
-
port: [value.cpuPort.$00, value.cpuPort.$01],
|
|
134
|
-
pages: Buffer.from(pages.join(""), "hex"),
|
|
135
|
-
};
|
|
61
|
+
stages?: Array<{ entry: number; wrote: AddressRange[] }>;
|
|
136
62
|
}
|
|
137
63
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
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.`;
|
|
159
86
|
}
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
const segments = own.map(moved);
|
|
167
|
-
if (!segments.every(inside)) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
|
|
168
|
-
const state = entryState(posix.join(directory, "entry_state.json"));
|
|
169
|
-
const stages = Array.isArray(manifest.stages)
|
|
170
|
-
? (manifest.stages as Array<Record<string, unknown>>).map((stage) => ({
|
|
171
|
-
entry: parse(stage.entry),
|
|
172
|
-
wrote: (Array.isArray(stage.wrote) ? (stage.wrote as Array<Record<string, unknown>>) : []).map((range) => ({ start: parse(range.start), end: parse(range.end) })),
|
|
173
|
-
}))
|
|
174
|
-
: 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);
|
|
175
93
|
return {
|
|
176
|
-
release: { name
|
|
94
|
+
release: { name, segments, byteAt: (at) => memory[at - shift]! },
|
|
177
95
|
shift,
|
|
178
|
-
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 },
|
|
179
97
|
memory,
|
|
180
|
-
...(
|
|
98
|
+
...(image.entry === undefined ? {} : { entry: image.entry }),
|
|
181
99
|
...(stages === undefined ? {} : { stages }),
|
|
182
100
|
};
|
|
183
101
|
}
|
|
184
102
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
allowPositionals: true,
|
|
190
|
-
options: {
|
|
191
|
-
shift: { type: "string", multiple: true },
|
|
192
|
-
from: { type: "string" },
|
|
193
|
-
to: { type: "string" },
|
|
194
|
-
run: { type: "string" },
|
|
195
|
-
steps: { type: "string" },
|
|
196
|
-
out: { type: "string" },
|
|
197
|
-
"max-listed": { type: "string" },
|
|
198
|
-
library: { type: "string" },
|
|
199
|
-
module: { type: "string" },
|
|
200
|
-
start: { type: "string" },
|
|
201
|
-
end: { type: "string" },
|
|
202
|
-
name: { type: "string" },
|
|
203
|
-
title: { type: "string" },
|
|
204
|
-
replace: { type: "boolean" },
|
|
205
|
-
help: { type: "boolean" },
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
if (values.help) throw new UsageError("");
|
|
209
|
-
/** Refuses the options that `command` does not take. */
|
|
210
|
-
const only = (command: string, allowed: readonly string[]) => {
|
|
211
|
-
const used = Object.keys(values).filter((option) => option !== "help" && !allowed.includes(option));
|
|
212
|
-
if (used.length > 0) throw new UsageError(`${command} does not take --${used.join(", --")}`);
|
|
213
|
-
};
|
|
214
|
-
if (positionals[0] === "separate") {
|
|
215
|
-
only("separate", ["out", "max-listed"]);
|
|
216
|
-
if (positionals.length !== 1) throw new UsageError("separate takes no release: it reads the program from the knowledge");
|
|
217
|
-
return separateCommand(values.out, values["max-listed"]);
|
|
218
|
-
}
|
|
219
|
-
if (positionals[0] === "routine") {
|
|
220
|
-
only("routine", ["library", "module", "start", "end", "name", "title", "replace"]);
|
|
221
|
-
if (positionals.length !== 1) throw new UsageError("routine takes no image: it reads the program from the knowledge");
|
|
222
|
-
return routineCommand(values);
|
|
223
|
-
}
|
|
224
|
-
if (positionals[0] === "match") {
|
|
225
|
-
only("match", ["library", "out", "max-listed"]);
|
|
226
|
-
if (positionals.length !== 1) throw new UsageError("match takes no image: it reads the program from the knowledge");
|
|
227
|
-
return matchCommand(values.library, values.out, values["max-listed"]);
|
|
228
|
-
}
|
|
229
|
-
only("a comparison", ["shift", "from", "to", "run", "steps"]);
|
|
230
|
-
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");
|
|
231
107
|
const shifts = new Map<number, number>();
|
|
232
|
-
for (const text of
|
|
108
|
+
for (const text of call.strings("shift")) {
|
|
233
109
|
const match = /^(\d+):([+-]?\d+)$/.exec(text);
|
|
234
110
|
if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
|
|
235
111
|
shifts.set(Number(match[1]), Number(match[2]));
|
|
236
112
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
frames = Number(values.run);
|
|
242
|
-
}
|
|
243
|
-
let steps: ActionStep[] = [];
|
|
244
|
-
if (values.steps !== undefined) {
|
|
245
|
-
let parsed: unknown;
|
|
246
|
-
try {
|
|
247
|
-
parsed = JSON.parse(readProjectFile(values.steps).bytes.toString("utf8"));
|
|
248
|
-
} catch (error) {
|
|
249
|
-
if (error instanceof WireFailure) throw error;
|
|
250
|
-
throw new UsageError(`the steps file ${values.steps} is not JSON: ${(error as Error).message}`);
|
|
251
|
-
}
|
|
252
|
-
steps = checkActionSteps(parsed, (message) => {
|
|
253
|
-
throw new UsageError(`steps file ${values.steps}: ${message}`);
|
|
254
|
-
});
|
|
255
|
-
}
|
|
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");
|
|
256
117
|
const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
|
|
257
118
|
const releases = loaded.map((item) => item.release);
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
...(values.to === undefined ? {} : { to: address(values.to, "--to") }),
|
|
261
|
-
});
|
|
119
|
+
const range = call.range("range");
|
|
120
|
+
const comparison = compareReleases(releases, range === undefined ? {} : { from: range.start, to: range.end });
|
|
262
121
|
const compared = comparison.identicalBytes + comparison.differingBytes;
|
|
263
122
|
const identicalShare = compared === 0 ? 0 : comparison.identicalBytes / compared;
|
|
264
123
|
// A poor match at the load addresses can mean that the releases are placed differently.
|
|
@@ -278,7 +137,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
278
137
|
const notSuggested = suggestions.filter((suggestion) => "reason" in suggestion);
|
|
279
138
|
|
|
280
139
|
// The runtime evidence: one emulator for each release, the differences at the release's own addresses.
|
|
281
|
-
const runtime = new Map<string, Array<
|
|
140
|
+
const runtime = new Map<string, Array<AddressRange & { first: string; changed: boolean }>>();
|
|
282
141
|
const runs: Record<string, unknown> = {};
|
|
283
142
|
if (frames !== undefined && comparison.differences.length > 0) {
|
|
284
143
|
for (const item of loaded) {
|
|
@@ -334,12 +193,13 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
334
193
|
};
|
|
335
194
|
}
|
|
336
195
|
|
|
337
|
-
/** The stages of
|
|
338
|
-
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 {
|
|
339
198
|
if (stages === undefined) return undefined;
|
|
340
199
|
const parse = (value: unknown) => {
|
|
341
|
-
|
|
342
|
-
|
|
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`);
|
|
343
203
|
};
|
|
344
204
|
return stages.map((stage) => ({
|
|
345
205
|
entry: parse(stage.entry),
|
|
@@ -347,10 +207,8 @@ function stagesOf(stages: Array<Record<string, unknown>> | undefined): Array<{ e
|
|
|
347
207
|
}));
|
|
348
208
|
}
|
|
349
209
|
|
|
350
|
-
/**
|
|
351
|
-
function
|
|
352
|
-
const max = maxListed ?? "50";
|
|
353
|
-
if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
|
|
210
|
+
/** The program in the knowledge with its stages and coverage, and the separation of its bytes. */
|
|
211
|
+
function programEvidence() {
|
|
354
212
|
const db = openForRead();
|
|
355
213
|
let program: ReturnType<typeof requireProgram>;
|
|
356
214
|
let coverage: Uint8Array | undefined;
|
|
@@ -365,7 +223,6 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
|
|
|
365
223
|
db?.close();
|
|
366
224
|
}
|
|
367
225
|
const stages = stagesOf(program.stages);
|
|
368
|
-
const loaded = { entry: program.entry, stages };
|
|
369
226
|
const found = separate({
|
|
370
227
|
segments: program.segments,
|
|
371
228
|
memory: program.memory,
|
|
@@ -375,68 +232,121 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
|
|
|
375
232
|
codeRegions: regions,
|
|
376
233
|
references,
|
|
377
234
|
});
|
|
378
|
-
|
|
235
|
+
return { program, stages, coverage, found };
|
|
236
|
+
}
|
|
237
|
+
|
|
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.";
|
|
239
|
+
|
|
240
|
+
/** separate: evidence from the program in the knowledge, with all ranges in --out and the first ones in the result. */
|
|
241
|
+
function separateCommand(call: Call): unknown {
|
|
242
|
+
const out = call.string("out");
|
|
243
|
+
const max = call.count("max-listed")!;
|
|
244
|
+
const { program, stages, coverage, found } = programEvidence();
|
|
245
|
+
const loaded = { entry: program.entry, stages };
|
|
246
|
+
const show = (range: AddressRange) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
379
247
|
const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
|
|
380
248
|
const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
|
|
381
249
|
const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
|
|
382
|
-
const
|
|
383
|
-
|
|
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);
|
|
384
253
|
const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
|
|
385
254
|
const lastStage = loaded.stages === undefined ? undefined : loaded.stages.length - 1;
|
|
386
255
|
return {
|
|
387
256
|
...(loaded.entry === undefined ? {} : { entry: formatC64Address(loaded.entry) }),
|
|
388
257
|
...(lastStage === undefined
|
|
389
|
-
? { noStages:
|
|
258
|
+
? { noStages: NO_STAGES }
|
|
390
259
|
: {
|
|
391
260
|
stages: lastStage + 1,
|
|
392
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)]]),
|
|
393
262
|
}),
|
|
394
263
|
...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
|
|
395
|
-
ranges:
|
|
264
|
+
ranges: list(ranges),
|
|
396
265
|
// total counts every code region; unreachable is a list like the others.
|
|
397
266
|
codeRegions: {
|
|
398
267
|
total: codeRegions.length,
|
|
399
|
-
...(loaded.entry === undefined ? {} : { unreachable:
|
|
268
|
+
...(loaded.entry === undefined ? {} : { unreachable: list(codeRegions.filter((region) => region.reachable === false)) }),
|
|
400
269
|
},
|
|
401
|
-
...(coverage === undefined ? {} : { outside: { read:
|
|
402
|
-
...(
|
|
270
|
+
...(coverage === undefined ? {} : { outside: { read: list(outside.read), executed: list(outside.executed), generated: list(outside.generated) } }),
|
|
271
|
+
...(written === undefined ? {} : written),
|
|
403
272
|
};
|
|
404
273
|
}
|
|
405
274
|
|
|
406
|
-
|
|
275
|
+
/** The longest text that the result shows; --out has all of it. */
|
|
276
|
+
const SHOWN_TEXT = 120;
|
|
277
|
+
|
|
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. */
|
|
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")!;
|
|
283
|
+
const { program, stages, coverage, found } = programEvidence();
|
|
284
|
+
const leftovers = findLeftovers({
|
|
285
|
+
segments: program.segments,
|
|
286
|
+
memory: program.memory,
|
|
287
|
+
separation: found,
|
|
288
|
+
...(program.segments.some((segment) => segment.start === 0x0801) ? { basicStart: 0x0801 } : {}),
|
|
289
|
+
minText: min,
|
|
290
|
+
});
|
|
291
|
+
const show = <T extends AddressRange & { stages?: number[]; access?: string[] }>({ start, end, stages: wrote, access, ...rest }: T) => ({
|
|
292
|
+
start: formatC64Address(start),
|
|
293
|
+
end: formatC64Address(end),
|
|
294
|
+
...rest,
|
|
295
|
+
...(wrote === undefined ? {} : { stages: wrote }),
|
|
296
|
+
...(access === undefined ? {} : { access }),
|
|
297
|
+
});
|
|
298
|
+
const all = {
|
|
299
|
+
text: leftovers.text.map(show),
|
|
300
|
+
basic: leftovers.basic.map(({ linkedAt, ...rest }) => ({ ...show(rest), linkedAt: formatC64Address(linkedAt) })),
|
|
301
|
+
fill: leftovers.fill.map(({ pattern, ...rest }) => ({ ...show(rest), pattern: hexBytes(pattern) })),
|
|
302
|
+
residue: leftovers.residue.map((range) => ({ ...show(range), bytes: hexBytes([...program.memory.subarray(range.start, Math.min(range.end, range.start + 15) + 1)]) + (range.end - range.start >= 16 ? " ..." : "") })),
|
|
303
|
+
copies: leftovers.copies.map(({ copyOf, ...rest }) => ({ ...show(rest), copyOf: formatC64Address(copyOf) })),
|
|
304
|
+
deadCode: leftovers.deadCode.map(show),
|
|
305
|
+
};
|
|
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>");
|
|
308
|
+
return {
|
|
309
|
+
...(program.entry === undefined ? { noEntry: "The program in the knowledge has no entry, so deadCode is empty." } : { entry: formatC64Address(program.entry) }),
|
|
310
|
+
...(stages === undefined ? { noStages: NO_STAGES } : { stages: stages.length }),
|
|
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." } : {}),
|
|
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),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
407
321
|
|
|
408
322
|
const platform = () => new Map(platformNames().map((entry) => [entry.name, entry.address]));
|
|
409
323
|
|
|
410
324
|
/** routine: saves a module or a range of the program in the knowledge, with its knowledge, as a routine of the library (D71). */
|
|
411
|
-
function routineCommand(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (values.module === undefined && values.start === undefined && values.end === undefined) throw new UsageError("routine needs --module <address>, or --start and --end");
|
|
415
|
-
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")!;
|
|
416
328
|
const db = openForRead();
|
|
417
329
|
try {
|
|
418
330
|
const image = requireProgram(db);
|
|
419
331
|
const segmentOf = (at: number) => image.segments.find((segment) => at >= segment.start && at <= segment.end);
|
|
420
332
|
let start: number;
|
|
421
333
|
let end: number;
|
|
422
|
-
let title =
|
|
423
|
-
if (
|
|
424
|
-
start =
|
|
334
|
+
let title = call.string("title");
|
|
335
|
+
if (!given.includes("-")) {
|
|
336
|
+
start = call.parseAddress(given, "<module>");
|
|
425
337
|
const modules = listModules(db);
|
|
426
338
|
const module = modules.find((item) => item.start === start);
|
|
427
|
-
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>.`);
|
|
428
340
|
const segment = segmentOf(start);
|
|
429
341
|
if (segment === undefined) throw new UsageError(`The module at ${formatC64Address(start)} is outside the segments of the program.`);
|
|
430
342
|
end = (modules.find((item) => item.start > start && item.start <= segment.end)?.start ?? segment.end + 1) - 1;
|
|
431
343
|
title ??= module.title;
|
|
432
344
|
} else {
|
|
433
|
-
start =
|
|
434
|
-
end = address(values.end!, "--end");
|
|
435
|
-
if (end < start) throw new UsageError("--end is before --start");
|
|
345
|
+
({ start, end } = call.parseRange(given, "<range>"));
|
|
436
346
|
if (segmentOf(start) === undefined || segmentOf(start) !== segmentOf(end)) throw new UsageError(`${formatC64Address(start)}-${formatC64Address(end)} is not inside one segment of the program.`);
|
|
437
347
|
}
|
|
438
348
|
const range = { start, end };
|
|
439
|
-
const name =
|
|
349
|
+
const name = call.string("name") ?? listSymbols(db, { start, end: start }, undefined, SEMANTIC_ORIGINS)[0]?.name;
|
|
440
350
|
if (name === undefined) throw new UsageError(`The knowledge has no user or LLM name at ${formatC64Address(start)}: give --name.`);
|
|
441
351
|
const { routine, leftOut } = routineFrom({
|
|
442
352
|
name,
|
|
@@ -447,17 +357,17 @@ function routineCommand(values: { library?: string; module?: string; start?: str
|
|
|
447
357
|
end,
|
|
448
358
|
byteAt: image.byteAt,
|
|
449
359
|
analyzed: listRegions(db, range),
|
|
450
|
-
symbols: listSymbols(db, range, undefined,
|
|
451
|
-
regions: listRegions(db, range, undefined,
|
|
452
|
-
comments: listComments(db, range,
|
|
453
|
-
references: listReferences(db, range, undefined,
|
|
454
|
-
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),
|
|
455
365
|
platform: platform(),
|
|
456
366
|
});
|
|
457
|
-
const file = saveRoutine(libraryFolder(
|
|
367
|
+
const file = saveRoutine(libraryFolder(library, { create: true, cwd: projectRoot() }), routine, call.flag("replace"));
|
|
458
368
|
return {
|
|
459
369
|
routine: routine.name,
|
|
460
|
-
file: relative(
|
|
370
|
+
file: relative(projectRoot(), file).split("\\").join("/"),
|
|
461
371
|
start: formatC64Address(start),
|
|
462
372
|
end: formatC64Address(end),
|
|
463
373
|
length: routine.bytes.length,
|
|
@@ -473,13 +383,13 @@ function routineCommand(values: { library?: string; module?: string; start?: str
|
|
|
473
383
|
}
|
|
474
384
|
|
|
475
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). */
|
|
476
|
-
function matchCommand(
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
const routines = readLibrary(libraryFolder(library, { create: false }));
|
|
482
|
-
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.`);
|
|
483
393
|
const programDb = openForRead();
|
|
484
394
|
let image: ReturnType<typeof requireProgram>;
|
|
485
395
|
try {
|
|
@@ -500,7 +410,7 @@ function matchCommand(library: string | undefined, out: string | undefined, maxL
|
|
|
500
410
|
})),
|
|
501
411
|
)
|
|
502
412
|
.sort((a, b) => a.start.localeCompare(b.start));
|
|
503
|
-
const result: Record<string, unknown> = { library, routines: routines.length, matches:
|
|
413
|
+
const result: Record<string, unknown> = { library, routines: routines.length, matches: list(matches) };
|
|
504
414
|
if (out === undefined) return result;
|
|
505
415
|
const db = openForRead();
|
|
506
416
|
let suggestions: ReturnType<typeof suggest>;
|
|
@@ -525,12 +435,72 @@ function matchCommand(library: string | undefined, out: string | undefined, maxL
|
|
|
525
435
|
db?.close();
|
|
526
436
|
}
|
|
527
437
|
const { batch, known, leftOut } = suggestions;
|
|
528
|
-
|
|
438
|
+
const written = writeReported(out, batch.length === 0 ? "[]\n" : `[\n${batch.map((write) => ` ${JSON.stringify(write)}`).join(",\n")}\n]\n`, { what: "--out" });
|
|
529
439
|
return {
|
|
530
440
|
...result,
|
|
531
|
-
batch: {
|
|
441
|
+
batch: { ...written, writes: batch.length, known, leftOut: list(leftOut) },
|
|
532
442
|
...(batch.length === 0 ? {} : { apply: `knowledge.ts apply ${out} --reason "names of library routines"` }),
|
|
533
443
|
};
|
|
534
444
|
}
|
|
535
445
|
|
|
536
|
-
|
|
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
|
+
});
|