@henols/c64-re-tools 2.1.0-rc.2 → 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
|
@@ -14,7 +14,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
14
14
|
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
15
15
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
16
16
|
import { READY_FRAMES } from "#src/host-client/entry-state.ts";
|
|
17
|
-
import { ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, keyboardBytes, stepKind, type ActionStep, type Location } from "#src/host-client/steps.ts";
|
|
17
|
+
import { ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, keyboardBytes, renamedField, stepKind, type ActionStep, type Location } from "#src/host-client/steps.ts";
|
|
18
18
|
import { readProjectFile, type Written } from "#src/host-client/transfer.ts";
|
|
19
19
|
import { PAL_FRAME_CYCLES, type C64RunSession, type CoreStatus } from "#src/native/c64run.ts";
|
|
20
20
|
import type { Workspace } from "#src/native/staging.ts";
|
|
@@ -46,7 +46,7 @@ export interface StateItem {
|
|
|
46
46
|
size: number;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/** The input of
|
|
49
|
+
/** The input of testing.ts record. */
|
|
50
50
|
export interface RecordFile {
|
|
51
51
|
name: string;
|
|
52
52
|
program: string;
|
|
@@ -153,13 +153,20 @@ export function parseStateItem(text: string): StateItem {
|
|
|
153
153
|
return { address: match[1]!, size };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
/** Checks the memory items of state or pin (`what`): names, $ addresses, or { address, size }. */
|
|
157
|
+
export function checkState(value: unknown, fail: (message: string) => never, what = "state"): StateItem[] {
|
|
158
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_STATE_ITEMS) fail(`${what} must be a list of 1 to ${MAX_STATE_ITEMS} memory items`);
|
|
158
159
|
return (value as unknown[]).map((item, index) => {
|
|
159
160
|
const spec = typeof item === "string" ? { address: item } : item;
|
|
160
|
-
if (
|
|
161
|
+
if (isObject(spec)) {
|
|
162
|
+
for (const key of Object.keys(spec)) {
|
|
163
|
+
const renamed = renamedField(key, ["address", "size"]);
|
|
164
|
+
if (key !== "address" && key !== "size") fail(`${what} item ${index + 1} has the field ${key}. ${renamed ?? "A memory item has address and size, and nothing else"}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!isObject(spec) || typeof spec.address !== "string") fail(`${what} item ${index + 1} must be a name, a $ address, or { address, size }`);
|
|
161
168
|
const size = (spec as { size?: unknown }).size ?? 1;
|
|
162
|
-
if (!isInteger(size, 1, MAX_STATE_BYTES)) fail(
|
|
169
|
+
if (!isInteger(size, 1, MAX_STATE_BYTES)) fail(`${what} item ${index + 1}: size must be from 1 to ${MAX_STATE_BYTES}`);
|
|
163
170
|
return { address: (spec as { address: string }).address, size: size as number };
|
|
164
171
|
});
|
|
165
172
|
}
|
|
@@ -195,7 +202,7 @@ export function readRecordFile(path: string): RecordFile {
|
|
|
195
202
|
if ((value.checkpoint === undefined) !== (value.state === undefined)) fail("give checkpoint and state together, or neither: a replay can give them");
|
|
196
203
|
if (value.checkpoint !== undefined && (typeof value.checkpoint !== "string" || value.checkpoint === "")) fail("checkpoint must be the name or $ address of a routine");
|
|
197
204
|
if (value.pin !== undefined && value.checkpoint === undefined) fail("pin needs checkpoint and state");
|
|
198
|
-
const pin = value.pin === undefined ? undefined : checkState(value.pin, fail);
|
|
205
|
+
const pin = value.pin === undefined ? undefined : checkState(value.pin, fail, "pin");
|
|
199
206
|
const compare = value.checkpoint === undefined ? undefined : { checkpoint: value.checkpoint as string, state: checkState(value.state, fail), ...(pin === undefined ? {} : { pin }) };
|
|
200
207
|
const inputs = checkInputs(value.inputs, fail);
|
|
201
208
|
if (!Array.isArray(value.steps) || value.steps.length === 0) fail("steps must be a list with at least one step");
|
|
@@ -219,7 +226,14 @@ export function readRecordFile(path: string): RecordFile {
|
|
|
219
226
|
/** What the replay core cannot do of a step that is right for VICE: it has the C64 alone, no raster wait and no conditions. */
|
|
220
227
|
function checkCoreStep(step: ActionStep, at: string, fail: (message: string) => never): void {
|
|
221
228
|
if ("write" in step && step.write.space !== "c64") fail(`${at}: the replay core has the C64 alone, not ${step.write.space}`);
|
|
222
|
-
if ("press" in step)
|
|
229
|
+
if ("press" in step) {
|
|
230
|
+
try {
|
|
231
|
+
keyCodes(step.press);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (isRefusal(error)) fail(`${at}: ${error.message}`);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
223
237
|
if ("type" in step) {
|
|
224
238
|
const keys = keyboardBytes(step.type);
|
|
225
239
|
if (typeof keys !== "string" && keys.length > QUEUE_SIZE) fail(`${at}: the KERNAL keyboard queue holds ${QUEUE_SIZE} keys; give more type steps, or press for keys of the keyboard matrix`);
|
|
@@ -242,10 +256,31 @@ export function readRecording(folder: string): Recording {
|
|
|
242
256
|
if (isRefusal(error)) throw error;
|
|
243
257
|
throw new Refusal("invalid-input", `recording ${path}: ${(error as Error).message}`);
|
|
244
258
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!
|
|
259
|
+
const fail = (message: string): never => {
|
|
260
|
+
throw new Refusal("invalid-input", `recording ${path}: ${message}`);
|
|
261
|
+
};
|
|
262
|
+
if (!isObject(value) || value.format !== RECORDING_FORMAT) return fail(`it is not a recording of format ${RECORDING_FORMAT}`);
|
|
263
|
+
for (const key of ["name", "program", "sha256", "symbols", "hash"]) if (typeof value[key] !== "string") fail(`${key} is missing`);
|
|
264
|
+
for (const key of ["inputs", "events", "sites"]) if (!Array.isArray(value[key])) fail(`${key} is missing`);
|
|
265
|
+
if (!isInteger(value.ticks, 1, Number.MAX_SAFE_INTEGER)) fail("ticks is missing");
|
|
266
|
+
// A recording of an older version can have the step forms of the 2.0 files: refused with the form to write, never replayed wrong.
|
|
267
|
+
const remedy = "Record the session again with record, or write the 2.1 form in recording.json";
|
|
268
|
+
if (value.compare !== undefined) {
|
|
269
|
+
const compare = isObject(value.compare) ? value.compare : fail("compare must be an object with checkpoint, state and pin");
|
|
270
|
+
const where = (message: string): never => fail(`compare: ${message}. ${remedy}`);
|
|
271
|
+
if (typeof compare.checkpoint !== "string" || compare.checkpoint === "") where("checkpoint must be the name or $ address of a routine");
|
|
272
|
+
checkState(compare.state, where);
|
|
273
|
+
if (compare.pin !== undefined) checkState(compare.pin, where, "pin");
|
|
274
|
+
}
|
|
275
|
+
(value.events as unknown[]).forEach((event, index) => {
|
|
276
|
+
const at = `event ${index + 1}`;
|
|
277
|
+
const where = (message: string): never => fail(`${message}. ${remedy}`);
|
|
278
|
+
if (!isObject(event) || !isObject(event.action) || !isObject(event.key)) return where(`${at} must be an object with action and key`);
|
|
279
|
+
const named = typeof event.step === "number" ? `${at} (step ${event.step})` : at;
|
|
280
|
+
if (event.action.restore === true && Object.keys(event.action).length === 1) return;
|
|
281
|
+
// The action with the defaults filled in, as the record file gave it.
|
|
282
|
+
event.action = checkActionStep(stepKind(event.action, ACTION_STEP_KINDS, named, where), event.action, named, where, { press: true });
|
|
283
|
+
});
|
|
249
284
|
return value as unknown as Recording;
|
|
250
285
|
}
|
|
251
286
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
|
|
9
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
10
|
+
import { readPrg } from "#src/host-client/image.ts";
|
|
9
11
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
10
12
|
import { C64RunSession } from "#src/native/c64run.ts";
|
|
11
13
|
import { localToolContext } from "#src/native/local.ts";
|
|
@@ -47,6 +49,15 @@ export async function replay(values: ReplayOptions): Promise<ReplayResult> {
|
|
|
47
49
|
compared: 0,
|
|
48
50
|
...fields,
|
|
49
51
|
});
|
|
52
|
+
// The files of the rebuild are checked before the replay core starts.
|
|
53
|
+
for (const [option, check] of [["--rebuild", () => values.rebuild !== undefined && readPrg(values.rebuild)], ["--symbols", () => values.symbols !== undefined && namesOf(values.symbols)]] as const) {
|
|
54
|
+
try {
|
|
55
|
+
check();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (isRefusal(error)) throw new Refusal(error.code, `${option}: ${error.message}`);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
50
61
|
const sha256 = createHash("sha256").update(readProjectFile(recording.program).bytes).digest("hex");
|
|
51
62
|
if (sha256 !== recording.sha256) return result("INCONCLUSIVE", "original", { reason: `${recording.program} is not the program of the recording (its SHA-256 changed); record the session again` });
|
|
52
63
|
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
7
7
|
import { runProgram } from "#src/host-client/autostart.ts";
|
|
8
8
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
9
|
-
import { ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, runActionStep, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
9
|
+
import { ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, refuseOldStep, renamedField, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
10
|
+
import { readPrg } from "#src/host-client/image.ts";
|
|
10
11
|
import { MAX_MEMORY_READ, MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, WireFailure, type TraceHit, type TraceKind } from "#src/host-client/tools.ts";
|
|
11
12
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
12
13
|
import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
@@ -35,6 +36,40 @@ const TRACE_KEYS = ["trace", "points", "frames", "maxHits"];
|
|
|
35
36
|
const OBSERVE_KEYS = ["observe", "memory", "registers", "vicii", "sprites"];
|
|
36
37
|
/** A name of a file that a step writes: letters, digits, dots, underscores or hyphens. */
|
|
37
38
|
const NAME = /^[A-Za-z0-9._-]{1,64}$/;
|
|
39
|
+
/** The file of the observations, which every run writes. */
|
|
40
|
+
const RESULTS = "results.json";
|
|
41
|
+
|
|
42
|
+
/** The file name of an observed memory item that goes into its own file: more than one read gives. */
|
|
43
|
+
const memoryFile = (observe: string, address: string) => `${observe}-${address.replace(/^\$/, "")}.bin`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The files that each step of a run writes into --out, by step. Two steps
|
|
47
|
+
* that write one name (also in another case: some file systems do not tell
|
|
48
|
+
* them apart) are refused: the second would overwrite the first.
|
|
49
|
+
*/
|
|
50
|
+
function checkFileNames(steps: RunStep[], fail: (message: string) => never): void {
|
|
51
|
+
const writers = new Map<string, string>([[RESULTS, `the run itself (${RESULTS})`]]);
|
|
52
|
+
steps.forEach((step, index) => {
|
|
53
|
+
const named: [string, string[]] =
|
|
54
|
+
"save" in step
|
|
55
|
+
? ["save", [`${step.save}.vsf`]]
|
|
56
|
+
: "screen" in step
|
|
57
|
+
? ["screen", [`${step.screen}.png`]]
|
|
58
|
+
: "coverage" in step
|
|
59
|
+
? ["coverage", [`${step.coverage}.bin`]]
|
|
60
|
+
: "trace" in step
|
|
61
|
+
? ["trace", [`${step.trace}.json`]]
|
|
62
|
+
: "observe" in step
|
|
63
|
+
? ["observe", step.memory === "all" ? [`${step.observe}.bin`] : (step.memory ?? []).flatMap((item) => (typeof item !== "string" && (item.size ?? 1) > MAX_MEMORY_READ ? [memoryFile(step.observe, item.address)] : []))]
|
|
64
|
+
: ["", []];
|
|
65
|
+
const [kind, files] = named;
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
const other = writers.get(file.toLowerCase());
|
|
68
|
+
if (other !== undefined) fail(`step ${index + 1} (${kind}) writes ${file}, and ${other} writes that file too. Give one of them another name`);
|
|
69
|
+
writers.set(file.toLowerCase(), `step ${index + 1} (${kind})`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
38
73
|
|
|
39
74
|
function readRun(path: string): Run {
|
|
40
75
|
let value: unknown;
|
|
@@ -60,8 +95,10 @@ function readRun(path: string): Run {
|
|
|
60
95
|
const at = `step ${index + 1}`;
|
|
61
96
|
if (!isObject(step)) return fail(`${at} must be an object`);
|
|
62
97
|
const kinds = "observe" in step ? ["observe"] : "trace" in step ? ["trace"] : Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key) || (OWN_KINDS as readonly string[]).includes(key));
|
|
63
|
-
if (kinds.length
|
|
98
|
+
if (kinds.length > 1) return fail(`${at} has ${kinds.length} step kinds: ${kinds.join(", ")}. Give each one its own step`);
|
|
99
|
+
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...VICE_STEP_KINDS, ...OWN_KINDS].join(", ")}`);
|
|
64
100
|
const kind = kinds[0]!;
|
|
101
|
+
refuseOldStep(kind, step, at, fail);
|
|
65
102
|
const allowed = kind === "observe" ? OBSERVE_KEYS : kind === "trace" ? TRACE_KEYS : [kind];
|
|
66
103
|
for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
67
104
|
if ((ACTION_STEP_KINDS as readonly string[]).includes(kind)) {
|
|
@@ -81,7 +118,8 @@ function readRun(path: string): Run {
|
|
|
81
118
|
const points = step.points;
|
|
82
119
|
if (!Array.isArray(points) || points.length === 0 || points.length > MAX_TRACE_POINTS) return fail(`${at}: points is a list of 1 to ${MAX_TRACE_POINTS} objects with kind and start`);
|
|
83
120
|
for (const point of points as unknown[]) {
|
|
84
|
-
if (!isObject(point)
|
|
121
|
+
if (!isObject(point)) return fail(`${at}: a point is an object with kind, start and end`);
|
|
122
|
+
for (const key of Object.keys(point)) if (!["kind", "start", "end"].includes(key)) fail(`${at}: a point has the field ${key}. ${renamedField(key, ["kind", "start", "end"]) ?? "A point has kind, start and end, and nothing else"}`);
|
|
85
123
|
if (!(TRACE_KINDS as readonly unknown[]).includes(point.kind)) fail(`${at}: a point kind is exec, read or write`);
|
|
86
124
|
if (typeof point.start !== "string" || (point.end !== undefined && typeof point.end !== "string")) fail(`${at}: a point needs start, and end when it is a range, as names or $ addresses`);
|
|
87
125
|
}
|
|
@@ -92,6 +130,16 @@ function readRun(path: string): Run {
|
|
|
92
130
|
if (kind !== "observe") return;
|
|
93
131
|
if (labels.has(name as string)) fail(`${at} needs a new observation name`);
|
|
94
132
|
labels.add(name as string);
|
|
133
|
+
const itemFields = ["address", "size", "view"];
|
|
134
|
+
if (Array.isArray(step.memory)) {
|
|
135
|
+
for (const item of step.memory as unknown[]) {
|
|
136
|
+
if (!isObject(item)) continue;
|
|
137
|
+
for (const key of Object.keys(item)) {
|
|
138
|
+
const renamed = renamedField(key, itemFields);
|
|
139
|
+
if (renamed !== undefined && !itemFields.includes(key)) fail(`${at}: a memory object has the field ${key}. ${renamed}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
95
143
|
const memoryItem = (item: unknown) =>
|
|
96
144
|
typeof item === "string" ||
|
|
97
145
|
(isObject(item) &&
|
|
@@ -106,9 +154,20 @@ function readRun(path: string): Run {
|
|
|
106
154
|
if (step.vicii !== undefined && typeof step.vicii !== "boolean") fail(`${at}: vicii must be true or false`);
|
|
107
155
|
if (step.sprites !== undefined && (!Array.isArray(step.sprites) || !step.sprites.every((sprite) => isInteger(sprite, 0, 7)))) fail(`${at}: sprites is a list of numbers from 0 to 7`);
|
|
108
156
|
});
|
|
157
|
+
checkFileNames(value.steps as RunStep[], fail);
|
|
109
158
|
return value as unknown as Run;
|
|
110
159
|
}
|
|
111
160
|
|
|
161
|
+
/** The remedy for a start program that is no PRG. */
|
|
162
|
+
const PROGRAM_REMEDY = "A run starts a PRG. Get the PRG out of the disk, tape or cartridge with disk.ts read (or disk.ts crt) of the c64-disk skill";
|
|
163
|
+
|
|
164
|
+
/** Checks each file that the run reads before the emulator starts: the start program or snapshot, and each snapshot step. */
|
|
165
|
+
function checkRunFiles(run: Run): void {
|
|
166
|
+
if ("program" in run.start) readPrg(run.start.program, PROGRAM_REMEDY);
|
|
167
|
+
else readProjectFile(run.start.snapshot);
|
|
168
|
+
for (const step of run.steps) if ("snapshot" in step) readProjectFile(step.snapshot);
|
|
169
|
+
}
|
|
170
|
+
|
|
112
171
|
/** A trace hit with $ addresses, as a person reads it. */
|
|
113
172
|
function showHit(hit: TraceHit) {
|
|
114
173
|
return { ...hit, pc: formatC64Address(hit.pc), ...(hit.address === undefined ? {} : { address: formatC64Address(hit.address) }) };
|
|
@@ -117,6 +176,7 @@ function showHit(hit: TraceHit) {
|
|
|
117
176
|
/** Runs the run file and writes its files into `out`. failed: a step did not run. */
|
|
118
177
|
export async function runFile(path: string, out: string): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
119
178
|
const run = readRun(path);
|
|
179
|
+
checkRunFiles(run);
|
|
120
180
|
const symbols = namesOf(run.symbols ?? "none");
|
|
121
181
|
const at = (location: string): number => {
|
|
122
182
|
if (location.startsWith("$")) return parseC64Address(location);
|
|
@@ -185,7 +245,7 @@ export async function runFile(path: string, out: string): Promise<{ output: Reco
|
|
|
185
245
|
// More than one read gives goes to a file: results.json stays small.
|
|
186
246
|
if (size <= MAX_MEMORY_READ) memory[spec.address] = bytes.toString("hex");
|
|
187
247
|
else {
|
|
188
|
-
const file =
|
|
248
|
+
const file = memoryFile(step.observe, spec.address);
|
|
189
249
|
write(file, bytes);
|
|
190
250
|
memory[spec.address] = { file };
|
|
191
251
|
}
|
|
@@ -7,8 +7,9 @@ import { isRefusal, Refusal } from "#src/errors.ts";
|
|
|
7
7
|
import { c64AddressOf, formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
8
8
|
import { MAX_BASELINES, MAX_MASK_RECTANGLES, maskCoversFrame, WireFailure } from "#src/host-client/tools.ts";
|
|
9
9
|
import { LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, runProgram } from "#src/host-client/autostart.ts";
|
|
10
|
-
import { ACTION_STEP_KINDS, checkActionStep, checkLocation, isInteger, isObject, runActionStep, type ActionStep, type ActionStepKind, type Location } from "#src/host-client/steps.ts";
|
|
10
|
+
import { ACTION_STEP_KINDS, checkActionStep, checkLocation, isInteger, isObject, refuseOldStep, renamedField, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind, type Location } from "#src/host-client/steps.ts";
|
|
11
11
|
import { applyEntryState, parseEntryState } from "#src/host-client/entry-state.ts";
|
|
12
|
+
import { readPrg } from "#src/host-client/image.ts";
|
|
12
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
13
14
|
import { readMemoryInParts, type ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
14
15
|
import { openForRead } from "#src/knowledge/database.ts";
|
|
@@ -90,6 +91,8 @@ export interface ScenarioResult {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
const STEP_KINDS = ["reset", "load", "autostart", "snapshot", "normalize", ...ACTION_STEP_KINDS, "observe"];
|
|
94
|
+
/** The step kinds that a scenario runs: press is for the replay core only. */
|
|
95
|
+
const SHOWN_KINDS = ["reset", "load", "autostart", "snapshot", "normalize", ...VICE_STEP_KINDS, "observe"];
|
|
93
96
|
|
|
94
97
|
/** The keys each kind of step can have. */
|
|
95
98
|
const STEP_KEYS: Record<string, readonly string[]> = {
|
|
@@ -128,8 +131,10 @@ export function readScenario(path: string): Scenario {
|
|
|
128
131
|
if (!isObject(step)) return fail(`${at} must be an object`);
|
|
129
132
|
// An observe step names what it observes with "memory" and "registers" too.
|
|
130
133
|
const kinds = "observe" in step ? ["observe"] : Object.keys(step).filter((key) => STEP_KINDS.includes(key));
|
|
131
|
-
if (kinds.length
|
|
134
|
+
if (kinds.length > 1) return fail(`${at} has ${kinds.length} step kinds: ${kinds.join(", ")}. Give each one its own step`);
|
|
135
|
+
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${SHOWN_KINDS.join(", ")}`);
|
|
132
136
|
const kind = kinds[0]!;
|
|
137
|
+
refuseOldStep(kind, step, at, fail);
|
|
133
138
|
const allowed = [...(STEP_KEYS[kind] ?? [kind]), ...(kind === "observe" ? [] : ["side"])];
|
|
134
139
|
for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
135
140
|
if (step.side !== undefined && !(SIDES as unknown[]).includes(step.side)) fail(`${at}: side must be original or rebuild`);
|
|
@@ -180,12 +185,15 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
180
185
|
checkLocation(item, `${at}: memory`, fail);
|
|
181
186
|
continue;
|
|
182
187
|
}
|
|
183
|
-
|
|
188
|
+
const itemFields = ["address", "size", "view", "tolerance", "exclude"];
|
|
189
|
+
if (!isObject(item)) return fail(`${at}: memory must be a list of names, $ addresses or objects with address`);
|
|
190
|
+
for (const key of Object.keys(item)) if (!itemFields.includes(key)) fail(`${at}: a memory object has the field ${key}. ${renamedField(key, itemFields) ?? `It has ${itemFields.join(", ")}, and nothing else`}`);
|
|
184
191
|
if (item.view !== undefined && item.view !== "cpu" && item.view !== "ram") fail(`${at}: memory.view must be cpu or ram`);
|
|
185
192
|
if (item.exclude !== undefined) {
|
|
186
193
|
if (!Array.isArray(item.exclude) || item.exclude.length === 0) fail(`${at}: memory.exclude must be a list of objects with address and reason`);
|
|
187
194
|
for (const excluded of item.exclude as unknown[]) {
|
|
188
|
-
if (!isObject(excluded)
|
|
195
|
+
if (!isObject(excluded)) return fail(`${at}: memory.exclude must be a list of objects with address and reason`);
|
|
196
|
+
for (const key of Object.keys(excluded)) if (key !== "address" && key !== "reason") fail(`${at}: an exclude has the field ${key}. ${renamedField(key, ["address", "reason"]) ?? "It has address and reason, and nothing else"}`);
|
|
189
197
|
checkLocation(excluded.address, `${at}: memory.exclude.address`, fail);
|
|
190
198
|
if (typeof excluded.reason !== "string" || excluded.reason.trim() === "") fail(`${at}: an exclude needs a reason: why this byte may differ`);
|
|
191
199
|
}
|
|
@@ -276,6 +284,33 @@ export function namesOf(source: string, kinds: "code" | "all" = "all"): Map<stri
|
|
|
276
284
|
return names;
|
|
277
285
|
}
|
|
278
286
|
|
|
287
|
+
/** The remedy for a scenario program that is no PRG. */
|
|
288
|
+
const PROGRAM_REMEDY = "A scenario program is a PRG. Get the PRG out of the disk, tape or cartridge with disk.ts read (or disk.ts crt) of the c64-disk skill";
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Checks each file that the scenario names before an emulator starts: the
|
|
292
|
+
* program and the symbols of each side, and each snapshot file. A missing
|
|
293
|
+
* file is not-found; a program that is no PRG and a symbols file with no
|
|
294
|
+
* names are invalid-input. Gives the names of each side.
|
|
295
|
+
*/
|
|
296
|
+
export function scenarioNames(scenario: Scenario): Map<Side, Map<string, number>> {
|
|
297
|
+
const names = new Map<Side, Map<string, number>>();
|
|
298
|
+
const named = <T>(field: string, read: () => T): T => {
|
|
299
|
+
try {
|
|
300
|
+
return read();
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (isRefusal(error)) throw new Refusal(error.code, `${field}: ${error.message}`);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
for (const side of SIDES) {
|
|
307
|
+
named(`${side}.program`, () => readPrg(scenario[side].program, PROGRAM_REMEDY));
|
|
308
|
+
names.set(side, named(`${side}.symbols`, () => namesOf(scenario[side].symbols)));
|
|
309
|
+
}
|
|
310
|
+
for (const step of scenario.steps) if ("snapshot" in step) readProjectFile(step.snapshot);
|
|
311
|
+
return names;
|
|
312
|
+
}
|
|
313
|
+
|
|
279
314
|
/** A failure that makes the comparison impossible: the result is INCONCLUSIVE. */
|
|
280
315
|
class Inconclusive extends Error {}
|
|
281
316
|
|
|
@@ -466,12 +501,14 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
|
|
|
466
501
|
}
|
|
467
502
|
|
|
468
503
|
/** Runs the scenario on the original, then on the rebuild, in one VICE session. With out, a FAIL leaves files there. */
|
|
469
|
-
export async function runScenario(client: ViceSessionClient, scenario: Scenario, options: { out?: string } = {}): Promise<ScenarioResult> {
|
|
504
|
+
export async function runScenario(client: ViceSessionClient, scenario: Scenario, options: { out?: string; names?: Map<Side, Map<string, number>> } = {}): Promise<ScenarioResult> {
|
|
470
505
|
const checkpoints = scenario.steps.flatMap((step) => ("observe" in step ? [step.observe] : []));
|
|
471
506
|
const observed = new Map<Side, Map<string, Observation>>();
|
|
507
|
+
// The names of both sides before either side runs: a bad symbols file is an error, not a result.
|
|
508
|
+
const names = options.names ?? new Map(SIDES.map((side) => [side, namesOf(scenario[side].symbols)]));
|
|
472
509
|
for (const side of SIDES) {
|
|
473
510
|
try {
|
|
474
|
-
observed.set(side, await runSide(client, scenario, side,
|
|
511
|
+
observed.set(side, await runSide(client, scenario, side, names.get(side)!, options.out !== undefined));
|
|
475
512
|
} catch (error) {
|
|
476
513
|
if (error instanceof Inconclusive) return { result: "INCONCLUSIVE", scenario: scenario.name, reason: error.message, checkpoints, differences: [] };
|
|
477
514
|
if (error instanceof WireFailure) {
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// checklist gives the Markdown list for a human tester.
|
|
6
6
|
|
|
7
7
|
import { COVERAGE } from "#src/c64.ts";
|
|
8
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
8
9
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
10
|
+
import { readPrg } from "#src/host-client/image.ts";
|
|
9
11
|
import { writeReported } from "#src/host-client/transfer.ts";
|
|
10
12
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
11
13
|
import { C64RunSession, PAL_FRAME_CYCLES } from "#src/native/c64run.ts";
|
|
@@ -18,7 +20,7 @@ import { play } from "./lib/play.ts";
|
|
|
18
20
|
import { readRecordFile, record, RECORDING_FILES } from "./lib/recording.ts";
|
|
19
21
|
import { replay } from "./lib/replay.ts";
|
|
20
22
|
import { runFile } from "./lib/runs.ts";
|
|
21
|
-
import { readScenario, runScenario, type Scenario, type ScenarioResult } from "./lib/scenario.ts";
|
|
23
|
+
import { namesOf, readScenario, runScenario, scenarioNames, type Scenario, type ScenarioResult } from "./lib/scenario.ts";
|
|
22
24
|
|
|
23
25
|
/** The default port of the --play page: next to the Host Runtime's 6464. */
|
|
24
26
|
const DEFAULT_HTTP_PORT = 6465;
|
|
@@ -35,6 +37,8 @@ const ALWAYS = [
|
|
|
35
37
|
async function test(call: Call): Promise<ScenarioResult> {
|
|
36
38
|
const scenario = readScenario(call.subject(0));
|
|
37
39
|
const out = call.string("out");
|
|
40
|
+
// Every file of the scenario is checked before the emulator starts.
|
|
41
|
+
const names = scenarioNames(scenario);
|
|
38
42
|
let client: ViceSessionClient;
|
|
39
43
|
try {
|
|
40
44
|
client = await ViceSessionClient.open({ videoStandard: scenario.videoStandard ?? "pal" });
|
|
@@ -43,7 +47,7 @@ async function test(call: Call): Promise<ScenarioResult> {
|
|
|
43
47
|
return { result: "INCONCLUSIVE", scenario: scenario.name, reason: `no emulator: ${error.message}`, checkpoints: [], differences: [] };
|
|
44
48
|
}
|
|
45
49
|
try {
|
|
46
|
-
return await runScenario(client, scenario, out === undefined ? {} : { out });
|
|
50
|
+
return await runScenario(client, scenario, { names, ...(out === undefined ? {} : { out }) });
|
|
47
51
|
} finally {
|
|
48
52
|
await client.close();
|
|
49
53
|
}
|
|
@@ -56,6 +60,15 @@ async function recordCommand(call: Call): Promise<Record<string, unknown>> {
|
|
|
56
60
|
const httpPort = call.count("http-port") ?? DEFAULT_HTTP_PORT;
|
|
57
61
|
const out = call.string("out")!;
|
|
58
62
|
const file = readRecordFile(call.subject(0));
|
|
63
|
+
// The program and the symbols are checked before the replay core starts.
|
|
64
|
+
for (const [field, check] of [["program", () => readPrg(file.program)], ["symbols", () => namesOf(file.symbols)]] as const) {
|
|
65
|
+
try {
|
|
66
|
+
check();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (isRefusal(error)) throw new Refusal(error.code, `record file ${call.subject(0)}: ${field}: ${error.message}`);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
59
72
|
const context = localToolContext();
|
|
60
73
|
const core = await C64RunSession.open(context);
|
|
61
74
|
const workspace = Workspace.create(context.supervisor, "c64-re-tools-record-");
|
|
@@ -13,7 +13,7 @@ This skill does not change project knowledge.
|
|
|
13
13
|
|
|
14
14
|
## Inputs
|
|
15
15
|
|
|
16
|
-
- The program: a PRG file, relative to the project folder.
|
|
16
|
+
- The program: a PRG file, relative to the project folder. `inspect`, `trace`, `stages` and `capture` refuse a disk, tape or cartridge image. For such an image, use `loads`, or get the PRG with `disk.ts read` of the c64-disk skill.
|
|
17
17
|
- For a capture: the address where the unpacked program starts, and the directory for the result.
|
|
18
18
|
|
|
19
19
|
## Tools / execution path
|
|
@@ -32,17 +32,18 @@ node <skill>/scripts/unpacker.ts loads original/game.d64 --steps .c64-re-tools/l
|
|
|
32
32
|
|---|---|
|
|
33
33
|
| `--until '$c000'` | `capture`: the address where the unpacked program starts. The capture stops there after the steps. To capture a later arrival at the address, add a `runUntil` step to an address after the first arrival. |
|
|
34
34
|
| `--out .c64-re-tools/local/capture` | `capture` and `loads`: the folder for the result, relative to the project folder. |
|
|
35
|
-
| `--range '$c000-$cfff'` | `capture` only: write this range as the one segment. Without it, the segments are the blocks that the stages wrote. |
|
|
36
|
-
| `--exclude '$a000-$a9ff'` | `capture` only: remove this range from the segments. You can give it more than once. `image.json` lists the ranges in `excluded`. The
|
|
35
|
+
| `--range '$c000-$cfff'` | `capture` only: write this range as the one segment. Without it, the segments are the blocks that the stages wrote. The range must hold the `--until` address. |
|
|
36
|
+
| `--exclude '$a000-$a9ff'` | `capture` only: remove this range from the segments. You can give it more than once. `image.json` lists the ranges in `excluded`. The range must not hold the `--until` address. |
|
|
37
37
|
| `--timeout-frames 3000` | `capture` only: how many frames the program can run before the capture stops (default 3000). |
|
|
38
38
|
| `--frames 3000` | `trace` and `stages`: how many frames the program runs (default 3000). |
|
|
39
39
|
| `--entry '$c000'` | `trace`, `stages` and `capture`: start the program at this address. Without it, the script types RUN. A file that is not a BASIC program at `$0801` needs it. |
|
|
40
40
|
| `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. |
|
|
41
|
+
| `--max-listed 50` | `trace` only: the most ranges that `writtenThenExecuted` lists (default 50). |
|
|
41
42
|
| `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
|
|
42
43
|
| `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
|
|
43
44
|
| `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
|
|
44
45
|
|
|
45
|
-
The capture reads RAM, also the RAM under the ROMs and the I/O area. The script refuses an option that the command does not use.
|
|
46
|
+
The capture reads RAM, also the RAM under the ROMs and the I/O area. The script refuses an option that the command does not use. It refuses a wrong `--range` or `--exclude` before the emulator starts.
|
|
46
47
|
|
|
47
48
|
A steps file is a JSON list of steps. Each step has the input of the MCP tool of the same action. An address in a step can be a name. For example, wait 50 frames and type a space:
|
|
48
49
|
|
|
@@ -59,6 +60,8 @@ A steps file is a JSON list of steps. Each step has the input of the MCP tool of
|
|
|
59
60
|
|
|
60
61
|
A program that reads the keyboard matrix does not see the keys of a `type` step. These scripts refuse a `press` step. A wait for any key that reads all keyboard columns can end without a key.
|
|
61
62
|
|
|
63
|
+
The scripts refuse a step in the form of version 2.0, for example `{ "runUntil": { "at": "main_loop" }, "timeoutFrames": 300 }`. The message names the old field and gives the step to write.
|
|
64
|
+
|
|
62
65
|
### Calling the script
|
|
63
66
|
|
|
64
67
|
- Call a script as `node <skill>/scripts/<script>.ts <command> [subjects] [options]`. The command comes first.
|
|
@@ -92,7 +95,7 @@ A program that reads the keyboard matrix does not see the keys of a `type` step.
|
|
|
92
95
|
- `strings`: readable text near the start of the code.
|
|
93
96
|
- `start`: the code at the SYS address. `jumpsOutside` and `writesOutside` give an address outside the loaded program, where the code jumps or writes.
|
|
94
97
|
- `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
|
|
95
|
-
2. Run `trace`. It gives the evidence from the run. Read `packing`:
|
|
98
|
+
2. Run `trace`. It gives the evidence from the run. `writtenThenExecuted` lists the ranges that the program wrote and then executed. Read `packing`:
|
|
96
99
|
- `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the written code that runs at the end of the trace. When the trace ends in ROM code, for example in an interrupt, it is the written code that runs next. Its `start` is code that runs after the unpacking. It is not always the entry point of the program. When no written code runs in 50 more frames, the result has no `runningRange` and no `suggestedUntil`.
|
|
97
100
|
- `unclear`: the program executed a small quantity of code that it wrote. This can be code that changes itself.
|
|
98
101
|
- `unlikely`: the program executed no code that it wrote. If the program waits for a key or a joystick, it can move code after that input. Then use the c64-emulator skill.
|
|
@@ -152,6 +155,7 @@ For `loads`: `captures`, with the name, the PC, the segments, the files and the
|
|
|
152
155
|
## Failure and conflicts
|
|
153
156
|
|
|
154
157
|
- `traced: false` or `followed: false`: the emulator did not load the program into memory in the time limit. The reason gives the load address. Make sure that the file is a PRG that the C64 can load.
|
|
158
|
+
- `traced: false` or `followed: false` with `stopReason`: the CPU stopped before the frames ran, for example at a JAM instruction. `pc` gives the address. Make sure that `--entry` is the start of the code that the file holds at that time, and that the steps are correct.
|
|
155
159
|
- `captured: false`: the program did not get to the address in the time limit. The reason tells where it stopped. Find a better address with the c64-emulator skill, or give more frames. If it stopped in a key wait, give `--steps` with a `type` step.
|
|
156
160
|
- A step did not reach its target: the reason names the step. Change the step or give it more frames.
|
|
157
161
|
- `loaded: false` and no capture has a segment: the program did not load a part, or it did not start. `keyWait: true` on a capture tells that the C64 waited at READY. or at an INPUT statement. For a tape image, read `emulatorFlaw`. A known VICE flaw can stop a tape load. Tell the user to install the latest VICE.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Local evidence of packing for the c64-unpacker script: where BASIC hands
|
|
2
2
|
// over and how compressed the bytes look. It never names a packer.
|
|
3
3
|
|
|
4
|
-
import { basicLineText, findHandoffs, formatC64Address, instructionLength, parseTokenized } from "#src/c64.ts";
|
|
4
|
+
import { basicLineText, findHandoffs, formatC64Address, instructionLength, parseTokenized, prgRange } from "#src/c64.ts";
|
|
5
5
|
import { Refusal } from "#src/errors.ts";
|
|
6
6
|
|
|
7
7
|
/** A 256-byte block above this many bits per byte looks compressed. */
|
|
@@ -111,9 +111,8 @@ function startSignals(program: Uint8Array, load: number, end: number, sys: numbe
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export function inspect(program: Uint8Array): PackingEvidence {
|
|
114
|
-
|
|
115
|
-
const load
|
|
116
|
-
const end = load + program.length - 3;
|
|
114
|
+
// A PRG ends at $ffff or before: a longer file (a disk image) is refused.
|
|
115
|
+
const { start: load, end } = prgRange(program);
|
|
117
116
|
const basic = basicStart(program);
|
|
118
117
|
const stub = basic?.sys;
|
|
119
118
|
// Without BASIC, all bytes count; with it, only the bytes after the BASIC text.
|
|
@@ -132,7 +131,7 @@ export function inspect(program: Uint8Array): PackingEvidence {
|
|
|
132
131
|
const packing = byEntropy === "unlikely" && startMoves ? "unclear" : byEntropy;
|
|
133
132
|
const sorted = [...blocks].sort((a, b) => a - b);
|
|
134
133
|
return {
|
|
135
|
-
loadRange: { start: formatC64Address(load), end: formatC64Address(
|
|
134
|
+
loadRange: { start: formatC64Address(load), end: formatC64Address(end) },
|
|
136
135
|
bytes: program.length - 2,
|
|
137
136
|
basicStart: stub === undefined ? null : stub.address === undefined ? { line: stub.line, sys: null, expression: stub.expression! } : { line: stub.line, sys: formatC64Address(stub.address) },
|
|
138
137
|
entropy: {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { COVERAGE, formatC64Address } from "#src/c64.ts";
|
|
8
8
|
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
9
9
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
10
|
-
import { ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
10
|
+
import { ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
11
11
|
import { knowledgeAt } from "#src/knowledge/names.ts";
|
|
12
12
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
13
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
@@ -54,7 +54,8 @@ export function readLoadSteps(path: string): LoadStep[] {
|
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
const kinds = Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key));
|
|
57
|
-
if (kinds.length
|
|
57
|
+
if (kinds.length > 1) return fail(`${at} has ${kinds.length} step kinds: ${kinds.join(", ")}. Give each one its own step`);
|
|
58
|
+
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...VICE_STEP_KINDS, "capture"].join(", ")}`);
|
|
58
59
|
// The step with its defaults filled in, as the shared schema of the action steps gives it.
|
|
59
60
|
(value as unknown[])[index] = checkActionStep(kinds[0] as ActionStepKind, step, at, fail);
|
|
60
61
|
});
|