@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +19 -59
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +5 -2
  4. package/skills/c64-assembler/scripts/assemble.ts +8 -1
  5. package/skills/c64-assembler/scripts/compare.ts +84 -0
  6. package/skills/c64-basic/SKILL.md +1 -2
  7. package/skills/c64-disk/SKILL.md +20 -6
  8. package/skills/c64-disk/scripts/containers.ts +124 -0
  9. package/skills/c64-disk/scripts/disk.ts +59 -5
  10. package/skills/c64-emulator/SKILL.md +11 -4
  11. package/skills/c64-knowledge/SKILL.md +53 -3
  12. package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
  13. package/skills/c64-memory-map/SKILL.md +4 -1
  14. package/skills/c64-memory-map/scripts/memmap.ts +14 -3
  15. package/skills/c64-provenance/SKILL.md +37 -9
  16. package/skills/c64-provenance/scripts/compare.ts +60 -29
  17. package/skills/c64-provenance/scripts/provenance.ts +248 -19
  18. package/skills/c64-provenance/scripts/runtime.ts +179 -0
  19. package/skills/c64-provenance/scripts/separate.ts +132 -0
  20. package/skills/c64-reverse-engineering/SKILL.md +65 -11
  21. package/skills/c64-source/SKILL.md +119 -0
  22. package/skills/c64-source/scripts/generate.ts +402 -0
  23. package/skills/c64-source/scripts/loader.ts +320 -0
  24. package/skills/c64-source/scripts/opcodes.ts +61 -0
  25. package/skills/c64-source/scripts/source.ts +558 -0
  26. package/skills/c64-static-analysis/SKILL.md +27 -4
  27. package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
  28. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  29. package/skills/c64-static-analysis/scripts/findings.ts +6 -4
  30. package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
  31. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  32. package/skills/c64-testing/SKILL.md +27 -3
  33. package/skills/c64-testing/references/scenario-format.md +43 -3
  34. package/skills/c64-testing/scripts/run.ts +189 -0
  35. package/skills/c64-testing/scripts/scenario.ts +153 -135
  36. package/skills/c64-testing/scripts/test.ts +16 -6
  37. package/skills/c64-unpacker/SKILL.md +65 -14
  38. package/skills/c64-unpacker/scripts/evidence.ts +86 -21
  39. package/skills/c64-unpacker/scripts/loads.ts +174 -0
  40. package/skills/c64-unpacker/scripts/machine.ts +68 -0
  41. package/skills/c64-unpacker/scripts/unpack.ts +302 -48
  42. package/src/c64.ts +91 -0
  43. package/src/cli/status.ts +3 -1
  44. package/src/host/main.ts +10 -25
  45. package/src/host/server.ts +8 -40
  46. package/src/host/vice/adapter.ts +25 -10
  47. package/src/host/vice/session.ts +64 -20
  48. package/src/host-client/autostart.ts +45 -0
  49. package/src/host-client/connect.ts +4 -5
  50. package/src/host-client/coverage.ts +59 -0
  51. package/src/host-client/steps.ts +170 -0
  52. package/src/host-client/transfer.ts +29 -3
  53. package/src/host-client/vice-session.ts +42 -13
  54. package/src/knowledge/history.ts +40 -13
  55. package/src/knowledge/import.ts +72 -56
  56. package/src/knowledge/read.ts +203 -52
  57. package/src/knowledge/schema.ts +94 -12
  58. package/src/knowledge/write.ts +432 -123
  59. package/src/mcp/main.ts +1 -4
  60. package/src/mcp/output.ts +32 -0
  61. package/src/mcp/server.ts +4 -2
  62. package/src/mcp/tools/debug.ts +110 -24
  63. package/src/mcp/tools/input.ts +1 -0
  64. package/src/mcp/tools/media.ts +22 -6
  65. package/src/mcp/tools/memory.ts +57 -10
  66. package/src/mcp/tools/video.ts +9 -3
  67. package/src/native/discover.ts +5 -2
  68. package/src/native/ghidra/index.ts +19 -3
  69. package/src/native/ghidra/scripts/C64Export.java +33 -1
  70. package/src/native/status.ts +15 -3
  71. package/src/native/types.ts +30 -1
  72. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  73. package/src/protocol/messages.ts +17 -10
  74. package/src/protocol/vice.ts +72 -18
  75. package/src/script.ts +3 -2
