@henols/c64-re-tools 2.1.1 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +6 -3
- package/skills/c64-assembler/scripts/assembler.ts +19 -11
- package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
- package/skills/c64-basic/SKILL.md +1 -0
- package/skills/c64-disk/SKILL.md +1 -0
- package/skills/c64-disk/scripts/disk.ts +2 -2
- package/skills/c64-disk/scripts/lib/containers.ts +3 -4
- package/skills/c64-docs/SKILL.md +2 -1
- package/skills/c64-docs/scripts/docs.ts +1 -1
- package/skills/c64-docs/scripts/lib/links.ts +2 -0
- package/skills/c64-emulator/SKILL.md +2 -1
- package/skills/c64-knowledge/SKILL.md +6 -4
- package/skills/c64-knowledge/scripts/knowledge.ts +35 -13
- package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
- package/skills/c64-memory-map/SKILL.md +1 -0
- package/skills/c64-memory-map/scripts/memory-map.ts +1 -1
- package/skills/c64-provenance/SKILL.md +1 -0
- package/skills/c64-provenance/scripts/provenance.ts +9 -6
- package/skills/c64-source/SKILL.md +1 -0
- package/skills/c64-source/scripts/lib/source.ts +7 -29
- package/skills/c64-source/scripts/source.ts +4 -4
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/static-analysis.ts +14 -5
- package/skills/c64-testing/SKILL.md +3 -2
- package/skills/c64-testing/scripts/lib/diff-files.ts +14 -2
- package/skills/c64-testing/scripts/lib/recording.ts +3 -3
- package/skills/c64-testing/scripts/lib/replay.ts +1 -1
- package/skills/c64-testing/scripts/lib/runs.ts +27 -7
- package/skills/c64-testing/scripts/testing.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +8 -5
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -6
- package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +18 -17
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +49 -3
- package/src/host-client/image.ts +10 -10
- package/src/host-client/steps.ts +39 -3
- package/src/host-client/transfer.ts +14 -26
- package/src/knowledge/history.ts +55 -2
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/program.ts +31 -2
- package/src/knowledge/read.ts +7 -4
- package/src/knowledge/schema.ts +3 -2
- package/src/knowledge/text.ts +1 -0
- package/src/knowledge/write.ts +22 -14
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools/debug.ts +29 -3
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/memory.ts +4 -3
- package/src/media.ts +42 -0
- package/src/opcodes.ts +51 -18
- package/src/project.ts +110 -2
- package/src/protocol/vice.ts +4 -1
- package/src/script.ts +55 -3
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import { type AddressRange, COVERAGE, EXECUTE, formatC64Address, READ, WRITE } from "#src/c64.ts";
|
|
10
10
|
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
11
11
|
import { readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
|
|
12
|
-
import { resetToReady } from "#src/host-client/entry-state.ts";
|
|
13
12
|
import { readActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
14
13
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
15
14
|
import { readPrg } from "#src/host-client/image.ts";
|
|
@@ -20,7 +19,7 @@ import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec, ty
|
|
|
20
19
|
import { basicStart, inspect, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
|
|
21
20
|
import { loads, readLoadSteps } from "./lib/loads.ts";
|
|
22
21
|
import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges } from "./lib/stages.ts";
|
|
23
|
-
import {
|
|
22
|
+
import { entryStateFile, machineState, readRam, readyState, vectorsOf } from "./lib/machine.ts";
|
|
24
23
|
|
|
25
24
|
/** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
|
|
26
25
|
const UNPACKED_BYTES = 256;
|
|
@@ -165,6 +164,13 @@ async function firstPcAfterBasic(client: ViceSessionClient, from: number, to: nu
|
|
|
165
164
|
return from > to ? undefined : firstPcIn(client, [{ start: from, end: to }]);
|
|
166
165
|
}
|
|
167
166
|
|
|
167
|
+
/**
|
|
168
|
+
* What a stage wrote: every written block, also a small one. --min-stage
|
|
169
|
+
* decides only where a stage starts; the wrote list and the capture segments
|
|
170
|
+
* keep each byte that a stage wrote.
|
|
171
|
+
*/
|
|
172
|
+
const stageWrote = (map: Uint8Array) => writtenBlocks(map, 1);
|
|
173
|
+
|
|
168
174
|
/** Frames between two looks at the memory map while stages are followed. */
|
|
169
175
|
const STAGE_CHUNK_FRAMES = 25;
|
|
170
176
|
const MAX_STAGES = 16;
|
|
@@ -238,7 +244,7 @@ async function follow(
|
|
|
238
244
|
let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as AddressRange[] };
|
|
239
245
|
const startStage = async (next: number, last: Uint8Array) => {
|
|
240
246
|
mergeMaps(stage.map, last);
|
|
241
|
-
found.push({ entry, firstFrame, frames: total - firstFrame, wrote:
|
|
247
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: stageWrote(stage.map), next });
|
|
242
248
|
entry = next;
|
|
243
249
|
firstFrame = total;
|
|
244
250
|
stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] };
|
|
@@ -428,7 +434,7 @@ async function follow(
|
|
|
428
434
|
else reached = await trackTo(options.until, left);
|
|
429
435
|
await client.snapshot({ action: "remove", name: STAGE_SNAPSHOT }).catch(() => undefined);
|
|
430
436
|
mergeMaps(stage.map, await readCoverage(client));
|
|
431
|
-
found.push({ entry, firstFrame, frames: total - firstFrame, wrote:
|
|
437
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: stageWrote(stage.map) });
|
|
432
438
|
return { stages: found, frames: total, reached, ...(halted === undefined ? {} : { halted }) };
|
|
433
439
|
}
|
|
434
440
|
|
|
@@ -529,14 +535,8 @@ async function capture(
|
|
|
529
535
|
),
|
|
530
536
|
);
|
|
531
537
|
// A clean machine at the READY prompt shows what the loader and the stages changed.
|
|
532
|
-
await
|
|
533
|
-
|
|
534
|
-
const differs: Array<{ address: string; entry: number; reset: number }> = [];
|
|
535
|
-
const compare = (base: number, entry: Buffer, reset: Buffer) => entry.forEach((value, offset) => value !== reset[offset] && differs.push({ address: formatC64Address(base + offset), entry: value, reset: reset[offset]! }));
|
|
536
|
-
compare(0x0000, state.port, clean.port);
|
|
537
|
-
compare(0x0002, state.pages.subarray(2), clean.pages.subarray(2));
|
|
538
|
-
for (const [name, [address]] of Object.entries(IO_AREAS)) compare(address, state.io[name as keyof typeof IO_AREAS], clean.io[name as keyof typeof IO_AREAS]);
|
|
539
|
-
write("entry_state.json", json(entryState(registers, state, vectors, differs)));
|
|
538
|
+
const entry = entryStateFile(registers, state, vectors, await readyState(client));
|
|
539
|
+
write("entry_state.json", entry.bytes);
|
|
540
540
|
return {
|
|
541
541
|
output: {
|
|
542
542
|
captured: true,
|
|
@@ -546,7 +546,7 @@ async function capture(
|
|
|
546
546
|
segments: segments.map(showRange),
|
|
547
547
|
...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
|
|
548
548
|
stages: followed.stages.length,
|
|
549
|
-
differsFromReset:
|
|
549
|
+
differsFromReset: entry.differsFromReset,
|
|
550
550
|
},
|
|
551
551
|
failed: false,
|
|
552
552
|
};
|
|
@@ -569,7 +569,7 @@ const entryOf = (call: Call) => {
|
|
|
569
569
|
};
|
|
570
570
|
const stepsOf = (call: Call) => {
|
|
571
571
|
const path = call.string("steps");
|
|
572
|
-
return path === undefined ? [] : readActionSteps(path, "--steps");
|
|
572
|
+
return path === undefined ? [] : readActionSteps(path, "--steps", knowledgeAt());
|
|
573
573
|
};
|
|
574
574
|
|
|
575
575
|
const COMMANDS: CommandSpec[] = [
|
|
@@ -595,7 +595,8 @@ const COMMANDS: CommandSpec[] = [
|
|
|
595
595
|
run: async (call) => {
|
|
596
596
|
const steps = stepsOf(call);
|
|
597
597
|
// A runUntil step to a raster line or a memory value cannot be tracked in chunks of frames; capture takes it.
|
|
598
|
-
|
|
598
|
+
const untracked = steps.findIndex((step) => "runUntil" in step && !trackedRunUntil(step));
|
|
599
|
+
if (untracked >= 0) throw new UsageError(`--steps ${call.string("steps")!}: step ${untracked + 1}: stages takes a runUntil step only with an address target without a condition. Use frames steps, or capture`);
|
|
599
600
|
return outcome(await stages(call.subject(0), { frames: call.count("frames")!, steps, minStage: call.count("min-stage")!, ...entryOf(call) }));
|
|
600
601
|
},
|
|
601
602
|
},
|
|
@@ -605,7 +606,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
605
606
|
subjects: [PROGRAM],
|
|
606
607
|
options: {
|
|
607
608
|
until: { kind: "address", value: "<address>", required: true, help: "the unpacked entry point" },
|
|
608
|
-
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the capture, relative to the project folder" },
|
|
609
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the capture, relative to the project folder" },
|
|
609
610
|
range: { ...OPTIONS.range, help: "one segment in place of the found ones" },
|
|
610
611
|
exclude: { kind: "range", value: "<start>-<end>", multiple: true, help: "a range to leave out of the segments, for example bytes that a crack left and the program never reads" },
|
|
611
612
|
"timeout-frames": { ...OPTIONS["timeout-frames"], max: 30_000, default: 3000, help: "stop the wait for --until after n frames (default 3000)" },
|
|
@@ -632,7 +633,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
632
633
|
subjects: [{ name: "<program>", help: "a PRG, or a disk, tape or cartridge image, relative to the project folder" }],
|
|
633
634
|
options: {
|
|
634
635
|
steps: { ...STEPS, required: true, help: "action steps and capture steps (JSON)" },
|
|
635
|
-
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the captures, relative to the project folder" },
|
|
636
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the captures, relative to the project folder" },
|
|
636
637
|
drive: { kind: "flag", help: "also write the 2 KiB drive RAM as drive8.prg, for knowledge space drive8" },
|
|
637
638
|
"min-block": { kind: "count", value: "<bytes>", min: 1, max: 0x10000, default: 64, help: "write a changed block of at least this many bytes (default 64)" },
|
|
638
639
|
},
|
package/src/host/vice/adapter.ts
CHANGED
|
@@ -59,7 +59,10 @@ export function conditionExpression(condition: Condition, space: Space): string
|
|
|
59
59
|
}
|
|
60
60
|
case "memory": {
|
|
61
61
|
if (condition.space !== "c64") {
|
|
62
|
-
throw new WireFailure(
|
|
62
|
+
throw new WireFailure(
|
|
63
|
+
"unsupported-in-space",
|
|
64
|
+
"A memory target or a memory condition tests only space c64 memory. For the drive, use a watchpoint in space drive8, or a c64_run_until address target in space drive8.",
|
|
65
|
+
);
|
|
63
66
|
}
|
|
64
67
|
return `(@${condition.view}:${formatC64Address(condition.address)} ${operator(condition.operator)} ${formatC64Byte(condition.value)})`;
|
|
65
68
|
}
|
package/src/host/vice/session.ts
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
import { decodeProgramCounter, MonitorConnectionError, MonitorError, MonitorTimeoutError, ResponseType, type MonitorResponse } from "./binary-monitor.ts";
|
|
31
31
|
import { decodeCia, decodeSid, decodeSprite, decodeVicii } from "./chips.ts";
|
|
32
32
|
import { compareFrames, differenceImage } from "./screen.ts";
|
|
33
|
+
import { isRefusal } from "../../errors.ts";
|
|
34
|
+
import { checkMedia } from "../../media.ts";
|
|
33
35
|
import { launchVice, startErrorLines, type ViceMode, type ViceProcess } from "./process.ts";
|
|
34
36
|
import { TextMonitorError, TextMonitorTimeoutError } from "./text-monitor.ts";
|
|
35
37
|
|
|
@@ -193,6 +195,13 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
193
195
|
* pc write, and reports the JAM only once.
|
|
194
196
|
*/
|
|
195
197
|
#jammed: number | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* An autostart can still be loading: VICE goes on with it after a snapshot
|
|
200
|
+
* restore and writes the program over the restored memory. Only a reset stops it.
|
|
201
|
+
*/
|
|
202
|
+
#autostarting = false;
|
|
203
|
+
/** The C64 clock just after the last snapshot restore: VICE keeps the history of before it. */
|
|
204
|
+
#historySince: bigint | undefined;
|
|
196
205
|
/** VICE records no CPU history after a JAM until the next reset. */
|
|
197
206
|
#historyStopped = false;
|
|
198
207
|
/** The CPU ran again after a JAM without a reset: the CPU history that VICE gives is old. */
|
|
@@ -396,6 +405,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
396
405
|
case "autostart": {
|
|
397
406
|
const { type, index, run } = params as ViceOperations["autostart"]["params"];
|
|
398
407
|
const bytes = onlyAttachment(attachments);
|
|
408
|
+
checkMedia(type, bytes);
|
|
399
409
|
if (index > 0 && this.#directory !== undefined && isOneOf(DISK_IMAGE_TYPES, type)) checkDirectoryEntry(await this.#directory(type, bytes), index);
|
|
400
410
|
const file = this.#stage(bytes, type);
|
|
401
411
|
// VICE keeps a cartridge through every reset and autostart, as a real C64 does.
|
|
@@ -415,6 +425,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
415
425
|
}
|
|
416
426
|
await this.#runningCommand(() => this.#machine.autostart(file, index, run));
|
|
417
427
|
this.#clearJam();
|
|
428
|
+
this.#autostarting = true;
|
|
418
429
|
if (type === "crt") this.#cartridge = true;
|
|
419
430
|
this.#replaceStaged(stagedKind(type), file);
|
|
420
431
|
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
@@ -423,7 +434,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
423
434
|
}
|
|
424
435
|
case "diskAttach": {
|
|
425
436
|
const { type } = params as ViceOperations["diskAttach"]["params"];
|
|
426
|
-
const
|
|
437
|
+
const bytes = onlyAttachment(attachments);
|
|
438
|
+
checkMedia(type, bytes);
|
|
439
|
+
const file = this.#stage(bytes, type);
|
|
427
440
|
await this.#observe(() => this.#machine.attachDisk(file));
|
|
428
441
|
this.#replaceStaged("disk", file);
|
|
429
442
|
return { attached: true };
|
|
@@ -543,11 +556,18 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
543
556
|
// VICE 3.9 sends no JAM event: the opcode at the pc tells.
|
|
544
557
|
const stop = this.#lastStop ?? ((await this.#jamAt(status.pc)) ? { stopReason: "jam" as const } : undefined);
|
|
545
558
|
if (stop !== undefined) Object.assign(status, stop);
|
|
559
|
+
// A point in the drive stopped it: the pc is the drive's, as advance-frames and run-until give it.
|
|
560
|
+
const point = status.pointId === undefined ? undefined : (status.stopReason === "watchpoint" ? this.#watchpoints : this.#breakpoints).get(status.pointId);
|
|
561
|
+
status.space = point?.space === "drive8" ? "drive8" : "c64";
|
|
562
|
+
if (status.space === "drive8") status.pc = (await this.#machine.readRegisters("drive8")).pc;
|
|
546
563
|
}
|
|
547
564
|
return status;
|
|
548
565
|
}
|
|
549
566
|
|
|
550
567
|
async #execution(params: ExecutionParams): Promise<ExecutionResult> {
|
|
568
|
+
if (params.space === "drive8" && (params.action === "pause" || params.action === "resume")) {
|
|
569
|
+
throw new WireFailure("unsupported-in-space", `${params.action} stops or runs the whole machine, the computer and the disk drive together: use space c64.`);
|
|
570
|
+
}
|
|
551
571
|
if (params.action === "resume") {
|
|
552
572
|
if (this.#state === "stopped") {
|
|
553
573
|
await this.#refuseJammed("resume");
|
|
@@ -896,6 +916,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
896
916
|
|
|
897
917
|
/** A reset or an autostart starts a jammed CPU and the CPU history again. */
|
|
898
918
|
#clearJam(): void {
|
|
919
|
+
this.#autostarting = false;
|
|
920
|
+
// A reset sets the clock back to 0: the history from before it is of this run again.
|
|
921
|
+
this.#historySince = undefined;
|
|
899
922
|
this.#jammed = undefined;
|
|
900
923
|
this.#historyStopped = false;
|
|
901
924
|
this.#historyOld = false;
|
|
@@ -1121,9 +1144,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1121
1144
|
);
|
|
1122
1145
|
}
|
|
1123
1146
|
return this.#observe(async () => {
|
|
1124
|
-
const
|
|
1125
|
-
if (params.space === "drive8") return { entries:
|
|
1147
|
+
const all = await this.#machine.history(params.limit, params.space);
|
|
1148
|
+
if (params.space === "drive8") return { entries: all.map(({ clock: _clock, ...entry }) => entry) };
|
|
1126
1149
|
const now = await this.#machine.clock();
|
|
1150
|
+
const raw = entriesSince(all, this.#historySince, now);
|
|
1127
1151
|
const position = await this.#machine.rasterPosition();
|
|
1128
1152
|
const { lines, cycles } = RASTER[this.#videoStandard];
|
|
1129
1153
|
const frame = BigInt(lines * cycles);
|
|
@@ -1218,7 +1242,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1218
1242
|
async #restoreFrom(file: string): Promise<{ restored: true; state: "stopped" }> {
|
|
1219
1243
|
await this.#stop();
|
|
1220
1244
|
const elapsed = (await this.#machine.clock()) - this.#timingStart;
|
|
1245
|
+
// The restore replaces the whole machine state, so the reset that stops the autostart leaves no trace.
|
|
1246
|
+
if (this.#autostarting) await this.#reset({ mode: "soft", run: false });
|
|
1221
1247
|
await this.#machine.restoreSnapshot(file);
|
|
1248
|
+
this.#historySince = await this.#machine.clock();
|
|
1222
1249
|
// A restore starts a jammed CPU again, but VICE still records no CPU history.
|
|
1223
1250
|
this.#jammed = undefined;
|
|
1224
1251
|
if (this.#historyStopped) this.#historyOld = true;
|
|
@@ -1534,6 +1561,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1534
1561
|
|
|
1535
1562
|
async #translate(error: unknown, runningBefore: boolean, halts: number): Promise<unknown> {
|
|
1536
1563
|
if (error instanceof WireFailure) return error;
|
|
1564
|
+
// A refusal of a shared module (for example the media check) goes on the wire as the host's own.
|
|
1565
|
+
if (isRefusal(error)) return new WireFailure(error.code, error.message);
|
|
1537
1566
|
if (this.#closed) return new WireFailure("machine-unavailable", "The emulator session is closed.");
|
|
1538
1567
|
if (error instanceof MonitorConnectionError || error instanceof TextMonitorError || error instanceof StepTimeout) {
|
|
1539
1568
|
this.#log(error.message);
|
|
@@ -1614,6 +1643,23 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1614
1643
|
}
|
|
1615
1644
|
}
|
|
1616
1645
|
|
|
1646
|
+
/**
|
|
1647
|
+
* The history entries (oldest first) that the CPU executed after a restore at
|
|
1648
|
+
* clock `since`, with the clock at `now`: the last entries, back to the first
|
|
1649
|
+
* one outside since to now or the first one whose clock does not go up to the
|
|
1650
|
+
* next. The others are of before the restore.
|
|
1651
|
+
*/
|
|
1652
|
+
export function entriesSince<T extends { clock: bigint }>(entries: readonly T[], since: bigint | undefined, now: bigint): T[] {
|
|
1653
|
+
if (since === undefined) return [...entries];
|
|
1654
|
+
let first = entries.length;
|
|
1655
|
+
while (first > 0) {
|
|
1656
|
+
const entry = entries[first - 1]!;
|
|
1657
|
+
if (entry.clock < since || entry.clock > now || (first < entries.length && entry.clock >= entries[first]!.clock)) break;
|
|
1658
|
+
first--;
|
|
1659
|
+
}
|
|
1660
|
+
return entries.slice(first);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1617
1663
|
/** The one attachment of an operation that takes a file (see attachmentCount); refuses any other count. */
|
|
1618
1664
|
function onlyAttachment(attachments: Buffer[]): Buffer {
|
|
1619
1665
|
const [bytes] = attachments;
|
package/src/host-client/image.ts
CHANGED
|
@@ -36,6 +36,12 @@ const MEDIA_KINDS: Record<string, string> = {
|
|
|
36
36
|
/** The remedy for a disk, tape or cartridge file where a PRG must be: get the PRG out of it. */
|
|
37
37
|
export const MEDIA_REMEDY = "Get the PRG out of it with disk.ts read (or disk.ts crt for a cartridge) of the c64-disk skill";
|
|
38
38
|
|
|
39
|
+
/** Refuses a disk, tape or cartridge file by its extension (`type`, lowercase, without the dot): invalid-input with `remedy`. */
|
|
40
|
+
function refuseMedia(path: string, type: string, remedy: string): void {
|
|
41
|
+
const media = MEDIA_KINDS[type];
|
|
42
|
+
if (media !== undefined) throw new Refusal("invalid-input", `${path} is ${media} (.${type}), not a PRG. ${remedy}.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
/**
|
|
40
46
|
* Reads a PRG of the project: a 2-byte load address, and bytes that end at
|
|
41
47
|
* $ffff or before. Refuses a disk, tape or cartridge file and any other file
|
|
@@ -43,8 +49,7 @@ export const MEDIA_REMEDY = "Get the PRG out of it with disk.ts read (or disk.ts
|
|
|
43
49
|
*/
|
|
44
50
|
export function readPrg(path: string, remedy = MEDIA_REMEDY): { bytes: Buffer; start: number; end: number; body: Uint8Array } {
|
|
45
51
|
const { bytes, type } = readProjectFile(path);
|
|
46
|
-
|
|
47
|
-
if (media !== undefined) throw new Refusal("invalid-input", `${path} is ${media} (.${type}), not a PRG. ${remedy}.`);
|
|
52
|
+
refuseMedia(path, type, remedy);
|
|
48
53
|
try {
|
|
49
54
|
return { bytes, ...prgRange(bytes) };
|
|
50
55
|
} catch (error) {
|
|
@@ -53,18 +58,13 @@ export function readPrg(path: string, remedy = MEDIA_REMEDY): { bytes: Buffer; s
|
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
/** Reads an image.json or a PRG, relative to the project folder. */
|
|
61
|
+
/** Reads an image.json or a PRG, relative to the project folder. Refuses a disk, tape or cartridge file as readPrg does. */
|
|
57
62
|
export function readImage(path: string): Image {
|
|
58
|
-
const { bytes } = readProjectFile(path);
|
|
59
63
|
if (!path.endsWith(".json")) {
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
range = prgRange(bytes);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
throw new Refusal("invalid-input", `${path}: ${(error as Error).message}`);
|
|
65
|
-
}
|
|
64
|
+
const range = readPrg(path);
|
|
66
65
|
return { segments: [{ start: range.start, end: range.end }], byteAt: (address) => range.body[address - range.start]! };
|
|
67
66
|
}
|
|
67
|
+
const { bytes } = readProjectFile(path);
|
|
68
68
|
const fail = (what: string): never => {
|
|
69
69
|
throw new Refusal("invalid-input", `${path} is not an image.json of the c64-unpacker skill: ${what}`);
|
|
70
70
|
};
|
package/src/host-client/steps.ts
CHANGED
|
@@ -384,8 +384,42 @@ export function checkActionSteps(value: unknown, fail: (message: string) => neve
|
|
|
384
384
|
});
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
/**
|
|
388
|
-
export function
|
|
387
|
+
/** The names and addresses of an action step: each location that the step resolves when it runs. */
|
|
388
|
+
export function stepLocations(step: ActionStep): Location[] {
|
|
389
|
+
if ("write" in step) return [step.write.address];
|
|
390
|
+
if ("registers" in step) return step.registers.pc === undefined ? [] : [step.registers.pc];
|
|
391
|
+
if (!("runUntil" in step)) return [];
|
|
392
|
+
const { target } = step.runUntil;
|
|
393
|
+
if (target.kind === "raster") return [];
|
|
394
|
+
if (target.kind === "memory") return [target.address];
|
|
395
|
+
return [target.address, ...(target.condition?.kind === "memory" ? [target.condition.address] : [])];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Resolves every name of the steps with `at` before the steps run, so that an
|
|
400
|
+
* unknown name stops the call before the emulator starts. A refusal of `at` becomes
|
|
401
|
+
* invalid-input that names `where` (for example the option and the file) and the step.
|
|
402
|
+
* `first` is the number of the first of the steps in the file.
|
|
403
|
+
*/
|
|
404
|
+
export function checkStepNames(steps: readonly ActionStep[], at: (location: Location) => number, where: string, first = 1): void {
|
|
405
|
+
steps.forEach((step, index) => {
|
|
406
|
+
for (const location of stepLocations(step)) {
|
|
407
|
+
try {
|
|
408
|
+
at(location);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (isRefusal(error)) throw new Refusal("invalid-input", `${where}: step ${index + first}: ${error.message}`);
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Reads a steps file of the project: a JSON list of action steps. `option`
|
|
419
|
+
* names the option of the file in a refusal. With `at`, every name of the
|
|
420
|
+
* steps must resolve (checkStepNames).
|
|
421
|
+
*/
|
|
422
|
+
export function readActionSteps(path: string, option: string, at?: (location: Location) => number): ActionStep[] {
|
|
389
423
|
const fail = (message: string): never => {
|
|
390
424
|
throw new Refusal("invalid-input", `${option} ${path}: ${message}`);
|
|
391
425
|
};
|
|
@@ -396,7 +430,9 @@ export function readActionSteps(path: string, option: string): ActionStep[] {
|
|
|
396
430
|
if (error instanceof SyntaxError) return fail(`it is not JSON: ${error.message}`);
|
|
397
431
|
throw error;
|
|
398
432
|
}
|
|
399
|
-
|
|
433
|
+
const steps = checkActionSteps(value, fail);
|
|
434
|
+
if (at !== undefined) checkStepNames(steps, at, `${option} ${path}`);
|
|
435
|
+
return steps;
|
|
400
436
|
}
|
|
401
437
|
|
|
402
438
|
/** Stops the CPU when it runs: a write, a register change or an exact frame count needs it stopped. */
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// The host never sees a client path: only bytes and the file's type.
|
|
3
3
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import { lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, extname, join, relative, sep } from "node:path";
|
|
7
7
|
|
|
8
|
-
import { isInside, projectRoot, resolveProject, resolveProjectFile,
|
|
8
|
+
import { checkOutputPath, isInside, projectRoot, resolveProject, resolveProjectFile, writeProblem } from "../project.ts";
|
|
9
9
|
import { isRefusal } from "../errors.ts";
|
|
10
10
|
import { MAX_ATTACHMENT_BYTES } from "../protocol/framing.ts";
|
|
11
11
|
import { WireFailure } from "../protocol/messages.ts";
|
|
@@ -58,49 +58,37 @@ 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
61
|
/**
|
|
74
62
|
* The refusal for a write that the file system refused because of the path
|
|
75
63
|
* (a folder, a file in place of a folder, no permission): invalid-input with
|
|
76
64
|
* the path. Undefined for another error, which is no fault of the path.
|
|
77
65
|
*/
|
|
78
66
|
export function writeRefusal(error: unknown, path: string, what?: string): WireFailure | undefined {
|
|
79
|
-
const
|
|
80
|
-
return
|
|
67
|
+
const message = writeProblem(error, path, what);
|
|
68
|
+
return message === undefined ? undefined : new WireFailure("invalid-input", message);
|
|
81
69
|
}
|
|
82
70
|
|
|
83
71
|
/**
|
|
84
72
|
* Writes bytes to a project file on the client side, making its directories
|
|
85
|
-
* and overwriting it.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
73
|
+
* and overwriting it. First the check of checkOutputPath, which a script also
|
|
74
|
+
* does on its output options before any work. Throws WireFailure(invalid-input)
|
|
75
|
+
* for a path outside the project, also one that leaves it through a symbolic
|
|
76
|
+
* link, for a path in the knowledge, and for a path that the file system
|
|
77
|
+
* refuses: a folder, a file in place of a folder, no permission.
|
|
88
78
|
*/
|
|
89
79
|
export function writeProjectFile(path: string, bytes: Uint8Array, options: { root?: string; what?: string } = {}): void {
|
|
90
80
|
const root = options.root ?? currentProject();
|
|
91
81
|
const refuse = () => new WireFailure("invalid-input", `${options.what ?? path}: give a path relative to the project folder that stays inside it, not ${path}.`);
|
|
92
82
|
let file: string;
|
|
93
83
|
try {
|
|
94
|
-
file =
|
|
95
|
-
} catch {
|
|
96
|
-
throw
|
|
84
|
+
file = checkOutputPath(path, "file", { root, ...(options.what === undefined ? {} : { what: options.what }) });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (isRefusal(error)) throw new WireFailure(error.code, error.message);
|
|
87
|
+
throw error;
|
|
97
88
|
}
|
|
98
|
-
// The nearest folder that exists must be inside the project before a folder is made below it.
|
|
99
|
-
let existing = dirname(file);
|
|
100
|
-
while (!existsSync(existing)) existing = dirname(existing);
|
|
101
|
-
if (!isInside(realpathSync(root), realpathSync(existing))) throw refuse();
|
|
102
89
|
try {
|
|
103
90
|
mkdirSync(dirname(file), { recursive: true });
|
|
91
|
+
// Again after the folders are made: another process can change the folders between the check and the write.
|
|
104
92
|
if (!isInside(realpathSync(root), realpathSync(dirname(file)))) throw refuse();
|
|
105
93
|
let link = false;
|
|
106
94
|
try {
|
package/src/knowledge/history.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
type SuppressionRow,
|
|
38
38
|
type SymbolRow,
|
|
39
39
|
} from "./read.ts";
|
|
40
|
-
import { DEFAULT_SPACE, type EvidenceKind, type Origin, type Space } from "./schema.ts";
|
|
40
|
+
import { DEFAULT_SPACE, type CoverageKind, type EvidenceKind, type Origin, type Space } from "./schema.ts";
|
|
41
41
|
import { compareText, NO_REVISION } from "./text.ts";
|
|
42
42
|
import type { Evidence } from "./write.ts";
|
|
43
43
|
|
|
@@ -51,12 +51,47 @@ export interface Revision {
|
|
|
51
51
|
toolVersion?: string;
|
|
52
52
|
/** A revert: the revision whose knowledge it restored. */
|
|
53
53
|
restores?: string;
|
|
54
|
+
/** A revert of one space: that space. A revert without it restored every space. */
|
|
55
|
+
restoresSpace?: Space;
|
|
54
56
|
/** What the change rests on. */
|
|
55
57
|
evidence?: Evidence[];
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/** A memory row of the program: the bytes from an address. */
|
|
61
|
+
export interface MemoryRow {
|
|
62
|
+
space: Space;
|
|
63
|
+
address: number;
|
|
64
|
+
/** The count of bytes in the row. */
|
|
65
|
+
bytes: number;
|
|
66
|
+
origin: Origin;
|
|
67
|
+
revision: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The entry of the program, and whether it has a machine state and stages. */
|
|
71
|
+
export interface EntryRow {
|
|
72
|
+
space: Space;
|
|
73
|
+
address: number;
|
|
74
|
+
state: boolean;
|
|
75
|
+
stages: number;
|
|
76
|
+
origin: Origin;
|
|
77
|
+
revision: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A coverage range of one kind. */
|
|
81
|
+
export interface CoverageRow {
|
|
82
|
+
space: Space;
|
|
83
|
+
kind: CoverageKind;
|
|
84
|
+
start: number;
|
|
85
|
+
end: number;
|
|
86
|
+
origin: Origin;
|
|
87
|
+
revision: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
58
90
|
/** One knowledge row with its validity: from its first revision until the revision that closed it. */
|
|
59
91
|
export type HistoryEntry =
|
|
92
|
+
| { entity: "memory"; row: MemoryRow; from: string; to?: string }
|
|
93
|
+
| { entity: "entry"; row: EntryRow; from: string; to?: string }
|
|
94
|
+
| { entity: "coverage"; row: CoverageRow; from: string; to?: string }
|
|
60
95
|
| { entity: "symbol"; row: SymbolRow; from: string; to?: string }
|
|
61
96
|
| { entity: "analyzer-name"; row: SymbolRow; from: string; to?: string }
|
|
62
97
|
| { entity: "region"; row: RegionRow; from: string; to?: string }
|
|
@@ -82,6 +117,7 @@ function toRevision(row: Raw): Revision {
|
|
|
82
117
|
if (row.input_hash !== null) revision.inputHash = row.input_hash as string;
|
|
83
118
|
if (row.tool_version !== null) revision.toolVersion = row.tool_version as string;
|
|
84
119
|
if (row.restored_revision !== null) revision.restores = row.restored_revision as string;
|
|
120
|
+
if (row.restored_space !== null && row.restored_space !== undefined) revision.restoresSpace = row.restored_space as Space;
|
|
85
121
|
return revision;
|
|
86
122
|
}
|
|
87
123
|
|
|
@@ -183,6 +219,7 @@ export interface InitialRevision {
|
|
|
183
219
|
inputHash?: undefined;
|
|
184
220
|
toolVersion?: undefined;
|
|
185
221
|
restores?: undefined;
|
|
222
|
+
restoresSpace?: undefined;
|
|
186
223
|
evidence?: undefined;
|
|
187
224
|
}
|
|
188
225
|
|
|
@@ -213,9 +250,25 @@ export function revision(db: DatabaseSync | undefined, text: string): RevisionCh
|
|
|
213
250
|
["macro", "macros", MACRO_COLUMNS, toMacro],
|
|
214
251
|
["macro-use", "macro_uses", MACRO_USE_COLUMNS, toMacroUse],
|
|
215
252
|
] as const;
|
|
253
|
+
// The program: its memory, its entry and its coverage.
|
|
254
|
+
const program = [
|
|
255
|
+
["memory", "memory", "space, address, length(bytes) / 2 AS bytes, origin, valid_from_revision", (row: Raw): MemoryRow => ({ space: row.space as Space, address: row.address as number, bytes: row.bytes as number, origin: row.origin as Origin, revision: row.valid_from_revision as string })],
|
|
256
|
+
[
|
|
257
|
+
"entry",
|
|
258
|
+
"entries",
|
|
259
|
+
"space, address, state IS NOT NULL AS has_state, json_array_length(coalesce(stages, '[]')) AS stage_count, origin, valid_from_revision",
|
|
260
|
+
(row: Raw): EntryRow => ({ space: row.space as Space, address: row.address as number, state: row.has_state === 1, stages: row.stage_count as number, origin: row.origin as Origin, revision: row.valid_from_revision as string }),
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
"coverage",
|
|
264
|
+
"coverage",
|
|
265
|
+
"space, kind, start_address, end_address, origin, valid_from_revision",
|
|
266
|
+
(row: Raw): CoverageRow => ({ space: row.space as Space, kind: row.kind as CoverageKind, start: row.start_address as number, end: row.end_address as number, origin: row.origin as Origin, revision: row.valid_from_revision as string }),
|
|
267
|
+
],
|
|
268
|
+
] as const;
|
|
216
269
|
const added: HistoryEntry[] = [];
|
|
217
270
|
const closed: HistoryEntry[] = [];
|
|
218
|
-
for (const [entity, table, columns, map] of tables) {
|
|
271
|
+
for (const [entity, table, columns, map] of [...tables, ...program]) {
|
|
219
272
|
for (const row of db.prepare(`SELECT ${columns}, valid_to_revision FROM ${table} WHERE valid_from_revision = ?`).all(id) as Raw[]) {
|
|
220
273
|
added.push(entry(entity, map(row), row));
|
|
221
274
|
}
|
package/src/knowledge/import.ts
CHANGED
|
@@ -77,7 +77,18 @@ export type ImportConflict =
|
|
|
77
77
|
current: { address: number; name: string; kind: SymbolKind; origin: Origin };
|
|
78
78
|
finding: { name: string; kind: SymbolKind };
|
|
79
79
|
}
|
|
80
|
-
| { category: "region"; start: number; end: number; current: { type: RegionType; origin: Origin }; finding: { type: RegionType } }
|
|
80
|
+
| { category: "region"; start: number; end: number; current: { type: RegionType; origin: Origin }; finding: { type: RegionType } }
|
|
81
|
+
| {
|
|
82
|
+
/** A data finding over code that a run executed or that the user or the LLM named: the finding is not imported. */
|
|
83
|
+
category: "code-evidence";
|
|
84
|
+
start: number;
|
|
85
|
+
end: number;
|
|
86
|
+
finding: { type: RegionType };
|
|
87
|
+
/** The first executed instruction start in the range. */
|
|
88
|
+
executed?: number;
|
|
89
|
+
/** The first code name (label or routine) of the user or the LLM in the range. */
|
|
90
|
+
symbol?: { address: number; name: string; kind: SymbolKind; origin: Origin };
|
|
91
|
+
};
|
|
81
92
|
|
|
82
93
|
export interface ImportCounts {
|
|
83
94
|
added: number;
|
|
@@ -252,16 +263,53 @@ const YIELDS_TO_HEURISTIC = new Set<Origin>(["dxa", "ghidra"]);
|
|
|
252
263
|
/** Analyzers whose data gives way to a code finding over an executed instruction start. */
|
|
253
264
|
const YIELDS_TO_EXECUTED_CODE = new Set<Origin>(["dxa", "ghidra"]);
|
|
254
265
|
|
|
266
|
+
/** The first address in the range that is an executed instruction start, or undefined. */
|
|
267
|
+
function firstExecuted(findings: NormalizedFindings, start: number, end: number): number | undefined {
|
|
268
|
+
if (findings.executed === undefined) return undefined;
|
|
269
|
+
for (let address = start; address <= end; address++) if (findings.executed(address)) return address;
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
255
273
|
/** True when an address in the range is an executed instruction start. */
|
|
256
274
|
function holdsExecuted(findings: NormalizedFindings, start: number, end: number): boolean {
|
|
257
|
-
|
|
258
|
-
for (let address = start; address <= end; address++) if (findings.executed(address)) return true;
|
|
259
|
-
return false;
|
|
275
|
+
return firstExecuted(findings, start, end) !== undefined;
|
|
260
276
|
}
|
|
261
277
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
278
|
+
/**
|
|
279
|
+
* The data findings of DXA or Ghidra that code evidence contradicts: an
|
|
280
|
+
* executed instruction start, or a code name (label or routine) of the user or
|
|
281
|
+
* the LLM, in the range. Evidence wins over an analyzer guess: such a finding
|
|
282
|
+
* is not imported, and the analyzer's own code there stays.
|
|
283
|
+
*/
|
|
284
|
+
function contradictedData(db: DatabaseSync, findings: NormalizedFindings, space: Space): Array<ImportConflict & { category: "code-evidence" }> {
|
|
285
|
+
if (!YIELDS_TO_EXECUTED_CODE.has(findings.analyzer)) return [];
|
|
286
|
+
const names = db.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_symbols WHERE space = ? AND origin IN ('user', 'llm') AND kind IN ('label', 'routine') AND address BETWEEN ? AND ? ORDER BY address LIMIT 1`);
|
|
287
|
+
const found: Array<ImportConflict & { category: "code-evidence" }> = [];
|
|
288
|
+
for (const region of findings.regions) {
|
|
289
|
+
if (isCode(region.type) || region.type === "dual-use") continue;
|
|
290
|
+
const executed = firstExecuted(findings, region.start, region.end);
|
|
291
|
+
const raw = names.get(space, region.start, region.end) as Record<string, unknown> | undefined;
|
|
292
|
+
if (executed === undefined && raw === undefined) continue;
|
|
293
|
+
const symbol = raw === undefined ? undefined : toSymbol(raw);
|
|
294
|
+
found.push({
|
|
295
|
+
category: "code-evidence",
|
|
296
|
+
start: region.start,
|
|
297
|
+
end: region.end,
|
|
298
|
+
finding: { type: region.type },
|
|
299
|
+
...(executed === undefined ? {} : { executed }),
|
|
300
|
+
...(symbol === undefined ? {} : { symbol: { address: symbol.address, name: symbol.name, kind: symbol.kind, origin: symbol.origin } }),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return found;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function importRegions(db: DatabaseSync, change: Change, given: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
|
|
307
|
+
const { analyzer } = given;
|
|
308
|
+
const space = given.space ?? DEFAULT_SPACE;
|
|
309
|
+
// Data findings over code evidence are conflicts, not facts.
|
|
310
|
+
const contradicted = contradictedData(db, given, space);
|
|
311
|
+
result.conflicts.push(...contradicted);
|
|
312
|
+
const findings = { ...given, regions: given.regions.filter((region) => !contradicted.some((item) => item.start === region.start && item.end === region.end)) };
|
|
265
313
|
if (analyzer === "heuristic") {
|
|
266
314
|
// An idiom is specific evidence about its bytes: the DXA or Ghidra region there gives way, its parts outside stay.
|
|
267
315
|
for (const region of findings.regions) {
|
|
@@ -333,6 +381,14 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
333
381
|
if (suppressed.length > 0) result.regions.suppressed++;
|
|
334
382
|
for (const piece of subtract(region, [...protectedRows, ...suppressed])) desired.set(`${piece.start}-${piece.end}`, { ...piece, type: region.type });
|
|
335
383
|
}
|
|
384
|
+
// Under a contradicted data finding, the analyzer's own regions stay: they join the findings of the same type next to them.
|
|
385
|
+
for (const row of own) {
|
|
386
|
+
for (const item of contradicted) {
|
|
387
|
+
const inside = { start: Math.max(row.start, item.start, range.start), end: Math.min(row.end, item.end, range.end) };
|
|
388
|
+
if (inside.start > inside.end) continue;
|
|
389
|
+
for (const piece of subtract(inside, [...protectedRows, ...suppressionsOverlapping(db, "region", inside.start, inside.end, space)])) keepPiece(desired, { ...piece, type: row.type });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
336
392
|
const retiredRanges = new Set<string>();
|
|
337
393
|
for (const row of own) {
|
|
338
394
|
const inside = { start: Math.max(row.start, range.start), end: Math.min(row.end, range.end) };
|
|
@@ -357,6 +413,22 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
357
413
|
}
|
|
358
414
|
}
|
|
359
415
|
|
|
416
|
+
/** Puts a kept piece into the desired regions, joined with the pieces of the same type that touch or overlap it. */
|
|
417
|
+
function keepPiece(desired: Map<string, { start: number; end: number; type: RegionType }>, piece: { start: number; end: number; type: RegionType }): void {
|
|
418
|
+
let joined = { ...piece };
|
|
419
|
+
for (let found = true; found; ) {
|
|
420
|
+
found = false;
|
|
421
|
+
for (const [key, other] of desired) {
|
|
422
|
+
if (other.type !== joined.type || other.end + 1 < joined.start || joined.end + 1 < other.start) continue;
|
|
423
|
+
desired.delete(key);
|
|
424
|
+
joined = { start: Math.min(joined.start, other.start), end: Math.max(joined.end, other.end), type: joined.type };
|
|
425
|
+
found = true;
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
desired.set(`${joined.start}-${joined.end}`, joined);
|
|
430
|
+
}
|
|
431
|
+
|
|
360
432
|
function importReferences(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
|
|
361
433
|
const { analyzer } = findings;
|
|
362
434
|
const space = findings.space ?? DEFAULT_SPACE;
|