@henols/c64-re-tools 2.1.0-rc.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/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
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// The c64-docs script. generate writes the reference pages of a program from
|
|
2
|
+
// the knowledge, the source map of c64-source and the program in the
|
|
3
|
+
// knowledge; the pages are generated and never edited. link links the name
|
|
4
|
+
// mentions of the documentation to the source and the reference, and checks
|
|
5
|
+
// every link and image of the pages.
|
|
6
|
+
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
8
|
+
import { join, posix } from "node:path";
|
|
9
|
+
|
|
10
|
+
import { Refusal } from "#src/errors.ts";
|
|
11
|
+
import { WireFailure } from "#src/host-client/tools.ts";
|
|
12
|
+
import { writeProjectFile, writeReported } from "#src/host-client/transfer.ts";
|
|
13
|
+
import { conflicts } from "#src/knowledge/conflicts.ts";
|
|
14
|
+
import { openForRead } from "#src/knowledge/database.ts";
|
|
15
|
+
import { programImage, type ProgramImage } from "#src/knowledge/program.ts";
|
|
16
|
+
import { currentRevision, listComments, listModules, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
|
|
17
|
+
import { resolveProjectFile, resolveProjectPath } from "#src/project.ts";
|
|
18
|
+
import { listed, OPTIONS, runScript, UsageError, type Call } from "#src/script.ts";
|
|
19
|
+
import { checkLinks, linkPage, type Files, type Problem } from "./lib/links.ts";
|
|
20
|
+
import { buildReference, GENERATED_MARK } from "./lib/reference.ts";
|
|
21
|
+
import { readSourceMap } from "./lib/source-map.ts";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// generate
|
|
25
|
+
|
|
26
|
+
/** The files of an earlier run in `directory`, the real folder of --out, or a refusal that names a file of a person. */
|
|
27
|
+
function oldFiles(out: string, directory: string): string[] {
|
|
28
|
+
if (!existsSync(directory)) return [];
|
|
29
|
+
const found: string[] = [];
|
|
30
|
+
const walk = (relative: string) => {
|
|
31
|
+
for (const entry of readdirSync(join(directory, relative), { withFileTypes: true })) {
|
|
32
|
+
const path = posix.join(relative, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
walk(path);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const generated = path.endsWith(".md") ? readFileSync(join(directory, path), "utf8").startsWith(GENERATED_MARK) : path.startsWith("images/") && path.endsWith(".png");
|
|
38
|
+
if (!generated) throw new WireFailure("invalid-input", `${posix.join(out, path)} is not a page of docs.ts generate. Nothing was written. Give --out a directory that holds only the reference, and keep the pages of a person outside it.`);
|
|
39
|
+
found.push(path);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
walk("");
|
|
43
|
+
return found;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function generate(call: Call): Record<string, unknown> {
|
|
47
|
+
const source = call.subject(0);
|
|
48
|
+
const out = call.string("out")!;
|
|
49
|
+
// The files of an earlier run are deleted: only in a folder that is inside the project, also after its symbolic links.
|
|
50
|
+
let directory = resolveProjectPath(out);
|
|
51
|
+
if (existsSync(directory)) {
|
|
52
|
+
try {
|
|
53
|
+
directory = resolveProjectFile(out);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, also through symbolic links, not ${out}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const map = readSourceMap(source);
|
|
59
|
+
const db = openForRead();
|
|
60
|
+
let knowledge;
|
|
61
|
+
let image: ProgramImage | undefined;
|
|
62
|
+
try {
|
|
63
|
+
image = programImage(db, map.space);
|
|
64
|
+
const found = conflicts(db).length;
|
|
65
|
+
if (found > 0) throw new Refusal("conflict", `The knowledge has ${found} conflict${found === 1 ? "" : "s"} from a git merge. Nothing was written. Run c64-knowledge's knowledge.ts merge, then generate the source and the reference again.`);
|
|
66
|
+
const revision = currentRevision(db);
|
|
67
|
+
// A source of another revision has other lines: the links would point to the wrong code.
|
|
68
|
+
if (map.revision !== revision) {
|
|
69
|
+
throw new WireFailure("invalid-input", `${map.pathOf("source-map.json")} is from knowledge revision ${map.revision}, and the knowledge is at revision ${revision}. Nothing was written. Generate the source again with c64-source, then run docs.ts generate.`);
|
|
70
|
+
}
|
|
71
|
+
knowledge = {
|
|
72
|
+
revision,
|
|
73
|
+
symbols: listSymbols(db, undefined, undefined, undefined, map.space),
|
|
74
|
+
regions: listRegions(db, undefined, undefined, undefined, map.space),
|
|
75
|
+
comments: listComments(db, undefined, undefined, map.space),
|
|
76
|
+
references: listReferences(db, undefined, undefined, undefined, map.space),
|
|
77
|
+
modules: listModules(db, undefined, undefined, map.space),
|
|
78
|
+
};
|
|
79
|
+
} finally {
|
|
80
|
+
db?.close();
|
|
81
|
+
}
|
|
82
|
+
const old = oldFiles(out, directory);
|
|
83
|
+
const built = buildReference({
|
|
84
|
+
out: posix.normalize(out),
|
|
85
|
+
map,
|
|
86
|
+
...knowledge,
|
|
87
|
+
...(image === undefined ? {} : { image: { byteAt: image.byteAt, inImage: (address: number) => image.segments.some((segment) => address >= segment.start && address <= segment.end) } }),
|
|
88
|
+
});
|
|
89
|
+
for (const path of old) rmSync(join(directory, path), { force: true });
|
|
90
|
+
const written = built.files.map((file) => writeReported(file.path, file.bytes, { what: "--out" }));
|
|
91
|
+
return { out, revision: knowledge.revision, index: posix.join(posix.normalize(out), "index.md"), ...built.counts, files: listed(written, call.count("max-listed")!), program: image === undefined ? "none: put the program into the knowledge with knowledge.ts image to decode the tables and show the graphics" : "the knowledge" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// link
|
|
96
|
+
|
|
97
|
+
/** A name outside the image covers the addresses up to the next name, at most this many. */
|
|
98
|
+
const MAX_BODY = 256;
|
|
99
|
+
|
|
100
|
+
const isDirectory = (path: string) => {
|
|
101
|
+
const real = resolveProjectPath(path);
|
|
102
|
+
return existsSync(real) && statSync(real).isDirectory();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function markdownFiles(directory: string): string[] {
|
|
106
|
+
if (!isDirectory(directory)) throw new WireFailure("not-found", `There is no directory ${directory} in the project folder.`);
|
|
107
|
+
let root: string;
|
|
108
|
+
try {
|
|
109
|
+
root = resolveProjectFile(directory);
|
|
110
|
+
} catch {
|
|
111
|
+
throw new WireFailure("invalid-input", `${directory} leads out of the project folder through a symbolic link.`);
|
|
112
|
+
}
|
|
113
|
+
const found: string[] = [];
|
|
114
|
+
const walk = (relative: string) => {
|
|
115
|
+
for (const entry of readdirSync(join(root, relative), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
116
|
+
const path = posix.join(relative, entry.name);
|
|
117
|
+
if (entry.isDirectory()) walk(path);
|
|
118
|
+
else if (entry.name.endsWith(".md")) found.push(posix.join(directory, path));
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
walk("");
|
|
122
|
+
return found;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The text of each file that the run read: a page, a source file or a reference page is read once. */
|
|
126
|
+
const texts = new Map<string, string | undefined>();
|
|
127
|
+
|
|
128
|
+
const files: Files = {
|
|
129
|
+
inside: (path) => {
|
|
130
|
+
try {
|
|
131
|
+
resolveProjectPath(path);
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
exists: (path) => {
|
|
138
|
+
try {
|
|
139
|
+
resolveProjectFile(path);
|
|
140
|
+
return true;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
text: (path) => {
|
|
146
|
+
if (texts.has(path)) return texts.get(path);
|
|
147
|
+
let text: string | undefined;
|
|
148
|
+
try {
|
|
149
|
+
const real = resolveProjectFile(path);
|
|
150
|
+
text = statSync(real).isFile() ? readFileSync(real, "utf8") : undefined;
|
|
151
|
+
} catch {
|
|
152
|
+
text = undefined;
|
|
153
|
+
}
|
|
154
|
+
texts.set(path, text);
|
|
155
|
+
return text;
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** A .md entry of a directory that is a symbolic link to no file, or to a file outside the project. */
|
|
160
|
+
const unreadable = (page: string): Problem => ({ page, line: 1, kind: "missing-file", message: `${page} is a symbolic link to no file or to a file outside the project folder.` });
|
|
161
|
+
|
|
162
|
+
function link(call: Call): Record<string, unknown> {
|
|
163
|
+
const check = call.flag("check");
|
|
164
|
+
const given = [...new Set(call.subjects.map((path) => posix.normalize(path)))];
|
|
165
|
+
for (const path of given) if (!files.inside(path)) throw new UsageError(`<page> must be a path relative to the project folder that stays inside it, not ${path}`);
|
|
166
|
+
for (const path of given) {
|
|
167
|
+
if (!path.endsWith(".md") && files.exists(path) && !isDirectory(path)) throw new UsageError(`<pages> takes .md files and directories, not ${path}`);
|
|
168
|
+
}
|
|
169
|
+
const directories = given.filter((path) => !path.endsWith(".md") || isDirectory(path));
|
|
170
|
+
const map = readSourceMap(call.string("source")!);
|
|
171
|
+
const givenReference = call.string("reference");
|
|
172
|
+
const reference = givenReference === undefined ? directories.map((path) => posix.join(path, "reference")).find((path) => isDirectory(path)) : posix.normalize(givenReference);
|
|
173
|
+
const problems: Problem[] = [];
|
|
174
|
+
const db = openForRead();
|
|
175
|
+
let symbols;
|
|
176
|
+
let revision;
|
|
177
|
+
try {
|
|
178
|
+
revision = currentRevision(db);
|
|
179
|
+
symbols = listSymbols(db, undefined, undefined, undefined, map.space);
|
|
180
|
+
} finally {
|
|
181
|
+
db?.close();
|
|
182
|
+
}
|
|
183
|
+
if (map.revision !== revision) {
|
|
184
|
+
problems.push({ page: map.pathOf("source-map.json"), line: 1, kind: "stale-reference", message: `The source is from knowledge revision ${map.revision}, and the knowledge is at ${revision}. Generate the source and the reference again, then run docs.ts link.` });
|
|
185
|
+
}
|
|
186
|
+
const addresses = new Map<string, number>();
|
|
187
|
+
for (const label of map.labels) addresses.set(label.name, label.address);
|
|
188
|
+
for (const symbol of symbols) addresses.set(symbol.name, symbol.address);
|
|
189
|
+
// A routine runs to the next routine; any other name to the next name of the knowledge, in its file, and outside the image at most MAX_BODY bytes.
|
|
190
|
+
const routines = symbols.filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address).sort((a, b) => a - b);
|
|
191
|
+
const named = [...new Set(symbols.map((symbol) => symbol.address))].sort((a, b) => a - b);
|
|
192
|
+
const kinds = new Map(symbols.map((symbol) => [symbol.name, symbol.kind]));
|
|
193
|
+
const bodyEnds = new Map<string, number | undefined>();
|
|
194
|
+
const bodyEnd = (name: string) => {
|
|
195
|
+
if (bodyEnds.has(name)) return bodyEnds.get(name);
|
|
196
|
+
const address = addresses.get(name);
|
|
197
|
+
let end: number | undefined;
|
|
198
|
+
if (address !== undefined) {
|
|
199
|
+
const next = ((kinds.get(name) === "routine" ? routines : named).find((other) => other > address) ?? 0x10000) - 1;
|
|
200
|
+
const file = map.lineOf(address)?.file;
|
|
201
|
+
if (file === undefined) end = Math.min(next, address + MAX_BODY - 1);
|
|
202
|
+
else {
|
|
203
|
+
end = address;
|
|
204
|
+
while (end < next && map.lineOf(end + 1)?.file === file) end++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
bodyEnds.set(name, end);
|
|
208
|
+
return end;
|
|
209
|
+
};
|
|
210
|
+
const nameAt = new Map(symbols.map((symbol) => [symbol.address, symbol.name]));
|
|
211
|
+
/** The routine, else the name, whose body holds an address. */
|
|
212
|
+
const placeOf = (address: number) => {
|
|
213
|
+
for (const list of [routines, named]) {
|
|
214
|
+
let start: number | undefined;
|
|
215
|
+
for (const other of list) if (other <= address) start = other;
|
|
216
|
+
const name = start === undefined ? undefined : nameAt.get(start);
|
|
217
|
+
if (name !== undefined && (bodyEnd(name) ?? start!) >= address) return name;
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// The entries of the reference: each <a id> that is a name.
|
|
223
|
+
const entries = new Map<string, string>();
|
|
224
|
+
const referencePages = reference === undefined ? [] : markdownFiles(reference);
|
|
225
|
+
for (const page of referencePages) {
|
|
226
|
+
const text = files.text(page);
|
|
227
|
+
if (text === undefined) {
|
|
228
|
+
problems.push(unreadable(page));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const stamp = /^<!-- Generated by c64-docs from knowledge revision (\S+) /.exec(text);
|
|
232
|
+
if (text.startsWith(GENERATED_MARK) && stamp !== null && stamp[1] !== map.revision) {
|
|
233
|
+
problems.push({ page, line: 1, kind: "stale-reference", message: `${page} is from knowledge revision ${stamp[1]}, and the source from ${map.revision}. Run docs.ts generate again.` });
|
|
234
|
+
}
|
|
235
|
+
for (const match of text.matchAll(/<a id="([^"]+)"><\/a>/g)) if (addresses.has(match[1]!) && !entries.has(match[1]!)) entries.set(match[1]!, `${page}#${match[1]}`);
|
|
236
|
+
}
|
|
237
|
+
const inside = (path: string, directory: string | undefined) => directory !== undefined && (path === directory || path.startsWith(`${directory}/`));
|
|
238
|
+
const context = {
|
|
239
|
+
addressOf: (name: string) => addresses.get(name),
|
|
240
|
+
codeOf: (name: string) => {
|
|
241
|
+
const label = map.labelOf(name);
|
|
242
|
+
return label === undefined ? undefined : `${map.pathOf(label.file)}#L${label.line}`;
|
|
243
|
+
},
|
|
244
|
+
entryOf: (name: string) => entries.get(name),
|
|
245
|
+
bodyEnd,
|
|
246
|
+
placeOf,
|
|
247
|
+
lineOf: (address: number) => {
|
|
248
|
+
const line = map.lineOf(address);
|
|
249
|
+
return line === undefined ? undefined : `${map.pathOf(line.file)}#L${line.line}`;
|
|
250
|
+
},
|
|
251
|
+
managed: (path: string) => inside(path, posix.normalize(map.directory)) || inside(path, reference),
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const pages = [...new Set(given.flatMap((path) => {
|
|
255
|
+
if (directories.includes(path)) return markdownFiles(path);
|
|
256
|
+
if (files.text(path) === undefined) throw new WireFailure("not-found", `There is no file ${path} in the project folder.`);
|
|
257
|
+
return [path];
|
|
258
|
+
}))];
|
|
259
|
+
const changedPages: string[] = [];
|
|
260
|
+
let links = 0;
|
|
261
|
+
for (const page of pages) {
|
|
262
|
+
const text = files.text(page);
|
|
263
|
+
if (text === undefined) {
|
|
264
|
+
problems.push(unreadable(page));
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
// A generated page is right by its making: it is only checked.
|
|
268
|
+
const generated = text.startsWith(GENERATED_MARK);
|
|
269
|
+
const linked = generated ? { text, changed: 0, problems: [] } : linkPage(text, { page, ...context });
|
|
270
|
+
problems.push(...linked.problems, ...checkLinks(page, linked.text, files));
|
|
271
|
+
if (linked.changed === 0) continue;
|
|
272
|
+
links += linked.changed;
|
|
273
|
+
changedPages.push(page);
|
|
274
|
+
if (check) continue;
|
|
275
|
+
writeProjectFile(page, Buffer.from(linked.text, "utf8"));
|
|
276
|
+
texts.set(page, linked.text);
|
|
277
|
+
}
|
|
278
|
+
const byKind: Record<string, number> = {};
|
|
279
|
+
for (const problem of problems) byKind[problem.kind] = (byKind[problem.kind] ?? 0) + 1;
|
|
280
|
+
const output: Record<string, unknown> = {
|
|
281
|
+
pages: pages.length,
|
|
282
|
+
...(reference === undefined ? { reference: "none: run docs.ts generate for links to the entries" } : { reference }),
|
|
283
|
+
...(check ? { notUpToDate: { links, pages: changedPages } } : { linked: { links, pages: changedPages } }),
|
|
284
|
+
problems: { byKind, ...listed(problems, call.count("max-listed")!, (problem) => ({ at: `${problem.page}:${problem.line}`, kind: problem.kind, message: problem.message })) },
|
|
285
|
+
};
|
|
286
|
+
return { result: problems.length > 0 || (check && links > 0) ? "FAIL" : "PASS", ...output };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const SOURCE = { kind: "path", value: "<dir>", help: "the source folder of c64-source, with its source-map.json, relative to the project folder" } as const;
|
|
290
|
+
|
|
291
|
+
await runScript({
|
|
292
|
+
name: "docs",
|
|
293
|
+
summary: "Generates the reference pages of a program, and links the name mentions of the documentation to the source and the reference.",
|
|
294
|
+
commands: [
|
|
295
|
+
{
|
|
296
|
+
name: "generate",
|
|
297
|
+
summary: "Writes the reference pages: a page for each module, and pages for the names, the variables, the tables, the memory map and the graphics.",
|
|
298
|
+
subjects: [{ name: "<source-dir>", help: SOURCE.help }],
|
|
299
|
+
options: {
|
|
300
|
+
out: { ...OPTIONS.out, value: "<dir>", help: "the folder for the reference pages, relative to the project folder, for example docs/reference. generate writes only there and refuses a page in it that it did not write", required: true },
|
|
301
|
+
"max-listed": OPTIONS["max-listed"],
|
|
302
|
+
},
|
|
303
|
+
notes: "When the knowledge has the program of the source (knowledge.ts image of the c64-knowledge skill), the reference decodes the tables and shows the graphics.",
|
|
304
|
+
run: (call) => {
|
|
305
|
+
if (!isDirectory(call.subject(0))) throw new Refusal("not-found", `There is no folder ${call.subject(0)} in the project folder. Give the source folder of c64-source.`);
|
|
306
|
+
return generate(call);
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "link",
|
|
311
|
+
summary: "Links the name mentions of the pages to the source and the reference, and checks every link and image. result is FAIL when a page has a problem, or with --check when a link is not up to date.",
|
|
312
|
+
subjects: [{ name: "<page>", help: ".md files and folders of the documentation, relative to the project folder, for example README.md docs. link reads each file and every .md file below each folder", list: true }],
|
|
313
|
+
options: {
|
|
314
|
+
source: { ...SOURCE, required: true },
|
|
315
|
+
reference: { kind: "path", value: "<dir>", help: "the reference pages of docs.ts generate. The default is the reference/ folder of the first folder that has one" },
|
|
316
|
+
check: { kind: "flag", help: "write nothing. FAIL when a link is not up to date or a page has a problem" },
|
|
317
|
+
"max-listed": OPTIONS["max-listed"],
|
|
318
|
+
},
|
|
319
|
+
notes: "A name in backticks gets a link to its line in the source. An address in backticks after the name gets a link to the entry of the name in the reference.",
|
|
320
|
+
run: link,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
});
|
|
@@ -38,7 +38,7 @@ export function readSourceMap(directory: string): SourceMap {
|
|
|
38
38
|
text = readProjectFile(path).bytes.toString("utf8");
|
|
39
39
|
} catch (error) {
|
|
40
40
|
if (error instanceof WireFailure && error.code === "not-found") {
|
|
41
|
-
throw new WireFailure("not-found", `There is no ${path}. Generate the source with c64-source
|
|
41
|
+
throw new WireFailure("not-found", `There is no ${path}. Generate the source with c64-source source.ts generate --out ${directory}: it writes the source map.`);
|
|
42
42
|
}
|
|
43
43
|
throw error;
|
|
44
44
|
}
|
|
@@ -17,7 +17,7 @@ This skill does not analyse code statically. It does not write project knowledge
|
|
|
17
17
|
- The target address, state or condition, when the question needs one.
|
|
18
18
|
- The project path of the program or disk to load, when the question needs one.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
An address is `$` and 1 to 4 hex digits, for example `$c000` or `$fb`. A tool takes no names. Get the address of a name with `knowledge.ts at <name>` of the c64-knowledge skill.
|
|
21
21
|
|
|
22
22
|
## Tools / execution path
|
|
23
23
|
|
|
@@ -34,20 +34,20 @@ Use only the MCP tools. Each MCP session has its own C64. Nothing else can chang
|
|
|
34
34
|
| Run to a place and read the machine there in one call | `c64_run_until` with `observe` and `history` |
|
|
35
35
|
| Each write, read or routine entry over many frames, with the instruction and the raster line | `c64_trace` |
|
|
36
36
|
| Stop on an instruction address | `c64_breakpoint` |
|
|
37
|
-
| Stop on a memory read or write | `c64_watchpoint` |
|
|
37
|
+
| Stop on a memory read or write from `start` to `end` | `c64_watchpoint` |
|
|
38
38
|
| Memory | `c64_memory_read`, `c64_memory_write`, `c64_memory_search`, `c64_memory_compare` |
|
|
39
|
-
| CPU registers | `c64_registers` |
|
|
39
|
+
| CPU registers | `c64_registers` (action `read` or `write`) |
|
|
40
40
|
| Code as it is in memory now | `c64_disassemble` |
|
|
41
41
|
| Executed instructions, call chain | `c64_cpu_history`, `c64_backtrace` |
|
|
42
42
|
| Which instruction reads or writes an address | `c64_trace` with a `read` or `write` point |
|
|
43
43
|
| Where the CPU spends cycles, which memory it uses | `c64_profile`, `c64_memmap` |
|
|
44
44
|
| Cycle count between two points | `c64_timing` |
|
|
45
45
|
| Video chip, sprites, CIAs, sound chip | `c64_vicii`, `c64_sprite`, `c64_cia`, `c64_sid` |
|
|
46
|
-
| Screen image, screen compare | `c64_screen` |
|
|
46
|
+
| Screen image, screen compare, remove a baseline | `c64_screen` |
|
|
47
47
|
| Several of these from one moment | `c64_observe` |
|
|
48
48
|
| Keys, joystick | `c64_keyboard`, `c64_joystick` |
|
|
49
49
|
| Program and disk media | `c64_autostart`, `c64_program_load`, `c64_disk_attach` |
|
|
50
|
-
| Machine state to go back to, also in a later session | `c64_snapshot` (with `out` and `file`) |
|
|
50
|
+
| Machine state to go back to, also in a later session, remove a snapshot | `c64_snapshot` (with `out` and `file`) |
|
|
51
51
|
|
|
52
52
|
Each memory and register tool has a `space`. Use `c64` for the computer. Use `drive8` for the CPU of the 1541 disk drive. The memory `view` is `cpu` or `ram`. With `cpu`, you see what the CPU sees now, with ROM and I/O where the memory configuration puts them. With `ram`, you see the RAM under them.
|
|
53
53
|
|
|
@@ -82,10 +82,12 @@ A cartridge from `c64_autostart` stays in the C64 through every reset, as on a r
|
|
|
82
82
|
|
|
83
83
|
Step, next and until-return work only in space `c64`. The emulator cannot stop the disk drive CPU after one instruction. To find when the drive executes an address, use `c64_run_until` with an address target in space `drive8`, or a breakpoint in space `drive8`. The drive can execute a few more instructions before it stops. The computer completes its current instruction. The `pc` in the result is the drive address where it stopped.
|
|
84
84
|
|
|
85
|
-
`c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard
|
|
85
|
+
`c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard matrix directly does not see them. Many games do this. No MCP tool presses a key of the keyboard matrix. For such a program:
|
|
86
86
|
|
|
87
87
|
- VICE 3.10 and older have a known flaw: bits 5-7 of `$DC00` and `$DC01` are always 0. Then `emulatorFlaw` in the result of `c64_keyboard` and `c64_joystick` gives the flaw. With this flaw, a wait for any key that reads all keyboard columns at one time ends without a key. A wait for one specific key, or a compare of the full byte of a joystick port, never ends. Tell the user about the flaw. The next VICE release and the VICE nightly builds do not have it. Joystick port 1 uses the same lines as the keyboard.
|
|
88
|
-
- For one specific key, for example F1 or RUN/STOP,
|
|
88
|
+
- For one specific key, for example F1 or RUN/STOP, use one of these two ways:
|
|
89
|
+
- Open the window and ask the user to press the key.
|
|
90
|
+
- Record a session with a `press` step. Use `testing.ts record` of the c64-testing skill.
|
|
89
91
|
- Do not change the program to make it accept a key.
|
|
90
92
|
|
|
91
93
|
Before a risky experiment, save the state with `c64_snapshot`. Restore it to try again from the same point.
|
|
@@ -126,6 +128,8 @@ If you cannot get the machine into the necessary state, say which state you coul
|
|
|
126
128
|
|
|
127
129
|
## Failure and conflicts
|
|
128
130
|
|
|
131
|
+
An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
132
|
+
|
|
129
133
|
- `machine-state-lost` and the message says that the VICE window closed: the window closed before `c64_window` action `close`. The emulator has no window again. The machine state is the same as at the time when the window opened. The breakpoints and the watchpoints stay. Do not use results from the window time. Read the machine state again, then continue.
|
|
130
134
|
- `machine-unavailable` and the message says that the VICE window closed: the session starts the emulator again. Wait a few seconds. Then do the operation again.
|
|
131
135
|
- `machine-state-lost` with a different message: the connection to the host runtime was lost, and the machine state is gone. Do not continue with results from before. The next call starts a new machine when the host runtime runs. Load the program again, and set the breakpoints and the watchpoints again. If the next call gives `machine-unavailable` with a message that the host runtime does not run, obey the rule for that message.
|
|
@@ -24,43 +24,41 @@ One of these:
|
|
|
24
24
|
## Tools / execution path
|
|
25
25
|
|
|
26
26
|
```
|
|
27
|
-
node <skill>/scripts/knowledge.ts <command> [
|
|
27
|
+
node <skill>/scripts/knowledge.ts <command> [subjects] [options]
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
The script prints one JSON object. An error prints `{"error": {"code": ..., "message": ...}}`. Addresses are `$` and four hex digits. Quote them in a shell, for example `'$2100'`. A name also gives an address, for example `at update_player`. The script tries the symbols first and then the analyzer names.
|
|
31
|
-
|
|
32
30
|
A symbol is the name of an address from the user or the LLM. An analyzer name is a second name from DXA, Ghidra or an idiom, for example `FUN_17e2`. Each analyzer keeps its own name at an address. `at`, `search` and `export` show the analyzer names. The generated source uses only the symbols, unless c64-source gets `--analyzer-names`. To use an analyzer name in the source, `rename` the address with it.
|
|
33
31
|
|
|
34
32
|
| Need | Command |
|
|
35
33
|
|---|---|
|
|
36
34
|
| Everything known at one address | `at '$2100'` |
|
|
37
35
|
| Find names and comment text | `search player` |
|
|
38
|
-
| Lists in a range | `symbols`, `regions`, `comments`, `references` with `--
|
|
36
|
+
| Lists in a range | `symbols`, `regions`, `comments`, `references` with `--range '$2000-$2fff'` |
|
|
39
37
|
| The names that DXA, Ghidra and the idioms gave | `symbols --analyzer` |
|
|
40
38
|
| Only some origins | add `--origin semantic`, `--origin analyzer` (dxa, ghidra and heuristic), or `--origin user,llm` |
|
|
41
39
|
| All knowledge in one file | `export --out .c64-re-tools/local/knowledge.json` |
|
|
42
40
|
| Put the program into the knowledge | `image .c64-re-tools/local/capture/image.json` or `image original/game.prg` |
|
|
43
41
|
| Add the coverage of runs | `coverage .c64-re-tools/local/runs/rooms/room0.bin ...` |
|
|
44
42
|
| What the knowledge holds of the program | `program` |
|
|
45
|
-
| The code of a range with names, comments, access and references | `listing '$0400
|
|
43
|
+
| The code of a range with names, comments, access and references | `listing '$0400-$04ff'` |
|
|
46
44
|
| Name the symbol at an address | `rename '$2100' update_player --kind routine` |
|
|
47
45
|
| Remove a symbol | `remove-symbol '$2100'` |
|
|
48
|
-
| Classify memory | `classify '$3000
|
|
49
|
-
| Classify a table of records | `classify '$3000
|
|
46
|
+
| Classify memory | `classify '$3000-$303f' sprite` |
|
|
47
|
+
| Classify a table of records | `classify '$3000-$303b' bytes --element 6` |
|
|
50
48
|
| Many writes as one revision | `apply -` with the JSON on standard input, or `apply <file>` |
|
|
51
49
|
| One batch from the batches of parallel agents | `join a.json b.json --out .c64-re-tools/local/joined.json` |
|
|
52
|
-
| Make memory unknown, and keep the analyzers out of it | `unclassify '$3000
|
|
50
|
+
| Make memory unknown, and keep the analyzers out of it | `unclassify '$3000-$303f'` |
|
|
53
51
|
| Set or remove a comment | `comment '$2100' line Reads joystick 2.` / `uncomment '$2100' line` |
|
|
54
52
|
| Add or remove a reference | `reference '$2000' '$2100' call` / `unreference ...` |
|
|
55
|
-
| Places that a removal blocks | `suppressions` with `--
|
|
53
|
+
| Places that a removal blocks | `suppressions` with `--range` |
|
|
56
54
|
| Start a file of generated source | `module '$2000' player.asm --title 'Player movement'` / `unmodule '$2000'` |
|
|
57
55
|
| Write the operand of an instruction as an expression | `operand '$8079' --lo irq_handler` / `unoperand '$8079'` |
|
|
58
|
-
| Operand expressions | `operands` with `--
|
|
59
|
-
| Replace a range with code when a switch is on | `patch '$100e
|
|
56
|
+
| Operand expressions | `operands` with `--range` |
|
|
57
|
+
| Replace a range with code when a switch is on | `patch '$100e-$1010' CRACK --code-file crack.asm` / `unpatch '$100e'` |
|
|
60
58
|
| Patches and switches | `patches` with `--switch`, and `switches` |
|
|
61
59
|
| Write a macro | `macro inc16 --params dst --body-file inc16.asm` / `unmacro inc16` |
|
|
62
|
-
| Write instructions as one use of a macro | `use '$1005
|
|
63
|
-
| Macros and their uses | `macros`, and `uses` with `--
|
|
60
|
+
| Write instructions as one use of a macro | `use '$1005-$100a' inc16 --args ptr` / `unuse '$1005'` |
|
|
61
|
+
| Macros and their uses | `macros`, and `uses` with `--range` and `--macro` |
|
|
64
62
|
| Files of generated source | `modules` |
|
|
65
63
|
| The address spaces that hold knowledge | `spaces` |
|
|
66
64
|
| History | `history '$2100'`, `history --name update_player`, `history --macro inc16`, `revisions`, `revision 20261015T2145` |
|
|
@@ -89,12 +87,24 @@ Region types:
|
|
|
89
87
|
|
|
90
88
|
`--element` gives the bytes of one element of a table. The length of the region must be a multiple of it. The result shows `element` and `count`.
|
|
91
89
|
|
|
90
|
+
### Calling the script
|
|
91
|
+
|
|
92
|
+
- Call a script as `node <skill>/scripts/<script>.ts <command> [subjects] [options]`. The command comes first.
|
|
93
|
+
- `<script>.ts --help` lists the commands. `<script>.ts <command> --help` gives the subjects and the options of one command. The help is JSON: `{"text": "..."}`.
|
|
94
|
+
- An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
|
|
95
|
+
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
96
|
+
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
97
|
+
- The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
|
|
98
|
+
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
99
|
+
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
100
|
+
|
|
92
101
|
### Project folder
|
|
93
102
|
|
|
94
103
|
- Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
|
|
95
104
|
- If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
|
|
96
105
|
- Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
|
|
97
|
-
- Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence
|
|
106
|
+
- Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence test:<file>` or `--evidence file:<file>` of c64-knowledge. These two refuse a file in `.c64-re-tools/local/`.
|
|
107
|
+
- Do not move a coverage map after `coverage` of c64-knowledge imported it. `--evidence coverage:<map>` names the map by its path at the import. The knowledge keeps the coverage and the SHA-256 of the map, so the evidence stays valid.
|
|
98
108
|
- Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
|
|
99
109
|
- A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
|
|
100
110
|
|
|
@@ -109,7 +119,10 @@ Write:
|
|
|
109
119
|
|
|
110
120
|
1. Read the current value with `at`. Note the `revision` in the result.
|
|
111
121
|
2. Decide the change. Keep observed facts apart from your conclusions.
|
|
112
|
-
3. Write it with `--reason` and `--expect-revision` set to the revision you read. Add `--evidence` for each thing that the change rests on: `coverage:<map>`, `test:<file>`, `file:<file>` or `note:<text>`.
|
|
122
|
+
3. Write it with `--reason` and `--expect-revision` set to the revision you read. Add `--evidence` for each thing that the change rests on: `coverage:<map>`, `test:<file>`, `file:<file>` or `note:<text>`.
|
|
123
|
+
- `coverage:<map>` names a map by its path at the import. The script uses the last import of that map into the space of the write.
|
|
124
|
+
- `test:<file>` and `file:<file>` name a file outside `.c64-re-tools/local/`. Move the file before the write.
|
|
125
|
+
- The script keeps the SHA-256 of each file and map.
|
|
113
126
|
4. Read the result. A `revision` of `null` means that nothing changed.
|
|
114
127
|
|
|
115
128
|
Each write is one revision. A classification inside a region splits that region, so regions never overlap.
|
|
@@ -170,7 +183,7 @@ The c64-provenance skill writes a batch with `match --out`. The batch holds the
|
|
|
170
183
|
|
|
171
184
|
## Batches of parallel agents
|
|
172
185
|
|
|
173
|
-
When more agents name the program at the same time, give each agent one range of addresses. Each agent writes one batch file and does not apply it. A batch of an agent has the form `{"owner": "$1000
|
|
186
|
+
When more agents name the program at the same time, give each agent one range of addresses. Each agent writes one batch file and does not apply it. A batch of an agent has the form `{"owner": {"start": "$1000", "end": "$1fff"}, "ops": [...]}`. `ops` holds the writes, as for `apply`. The owner ranges must not overlap.
|
|
174
187
|
|
|
175
188
|
`join` makes one batch from the batch files. It writes no knowledge.
|
|
176
189
|
|
|
@@ -179,7 +192,7 @@ When more agents name the program at the same time, give each agent one range of
|
|
|
179
192
|
- The regions of the owner come after the regions of the other batches. Thus the regions of the owner stay where they overlap.
|
|
180
193
|
- A name from a batch stays when another batch removes the symbol at that address.
|
|
181
194
|
- `choices` lists each place where the batches gave different values, with the value that stays.
|
|
182
|
-
- When one name or one module file is at two addresses, `join` writes nothing
|
|
195
|
+
- When one name or one module file is at two addresses, `join` writes nothing and gives the error `conflict`. The message names the addresses and the batches. Correct the batches, then do `join` again.
|
|
183
196
|
|
|
184
197
|
Then apply the joined batch with the command in `apply`.
|
|
185
198
|
|
|
@@ -235,7 +248,7 @@ A macro writes a run of instructions as one line of the source, for example `+in
|
|
|
235
248
|
- The knowledge refuses `unmacro` while the macro has uses. It also refuses a change of the number of params while the macro has uses.
|
|
236
249
|
- A use cannot overlap a patch or another use.
|
|
237
250
|
|
|
238
|
-
The c64-source skill writes a use only when the use gives the bytes of the program. If not, it writes the instructions and gives the reason. The c64-static-analysis skill finds uses of known 6502 idioms and writes them to a batch with `--macro-batch`.
|
|
251
|
+
The c64-source skill writes a use only when the use gives the bytes of the program. If not, it writes the instructions and gives the reason. The c64-static-analysis skill finds uses of known 6502 idioms and writes them to a batch with `--out-macro-batch`.
|
|
239
252
|
|
|
240
253
|
## Removals
|
|
241
254
|
|
|
@@ -260,7 +273,7 @@ Do not store:
|
|
|
260
273
|
- a guess, only because a routine needs a temporary name
|
|
261
274
|
- confidence or status words in comment text.
|
|
262
275
|
|
|
263
|
-
The script records the origin `llm`. Use `--
|
|
276
|
+
The script records the origin `llm`. Use `--by user` only when the user decides the change.
|
|
264
277
|
|
|
265
278
|
## Result
|
|
266
279
|
|
|
@@ -268,13 +281,13 @@ For a read: the current or historical knowledge in compact JSON.
|
|
|
268
281
|
|
|
269
282
|
For a write: the new revision and the current value after the write. If the write replaces a symbol, a region or an operand expression of a different origin, `previous` gives that row as it was before the write. For example, an `llm` write over a `user` name gives `previous`. Read `previous` and tell the user about the change. For `apply`, `replaced` lists each write that replaced a row. Each item has the number of the write, its `op` and its `previous`.
|
|
270
283
|
|
|
271
|
-
|
|
284
|
+
For `export`, `listing --out` and `join`: the written file as `{out, bytes, sha256}`.
|
|
272
285
|
|
|
273
286
|
## Failure and conflicts
|
|
274
287
|
|
|
275
288
|
- `conflict`: the change contradicts current knowledge. For example, the name belongs to another address. Read that address and its history, then decide.
|
|
276
289
|
- `stale-revision`: the knowledge changed after you read it. Read it again, then decide again.
|
|
277
|
-
- `not-found`: there is nothing to remove or no such
|
|
290
|
+
- `not-found`: there is nothing to remove, no such revision or no such file.
|
|
278
291
|
- `conflict` with "from a git merge": git joined the knowledge of two computers, and two rows are current where the knowledge permits one row. For example, two names are at one address. Every read shows these rows in `conflicts`. Writes stop until you run `merge`. The `merge` command keeps the newest row of each conflict and closes the other rows, in one revision. Its result lists each choice. Tell the user the choices.
|
|
279
292
|
- `invalid-database` or `unsupported-schema`: tell the user the message. Do not edit the knowledge files.
|
|
280
293
|
|