@@ -0,0 +1,170 @@
1
+ // The action steps of the step language (D43): write, registers, joystick,
2
+ // type, frames and runUntil. c64-testing scenarios and the c64-unpacker
3
+ // steps file share them; each caller adds its own steps around them.
4
+
5
+ import { parseC64Address, textToPetscii } from "../c64.ts";
6
+ import { JOYSTICK_DIRECTIONS, MAX_EXECUTION_COUNT, MAX_KEYBOARD_BYTES, MAX_MEMORY_WRITE, MAX_TIMEOUT_FRAMES } from "../protocol/vice.ts";
7
+ import type { RunTarget, RunUntilResult } from "../protocol/vice.ts";
8
+ import type { ViceSessionClient } from "./vice-session.ts";
9
+
10
+ /** A "$xxxx" address, or a symbol name where the caller resolves names. */
11
+ export type Location = string;
12
+
13
+ type JoystickDirection = (typeof JOYSTICK_DIRECTIONS)[number];
14
+
15
+ export type ActionStep =
16
+ | { write: { at: Location; bytes: number[] } }
17
+ | { registers: Partial<Record<"pc" | "a" | "x" | "y" | "sp", number | Location>> }
18
+ | { joystick: { port: 1 | 2; direction: JoystickDirection; fire?: boolean } }
19
+ | { type: string }
20
+ | { frames: number }
21
+ | { runUntil: { at: Location; raster?: number } | { raster: number } | { memory: { at: Location; equals: number } }; timeoutFrames: number };
22
+
23
+ /** The highest raster line of a PAL frame; an NTSC frame has fewer, which the emulator checks. */
24
+ const MAX_RASTER_LINE = 311;
25
+
26
+ export const ACTION_STEP_KINDS = ["write", "registers", "joystick", "type", "frames", "runUntil"] as const;
27
+
28
+ /** The keys a step of each kind can have, when it is more than the kind itself. */
29
+ export const ACTION_STEP_KEYS: Record<string, readonly string[]> = { runUntil: ["runUntil", "timeoutFrames"] };
30
+
31
+ /** The registers a registers step can set. */
32
+ export const SET_REGISTERS = ["pc", "a", "x", "y", "sp"];
33
+
34
+ export const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
35
+ export const isInteger = (value: unknown, min: number, max: number): value is number => Number.isInteger(value) && (value as number) >= min && (value as number) <= max;
36
+
37
+ /** Checks a "$xxxx" address, or also a symbol name when `names` allows it. */
38
+ export function checkLocation(value: unknown, what: string, fail: (message: string) => never, names = true): void {
39
+ if (typeof value !== "string" || value === "") fail(`${what} must be a $ address${names ? " or a symbol name" : ""}`);
40
+ if ((value as string).startsWith("$")) {
41
+ try {
42
+ parseC64Address(value as string);
43
+ } catch {
44
+ fail(`${what} must be $ followed by four hex digits, not ${value as string}`);
45
+ }
46
+ } else if (!names) {
47
+ fail(`${what} must be $ followed by four hex digits, not ${value as string}`);
48
+ }
49
+ }
50
+
51
+ /** Checks one action step of the given kind. `names` says whether symbol names may stand for addresses. */
52
+ export function checkActionStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never, names = true): void {
53
+ switch (kind) {
54
+ case "write": {
55
+ const write = step.write;
56
+ if (!isObject(write) || Object.keys(write).some((key) => key !== "at" && key !== "bytes")) return fail(`${at}: write needs at and bytes, and nothing else`);
57
+ checkLocation(write.at, `${at}: write.at`, fail, names);
58
+ if (!Array.isArray(write.bytes) || write.bytes.length === 0 || write.bytes.length > MAX_MEMORY_WRITE || !write.bytes.every((byte) => isInteger(byte, 0, 0xff))) {
59
+ fail(`${at}: write.bytes must be a list of 1 to ${MAX_MEMORY_WRITE} numbers from 0 to 255`);
60
+ }
61
+ return;
62
+ }
63
+ case "registers": {
64
+ const registers = step.registers;
65
+ if (!isObject(registers) || Object.keys(registers).length === 0) return fail(`${at}: registers must name at least one of ${SET_REGISTERS.join(", ")}`);
66
+ for (const [name, value] of Object.entries(registers)) {
67
+ if (!SET_REGISTERS.includes(name)) fail(`${at}: registers can set only ${SET_REGISTERS.join(", ")}, not ${name}`);
68
+ if (typeof value === "string") checkLocation(value, `${at}: registers.${name}`, fail, names);
69
+ else if (!isInteger(value, 0, name === "pc" ? 0xffff : 0xff)) fail(`${at}: registers.${name} must be ${names ? "a symbol name, " : ""}a $ address or a number from 0 to ${name === "pc" ? 65535 : 255}`);
70
+ }
71
+ return;
72
+ }
73
+ case "joystick": {
74
+ const joystick = step.joystick;
75
+ if (!isObject(joystick) || Object.keys(joystick).some((key) => !["port", "direction", "fire"].includes(key))) return fail(`${at}: joystick needs port and direction, and fire is optional`);
76
+ if (joystick.port !== 1 && joystick.port !== 2) fail(`${at}: joystick.port must be 1 or 2`);
77
+ if (!(JOYSTICK_DIRECTIONS as readonly unknown[]).includes(joystick.direction)) fail(`${at}: joystick.direction must be one of ${JOYSTICK_DIRECTIONS.join(", ")}`);
78
+ if (joystick.fire !== undefined && typeof joystick.fire !== "boolean") fail(`${at}: joystick.fire must be true or false`);
79
+ return;
80
+ }
81
+ case "type": {
82
+ if (typeof step.type !== "string" || step.type === "") return fail(`${at}: type needs text`);
83
+ let bytes: Uint8Array;
84
+ try {
85
+ bytes = textToPetscii(step.type);
86
+ } catch (error) {
87
+ return fail(`${at}: ${(error as Error).message}`);
88
+ }
89
+ if (bytes.length > MAX_KEYBOARD_BYTES) fail(`${at}: type can have at most ${MAX_KEYBOARD_BYTES} keys`);
90
+ return;
91
+ }
92
+ case "frames":
93
+ if (!isInteger(step.frames, 1, MAX_EXECUTION_COUNT)) fail(`${at} needs a frame count from 1 to ${MAX_EXECUTION_COUNT}`);
94
+ return;
95
+ case "runUntil": {
96
+ const target = step.runUntil;
97
+ const keys = isObject(target) ? Object.keys(target).sort().join(",") : "";
98
+ // at, raster, at with raster (the address only on that line, D46), or memory.
99
+ if (!["at", "raster", "at,raster", "memory"].includes(keys)) return fail(`${at}: runUntil needs at, raster, at and raster, or memory`);
100
+ const until = target as Record<string, unknown>;
101
+ if ("raster" in until && !isInteger(until.raster, 0, MAX_RASTER_LINE)) fail(`${at}: runUntil.raster must be a raster line from 0 to ${MAX_RASTER_LINE}`);
102
+ if ("at" in until) checkLocation(until.at, `${at}: runUntil.at`, fail, names);
103
+ else if ("memory" in until) {
104
+ const memory = until.memory;
105
+ if (!isObject(memory) || Object.keys(memory).some((key) => key !== "at" && key !== "equals")) return fail(`${at}: runUntil.memory needs at and equals, and nothing else`);
106
+ checkLocation(memory.at, `${at}: runUntil.memory.at`, fail, names);
107
+ if (!isInteger(memory.equals, 0, 0xff)) fail(`${at}: runUntil.memory.equals must be a number from 0 to 255`);
108
+ }
109
+ if (!isInteger(step.timeoutFrames, 1, MAX_TIMEOUT_FRAMES)) fail(`${at} needs timeoutFrames from 1 to ${MAX_TIMEOUT_FRAMES}`);
110
+ return;
111
+ }
112
+ default:
113
+ fail(`${at}: ${kind} is not an action step`);
114
+ }
115
+ }
116
+
117
+ /** Checks a list of action steps only, with $ addresses only: the steps file of c64-unpacker. */
118
+ export function checkActionSteps(value: unknown, fail: (message: string) => never): ActionStep[] {
119
+ if (!Array.isArray(value) || value.length === 0) return fail("the steps are a JSON list with at least one step");
120
+ value.forEach((step: unknown, index) => {
121
+ const at = `step ${index + 1}`;
122
+ if (!isObject(step)) return fail(`${at} must be an object`);
123
+ const kinds = Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key));
124
+ if (kinds.length !== 1) return fail(`${at} must have exactly one of ${ACTION_STEP_KINDS.join(", ")}`);
125
+ const kind = kinds[0]!;
126
+ const allowed = ACTION_STEP_KEYS[kind] ?? [kind];
127
+ for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
128
+ checkActionStep(kind, step, at, fail, false);
129
+ });
130
+ return value as ActionStep[];
131
+ }
132
+
133
+ /** Stops the CPU when it runs: a write, a register change or an exact frame count needs it stopped. */
134
+ async function stop(client: ViceSessionClient): Promise<void> {
135
+ if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
136
+ }
137
+
138
+ /**
139
+ * Runs one action step. `at` turns a location into an address. A runUntil
140
+ * step that does not reach its target returns the stop, for the caller to judge.
141
+ */
142
+ export async function runActionStep(client: ViceSessionClient, step: ActionStep, at: (location: Location) => number): Promise<RunUntilResult | undefined> {
143
+ if ("write" in step) {
144
+ await stop(client);
145
+ await client.memoryWrite({ address: at(step.write.at), data: Buffer.from(step.write.bytes).toString("hex"), space: "c64", view: "cpu" });
146
+ } else if ("registers" in step) {
147
+ await stop(client);
148
+ const values: Record<string, number> = {};
149
+ for (const [name, value] of Object.entries(step.registers)) values[name] = typeof value === "number" ? value : at(value as Location);
150
+ await client.registersSet("c64", values);
151
+ } else if ("joystick" in step) {
152
+ await client.joystick({ port: step.joystick.port, direction: step.joystick.direction, fire: step.joystick.fire ?? false });
153
+ } else if ("type" in step) {
154
+ await client.keyboard(textToPetscii(step.type));
155
+ } else if ("frames" in step) {
156
+ await stop(client);
157
+ await client.execution({ action: "advance-frames", count: step.frames, space: "c64" });
158
+ } else {
159
+ const until = step.runUntil;
160
+ const target: RunTarget =
161
+ "at" in until
162
+ ? { kind: "address", address: at(until.at), space: "c64", ...(until.raster === undefined ? {} : { condition: { kind: "raster" as const, line: until.raster } }) }
163
+ : "raster" in until
164
+ ? { kind: "raster", line: until.raster }
165
+ : { kind: "memory", address: at(until.memory.at), operator: "eq", value: until.memory.equals, space: "c64", view: "cpu" };
166
+ const reached = await client.runUntil({ target, timeoutFrames: step.timeoutFrames });
167
+ if (!reached.reached) return reached;
168
+ }
169
+ return undefined;
170
+ }
@@ -1,10 +1,10 @@
1
1
  // Reads project files on the client side for transfer to the Host Runtime.
