@henols/c64-re-tools 2.0.0-rc.18 → 2.0.0-rc.19
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 +5 -4
- package/skills/c64-assembler/scripts/assemble.ts +25 -5
- package/skills/c64-assembler/scripts/lib/compare.ts +11 -47
- package/skills/c64-basic/SKILL.md +3 -2
- package/skills/c64-disk/SKILL.md +3 -2
- package/skills/c64-docs/SKILL.md +3 -4
- package/skills/c64-docs/scripts/generate.ts +11 -19
- package/skills/c64-docs/scripts/lib/reference.ts +3 -3
- package/skills/c64-docs/scripts/lib/source-map.ts +0 -2
- package/skills/c64-emulator/SKILL.md +2 -2
- package/skills/c64-knowledge/SKILL.md +37 -19
- package/skills/c64-knowledge/scripts/knowledge.ts +138 -55
- package/skills/c64-memory-map/SKILL.md +3 -2
- package/skills/c64-provenance/SKILL.md +20 -19
- package/skills/c64-provenance/scripts/provenance.ts +67 -57
- package/skills/c64-reverse-engineering/SKILL.md +30 -28
- package/skills/c64-source/SKILL.md +33 -29
- package/skills/c64-source/scripts/generate.ts +46 -61
- package/skills/c64-static-analysis/SKILL.md +18 -18
- package/skills/c64-static-analysis/scripts/analyze.ts +33 -43
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +2 -10
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +4 -6
- package/skills/c64-testing/SKILL.md +10 -9
- package/skills/c64-testing/references/scenario-format.md +4 -4
- package/skills/c64-testing/scripts/lib/scenario.ts +13 -11
- package/skills/c64-unpacker/SKILL.md +9 -7
- package/src/host-client/image.ts +3 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +1 -1
- package/src/knowledge/schema.ts +46 -1
- package/src/knowledge/text.ts +25 -3
- package/src/knowledge/write.ts +43 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
7
8
|
import type { DatabaseSync } from "node:sqlite";
|
|
8
9
|
import { parseArgs } from "node:util";
|
|
9
10
|
|
|
@@ -13,7 +14,6 @@ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts"
|
|
|
13
14
|
import { CONFLICT_RULES, conflicts, type Conflict } from "#src/knowledge/conflicts.ts";
|
|
14
15
|
import { joinBatches, type Batch, type JoinChoice, type JoinClash } from "#src/knowledge/join.ts";
|
|
15
16
|
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
16
|
-
import { readCoverageFiles } from "#src/host-client/coverage.ts";
|
|
17
17
|
import { operandText } from "#src/knowledge/expression.ts";
|
|
18
18
|
import { historyAt, historyNamed, historyOfMacro, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
19
19
|
import {
|
|
@@ -98,6 +98,9 @@ import {
|
|
|
98
98
|
type WriteContext,
|
|
99
99
|
} from "#src/knowledge/write.ts";
|
|
100
100
|
import { platformNames } from "#src/platform.ts";
|
|
101
|
+
import { addCoverage, coverageRuns, programCoverage, programImage, requireProgram, setImage, type ImageRequest } from "#src/knowledge/program.ts";
|
|
102
|
+
import { WireFailure } from "#src/host-client/tools.ts";
|
|
103
|
+
import { isInside, LOCAL_DIRECTORY, PROJECT_DIRECTORY, projectRoot, resolveProjectPath } from "#src/project.ts";
|
|
101
104
|
import { listing, listingText, type ListingSource } from "./lib/listing.ts";
|
|
102
105
|
import { runScript, UsageError } from "#src/script.ts";
|
|
103
106
|
|
|
@@ -120,15 +123,19 @@ Read (no database needed):
|
|
|
120
123
|
macros [--origin O] the macros of the generated source
|
|
121
124
|
uses [--start A --end A --macro M --origin O] instructions written as a macro
|
|
122
125
|
spaces the address spaces that hold knowledge
|
|
126
|
+
program the program in the knowledge (D83): its
|
|
127
|
+
segments and entry, and the coverage maps
|
|
128
|
+
that coverage imported, with the ranges
|
|
123
129
|
history <address> | history --name <name> | history --macro <name>
|
|
124
130
|
revisions [--limit N --before R]
|
|
125
131
|
revision <revision>
|
|
126
132
|
export --out <file> [--start A --end A --origin O] all current knowledge to a project file
|
|
127
|
-
listing <start> <end>
|
|
128
|
-
the
|
|
133
|
+
listing <start> <end> [--out <file>]
|
|
134
|
+
the program as ACME lines with names, operand
|
|
129
135
|
expressions, comments, the access of each line
|
|
130
|
-
(x executed, r read, w written)
|
|
131
|
-
references in and out; --out writes it
|
|
136
|
+
in the coverage (x executed, r read, w written)
|
|
137
|
+
and the references in and out; --out writes it
|
|
138
|
+
as text
|
|
132
139
|
|
|
133
140
|
Every command takes --space S: c64 (the default), drive8 for code in the 1541
|
|
134
141
|
drive, or bank0 to bank255 for cartridge banks. A write in a batch can name its
|
|
@@ -148,15 +155,15 @@ Write (one revision each; creates .c64-re-tools/knowledge/ when needed):
|
|
|
148
155
|
unreference <from> <to> <kind>
|
|
149
156
|
module <start> <file.asm> [--title T] a file of generated source starts here
|
|
150
157
|
unmodule <start>
|
|
151
|
-
operand <address> --lo|--hi|--value <symbol> [--offset N]
|
|
152
|
-
operand <address> --expr <text>
|
|
158
|
+
operand <address> --lo|--hi|--value <symbol> [--offset N]
|
|
159
|
+
operand <address> --expr <text>
|
|
153
160
|
write the operand of the instruction at <address> as
|
|
154
161
|
an expression: the low or high byte of symbol + N, the
|
|
155
162
|
value symbol + N, or ACME expression text; refused when
|
|
156
|
-
it does not give the operand in the
|
|
157
|
-
|
|
163
|
+
it does not give the operand in the program of the
|
|
164
|
+
knowledge; give a negative offset as --offset=-1
|
|
158
165
|
unoperand <address>
|
|
159
|
-
patch <start> <end> <SWITCH> --code <text>|--code-file <file>|--nop [--comment T]
|
|
166
|
+
patch <start> <end> <SWITCH> --code <text>|--code-file <file>|--nop [--comment T]
|
|
160
167
|
ACME code that replaces <start>-<end> when the build
|
|
161
168
|
defines SWITCH as 1 (c64-assembler --define SWITCH=1);
|
|
162
169
|
NOP fills the rest of the range; --nop puts only NOP
|
|
@@ -164,10 +171,17 @@ Write (one revision each; creates .c64-re-tools/knowledge/ when needed):
|
|
|
164
171
|
macro <name> [--params a,b] --body <text>|--body-file <file> [--comment T]
|
|
165
172
|
an ACME macro; the body names a param with a dot: .a
|
|
166
173
|
unmacro <name>
|
|
167
|
-
use <start> <end> <macro> [--args "x, y"]
|
|
174
|
+
use <start> <end> <macro> [--args "x, y"]
|
|
168
175
|
write the instructions <start>-<end> as one use of
|
|
169
176
|
the macro, with one expression for each param
|
|
170
177
|
unuse <start>
|
|
178
|
+
image <image.json|prg> put the program into the knowledge (D83): its bytes, and
|
|
179
|
+
for an image.json of c64-unpacker capture also the
|
|
180
|
+
entry, the entry state and the stages; with --space for
|
|
181
|
+
drive code or a cartridge bank. Only changed bytes write
|
|
182
|
+
coverage <map.bin>... join coverage maps of c64_memmap or a run into the
|
|
183
|
+
coverage of the knowledge (D83); each map is evidence
|
|
184
|
+
of the revision, named by its path
|
|
171
185
|
revert <revision>
|
|
172
186
|
merge decide the conflicts that a git merge of two
|
|
173
187
|
computers' work left: of the rows of one conflict,
|
|
@@ -180,12 +194,14 @@ Write (one revision each; creates .c64-re-tools/knowledge/ when needed):
|
|
|
180
194
|
operand and module win, and its regions come last
|
|
181
195
|
apply <file> many writes from a JSON file ('-' reads standard input),
|
|
182
196
|
all as one revision, or none when one is refused;
|
|
183
|
-
operand, patch and use writes
|
|
197
|
+
operand, patch and use writes are checked against the
|
|
198
|
+
program of --space in the knowledge
|
|
184
199
|
|
|
185
200
|
Write options:
|
|
186
201
|
--reason <text> why, kept with the revision
|
|
187
|
-
--evidence <kind:ref> what the change rests on: coverage:<
|
|
188
|
-
file:<file> or
|
|
202
|
+
--evidence <kind:ref> what the change rests on: coverage:<map> (a map that
|
|
203
|
+
coverage imported), test:<file>, file:<file> or
|
|
204
|
+
note:<text> (repeatable)
|
|
189
205
|
--expect-revision <R> refuse the write if knowledge changed after revision R
|
|
190
206
|
--origin user|llm who decided (default llm)
|
|
191
207
|
|
|
@@ -364,7 +380,6 @@ function run(argv: string[]): unknown {
|
|
|
364
380
|
value: { type: "string" },
|
|
365
381
|
expr: { type: "string" },
|
|
366
382
|
offset: { type: "string" },
|
|
367
|
-
image: { type: "string" },
|
|
368
383
|
code: { type: "string" },
|
|
369
384
|
"code-file": { type: "string" },
|
|
370
385
|
nop: { type: "boolean" },
|
|
@@ -376,7 +391,6 @@ function run(argv: string[]): unknown {
|
|
|
376
391
|
switch: { type: "string" },
|
|
377
392
|
macro: { type: "string" },
|
|
378
393
|
analyzer: { type: "boolean" },
|
|
379
|
-
coverage: { type: "string", multiple: true },
|
|
380
394
|
help: { type: "boolean" },
|
|
381
395
|
},
|
|
382
396
|
});
|
|
@@ -387,8 +401,8 @@ function run(argv: string[]): unknown {
|
|
|
387
401
|
nameSpace = space;
|
|
388
402
|
const range = { start: values.start === undefined ? 0x0000 : address(values.start, "--start"), end: values.end === undefined ? 0xffff : address(values.end, "--end") };
|
|
389
403
|
|
|
390
|
-
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "operands", "patches", "switches", "macros", "uses", "spaces", "history", "revisions", "revision", "export", "listing"]);
|
|
391
|
-
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "operand", "unoperand", "patch", "unpatch", "macro", "unmacro", "use", "unuse", "revert", "apply", "merge"]);
|
|
404
|
+
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "operands", "patches", "switches", "macros", "uses", "spaces", "program", "history", "revisions", "revision", "export", "listing"]);
|
|
405
|
+
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "operand", "unoperand", "patch", "unpatch", "macro", "unmacro", "use", "unuse", "revert", "apply", "merge", "image", "coverage"]);
|
|
392
406
|
// join reads batch files and writes one: no knowledge opens (D79).
|
|
393
407
|
if (command === "join") return joinCommand(args, values.out, space);
|
|
394
408
|
// Refuse a mistyped command before anything opens, so no database appears by accident.
|
|
@@ -417,7 +431,6 @@ function run(argv: string[]): unknown {
|
|
|
417
431
|
if (values[option] !== undefined && command !== "operand") throw new UsageError(`--${option} works only with operand`);
|
|
418
432
|
}
|
|
419
433
|
const only: Array<[keyof typeof values, readonly string[]]> = [
|
|
420
|
-
["image", ["operand", "patch", "use", "apply"]],
|
|
421
434
|
["code", ["patch"]],
|
|
422
435
|
["code-file", ["patch"]],
|
|
423
436
|
["nop", ["patch"]],
|
|
@@ -431,8 +444,10 @@ function run(argv: string[]): unknown {
|
|
|
431
444
|
if (values[option] !== undefined && !commands.includes(command)) throw new UsageError(`--${option} works only with ${commands.join(", ")}`);
|
|
432
445
|
}
|
|
433
446
|
const operation =
|
|
434
|
-
command === "
|
|
435
|
-
?
|
|
447
|
+
command === "image" || command === "coverage"
|
|
448
|
+
? programOperation(command, args, context, space)
|
|
449
|
+
: command === "apply"
|
|
450
|
+
? applyOperation(args, context, space)
|
|
436
451
|
: command === "operand"
|
|
437
452
|
? operandOperation(args, values, context, space)
|
|
438
453
|
: command === "patch" || command === "macro" || command === "use"
|
|
@@ -443,8 +458,6 @@ function run(argv: string[]): unknown {
|
|
|
443
458
|
|
|
444
459
|
type ReadValues = {
|
|
445
460
|
analyzer?: boolean | undefined;
|
|
446
|
-
image?: string | undefined;
|
|
447
|
-
coverage?: string[] | undefined;
|
|
448
461
|
kind?: string | undefined;
|
|
449
462
|
type?: string | undefined;
|
|
450
463
|
name?: string | undefined;
|
|
@@ -455,19 +468,10 @@ type ReadValues = {
|
|
|
455
468
|
macro?: string | undefined;
|
|
456
469
|
};
|
|
457
470
|
|
|
458
|
-
/** The lookups of a listing (D78): the
|
|
459
|
-
function listingSource(db: DatabaseSync | undefined,
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
try {
|
|
463
|
-
image = readImage(imagePath);
|
|
464
|
-
} catch (error) {
|
|
465
|
-
if (error instanceof ImageError) throw new UsageError(error.message);
|
|
466
|
-
throw error;
|
|
467
|
-
}
|
|
468
|
-
const coverage = readCoverageFiles(coveragePaths, (message) => {
|
|
469
|
-
throw new UsageError(message);
|
|
470
|
-
});
|
|
471
|
+
/** The lookups of a listing (D78): the program and the coverage in the knowledge (D83), the knowledge of the space and the platform names of the C64. */
|
|
472
|
+
function listingSource(db: DatabaseSync | undefined, space: string): ListingSource {
|
|
473
|
+
const image = requireProgram(db, space);
|
|
474
|
+
const coverage = programCoverage(db, space)?.map;
|
|
471
475
|
const inside = (at: number) => image.segments.some((segment) => at >= segment.start && at <= segment.end);
|
|
472
476
|
const platform = new Map(space === DEFAULT_SPACE ? platformNames().map((entry) => [entry.address, entry.name]) : []);
|
|
473
477
|
return {
|
|
@@ -501,7 +505,7 @@ function readAnswer(db: DatabaseSync | undefined, command: string, args: string[
|
|
|
501
505
|
const start = address(args[0], "start");
|
|
502
506
|
const end = address(args[1], "end");
|
|
503
507
|
if (end < start) throw new UsageError("the end must not be before the start");
|
|
504
|
-
const source = listingSource(db,
|
|
508
|
+
const source = listingSource(db, space);
|
|
505
509
|
const lines = listing(source, start, end);
|
|
506
510
|
if (values.out !== undefined) {
|
|
507
511
|
writeProjectFile(values.out, Buffer.from(listingText(lines), "utf8"), { what: "--out" });
|
|
@@ -543,6 +547,24 @@ function readAnswer(db: DatabaseSync | undefined, command: string, args: string[
|
|
|
543
547
|
return { comments: listComments(db, range, origins, space).map(comment) };
|
|
544
548
|
case "references":
|
|
545
549
|
return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind"), origins, space).map(reference) };
|
|
550
|
+
case "program": {
|
|
551
|
+
const image = programImage(db, space);
|
|
552
|
+
const ranges = db === undefined ? [] : (db.prepare("SELECT kind, count(*) AS count FROM current_coverage WHERE space = ? GROUP BY kind ORDER BY kind").all(space) as Array<{ kind: string; count: number }>);
|
|
553
|
+
return {
|
|
554
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
555
|
+
image:
|
|
556
|
+
image === undefined
|
|
557
|
+
? null
|
|
558
|
+
: {
|
|
559
|
+
segments: image.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })),
|
|
560
|
+
bytes: image.segments.reduce((sum, segment) => sum + segment.end - segment.start + 1, 0),
|
|
561
|
+
...(image.entry === undefined ? {} : { entry: formatC64Address(image.entry) }),
|
|
562
|
+
state: image.state !== undefined,
|
|
563
|
+
stages: image.stages?.length ?? 0,
|
|
564
|
+
},
|
|
565
|
+
coverage: { maps: coverageRuns(db).map((run) => run.name), ranges: Object.fromEntries(ranges.map((row) => [row.kind, row.count])) },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
546
568
|
case "export": {
|
|
547
569
|
const exported = {
|
|
548
570
|
revision: currentRevision(db),
|
|
@@ -622,23 +644,37 @@ function originFilter(value: string): Origin[] {
|
|
|
622
644
|
return [...origins];
|
|
623
645
|
}
|
|
624
646
|
|
|
625
|
-
/**
|
|
647
|
+
/**
|
|
648
|
+
* --evidence kind:ref. A file kind names a project file and keeps its SHA-256, so a later change of the file shows.
|
|
649
|
+
* A file in .c64-re-tools/local/ is refused (D82): git ignores it, and the move that keeps it changes the path.
|
|
650
|
+
*/
|
|
626
651
|
function evidenceOf(value: string): Evidence {
|
|
627
652
|
const colon = value.indexOf(":");
|
|
628
653
|
const kind = oneOf(EVIDENCE_KINDS, colon < 0 ? value : value.slice(0, colon), "--evidence kind");
|
|
629
654
|
const ref = colon < 0 ? "" : value.slice(colon + 1);
|
|
630
655
|
if (ref === "") throw new UsageError(`--evidence needs ${kind}:<${kind === "note" ? "text" : "file"}>`);
|
|
631
656
|
if (kind === "note") return { kind, ref };
|
|
632
|
-
|
|
657
|
+
// A coverage map is evidence by the name that coverage imported it with (D83).
|
|
658
|
+
if (kind === "coverage") {
|
|
659
|
+
const db = openForRead();
|
|
660
|
+
try {
|
|
661
|
+
const run = coverageRuns(db).find((item) => item.name === ref);
|
|
662
|
+
if (run === undefined) throw new UsageError(`--evidence coverage:${ref} names no coverage map of the knowledge: import the map with coverage first`);
|
|
663
|
+
return { kind, ref, sha256: run.sha256 };
|
|
664
|
+
} finally {
|
|
665
|
+
db?.close();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const bytes = readProjectFile(ref).bytes;
|
|
669
|
+
const root = projectRoot();
|
|
670
|
+
if (isInside(join(root, PROJECT_DIRECTORY, LOCAL_DIRECTORY), resolveProjectPath(ref, root))) {
|
|
671
|
+
throw new UsageError(`--evidence ${kind}:${ref} is in ${PROJECT_DIRECTORY}/${LOCAL_DIRECTORY}/, which git ignores: move it to a folder that git keeps (a run folder to runs/<name>/) and give the new path`);
|
|
672
|
+
}
|
|
673
|
+
return { kind, ref, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
633
674
|
}
|
|
634
675
|
|
|
635
|
-
/**
|
|
636
|
-
|
|
637
|
-
* D72, D73), with the platform names. A byte outside the segments of the image
|
|
638
|
-
* is not in the image.
|
|
639
|
-
*/
|
|
640
|
-
function operandImage(path: string | undefined, what: string): OperandImage {
|
|
641
|
-
if (path === undefined) throw new UsageError(`${what} checks the write against the image: give --image <image.json|prg>`);
|
|
676
|
+
/** An image.json of c64-unpacker capture or a PRG as a write of the program (D83). */
|
|
677
|
+
function imageRequest(path: string, space: string): ImageRequest {
|
|
642
678
|
let image: ReturnType<typeof readImage>;
|
|
643
679
|
try {
|
|
644
680
|
image = readImage(path);
|
|
@@ -646,6 +682,57 @@ function operandImage(path: string | undefined, what: string): OperandImage {
|
|
|
646
682
|
if (error instanceof ImageError) throw new UsageError(error.message);
|
|
647
683
|
throw error;
|
|
648
684
|
}
|
|
685
|
+
const request: ImageRequest = { space, segments: image.segments, byteAt: image.byteAt };
|
|
686
|
+
if (image.entry !== undefined) request.entry = image.entry;
|
|
687
|
+
if (image.stages !== undefined) request.stages = image.stages;
|
|
688
|
+
if (image.statePath !== undefined) {
|
|
689
|
+
let bytes: Buffer | undefined;
|
|
690
|
+
try {
|
|
691
|
+
bytes = readProjectFile(image.statePath).bytes;
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (!(error instanceof WireFailure && error.code === "not-found")) throw error;
|
|
694
|
+
}
|
|
695
|
+
if (bytes !== undefined) {
|
|
696
|
+
let state: unknown;
|
|
697
|
+
try {
|
|
698
|
+
state = JSON.parse(bytes.toString("utf8"));
|
|
699
|
+
} catch (error) {
|
|
700
|
+
throw new UsageError(`${image.statePath} is not JSON: ${(error as Error).message}`);
|
|
701
|
+
}
|
|
702
|
+
if (typeof state !== "object" || state === null || Array.isArray(state)) throw new UsageError(`${image.statePath} is not an entry state of c64-unpacker capture`);
|
|
703
|
+
// The comparison with a clean reset only tells the capture what it found.
|
|
704
|
+
const { differsFromReset: _differs, ...kept } = state as Record<string, unknown>;
|
|
705
|
+
request.state = kept;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return request;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** image and coverage (D83): the program and what the runs did go into the knowledge, so no project file is made again. */
|
|
712
|
+
function programOperation(command: "image" | "coverage", args: string[], context: WriteContext, space: string): WriteOperation {
|
|
713
|
+
if (command === "image") {
|
|
714
|
+
if (args.length !== 1) throw new UsageError("image needs one image.json or PRG");
|
|
715
|
+
const request = imageRequest(args[0]!, space);
|
|
716
|
+
return (db) => {
|
|
717
|
+
const { revision, current } = setImage(db, context, request);
|
|
718
|
+
return { revision, ...spaceOf(current), bytes: current.bytes, rowsAdded: current.rowsAdded, rowsClosed: current.rowsClosed, entryChanged: current.entry };
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (args.length === 0) throw new UsageError("coverage needs one or more coverage maps");
|
|
722
|
+
const maps = args.map((path) => ({ name: path, bytes: readProjectFile(path).bytes }));
|
|
723
|
+
return (db) => {
|
|
724
|
+
const { revision, current } = addCoverage(db, context, { space, maps });
|
|
725
|
+
return { revision, ...spaceOf(current), imported: current.imported, known: current.known, ranges: current.ranges };
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* The program in the knowledge (D83) that operand, patch and macro use writes
|
|
731
|
+
* are checked against (D65, D72, D73), with the platform names. A byte outside
|
|
732
|
+
* the segments of the program is not in it.
|
|
733
|
+
*/
|
|
734
|
+
function operandImage(db: DatabaseSync, space: string): OperandImage {
|
|
735
|
+
const image = requireProgram(db, space);
|
|
649
736
|
const inside = (at: number) => image.segments.some((segment) => at >= segment.start && at <= segment.end);
|
|
650
737
|
return { byteAt: (at) => (inside(at) ? image.byteAt(at) : undefined), platform: new Map(platformNames().map((entry) => [entry.name, entry.address])) };
|
|
651
738
|
}
|
|
@@ -672,9 +759,8 @@ function operandOperation(args: string[], values: Record<string, unknown>, conte
|
|
|
672
759
|
request.symbol = values[select] as string;
|
|
673
760
|
if (values.offset !== undefined) request.offset = offsetOf(values.offset, "--offset");
|
|
674
761
|
}
|
|
675
|
-
const image = operandImage(values.image as string | undefined, "operand");
|
|
676
762
|
return (db) => {
|
|
677
|
-
const result = setOperand(db, { ...context, image }, request);
|
|
763
|
+
const result = setOperand(db, { ...context, image: operandImage(db, space) }, request);
|
|
678
764
|
return { revision: result.revision, operand: operand(result.current), ...(result.previous === undefined ? {} : { previous: operand(result.previous) }) };
|
|
679
765
|
};
|
|
680
766
|
}
|
|
@@ -702,9 +788,8 @@ function sourceOperation(command: "patch" | "macro" | "use", args: string[], val
|
|
|
702
788
|
const code = textOrFile(values, "code", "--code");
|
|
703
789
|
if ((code === undefined) === (values.nop !== true)) throw new UsageError("patch needs exactly one of --code, --code-file or --nop");
|
|
704
790
|
const request = { space, start: address(args[0], "start"), end: address(args[1], "end"), switch: args[2], code: code ?? "", ...(comment === undefined ? {} : { comment }) };
|
|
705
|
-
const image = operandImage(values.image as string | undefined, "patch");
|
|
706
791
|
return (db) => {
|
|
707
|
-
const result = setPatch(db, { ...context, image }, request);
|
|
792
|
+
const result = setPatch(db, { ...context, image: operandImage(db, space) }, request);
|
|
708
793
|
return { revision: result.revision, patch: patch(result.current), ...(result.previous === undefined ? {} : { previous: patch(result.previous) }) };
|
|
709
794
|
};
|
|
710
795
|
}
|
|
@@ -721,9 +806,8 @@ function sourceOperation(command: "patch" | "macro" | "use", args: string[], val
|
|
|
721
806
|
case "use": {
|
|
722
807
|
if (args[2] === undefined) throw new UsageError("the macro name is missing");
|
|
723
808
|
const request = { space, start: address(args[0], "start"), end: address(args[1], "end"), macro: args[2], args: commaList((values.args as string | undefined) ?? "") };
|
|
724
|
-
const image = operandImage(values.image as string | undefined, "use");
|
|
725
809
|
return (db) => {
|
|
726
|
-
const result = setMacroUse(db, { ...context, image }, request);
|
|
810
|
+
const result = setMacroUse(db, { ...context, image: operandImage(db, space) }, request);
|
|
727
811
|
return { revision: result.revision, use: macroUse(result.current), ...(result.previous === undefined ? {} : { previous: macroUse(result.previous) }) };
|
|
728
812
|
};
|
|
729
813
|
}
|
|
@@ -731,7 +815,7 @@ function sourceOperation(command: "patch" | "macro" | "use", args: string[], val
|
|
|
731
815
|
}
|
|
732
816
|
|
|
733
817
|
/** apply <file>: the batch as BatchOperation values, checked before the database opens. */
|
|
734
|
-
function applyOperation(args: string[], context: WriteContext, space: string
|
|
818
|
+
function applyOperation(args: string[], context: WriteContext, space: string): WriteOperation {
|
|
735
819
|
if (args.length !== 1) throw new UsageError("apply needs one file, or - for standard input");
|
|
736
820
|
const text = args[0] === "-" ? readFileSync(0, "utf8") : readProjectFile(args[0]!).bytes.toString("utf8");
|
|
737
821
|
let parsed: unknown;
|
|
@@ -743,9 +827,8 @@ function applyOperation(args: string[], context: WriteContext, space: string, im
|
|
|
743
827
|
if (!Array.isArray(parsed)) throw new UsageError("the batch is a JSON list of writes, for example [{\"op\": \"rename\", \"address\": \"$2100\", \"name\": \"update_player\"}]");
|
|
744
828
|
const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
|
|
745
829
|
const checked = operations.some((operation) => operation.op === "operand" || operation.op === "patch" || operation.op === "use");
|
|
746
|
-
const withImage = checked ? { ...context, image: operandImage(imagePath, "apply with operand, patch or use writes") } : context;
|
|
747
830
|
return (db) => {
|
|
748
|
-
const applied = applyBatch(db,
|
|
831
|
+
const applied = applyBatch(db, checked ? { ...context, image: operandImage(db, space) } : context, operations);
|
|
749
832
|
// replaced: each write that replaced a row of another origin, with that row as previous gives it for one write (b2/07).
|
|
750
833
|
const replaced = applied.steps.flatMap((step, index) =>
|
|
751
834
|
step.previous === undefined
|
|
@@ -42,8 +42,9 @@ The script prints one JSON object.
|
|
|
42
42
|
|
|
43
43
|
- Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
|
|
44
44
|
- If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
|
|
45
|
-
-
|
|
46
|
-
-
|
|
45
|
+
- Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
|
|
46
|
+
- Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence` of c64-knowledge. `--evidence` refuses a file in `.c64-re-tools/local/`.
|
|
47
|
+
- Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
|
|
47
48
|
- A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
|
|
48
49
|
|
|
49
50
|
## Workflow
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: c64-provenance
|
|
3
|
-
description: Use this skill to compare two or more releases of a C64 program, for example cracked versions from different groups. It finds the bytes where the releases differ and shows which releases agree there. It can run each release and tell how the program uses each difference.
|
|
3
|
+
description: Use this skill to compare two or more releases of a C64 program, for example cracked versions from different groups. It finds the bytes where the releases differ and shows which releases agree there. It can run each release and tell how the program uses each difference. It also gives evidence for each range of the program in the knowledge. The evidence is the unpack stage, the access, and the code that the entry does not reach. Use the result to separate the content of the application from changes that a release added. It also keeps a library of routines that several programs share. It finds them in the program of the knowledge and writes their names as a batch for c64-knowledge. Do not use it to analyze the code.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# c64-provenance
|
|
@@ -19,8 +19,8 @@ This skill does not change project knowledge.
|
|
|
19
19
|
- Optional: an address range to compare.
|
|
20
20
|
- Optional: a shift for a release that loads at a different address.
|
|
21
21
|
- Optional: a number of frames to run each release, and a steps file.
|
|
22
|
-
- For one release: the
|
|
23
|
-
- For the library: the
|
|
22
|
+
- For one release: the program, the coverage and the other knowledge of the project. Put the program and the coverage in with `image` and `coverage` of the c64-knowledge skill.
|
|
23
|
+
- For the library: the program and the other knowledge of its project, and the library folder.
|
|
24
24
|
|
|
25
25
|
## Tools / execution path
|
|
26
26
|
|
|
@@ -29,8 +29,8 @@ The script compares the files locally. Only `--run` uses the host runtime: each
|
|
|
29
29
|
```
|
|
30
30
|
node <skill>/scripts/provenance.ts releases/a.prg releases/b.prg releases/c.prg
|
|
31
31
|
node <skill>/scripts/provenance.ts releases/a.prg releases/b.prg --shift 2:-256 --from '$0900' --to '$09ff'
|
|
32
|
-
node <skill>/scripts/provenance.ts releases/a.prg
|
|
33
|
-
node <skill>/scripts/provenance.ts separate
|
|
32
|
+
node <skill>/scripts/provenance.ts releases/a.prg .c64-re-tools/local/b/image.json --run 600 --steps .c64-re-tools/local/start-game.json
|
|
33
|
+
node <skill>/scripts/provenance.ts separate --out .c64-re-tools/local/separate.json
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
| Option | Meaning |
|
|
@@ -51,8 +51,9 @@ With `--run`, a PRG starts with autostart and RUN. An image starts from its byte
|
|
|
51
51
|
|
|
52
52
|
- Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
|
|
53
53
|
- If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
|
|
54
|
-
-
|
|
55
|
-
-
|
|
54
|
+
- Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
|
|
55
|
+
- Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence` of c64-knowledge. `--evidence` refuses a file in `.c64-re-tools/local/`.
|
|
56
|
+
- Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
|
|
56
57
|
- A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
|
|
57
58
|
|
|
58
59
|
## Workflow
|
|
@@ -76,15 +77,15 @@ With `--run`, a PRG starts with autostart and RUN. An image starts from its byte
|
|
|
76
77
|
|
|
77
78
|
## One release
|
|
78
79
|
|
|
79
|
-
Use `separate` when you have only one release. It gives evidence, not a decision:
|
|
80
|
+
Use `separate` when you have only one release. It reads the program, its stages and the coverage from the knowledge. It gives evidence, not a decision:
|
|
80
81
|
|
|
81
|
-
- `ranges`: the
|
|
82
|
-
- `access`: from the
|
|
82
|
+
- `ranges`: the program in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. The last stage is the program. An earlier stage is a depacker, an intro or a loader. Only a capture of c64-unpacker `capture` has stages. For a program from `loads` or a PRG, `noStages` tells this.
|
|
83
|
+
- `access`: from the coverage of the knowledge: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs. A `read` byte right after executed code, or at a return address, can be a dummy read of the 6502 that the program does not use.
|
|
83
84
|
- `codeRegions.total`: the number of code regions in the knowledge.
|
|
84
85
|
- `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from the entry. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
|
|
85
|
-
- `outside`: RAM outside the
|
|
86
|
+
- `outside`: RAM outside the program that the program `read` or `executed` but did not write, and code it `generated`. A loader or an earlier stage put the bytes that the program did not write. A `generated` range can have gaps of two bytes or less: these are operand bytes.
|
|
86
87
|
|
|
87
|
-
Each list has `count` and `listed`. `listed` has the first ranges, at most `--max-listed`. `--out` writes all of them. Run more coverage runs for parts that are `untouched` before you decide.
|
|
88
|
+
Each list has `count` and `listed`. `listed` has the first ranges, at most `--max-listed`. `--out` writes all of them. Run more coverage runs for parts that are `untouched`, and add them with `coverage` of the c64-knowledge skill, before you decide.
|
|
88
89
|
|
|
89
90
|
## Library of routines
|
|
90
91
|
|
|
@@ -93,14 +94,14 @@ A library is a folder of routine files. Each file holds one routine: its bytes a
|
|
|
93
94
|
`--library` is relative to the working directory, as `--project`. All other paths are relative to the project folder.
|
|
94
95
|
|
|
95
96
|
```
|
|
96
|
-
node <skill>/scripts/provenance.ts routine
|
|
97
|
-
node <skill>/scripts/provenance.ts routine
|
|
98
|
-
node <skill>/scripts/provenance.ts match
|
|
97
|
+
node <skill>/scripts/provenance.ts routine --module '$1000' --library library --project game-a
|
|
98
|
+
node <skill>/scripts/provenance.ts routine --start '$1000' --end '$1026' --name play --library library --project game-a
|
|
99
|
+
node <skill>/scripts/provenance.ts match --library library --out .c64-re-tools/local/match.json --project game-b
|
|
99
100
|
```
|
|
100
101
|
|
|
101
102
|
To save a routine:
|
|
102
103
|
|
|
103
|
-
1.
|
|
104
|
+
1. Put the program into the knowledge, and name the routine there first. The file keeps the user and LLM names, comments, regions, references and operand expressions in the range. It also keeps the code regions of each origin.
|
|
104
105
|
2. Run `routine` with the start of a module (`--module`), or with `--start` and `--end`. A module ends at the next module or at the end of its segment.
|
|
105
106
|
- `--name` is the name of the routine and of its file. When you do not give it, the script uses the user or LLM name at the start.
|
|
106
107
|
- `--title` is the title of the routine. When you do not give it, the script uses the module title.
|
|
@@ -109,7 +110,7 @@ To save a routine:
|
|
|
109
110
|
|
|
110
111
|
To find the routines in a program:
|
|
111
112
|
|
|
112
|
-
1.
|
|
113
|
+
1. Put the program into the knowledge. Then run `match`. The script examines each routine of the library at each address of the program.
|
|
113
114
|
- The opcodes and the other bytes must be equal.
|
|
114
115
|
- An address into the routine must point into the routine at its new address. `shift` is the address in the program minus the address in the library.
|
|
115
116
|
- An operand that addresses something outside the routine can be different. `differing` lists each one with its value in the library and in the program. Examine them. For example, the program can use other variables or another SID voice.
|
|
@@ -119,7 +120,7 @@ To find the routines in a program:
|
|
|
119
120
|
- The program uses the name at another address.
|
|
120
121
|
- A removal holds at that place.
|
|
121
122
|
- The routine occurs two times. Only the first copy gets the names.
|
|
122
|
-
- The script leaves out the name that an operand expression uses, or the expression does not give the operand in the
|
|
123
|
+
- The script leaves out the name that an operand expression uses, or the expression does not give the operand in the program.
|
|
123
124
|
4. Apply the batch with the command in `apply`. Add `--project` when you are not in the project folder. The batch is one revision of the LLM. `batch.known` counts the writes that the program has already.
|
|
124
125
|
|
|
125
126
|
## Knowledge
|
|
@@ -151,7 +152,7 @@ The exit status is 0 for a result. The exit status is 1 for an error. The exit s
|
|
|
151
152
|
- `not-found` from `match`: the library folder does not exist, or it has no routine files. Save a routine with `routine` first.
|
|
152
153
|
- `conflict` from `routine`: the library has a routine with that name. Give another `--name`, or give `--replace` to save over it.
|
|
153
154
|
- `invalid-input` with "is a project folder": the library folder is a project. Give a folder beside the projects.
|
|
154
|
-
- `match` gives `count: 0`: the routine is not in the
|
|
155
|
+
- `match` gives `count: 0`: the routine is not in the program. A packed program can hold the routine in a packed form. Unpack the program with c64-unpacker first.
|
|
155
156
|
- `invalid-input`: a file, an address or a shift is wrong. A path that goes out of the project folder is also wrong, also through a link. Read the message.
|
|
156
157
|
|
|
157
158
|
## Handoffs
|