@henols/c64-re-tools 2.1.0 → 2.1.1
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 +1 -0
- package/skills/c64-assembler/scripts/assembler.ts +10 -0
- package/skills/c64-disk/SKILL.md +2 -2
- package/skills/c64-disk/scripts/disk.ts +24 -10
- package/skills/c64-disk/scripts/lib/containers.ts +9 -9
- package/skills/c64-docs/scripts/lib/links.ts +3 -3
- package/skills/c64-docs/scripts/lib/reference.ts +2 -2
- package/skills/c64-emulator/SKILL.md +8 -2
- package/skills/c64-knowledge/SKILL.md +3 -1
- package/skills/c64-knowledge/scripts/knowledge.ts +59 -43
- package/skills/c64-memory-map/SKILL.md +1 -1
- package/skills/c64-memory-map/scripts/memory-map.ts +3 -2
- package/skills/c64-provenance/SKILL.md +5 -4
- package/skills/c64-provenance/scripts/lib/library.ts +18 -6
- package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
- package/skills/c64-provenance/scripts/provenance.ts +5 -4
- package/skills/c64-source/SKILL.md +2 -0
- package/skills/c64-source/scripts/lib/source.ts +2 -2
- package/skills/c64-source/scripts/source.ts +10 -6
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
- package/skills/c64-static-analysis/scripts/static-analysis.ts +4 -3
- package/skills/c64-testing/SKILL.md +11 -1
- package/skills/c64-testing/references/recording-format.md +2 -0
- package/skills/c64-testing/references/scenario-format.md +6 -0
- package/skills/c64-testing/scripts/lib/diff-files.ts +63 -13
- package/skills/c64-testing/scripts/lib/diff.ts +3 -3
- package/skills/c64-testing/scripts/lib/recording.ts +47 -12
- package/skills/c64-testing/scripts/lib/replay.ts +11 -0
- package/skills/c64-testing/scripts/lib/runs.ts +64 -4
- package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
- package/skills/c64-testing/scripts/testing.ts +15 -2
- package/skills/c64-unpacker/SKILL.md +9 -5
- package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
- package/skills/c64-unpacker/scripts/lib/loads.ts +3 -2
- package/skills/c64-unpacker/scripts/unpacker.ts +75 -25
- package/src/host/vice/session.ts +99 -10
- package/src/host-client/image.ts +32 -0
- package/src/host-client/steps.ts +106 -6
- package/src/host-client/tools.ts +1 -1
- package/src/host-client/transfer.ts +35 -8
- package/src/knowledge/expression.ts +1 -1
- package/src/knowledge/history.ts +19 -2
- package/src/knowledge/names.ts +2 -1
- package/src/knowledge/program.ts +1 -16
- package/src/knowledge/read.ts +43 -0
- package/src/knowledge/schema.ts +3 -2
- package/src/knowledge/text.ts +1 -0
- package/src/knowledge/write.ts +40 -6
- package/src/mcp/output.ts +5 -0
- package/src/mcp/server.ts +19 -4
- package/src/mcp/tools/debug.ts +7 -6
- package/src/mcp/tools/execution.ts +18 -5
- package/src/mcp/tools/media.ts +14 -2
- package/src/mcp/tools/memory.ts +7 -3
- package/src/mcp/tools/video.ts +5 -3
- package/src/project.ts +1 -1
- package/src/protocol/vice.ts +4 -1
package/src/host-client/steps.ts
CHANGED
|
@@ -63,6 +63,8 @@ export type ActionStep =
|
|
|
63
63
|
|
|
64
64
|
export const ACTION_STEP_KINDS = ["write", "registers", "joystick", "type", "press", "frames", "runUntil"] as const;
|
|
65
65
|
export type ActionStepKind = (typeof ACTION_STEP_KINDS)[number];
|
|
66
|
+
/** The step kinds that VICE runs: all but press, which only the replay core has. */
|
|
67
|
+
export const VICE_STEP_KINDS = ACTION_STEP_KINDS.filter((kind) => kind !== "press");
|
|
66
68
|
|
|
67
69
|
/** The fields of each object of the steps: the same as the inputs of the MCP tools. */
|
|
68
70
|
export const STEP_FIELDS = {
|
|
@@ -110,6 +112,32 @@ export function keyboardBytes(input: { mode: "text" | "petscii"; text?: string |
|
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
/**
|
|
116
|
+
* One line of JSON with a space after each colon and comma: the form that a
|
|
117
|
+
* refusal tells the caller to write.
|
|
118
|
+
*/
|
|
119
|
+
export function jsonLine(value: unknown): string {
|
|
120
|
+
if (Array.isArray(value)) return `[${value.map(jsonLine).join(", ")}]`;
|
|
121
|
+
if (isObject(value)) return `{${Object.entries(value).filter(([, item]) => item !== undefined).map(([key, item]) => `${JSON.stringify(key)}: ${jsonLine(item)}`).join(", ")}}`;
|
|
122
|
+
return JSON.stringify(value) ?? "null";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Field names of the 2.0 files and what a 2.1 object has in their place, when it has that field. */
|
|
126
|
+
const RENAMED: Record<string, Array<{ field: string; write: string }>> = {
|
|
127
|
+
at: [
|
|
128
|
+
{ field: "address", write: "address" },
|
|
129
|
+
{ field: "start", write: "start" },
|
|
130
|
+
],
|
|
131
|
+
bytes: [{ field: "data", write: 'data, the bytes as hex text, for example "data": "a900",' }],
|
|
132
|
+
equals: [{ field: "operator", write: '"operator": "eq" and "value"' }],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** What to write in place of a field of the 2.0 files, for an object with these fields; undefined when the field has no 2.1 name there. */
|
|
136
|
+
export function renamedField(key: string, fields: readonly string[]): string | undefined {
|
|
137
|
+
const renamed = RENAMED[key]?.find((item) => fields.includes(item.field));
|
|
138
|
+
return renamed === undefined ? undefined : `${key} is a 2.0 field. Write ${renamed.write} in place of ${key}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
113
141
|
/** The checks of one input: `fail` names the field of the problem. */
|
|
114
142
|
class Fields {
|
|
115
143
|
readonly #value: Record<string, unknown>;
|
|
@@ -121,7 +149,11 @@ class Fields {
|
|
|
121
149
|
this.#fail = fail;
|
|
122
150
|
if (!isObject(value)) fail(`${where} must be an object with ${fields.join(", ")}`);
|
|
123
151
|
this.#value = value as Record<string, unknown>;
|
|
124
|
-
for (const key of Object.keys(this.#value))
|
|
152
|
+
for (const key of Object.keys(this.#value)) {
|
|
153
|
+
if (fields.includes(key)) continue;
|
|
154
|
+
const renamed = renamedField(key, fields);
|
|
155
|
+
fail(`${where} has the field ${key}, which it does not take. ${renamed === undefined ? "" : `${renamed}. `}Its fields are ${fields.join(", ")}`);
|
|
156
|
+
}
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
fail(field: string, message: string): never {
|
|
@@ -256,22 +288,90 @@ function checkInput(kind: ActionStepKind, value: unknown, at: string, fail: (mes
|
|
|
256
288
|
}
|
|
257
289
|
}
|
|
258
290
|
|
|
291
|
+
/** A "$xxxx" text of a number, or the value as it is. */
|
|
292
|
+
const asAddress = (value: unknown) => (isInteger(value, 0, 0xffff) ? `$${value.toString(16).padStart(4, "0")}` : value);
|
|
293
|
+
|
|
294
|
+
/** The hex text of a list of bytes, or the value as it is. */
|
|
295
|
+
const asHex = (value: unknown) => (Array.isArray(value) && value.every((byte) => isInteger(byte, 0, 0xff)) ? Buffer.from(value as number[]).toString("hex") : value);
|
|
296
|
+
|
|
297
|
+
/** A runUntil target from the 2.0 input: at (with raster), raster, or memory with at and equals. */
|
|
298
|
+
function targetOf(input: Record<string, unknown>): unknown {
|
|
299
|
+
if ("target" in input) return input.target;
|
|
300
|
+
if (isObject(input.memory)) return { kind: "memory", address: input.memory.at ?? input.memory.address, operator: "eq", value: input.memory.equals };
|
|
301
|
+
if ("at" in input) return { kind: "address", address: input.at, ...(input.raster === undefined ? {} : { condition: { kind: "raster", line: input.raster } }) };
|
|
302
|
+
return { kind: "raster", line: input.raster };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The 2.1 form of a step that has a form of the 2.0 files: the 2.0 fields
|
|
307
|
+
* that it has and the step to write. Undefined when the step has no 2.0 form.
|
|
308
|
+
* Keys of the step other than its kind (for example side) stay.
|
|
309
|
+
*/
|
|
310
|
+
export function newStepForm(kind: string, step: Record<string, unknown>): { fields: string[]; step: Record<string, unknown> } | undefined {
|
|
311
|
+
const input = step[kind];
|
|
312
|
+
const { [kind]: _input, ...rest } = step;
|
|
313
|
+
const form = (fields: string[], value: unknown, others = rest) => ({ fields, step: { [kind]: value, ...others } });
|
|
314
|
+
switch (kind) {
|
|
315
|
+
case "runUntil": {
|
|
316
|
+
const { timeoutFrames: beside, ...others } = rest;
|
|
317
|
+
const old = isObject(input) && !("target" in input) && ("at" in input || "raster" in input || "memory" in input);
|
|
318
|
+
if (!isObject(input) || (!old && beside === undefined)) return undefined;
|
|
319
|
+
const fields = [...(old ? ["at", "raster", "memory"].filter((key) => key in input).map((key) => `runUntil.${key}`) : []), ...(beside === undefined ? [] : ["timeoutFrames beside runUntil"])];
|
|
320
|
+
const timeoutFrames = beside ?? input.timeoutFrames;
|
|
321
|
+
return form(fields, { target: targetOf(input), ...(timeoutFrames === undefined ? {} : { timeoutFrames }) }, others);
|
|
322
|
+
}
|
|
323
|
+
case "type":
|
|
324
|
+
return typeof input === "string" ? form(["type as text"], { mode: "text", text: input }) : undefined;
|
|
325
|
+
case "write": {
|
|
326
|
+
if (!isObject(input) || !("at" in input || "bytes" in input || Array.isArray(input.data))) return undefined;
|
|
327
|
+
const { at: old, bytes, address, data, ...others } = input;
|
|
328
|
+
return form(
|
|
329
|
+
["at", "bytes"].filter((key) => key in input).map((key) => `write.${key}`).concat(Array.isArray(data) ? ["write.data as a list"] : []),
|
|
330
|
+
{ address: address ?? old, data: asHex(data ?? bytes), ...others },
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
case "registers": {
|
|
334
|
+
if (!isObject(input)) return undefined;
|
|
335
|
+
const fields = Object.entries(input).flatMap(([name, value]) => ((name === "pc" && typeof value === "number") || (name !== "pc" && name !== "flags" && typeof value === "string" && c64AddressOf(value) !== undefined) ? [`registers.${name} as ${typeof value === "number" ? "a number" : "text"}`] : []));
|
|
336
|
+
if (fields.length === 0) return undefined;
|
|
337
|
+
const values = Object.fromEntries(Object.entries(input).map(([name, value]) => [name, name === "pc" ? asAddress(value) : typeof value === "string" ? (c64AddressOf(value) ?? value) : value]));
|
|
338
|
+
return form(fields, values);
|
|
339
|
+
}
|
|
340
|
+
default:
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Refuses a step in a form of the 2.0 files: the message names its 2.0 fields and gives the step to write. */
|
|
346
|
+
export function refuseOldStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
|
|
347
|
+
const found = newStepForm(kind, step);
|
|
348
|
+
if (found === undefined) return;
|
|
349
|
+
const petscii = kind === "type" ? ', or {"type": {"mode": "petscii", "bytes": [...]}} for PETSCII codes' : "";
|
|
350
|
+
fail(`${at} has the 2.0 form (${found.fields.join(", ")}). Write ${jsonLine(found.step)}${petscii}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
259
353
|
/**
|
|
260
354
|
* Checks one action step of the given kind and returns it with the defaults
|
|
261
355
|
* filled in. The step has no other key. `press` is refused unless the caller
|
|
262
|
-
* runs the steps on the replay core.
|
|
356
|
+
* runs the steps on the replay core. A step in a form of the 2.0 files is
|
|
357
|
+
* refused with the step to write.
|
|
263
358
|
*/
|
|
264
359
|
export function checkActionStep(kind: ActionStepKind, step: Record<string, unknown>, at: string, fail: (message: string) => never, options: { press?: boolean } = {}): ActionStep {
|
|
360
|
+
refuseOldStep(kind, step, at, fail);
|
|
265
361
|
for (const key of Object.keys(step)) if (key !== kind) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
266
362
|
if (kind === "press" && options.press !== true) fail(`${at}: ${PRESS_REMEDY}`);
|
|
267
363
|
return checkInput(kind, step[kind], at, fail);
|
|
268
364
|
}
|
|
269
365
|
|
|
270
|
-
/**
|
|
271
|
-
|
|
366
|
+
/**
|
|
367
|
+
* The kind of a step: its one key from `kinds`. A refusal lists `shown`: the
|
|
368
|
+
* kinds that the caller runs.
|
|
369
|
+
*/
|
|
370
|
+
export function stepKind<K extends string>(step: unknown, kinds: readonly K[], at: string, fail: (message: string) => never, shown: readonly string[] = kinds): K {
|
|
272
371
|
if (!isObject(step)) return fail(`${at} must be an object`);
|
|
273
372
|
const found = Object.keys(step).filter((key) => (kinds as readonly string[]).includes(key));
|
|
274
|
-
if (found.length
|
|
373
|
+
if (found.length > 1) return fail(`${at} has ${found.length} step kinds: ${found.join(", ")}. Give each one its own step`);
|
|
374
|
+
if (found.length !== 1) return fail(`${at} must have exactly one of ${shown.join(", ")}`);
|
|
275
375
|
return found[0] as K;
|
|
276
376
|
}
|
|
277
377
|
|
|
@@ -280,7 +380,7 @@ export function checkActionSteps(value: unknown, fail: (message: string) => neve
|
|
|
280
380
|
if (!Array.isArray(value) || value.length === 0) return fail("the steps are a JSON list with at least one step");
|
|
281
381
|
return value.map((step: unknown, index) => {
|
|
282
382
|
const at = `step ${index + 1}`;
|
|
283
|
-
return checkActionStep(stepKind(step, ACTION_STEP_KINDS, at, fail), step as Record<string, unknown>, at, fail, options);
|
|
383
|
+
return checkActionStep(stepKind(step, ACTION_STEP_KINDS, at, fail, options.press === true ? ACTION_STEP_KINDS : VICE_STEP_KINDS), step as Record<string, unknown>, at, fail, options);
|
|
284
384
|
});
|
|
285
385
|
}
|
|
286
386
|
|
package/src/host-client/tools.ts
CHANGED
|
@@ -85,6 +85,6 @@ export async function hostTools(options: ToolCallOptions = {}): Promise<{ host:
|
|
|
85
85
|
|
|
86
86
|
/** Skill scripts see failures and wire limits through the host-client, never the private protocol module. */
|
|
87
87
|
export { WireFailure } from "../protocol/messages.ts";
|
|
88
|
-
export { DISK_ACTIONS } from "../protocol/tools.ts";
|
|
88
|
+
export { DISK_ACTIONS, DISK_IMAGE_TYPES } from "../protocol/tools.ts";
|
|
89
89
|
export { JOYSTICK_DIRECTIONS, MAX_BASELINES, MAX_EXECUTION_COUNT, MAX_KEYBOARD_BYTES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_TIMEOUT_FRAMES, maskCoversFrame } from "../protocol/vice.ts";
|
|
90
90
|
export { MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, type TraceHit, type TraceKind } from "../protocol/vice.ts";
|
|
@@ -58,10 +58,33 @@ export function readProjectFile(path: string, options: { root?: string } = {}):
|
|
|
58
58
|
return { bytes: readFileSync(real), type: extname(path).slice(1).toLowerCase() };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Why a file cannot be written, by the error code of the file system; the codes that are a fault of the path. */
|
|
62
|
+
const WRITE_PROBLEMS: Record<string, string> = {
|
|
63
|
+
EISDIR: "is a folder. Give the path of a file",
|
|
64
|
+
EEXIST: "has a file where a folder must be. Give a path whose folders are folders",
|
|
65
|
+
ENOTDIR: "has a file where a folder must be. Give a path whose folders are folders",
|
|
66
|
+
ENOENT: "is in a folder that does not exist",
|
|
67
|
+
EACCES: "cannot be written: there is no permission. Give a path that you can write",
|
|
68
|
+
EPERM: "cannot be written: there is no permission. Give a path that you can write",
|
|
69
|
+
EROFS: "is on a file system that is read-only. Give another path",
|
|
70
|
+
ENAMETOOLONG: "is too long. Give a shorter path",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The refusal for a write that the file system refused because of the path
|
|
75
|
+
* (a folder, a file in place of a folder, no permission): invalid-input with
|
|
76
|
+
* the path. Undefined for another error, which is no fault of the path.
|
|
77
|
+
*/
|
|
78
|
+
export function writeRefusal(error: unknown, path: string, what?: string): WireFailure | undefined {
|
|
79
|
+
const problem = WRITE_PROBLEMS[(error as NodeJS.ErrnoException | undefined)?.code ?? ""];
|
|
80
|
+
return problem === undefined ? undefined : new WireFailure("invalid-input", `${what === undefined ? "" : `${what}: `}${path} ${problem}.`);
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
/**
|
|
62
84
|
* Writes bytes to a project file on the client side, making its directories
|
|
63
85
|
* and overwriting it. Throws WireFailure(invalid-input) for a path outside the
|
|
64
|
-
* project, also one that leaves it through a symbolic link
|
|
86
|
+
* project, also one that leaves it through a symbolic link, and for a path
|
|
87
|
+
* that the file system refuses: a folder, a file in place of a folder, no permission.
|
|
65
88
|
*/
|
|
66
89
|
export function writeProjectFile(path: string, bytes: Uint8Array, options: { root?: string; what?: string } = {}): void {
|
|
67
90
|
const root = options.root ?? currentProject();
|
|
@@ -76,16 +99,20 @@ export function writeProjectFile(path: string, bytes: Uint8Array, options: { roo
|
|
|
76
99
|
let existing = dirname(file);
|
|
77
100
|
while (!existsSync(existing)) existing = dirname(existing);
|
|
78
101
|
if (!isInside(realpathSync(root), realpathSync(existing))) throw refuse();
|
|
79
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
80
|
-
if (!isInside(realpathSync(root), realpathSync(dirname(file)))) throw refuse();
|
|
81
|
-
let link = false;
|
|
82
102
|
try {
|
|
83
|
-
|
|
103
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
104
|
+
if (!isInside(realpathSync(root), realpathSync(dirname(file)))) throw refuse();
|
|
105
|
+
let link = false;
|
|
106
|
+
try {
|
|
107
|
+
link = lstatSync(file).isSymbolicLink();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
110
|
+
}
|
|
111
|
+
if (link) throw refuse();
|
|
112
|
+
writeFileSync(file, bytes);
|
|
84
113
|
} catch (error) {
|
|
85
|
-
|
|
114
|
+
throw writeRefusal(error, path, options.what) ?? error;
|
|
86
115
|
}
|
|
87
|
-
if (link) throw refuse();
|
|
88
|
-
writeFileSync(file, bytes);
|
|
89
116
|
}
|
|
90
117
|
|
|
91
118
|
/** Directories never staged: version control and the toolkit's own project state. */
|
|
@@ -119,7 +119,7 @@ function value(node: Node, resolve: (name: string) => number | undefined): numbe
|
|
|
119
119
|
return node.value;
|
|
120
120
|
case "name": {
|
|
121
121
|
const found = resolve(node.name);
|
|
122
|
-
if (found === undefined) throw new Refusal("invalid-input",
|
|
122
|
+
if (found === undefined) throw new Refusal("invalid-input", `${node.name} is not a symbol of the knowledge or a platform name. Give a number, a symbol that knowledge.ts symbols lists, or a platform name that memory-map.ts at gives.`);
|
|
123
123
|
return found;
|
|
124
124
|
}
|
|
125
125
|
case "unary": {
|
package/src/knowledge/history.ts
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type SymbolRow,
|
|
39
39
|
} from "./read.ts";
|
|
40
40
|
import { DEFAULT_SPACE, type EvidenceKind, type Origin, type Space } from "./schema.ts";
|
|
41
|
-
import { compareText } from "./text.ts";
|
|
41
|
+
import { compareText, NO_REVISION } from "./text.ts";
|
|
42
42
|
import type { Evidence } from "./write.ts";
|
|
43
43
|
|
|
44
44
|
export interface Revision {
|
|
@@ -49,6 +49,8 @@ export interface Revision {
|
|
|
49
49
|
description?: string;
|
|
50
50
|
inputHash?: string;
|
|
51
51
|
toolVersion?: string;
|
|
52
|
+
/** A revert: the revision whose knowledge it restored. */
|
|
53
|
+
restores?: string;
|
|
52
54
|
/** What the change rests on. */
|
|
53
55
|
evidence?: Evidence[];
|
|
54
56
|
}
|
|
@@ -79,6 +81,7 @@ function toRevision(row: Raw): Revision {
|
|
|
79
81
|
if (row.description !== null) revision.description = row.description as string;
|
|
80
82
|
if (row.input_hash !== null) revision.inputHash = row.input_hash as string;
|
|
81
83
|
if (row.tool_version !== null) revision.toolVersion = row.tool_version as string;
|
|
84
|
+
if (row.restored_revision !== null) revision.restores = row.restored_revision as string;
|
|
82
85
|
return revision;
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -170,8 +173,21 @@ export function revisions(db: DatabaseSync | undefined, options: { limit?: numbe
|
|
|
170
173
|
return (rows as Raw[]).map((row) => withEvidence(db, toRevision(row)));
|
|
171
174
|
}
|
|
172
175
|
|
|
176
|
+
/** Revision 0: the knowledge before every revision. It is empty, and no write made it, so it has no time, origin or evidence. */
|
|
177
|
+
export interface InitialRevision {
|
|
178
|
+
id: typeof NO_REVISION;
|
|
179
|
+
operation: "none";
|
|
180
|
+
description: string;
|
|
181
|
+
createdAt?: undefined;
|
|
182
|
+
origin?: undefined;
|
|
183
|
+
inputHash?: undefined;
|
|
184
|
+
toolVersion?: undefined;
|
|
185
|
+
restores?: undefined;
|
|
186
|
+
evidence?: undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
173
189
|
export interface RevisionChanges {
|
|
174
|
-
revision: Revision;
|
|
190
|
+
revision: Revision | InitialRevision;
|
|
175
191
|
/** Rows this revision made current. */
|
|
176
192
|
added: HistoryEntry[];
|
|
177
193
|
/** Rows this revision closed. */
|
|
@@ -181,6 +197,7 @@ export interface RevisionChanges {
|
|
|
181
197
|
/** One revision and exactly what it changed. */
|
|
182
198
|
export function revision(db: DatabaseSync | undefined, text: string): RevisionChanges {
|
|
183
199
|
const id = resolveRevision(db, text);
|
|
200
|
+
if (id === NO_REVISION) return { revision: { id: NO_REVISION, operation: "none", description: "The knowledge before every revision. It is empty." }, added: [], closed: [] };
|
|
184
201
|
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
185
202
|
if (db === undefined || raw === undefined) throw new Refusal("not-found", `There is no revision ${id}.`);
|
|
186
203
|
const tables = [
|
package/src/knowledge/names.ts
CHANGED
|
@@ -20,7 +20,8 @@ export function addressOfName(db: DatabaseSync | undefined, name: string, space:
|
|
|
20
20
|
if (addresses.length === 1) return addresses[0]!;
|
|
21
21
|
const where = space === DEFAULT_SPACE ? "" : ` in ${space}`;
|
|
22
22
|
if (addresses.length > 1) throw new Refusal("invalid-input", `the analyzers give ${name} to ${rows.map((row) => `${formatC64Address(row.address)} (${row.origin})`).join(" and ")}${where}. Give the address`);
|
|
23
|
-
|
|
23
|
+
const option = space === DEFAULT_SPACE ? "" : ` --space ${space}`;
|
|
24
|
+
throw new Refusal("not-found", `${name} is not a name in the knowledge${where}. Give a $ address, or a name that knowledge.ts symbols${option} lists, with or without --analyzer.`);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/** Every name of a space with its address: the symbols, and each analyzer name that addressOfName accepts. With kinds, only names of those kinds. */
|
package/src/knowledge/program.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { Refusal } from "../errors.ts";
|
|
|
10
10
|
import { type AddressRange, COVERAGE, formatC64Address } from "../c64.ts";
|
|
11
11
|
|
|
12
12
|
import { COVERAGE_KINDS, DEFAULT_SPACE, MEMORY_ROW, type CoverageKind, type Space } from "./schema.ts";
|
|
13
|
+
import { coverageRuns } from "./read.ts";
|
|
13
14
|
import { checkSpace, write, type Evidence, type WriteContext } from "./write.ts";
|
|
14
15
|
|
|
15
16
|
/** The 64 KiB of one space: offset n is address n. */
|
|
@@ -147,22 +148,6 @@ function currentCoverage(db: DatabaseSync, space: Space): Map<CoverageKind, Uint
|
|
|
147
148
|
return maps;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
/** One import of a coverage map: its project path, its SHA-256, the space it went into and the revision. */
|
|
151
|
-
export interface CoverageRun {
|
|
152
|
-
name: string;
|
|
153
|
-
sha256: string;
|
|
154
|
-
space: Space;
|
|
155
|
-
revision: string;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** The imports of coverage maps, by name and then oldest first: the last import of a name is the latest. */
|
|
159
|
-
export function coverageRuns(db: DatabaseSync | undefined): CoverageRun[] {
|
|
160
|
-
if (db === undefined) return [];
|
|
161
|
-
return db
|
|
162
|
-
.prepare(`SELECT DISTINCT e.ref AS name, e.sha256 AS sha256, coalesce(e.space, '${DEFAULT_SPACE}') AS space, e.revision AS revision FROM evidence e JOIN revisions r ON r.id = e.revision WHERE r.operation = 'coverage' AND e.kind = 'coverage' ORDER BY e.ref, e.revision`)
|
|
163
|
-
.all() as unknown as CoverageRun[];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
151
|
/**
|
|
167
152
|
* Joins coverage maps into the coverage of a space: adds the ranges of
|
|
168
153
|
* the flags that no imported run set before, and as wx the addresses that one
|
package/src/knowledge/read.ts
CHANGED
|
@@ -243,6 +243,49 @@ export function currentRevision(db: DatabaseSync | undefined): string {
|
|
|
243
243
|
return (db.prepare("SELECT max(id) AS id FROM revisions").get() as { id: string | null }).id ?? "0";
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/** One import of a coverage map: its project path, its SHA-256, the space it went into and the revision. */
|
|
247
|
+
export interface CoverageRun {
|
|
248
|
+
name: string;
|
|
249
|
+
sha256: string;
|
|
250
|
+
space: Space;
|
|
251
|
+
revision: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The imports of coverage maps that the knowledge has, by name and then oldest
|
|
256
|
+
* first: the last import of a name is the latest. A revert gives back the maps
|
|
257
|
+
* of the revision that it restores, so a map that a revert removed is not known.
|
|
258
|
+
* With `at`, the maps of the knowledge at that revision.
|
|
259
|
+
*/
|
|
260
|
+
export function coverageRuns(db: DatabaseSync | undefined, at?: string): CoverageRun[] {
|
|
261
|
+
if (db === undefined || at === NO_REVISION) return [];
|
|
262
|
+
const rows = db
|
|
263
|
+
.prepare(
|
|
264
|
+
`SELECT r.id AS id, r.operation AS operation, r.restored_revision AS restored, e.ref AS name, e.sha256 AS sha256, coalesce(e.space, '${DEFAULT_SPACE}') AS space
|
|
265
|
+
FROM revisions r LEFT JOIN evidence e ON e.revision = r.id AND e.kind = 'coverage'
|
|
266
|
+
WHERE (r.operation = 'coverage' AND e.id IS NOT NULL) OR (r.operation = 'revert' AND r.restored_revision IS NOT NULL)
|
|
267
|
+
ORDER BY r.id, e.id`,
|
|
268
|
+
)
|
|
269
|
+
.all() as Array<{ id: string; operation: string; restored: string | null; name: string | null; sha256: string | null; space: Space }>;
|
|
270
|
+
// The maps after each revision that changed them, oldest first.
|
|
271
|
+
const after: Array<{ id: string; runs: CoverageRun[] }> = [];
|
|
272
|
+
let runs: CoverageRun[] = [];
|
|
273
|
+
for (const row of rows) {
|
|
274
|
+
if (at !== undefined && row.id > at) break;
|
|
275
|
+
if (row.operation === "revert") {
|
|
276
|
+
const restored = row.restored!;
|
|
277
|
+
runs = [];
|
|
278
|
+
for (const item of after) if (item.id <= restored) runs = item.runs;
|
|
279
|
+
} else {
|
|
280
|
+
const run: CoverageRun = { name: row.name!, sha256: row.sha256!, space: row.space, revision: row.id };
|
|
281
|
+
if (!runs.some((item) => item.name === run.name && item.sha256 === run.sha256 && item.space === run.space && item.revision === run.revision)) runs = [...runs, run];
|
|
282
|
+
}
|
|
283
|
+
if (after.at(-1)?.id === row.id) after[after.length - 1]!.runs = runs;
|
|
284
|
+
else after.push({ id: row.id, runs });
|
|
285
|
+
}
|
|
286
|
+
return [...runs].sort((a, b) => (a.name === b.name ? (a.revision < b.revision ? -1 : a.revision > b.revision ? 1 : 0) : a.name < b.name ? -1 : 1));
|
|
287
|
+
}
|
|
288
|
+
|
|
246
289
|
/** At most this many ids are named when a start of an id fits several revisions. */
|
|
247
290
|
const NAMED_REVISIONS = 5;
|
|
248
291
|
|
package/src/knowledge/schema.ts
CHANGED
|
@@ -113,7 +113,8 @@ function createSchema(db: DatabaseSync): void {
|
|
|
113
113
|
operation TEXT NOT NULL,
|
|
114
114
|
input_hash TEXT,
|
|
115
115
|
tool_version TEXT,
|
|
116
|
-
description TEXT
|
|
116
|
+
description TEXT,
|
|
117
|
+
restored_revision TEXT
|
|
117
118
|
);
|
|
118
119
|
CREATE TABLE symbols (
|
|
119
120
|
id TEXT PRIMARY KEY,
|
|
@@ -316,7 +317,7 @@ function createSchema(db: DatabaseSync): void {
|
|
|
316
317
|
}
|
|
317
318
|
|
|
318
319
|
/** The version of the index this build makes. An index with another version is emptied and made again. */
|
|
319
|
-
export const INDEX_VERSION =
|
|
320
|
+
export const INDEX_VERSION = 7;
|
|
320
321
|
|
|
321
322
|
function indexVersion(db: DatabaseSync): number | undefined {
|
|
322
323
|
const hasMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'meta'").get();
|
package/src/knowledge/text.ts
CHANGED
|
@@ -187,6 +187,7 @@ const REVISION_FIELDS: readonly Field[] = [
|
|
|
187
187
|
field("reason", "description", "text", true),
|
|
188
188
|
field("inputHash", "input_hash", "text", true),
|
|
189
189
|
field("toolVersion", "tool_version", "text", true),
|
|
190
|
+
field("restores", "restored_revision", "text", true),
|
|
190
191
|
];
|
|
191
192
|
|
|
192
193
|
/** The SQL name of a table: "references" is a keyword. */
|
package/src/knowledge/write.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
referencesFrom,
|
|
33
33
|
REGION_COLUMNS,
|
|
34
34
|
regionsOverlapping,
|
|
35
|
+
coverageRuns,
|
|
35
36
|
resolveRevision,
|
|
36
37
|
SUPPRESSION_COLUMNS,
|
|
37
38
|
suppressionsOverlapping,
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
49
50
|
toSuppression,
|
|
50
51
|
toSymbol,
|
|
51
52
|
type CommentRow,
|
|
53
|
+
type CoverageRun,
|
|
52
54
|
type MacroRow,
|
|
53
55
|
type MacroUseRow,
|
|
54
56
|
type ModuleRow,
|
|
@@ -59,7 +61,7 @@ import {
|
|
|
59
61
|
type SuppressionRow,
|
|
60
62
|
type SymbolRow,
|
|
61
63
|
} from "./read.ts";
|
|
62
|
-
import { COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, MACRO_NAME, MACRO_PARAM, SWITCH_NAME, NO_ELEMENT_TYPES, OPERAND_SELECTS, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type EvidenceKind, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
64
|
+
import { CODE_REGION_TYPES, COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, MACRO_NAME, MACRO_PARAM, SWITCH_NAME, NO_ELEMENT_TYPES, OPERAND_SELECTS, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type EvidenceKind, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
63
65
|
import { newRevisionId, rowId, sqlName } from "./text.ts";
|
|
64
66
|
|
|
65
67
|
/** What a change rests on: a coverage run, a test or another project file with its SHA-256, or a note. */
|
|
@@ -136,6 +138,8 @@ export interface ChangeContext {
|
|
|
136
138
|
inputHash?: string;
|
|
137
139
|
toolVersion?: string;
|
|
138
140
|
evidence?: Evidence[];
|
|
141
|
+
/** A revert: the revision whose knowledge it restores. */
|
|
142
|
+
restores?: string;
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
/**
|
|
@@ -177,8 +181,8 @@ export class Change {
|
|
|
177
181
|
const now = new Date();
|
|
178
182
|
const id = newRevisionId(currentRevision(this.#db), now);
|
|
179
183
|
this.#db
|
|
180
|
-
.prepare("INSERT INTO revisions (id, created_at, origin, operation, input_hash, tool_version, description) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
181
|
-
.run(id, isoCet(now), this.#context.origin, this.#operation, this.#context.inputHash ?? null, this.#context.toolVersion ?? null, this.#context.description ?? null);
|
|
184
|
+
.prepare("INSERT INTO revisions (id, created_at, origin, operation, input_hash, tool_version, description, restored_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
185
|
+
.run(id, isoCet(now), this.#context.origin, this.#operation, this.#context.inputHash ?? null, this.#context.toolVersion ?? null, this.#context.description ?? null, this.#context.restores ?? null);
|
|
182
186
|
this.revision = id;
|
|
183
187
|
const evidence = this.#db.prepare("INSERT INTO evidence (revision, kind, ref, sha256, space) VALUES (?, ?, ?, ?, ?)");
|
|
184
188
|
for (const item of this.#context.evidence ?? []) evidence.run(id, item.kind, item.ref, item.sha256 ?? null, item.space ?? null);
|
|
@@ -321,7 +325,7 @@ function checkEvidence(evidence: Evidence[] | undefined): void {
|
|
|
321
325
|
}
|
|
322
326
|
|
|
323
327
|
/** Runs semantic writes in one transaction, as one revision. */
|
|
324
|
-
export function write<T>(db: DatabaseSync, context: WriteContext, operation: string, work: (change: Change) => T): { revision: string | null; current: T } {
|
|
328
|
+
export function write<T>(db: DatabaseSync, context: WriteContext & Pick<ChangeContext, "restores">, operation: string, work: (change: Change) => T): { revision: string | null; current: T } {
|
|
325
329
|
if (context.origin !== "user" && context.origin !== "llm") {
|
|
326
330
|
throw new Refusal("invalid-input", "A semantic write comes from the user or the LLM.");
|
|
327
331
|
}
|
|
@@ -572,6 +576,32 @@ function checkOperand(request: OperandRequest): void {
|
|
|
572
576
|
|
|
573
577
|
const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${value.toString(16).padStart(size === 0x100 ? 2 : 4, "0")}`);
|
|
574
578
|
|
|
579
|
+
/**
|
|
580
|
+
* Refuses an address that is not the start of an instruction. The code
|
|
581
|
+
* regions of the knowledge give the instructions: they are decoded from the
|
|
582
|
+
* start of the region, as the listing and the source decode them.
|
|
583
|
+
*/
|
|
584
|
+
function checkInstructionStart(db: DatabaseSync, space: Space, image: OperandImage, address: number): void {
|
|
585
|
+
const where = formatC64Address(address);
|
|
586
|
+
const code = regionsOverlapping(db, address, address, space).find((region) => CODE_REGION_TYPES.includes(region.type));
|
|
587
|
+
if (code === undefined) {
|
|
588
|
+
throw new Refusal("invalid-input", `${where} is in no code region, so the knowledge does not know an instruction that starts there. Classify the code that holds the instruction first (classify <range> code), then write the operand.`);
|
|
589
|
+
}
|
|
590
|
+
let start = code.start;
|
|
591
|
+
let next = code.start;
|
|
592
|
+
while (next < address) {
|
|
593
|
+
const opcode = image.byteAt(next);
|
|
594
|
+
if (opcode === undefined) {
|
|
595
|
+
throw new Refusal("invalid-input", `The code region ${formatC64Address(code.start)}-${formatC64Address(code.end)} has no program byte at ${formatC64Address(next)}, so its instructions up to ${where} are not known. Correct the region (classify), then write the operand.`);
|
|
596
|
+
}
|
|
597
|
+
start = next;
|
|
598
|
+
next += instructionLength(opcode);
|
|
599
|
+
}
|
|
600
|
+
if (next !== address) {
|
|
601
|
+
throw new Refusal("invalid-input", `${where} is not the start of an instruction. It is an operand byte of the instruction at ${formatC64Address(start)}, decoded from the start of the code region at ${formatC64Address(code.start)}. Give ${formatC64Address(start)}.`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
575
605
|
/**
|
|
576
606
|
* Refuses an expression whose value is not the operand in the image, and
|
|
577
607
|
* returns the addresses of its names. Symbols of the knowledge come first, then
|
|
@@ -581,6 +611,7 @@ function checkOperandValue(db: DatabaseSync, space: Space, image: OperandImage,
|
|
|
581
611
|
const where = formatC64Address(request.address);
|
|
582
612
|
const opcode = image.byteAt(request.address);
|
|
583
613
|
if (opcode === undefined) throw new Refusal("invalid-input", `${where} is outside the image.`);
|
|
614
|
+
checkInstructionStart(db, space, image, request.address);
|
|
584
615
|
const length = instructionLength(opcode);
|
|
585
616
|
if (length === 1) throw new Refusal("invalid-input", `The instruction at ${where} (opcode ${shown(opcode, 0x100)}) has no operand.`);
|
|
586
617
|
if ((opcode & 0x1f) === 0x10) throw new Refusal("invalid-input", `The instruction at ${where} is a branch: its operand is a distance, not a value.`);
|
|
@@ -1171,8 +1202,11 @@ const KEYS = {
|
|
|
1171
1202
|
* revision. History is never erased; restored rows keep their origin.
|
|
1172
1203
|
*/
|
|
1173
1204
|
export function revert(db: DatabaseSync, context: WriteContext, request: { revision: string }): WriteResult<{ restoredRevision: string }> {
|
|
1174
|
-
|
|
1175
|
-
|
|
1205
|
+
const restored = resolveRevision(db, request.revision);
|
|
1206
|
+
return write(db, { ...context, restores: restored }, "revert", (change) => {
|
|
1207
|
+
// The coverage maps that the knowledge has follow the revert, also when no coverage range changes.
|
|
1208
|
+
const maps = (runs: CoverageRun[]) => JSON.stringify(runs.map((run) => [run.name, run.sha256, run.space, run.revision]));
|
|
1209
|
+
if (maps(coverageRuns(db, restored)) !== maps(coverageRuns(db))) change.id();
|
|
1176
1210
|
const at = "valid_from_revision <= ? AND (valid_to_revision IS NULL OR valid_to_revision > ?)";
|
|
1177
1211
|
for (const [table, spec] of Object.entries(KEYS)) {
|
|
1178
1212
|
const name = table === "references" ? '"references"' : table;
|
package/src/mcp/output.ts
CHANGED
|
@@ -22,6 +22,11 @@ export const WrittenOutput = {
|
|
|
22
22
|
sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/** Lines as file text: each line ends with a newline, and no lines give an empty file. */
|
|
26
|
+
export function linesText(lines: readonly string[]): string {
|
|
27
|
+
return lines.map((line) => `${line}\n`).join("");
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
/** Writes `data` to the file `path` of the project that `project` names (or the project lookup finds), and gives what the result shows in its place. */
|
|
26
31
|
export function writeOut(path: string, data: Uint8Array | string, project: string | undefined): Written {
|
|
27
32
|
return writeReported(path, data, { what: "out", root: projectFolder(project) });
|
package/src/mcp/server.ts
CHANGED
|
@@ -91,6 +91,17 @@ export function requireFields(action: string, given: Record<string, unknown>, al
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Checks the fields that go with out: project only with out, and the
|
|
96
|
+
* fields of `notWithOut` only without it.
|
|
97
|
+
*/
|
|
98
|
+
export function requireOut(given: { out?: unknown; project?: unknown } & Record<string, unknown>, notWithOut: readonly string[] = []): void {
|
|
99
|
+
if (given.out === undefined && given.project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
|
|
100
|
+
for (const field of notWithOut) {
|
|
101
|
+
if (given.out !== undefined && given[field] !== undefined) throw new WireFailure("invalid-input", `${field} is not used with out.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
94
105
|
/** A tool result that carries content blocks (for example an image) beside its structured result. */
|
|
95
106
|
export class ToolOutput<T> {
|
|
96
107
|
readonly structured: T;
|
|
@@ -163,12 +174,16 @@ function errorResult(error: WireError): CallToolResult {
|
|
|
163
174
|
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: { code: error.code, message: shownMessage(error) } }) }] };
|
|
164
175
|
}
|
|
165
176
|
|
|
166
|
-
/**
|
|
167
|
-
|
|
177
|
+
/**
|
|
178
|
+
* "address: Invalid string: must match ..." style messages, one sentence per problem.
|
|
179
|
+
* An unknown field of the tool input also gets the fields that the tool takes.
|
|
180
|
+
*/
|
|
181
|
+
function describeIssues(error: z.ZodError, input?: z.ZodObject): string {
|
|
168
182
|
return error.issues
|
|
169
183
|
.map((issue) => {
|
|
170
184
|
const where = issue.path.length === 0 ? "input" : issue.path.join(".");
|
|
171
|
-
|
|
185
|
+
const fields = issue.code === "unrecognized_keys" && issue.path.length === 0 && input !== undefined ? `. The fields are ${Object.keys(input.shape).join(", ")}` : "";
|
|
186
|
+
return `${where}: ${issue.message}${fields}`;
|
|
172
187
|
})
|
|
173
188
|
.join(". ");
|
|
174
189
|
}
|
|
@@ -211,7 +226,7 @@ async function runCall(
|
|
|
211
226
|
const tool = tools.get(name);
|
|
212
227
|
if (tool === undefined) return errorResult({ code: "invalid-input", message: `There is no tool named ${name}.` });
|
|
213
228
|
const parsed = tool.inputSchema.safeParse(args ?? {});
|
|
214
|
-
if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error) });
|
|
229
|
+
if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error, tool.inputSchema) });
|
|
215
230
|
try {
|
|
216
231
|
let output: unknown;
|
|
217
232
|
try {
|
package/src/mcp/tools/debug.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { COVERAGE, executedRamCode, EXECUTE, formatC64Address, READ, WRITE, type
|
|
|
7
7
|
import { WireFailure } from "../../protocol/messages.ts";
|
|
8
8
|
import { CHECKPOINT_ACTIONS, MAX_BACKTRACE, MAX_HISTORY, MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, TRACE_ENDS, MAX_MEMMAP_RANGES, MAX_MEMORY_READ, MAX_PROFILE, MAX_WATCH_SIZE, SPACES, TIMING_ACTIONS, WATCH_ACCESS, type Breakpoint, type Watchpoint } from "../../protocol/vice.ts";
|
|
9
9
|
import { readCoverage } from "../../host-client/coverage.ts";
|
|
10
|
-
import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
10
|
+
import { linesText, OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
11
11
|
import { AddressInput, Byte, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
|
|
12
|
-
import { AddressOutput, ConditionOutput, defineTool, HexData, requireFields, showCondition, toCondition } from "../server.ts";
|
|
12
|
+
import { AddressOutput, ConditionOutput, defineTool, HexData, requireFields, requireOut, showCondition, toCondition } from "../server.ts";
|
|
13
13
|
import { spaced } from "./memory.ts";
|
|
14
14
|
|
|
15
15
|
/** No default here: a filled-in default would count as a field the other actions refuse. */
|
|
@@ -169,7 +169,7 @@ export const c64CpuHistory = defineTool({
|
|
|
169
169
|
const raster = entry.rasterLine === undefined ? "" : ` line ${entry.rasterLine} cycle ${entry.rasterCycle ?? 0}`;
|
|
170
170
|
return `${formatC64Address(entry.address)} ${spaced(entry.bytes).padEnd(9)} ${entry.text.padEnd(16)} a=${byte(entry.a)} x=${byte(entry.x)} y=${byte(entry.y)} sp=${byte(entry.sp)}${raster}`;
|
|
171
171
|
});
|
|
172
|
-
return { count: entries.length, ...writeOut(out,
|
|
172
|
+
return { count: entries.length, ...writeOut(out, linesText(lines), project) };
|
|
173
173
|
},
|
|
174
174
|
});
|
|
175
175
|
|
|
@@ -184,6 +184,7 @@ export const c64Trace = defineTool({
|
|
|
184
184
|
"A point is exec (an instruction at an address from start to end), read or write (an access from start to end). " +
|
|
185
185
|
`Give 1 to ${MAX_TRACE_POINTS} points. ` +
|
|
186
186
|
"Each hit gives the instruction (pc, bytes, text) and the registers a, x, y and sp before it. " +
|
|
187
|
+
"point in a hit is the number of its point in points, 0 for the first. " +
|
|
187
188
|
"It also gives the frame from 0, and the raster line and cycle where the instruction started. " +
|
|
188
189
|
"A read or write hit also gives the address that the instruction used, and the byte there after the access. " +
|
|
189
190
|
`The trace ends after frames frames (1 to ${MAX_TRACE_FRAMES}), at maxHits hits (at most ${MAX_TRACE_HITS}), or at a breakpoint, a watchpoint or a JAM. ` +
|
|
@@ -233,7 +234,7 @@ export const c64Trace = defineTool({
|
|
|
233
234
|
const shown = traced.hits.map(({ address, ...hit }) => ({ ...hit, pc: formatC64Address(hit.pc), ...(address === undefined ? {} : { address: formatC64Address(address) }) }));
|
|
234
235
|
const summary = { end: traced.end, frames: traced.frames, pc: formatC64Address(traced.pc), count: traced.hits.length, hits: shown.slice(0, TRACE_LISTED) };
|
|
235
236
|
if (out === undefined) return summary;
|
|
236
|
-
return { ...summary, ...writeOut(out,
|
|
237
|
+
return { ...summary, ...writeOut(out, linesText(shown.map((hit) => JSON.stringify(hit))), project) };
|
|
237
238
|
},
|
|
238
239
|
});
|
|
239
240
|
|
|
@@ -392,11 +393,11 @@ export const c64Memmap = defineTool({
|
|
|
392
393
|
...(written.length > MAX_SUMMARY_RANGES ? { writtenAndExecutedTruncated: true } : {}),
|
|
393
394
|
};
|
|
394
395
|
}
|
|
396
|
+
requireOut(fields, ["maxRanges"]);
|
|
397
|
+
requireFields("read", fields, ["start", "end", "maxRanges", "out", "project"]);
|
|
395
398
|
if (fields.out !== undefined) {
|
|
396
|
-
requireFields("read", fields, ["start", "end", "out", "project"]);
|
|
397
399
|
return writeOut(fields.out, await readCoverage(session, start, end), fields.project);
|
|
398
400
|
}
|
|
399
|
-
requireFields("read", fields, ["start", "end", "maxRanges"]);
|
|
400
401
|
const result = await session.memmap({ action: "read", start, end, maxRanges: fields.maxRanges ?? 256 });
|
|
401
402
|
const ranges = result.ranges.map((range) => ({
|
|
402
403
|
start: formatC64Address(range.start),
|