2
2
  // The host never sees a client path: only bytes and the file's type.
3
3
 
4
- import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
5
- import { extname, join, relative, sep } from "node:path";
4
+ import { lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
5
+ import { dirname, extname, join, relative, sep } from "node:path";
6
6
 
7
- import { isInside, projectRoot, resolveProjectFile } from "../project.ts";
7
+ import { isInside, projectRoot, resolveProjectFile, resolveProjectPath } from "../project.ts";
8
8
  import { MAX_ATTACHMENT_BYTES } from "../protocol/framing.ts";
9
9
  import { WireFailure } from "../protocol/messages.ts";
10
10
  import { MAX_TREE_FILES, type SourceTree } from "../protocol/tools.ts";
@@ -42,6 +42,32 @@ export function readProjectFile(path: string, options: { root?: string } = {}):
42
42
  return { bytes: readFileSync(real), type: extname(path).slice(1).toLowerCase() };
43
43
  }
44
44
 
45
+ /**
46
+ * Writes bytes to a project file on the client side, making its directories
47
+ * and overwriting it. Throws WireFailure(invalid-input) for a path outside the
48
+ * project, also one that leaves it through a symbolic link.
49
+ */
50
+ export function writeProjectFile(path: string, bytes: Uint8Array, options: { root?: string; what?: string } = {}): void {
51
+ const root = options.root ?? projectRoot();
52
+ const refuse = () => new WireFailure("invalid-input", `${options.what ?? path}: give a path relative to the project directory that stays inside it, not ${path}.`);
53
+ let file: string;
54
+ try {
55
+ file = resolveProjectPath(path, root);
56
+ } catch {
57
+ throw refuse();
58
+ }
59
+ mkdirSync(dirname(file), { recursive: true });
60
+ if (!isInside(realpathSync(root), realpathSync(dirname(file)))) throw refuse();
61
+ let link = false;
62
+ try {
63
+ link = lstatSync(file).isSymbolicLink();
64
+ } catch (error) {
65
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
66
+ }
67
+ if (link) throw refuse();
68
+ writeFileSync(file, bytes);
69
+ }
70
+
45
71
  /** Directories never staged: version control and the toolkit's own project state. */
46
72
  const SKIPPED_DIRECTORIES = new Set([".git", ".hg", ".svn", ".c64-re-tools"]);
47
73
 
@@ -2,7 +2,7 @@
2
2
  // one VICE. Exposes C64-domain operations only.
3
3
 
4
4
  import { ProtocolError, WireFailure, type VideoStandard } from "../protocol/messages.ts";
5
- import { validateViceResult, type Breakpoint, type BreakpointParams, type Watchpoint, type WatchpointParams, type ExecutionParams, type ExecutionResult, type JoystickState, type MachineStatus, type MemmapRange, type MemoryWriteParams, type RegisterValues, type ResetMode, type RunState, type RunTarget, type RunUntilResult, type MemoryReadParams, type MemoryReadResult, type Registers, type Space, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
5
+ import { validateViceResult, type Breakpoint, type BreakpointParams, type PointRemoved, type Watchpoint, type WatchpointParams, type ExecutionParams, type ExecutionResult, type JoystickState, type MachineStatus, type MemmapRange, type MemoryWriteParams, type RegisterValues, type ResetMode, type RunState, type RunTarget, type RunUntilResult, type MemoryReadParams, type MemoryReadResult, type Registers, type Space, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
6
6
  import { HostConnection } from "./connect.ts";
7
7
  import { readProjectFile } from "./transfer.ts";
8
8
 
@@ -13,7 +13,7 @@ const SESSION_CLOSED = "The C64 session is closed because the c64-re-tools MCP s
13
13
 
14
14
  interface Pending {
15
15
  op: ViceOperation;
16
- resolve: (value: unknown) => void;
16
+ resolve: (value: unknown, attachments: Buffer[]) => void;
17
17
  reject: (error: Error) => void;
18
18
  }
19
19
 
@@ -27,7 +27,7 @@ export class ViceSessionClient {
27
27
 
28
28
  private constructor(connection: HostConnection) {
29
29
  this.#connection = connection;
30
- connection.onMessage((message) => {
30
+ connection.onMessage((message, attachments) => {
31
31
  // Anything but a reply after the handshake breaks the contract.
32
32
  if (message.type !== "reply") return this.#fail();
33
33
  const pending = this.#pending.get(message.id);
@@ -38,7 +38,7 @@ export class ViceSessionClient {
38
38
  return;
39
39
  }
40
40
  try {
41
- pending.resolve(validateViceResult(pending.op, message.result));
41
+ pending.resolve(validateViceResult(pending.op, message.result), attachments);
42
42
  } catch (error) {
43
43
  pending.reject(
44
44
  new WireFailure(
@@ -75,6 +75,11 @@ export class ViceSessionClient {
75
75
  return this.#request("memoryRead", params);
76
76
  }
77
77
 
78
+ /** The I/O registers at $d000-$dfff as the chips hold them, without side effects. */
79
+ ioRead(address: number, size: number): Promise<MemoryReadResult> {
80
+ return this.#request("ioRead", { address, size });
81
+ }
82
+
78
83
  registersGet(space: Space): Promise<Registers> {
79
84
  return this.#request("registersGet", { space });
80
85
  }
@@ -138,17 +143,19 @@ export class ViceSessionClient {
138
143
  return this.#request("joystick", state);
139
144
  }
140
145
 
141
- /** Action list gives all breakpoints; every other action gives the one breakpoint it added or changed. */
146
+ /** Action list gives all breakpoints, remove gives `{removed, id}`; add, enable and disable give the one breakpoint. */
142
147
  breakpoint(params: Extract<BreakpointParams, { action: "list" }>): Promise<{ breakpoints: Breakpoint[] }>;
143
- breakpoint(params: Exclude<BreakpointParams, { action: "list" }>): Promise<Breakpoint>;
144
- breakpoint(params: BreakpointParams): Promise<Breakpoint | { breakpoints: Breakpoint[] }> {
148
+ breakpoint(params: Extract<BreakpointParams, { action: "remove" }>): Promise<PointRemoved>;
149
+ breakpoint(params: Exclude<BreakpointParams, { action: "list" | "remove" }>): Promise<Breakpoint>;
150
+ breakpoint(params: BreakpointParams): Promise<Breakpoint | PointRemoved | { breakpoints: Breakpoint[] }> {
145
151
  return this.#request("breakpoint", params);
146
152
  }
147
153
 
148
- /** Action list gives all watchpoints; every other action gives the one watchpoint it added or changed. */
154
+ /** Action list gives all watchpoints, remove gives `{removed, id}`; add, enable and disable give the one watchpoint. */
149
155
  watchpoint(params: Extract<WatchpointParams, { action: "list" }>): Promise<{ watchpoints: Watchpoint[] }>;
150
- watchpoint(params: Exclude<WatchpointParams, { action: "list" }>): Promise<Watchpoint>;
151
- watchpoint(params: WatchpointParams): Promise<Watchpoint | { watchpoints: Watchpoint[] }> {
156
+ watchpoint(params: Extract<WatchpointParams, { action: "remove" }>): Promise<PointRemoved>;
157
+ watchpoint(params: Exclude<WatchpointParams, { action: "list" | "remove" }>): Promise<Watchpoint>;
158
+ watchpoint(params: WatchpointParams): Promise<Watchpoint | PointRemoved | { watchpoints: Watchpoint[] }> {
152
159
  return this.#request("watchpoint", params);
153
160
  }
154
161
 
@@ -169,7 +176,7 @@ export class ViceSessionClient {
169
176
  }
170
177
 
171
178
  /** Action read gives the ranges; action clear gives cleared. */
172
- memmap(params: Extract<ViceOperations["memmap"]["params"], { action: "read" }>): Promise<{ ranges: MemmapRange[] }>;
179
+ memmap(params: Extract<ViceOperations["memmap"]["params"], { action: "read" }>): Promise<{ ranges: MemmapRange[]; next?: number }>;
173
180
  memmap(params: { action: "clear" }): Promise<{ cleared: boolean }>;
174
181
  memmap(params: ViceOperations["memmap"]["params"]): Promise<ViceOperations["memmap"]["result"]> {
175
182
  return this.#request("memmap", params);
@@ -209,6 +216,19 @@ export class ViceSessionClient {
209
216
  return this.#request("snapshot", params);
210
217
  }
211
218
 
219
+ /** The bytes of a named snapshot, as a .vsf file holds them (D41). */
220
+ async snapshotExport(name: string): Promise<Buffer> {
221
+ const { attachments } = await this.#requestWithAttachments("snapshotFile", { action: "export", name });
222
+ const [bytes] = attachments;
223
+ if (bytes === undefined || attachments.length !== 1) throw new WireFailure("operation-failed", "The c64-re-tools host runtime sent no snapshot file.");
224
+ return bytes;
225
+ }
226
+
227
+ /** Restores the machine from the bytes of a .vsf file; the CPU stays stopped. */
228
+ snapshotRestore(bytes: Uint8Array): Promise<ViceOperations["snapshotFile"]["result"]> {
229
+ return this.#request("snapshotFile", { action: "restore" }, [bytes]);
230
+ }
231
+
212
232
  screenBaselines(): Promise<{ baselines: string[] }> {
213
233
  return this.#request("screenBaselines", {});
214
234
  }
@@ -233,16 +253,25 @@ export class ViceSessionClient {
233
253
  await this.closed;
234
254
  }
235
255
 
236
- #request<O extends ViceOperation>(
256
+ async #request<O extends ViceOperation>(
237
257
  op: O,
238
258
  params: ViceOperations[O]["params"],
239
259
  attachments: readonly Uint8Array[] = [],
240
260
  ): Promise<ViceOperations[O]["result"]> {
261
+ return (await this.#requestWithAttachments(op, params, attachments)).result;
262
+ }
263
+
264
+ /** One request; the reply's result and the attachments that came with it. */
265
+ #requestWithAttachments<O extends ViceOperation>(
266
+ op: O,
267
+ params: ViceOperations[O]["params"],
268
+ attachments: readonly Uint8Array[] = [],
269
+ ): Promise<{ result: ViceOperations[O]["result"]; attachments: Buffer[] }> {
241
270
  if (this.#lost !== undefined) return Promise.reject(this.#lost);
242
271
  const id = this.#nextId;
243
272
  this.#nextId = this.#nextId >= 0xffff_ffff ? 1 : this.#nextId + 1;
244
273
  return new Promise((resolve, reject) => {
245
- this.#pending.set(id, { op, resolve: resolve as (value: unknown) => void, reject });
274
+ this.#pending.set(id, { op, resolve: (result, received) => resolve({ result: result as ViceOperations[O]["result"], attachments: received }), reject });
246
275
  this.#connection.send({ type: "request", id, op, params }, attachments);
247
276
  });
248
277
  }
@@ -6,19 +6,26 @@ import type { DatabaseSync } from "node:sqlite";
6
6
  import { KnowledgeError } from "./database.ts";
7
7
  import {
8
8
  COMMENT_COLUMNS,
9
+ MODULE_COLUMNS,
9
10
  REFERENCE_COLUMNS,
10
11
  REGION_COLUMNS,
12
+ SUPPRESSION_COLUMNS,
11
13
  SYMBOL_COLUMNS,
12
14
  toComment,
15
+ toModule,
13
16
  toReference,
14
17
  toRegion,
18
+ toSuppression,
15
19
  toSymbol,
16
20
  type CommentRow,
21
+ type ModuleRow,
17
22
  type ReferenceRow,
18
23
  type RegionRow,
24
+ type SuppressionRow,
19
25
  type SymbolRow,
20
26
  } from "./read.ts";
21
- import type { Origin } from "./schema.ts";
27
+ import { DEFAULT_SPACE, type EvidenceKind, type Origin, type Space } from "./schema.ts";
28
+ import type { Evidence } from "./write.ts";
22
29
 
23
30
  export interface Revision {
24
31
  id: number;
@@ -28,6 +35,8 @@ export interface Revision {
28
35
  description?: string;
29
36
  inputHash?: string;
30
37
  toolVersion?: string;
38
+ /** What the change rests on (D42). */
39
+ evidence?: Evidence[];
31
40
  }
32
41
 
33
42
  /** One knowledge row with its validity: from its first revision until the revision that closed it. */
@@ -35,7 +44,9 @@ export type HistoryEntry =
35
44
  | { entity: "symbol"; row: SymbolRow; from: number; to?: number }
36
45
  | { entity: "region"; row: RegionRow; from: number; to?: number }
37
46
  | { entity: "comment"; row: CommentRow; from: number; to?: number }
38
- | { entity: "reference"; row: ReferenceRow; from: number; to?: number };
47
+ | { entity: "reference"; row: ReferenceRow; from: number; to?: number }
48
+ | { entity: "suppression"; row: SuppressionRow; from: number; to?: number }
49
+ | { entity: "module"; row: ModuleRow; from: number; to?: number };
39
50
 
40
51
  type Raw = Record<string, unknown>;
41
52
 
@@ -62,30 +73,44 @@ function bySequence(a: HistoryEntry, b: HistoryEntry): number {
62
73
  return a.from - b.from || (a.to ?? Infinity) - (b.to ?? Infinity);
63
74
  }
64
75
 
65
- /** Every row, current or closed, that concerns one address, oldest first. */
66
- export function historyAt(db: DatabaseSync | undefined, address: number): HistoryEntry[] {
76
+ /** Every row, current or closed, that concerns one address of a space, oldest first. */
77
+ export function historyAt(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): HistoryEntry[] {
67
78
  if (db === undefined) return [];
68
79
  const validity = ", valid_to_revision";
69
80
  const entries: HistoryEntry[] = [];
70
- for (const raw of db.prepare(`SELECT ${SYMBOL_COLUMNS}${validity} FROM symbols WHERE address = ?`).all(address) as Raw[]) entries.push(entry("symbol", toSymbol(raw), raw));
71
- for (const raw of db.prepare(`SELECT ${REGION_COLUMNS}${validity} FROM regions WHERE start_address <= ? AND end_address >= ?`).all(address, address) as Raw[]) {
81
+ for (const raw of db.prepare(`SELECT ${SYMBOL_COLUMNS}${validity} FROM symbols WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("symbol", toSymbol(raw), raw));
82
+ for (const raw of db.prepare(`SELECT ${REGION_COLUMNS}${validity} FROM regions WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
72
83
  entries.push(entry("region", toRegion(raw), raw));
73
84
  }
74
- for (const raw of db.prepare(`SELECT ${COMMENT_COLUMNS}${validity} FROM comments WHERE address = ?`).all(address) as Raw[]) entries.push(entry("comment", toComment(raw), raw));
75
- for (const raw of db.prepare(`SELECT ${REFERENCE_COLUMNS}${validity} FROM "references" WHERE from_address = ? OR to_address = ?`).all(address, address) as Raw[]) {
85
+ for (const raw of db.prepare(`SELECT ${COMMENT_COLUMNS}${validity} FROM comments WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("comment", toComment(raw), raw));
86
+ for (const raw of db.prepare(`SELECT ${REFERENCE_COLUMNS}${validity} FROM "references" WHERE space = ? AND (from_address = ? OR to_address = ?)`).all(space, address, address) as Raw[]) {
76
87
  entries.push(entry("reference", toReference(raw), raw));
77
88
  }
89
+ for (const raw of db
90
+ .prepare(`SELECT ${SUPPRESSION_COLUMNS}${validity} FROM suppressions WHERE space = ? AND ((start_address <= ? AND end_address >= ?) OR to_address = ?)`)
91
+ .all(space, address, address, address) as Raw[]) {
92
+ entries.push(entry("suppression", toSuppression(raw), raw));
93
+ }
94
+ for (const raw of db.prepare(`SELECT ${MODULE_COLUMNS}${validity} FROM modules WHERE space = ? AND start_address = ?`).all(space, address) as Raw[]) entries.push(entry("module", toModule(raw), raw));
78
95
  return entries.sort(bySequence);
79
96
  }
80
97
 
81
- /** Every symbol row that ever had this name, oldest first: a name can move between addresses. */
82
- export function historyNamed(db: DatabaseSync | undefined, name: string): HistoryEntry[] {
98
+ /** Every symbol row of a space that ever had this name, oldest first: a name can move between addresses. */
99
+ export function historyNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
83
100
  if (db === undefined) return [];
84
- return (db.prepare(`SELECT ${SYMBOL_COLUMNS}, valid_to_revision FROM symbols WHERE name = ?`).all(name) as Raw[])
101
+ return (db.prepare(`SELECT ${SYMBOL_COLUMNS}, valid_to_revision FROM symbols WHERE space = ? AND name = ?`).all(space, name) as Raw[])
85
102
  .map((raw) => entry("symbol", toSymbol(raw), raw))
86
103
  .sort(bySequence);
87
104
  }
88
105
 
106
+ /** The revision with its evidence, when it has any. */
107
+ function withEvidence(db: DatabaseSync, revision: Revision): Revision {
108
+ const rows = db.prepare("SELECT kind, ref, sha256 FROM evidence WHERE revision = ? ORDER BY id").all(revision.id) as Raw[];
109
+ if (rows.length === 0) return revision;
110
+ const evidence = rows.map((row): Evidence => ({ kind: row.kind as EvidenceKind, ref: row.ref as string, ...(row.sha256 === null ? {} : { sha256: row.sha256 as string }) }));
111
+ return { ...revision, evidence };
112
+ }
113
+
89
114
  /** Revisions, newest first; with `before`, only revisions older than it. */
90
115
  export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?: number } = {}): Revision[] {
91
116
  if (db === undefined) return [];
@@ -93,7 +118,7 @@ export function revisions(db: DatabaseSync | undefined, options: { limit?: numbe
93
118
  db
94
119
  .prepare("SELECT * FROM revisions WHERE id < ? ORDER BY id DESC LIMIT ?")
95
120
  .all(options.before ?? Number.MAX_SAFE_INTEGER, options.limit ?? 50) as Raw[]
96
- ).map(toRevision);
121
+ ).map((row) => withEvidence(db, toRevision(row)));
97
122
  }
98
123
 
99
124
  export interface RevisionChanges {
@@ -113,6 +138,8 @@ export function revision(db: DatabaseSync | undefined, id: number): RevisionChan
113
138
  ["region", "regions", REGION_COLUMNS, toRegion],
114
139
  ["comment", "comments", COMMENT_COLUMNS, toComment],
115
140
  ["reference", '"references"', REFERENCE_COLUMNS, toReference],
141
+ ["suppression", "suppressions", SUPPRESSION_COLUMNS, toSuppression],
142
+ ["module", "modules", MODULE_COLUMNS, toModule],
116
143
  ] as const;
117
144
  const added: HistoryEntry[] = [];
118
145
  const closed: HistoryEntry[] = [];
@@ -124,5 +151,5 @@ export function revision(db: DatabaseSync | undefined, id: number): RevisionChan
124
151
  closed.push(entry(entity, map(row), row));
125
152
  }
126
153
  }
127
- return { revision: toRevision(raw), added, closed };
154
+ return { revision: withEvidence(db, toRevision(raw)), added, closed };
128
155
  }