@henols/c64-re-tools 1.1.0 → 2.0.0-rc.3
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/LICENSE +21 -0
- package/README.md +138 -37
- package/distribution/package.ts +36 -0
- package/distribution/plugin.ts +120 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +73 -0
- package/skills/c64-assembler/scripts/assemble.ts +103 -0
- package/skills/c64-basic/SKILL.md +67 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +79 -0
- package/skills/c64-disk/scripts/disk.ts +65 -0
- package/skills/c64-emulator/SKILL.md +124 -0
- package/skills/c64-knowledge/SKILL.md +102 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
- package/skills/c64-memory-map/SKILL.md +76 -0
- package/skills/c64-memory-map/scripts/memmap.ts +81 -0
- package/skills/c64-memory-map/scripts/platform.ts +424 -0
- package/skills/c64-provenance/SKILL.md +76 -0
- package/skills/c64-provenance/scripts/compare.ts +92 -0
- package/skills/c64-provenance/scripts/provenance.ts +83 -0
- package/skills/c64-reverse-engineering/SKILL.md +90 -0
- package/skills/c64-static-analysis/SKILL.md +96 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
- package/skills/c64-static-analysis/scripts/findings.ts +73 -0
- package/skills/c64-testing/SKILL.md +79 -0
- package/skills/c64-testing/references/scenario-format.md +76 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/scenario.ts +473 -0
- package/skills/c64-testing/scripts/test.ts +55 -0
- package/skills/c64-unpacker/SKILL.md +82 -0
- package/skills/c64-unpacker/scripts/evidence.ts +135 -0
- package/skills/c64-unpacker/scripts/unpack.ts +216 -0
- package/src/c64.ts +281 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +122 -0
- package/src/cli/status.ts +34 -0
- package/src/host/main.ts +106 -0
- package/src/host/server.ts +376 -0
- package/src/host/tools/c1541.ts +255 -0
- package/src/host/tools/index.ts +31 -0
- package/src/host/tools/petcat.ts +70 -0
- package/src/host/tools/status.ts +30 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +761 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +108 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +402 -0
- package/src/host/vice/screen.ts +183 -0
- package/src/host/vice/session.ts +1316 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/connect.ts +238 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +100 -0
- package/src/host-client/vice-session.ts +254 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +128 -0
- package/src/knowledge/import.ts +344 -0
- package/src/knowledge/read.ts +191 -0
- package/src/knowledge/schema.ts +140 -0
- package/src/knowledge/write.ts +348 -0
- package/src/mcp/main.ts +106 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/tools/debug.ts +237 -0
- package/src/mcp/tools/execution.ts +116 -0
- package/src/mcp/tools/input.ts +69 -0
- package/src/mcp/tools/machine.ts +97 -0
- package/src/mcp/tools/media.ts +106 -0
- package/src/mcp/tools/memory.ts +203 -0
- package/src/mcp/tools/video.ts +260 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +141 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +157 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +242 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +101 -0
- package/src/native/types.ts +228 -0
- package/src/native/watchdog.ts +78 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +243 -0
- package/src/protocol/tools.ts +247 -0
- package/src/protocol/vice.ts +1235 -0
- package/src/script.ts +59 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +162 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The c64-provenance script: compares two or more releases byte by byte at
|
|
2
|
+
// their addresses and reports where they differ and which releases agree.
|
|
3
|
+
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
|
|
6
|
+
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
7
|
+
import { readProjectFile } from "#src/host-client/transfer.ts";
|
|
8
|
+
import { runScript, UsageError } from "#src/script.ts";
|
|
9
|
+
import { compareReleases, suggestShift, type Release } from "./compare.ts";
|
|
10
|
+
|
|
11
|
+
const USAGE = `provenance.ts <release.prg> <release.prg> [<release.prg> ...] [options]
|
|
12
|
+
|
|
13
|
+
--shift <n>:<bytes> move release n (1 = the first) by a number of bytes, for example 2:-256
|
|
14
|
+
--from <address> compare from this address
|
|
15
|
+
--to <address> compare up to this address
|
|
16
|
+
|
|
17
|
+
Each release is a PRG file relative to the project directory, placed at its
|
|
18
|
+
load address. The result is one JSON object.`;
|
|
19
|
+
|
|
20
|
+
function address(text: string, option: string): number {
|
|
21
|
+
try {
|
|
22
|
+
return parseC64Address(text);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new UsageError(`${option} must be $ followed by four hex digits, not ${text}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const hexBytes = (bytes: number[]) => Buffer.from(bytes).toString("hex").replace(/(..)(?!$)/g, "$1 ");
|
|
29
|
+
|
|
30
|
+
function run(argv: string[]): unknown {
|
|
31
|
+
const { values, positionals } = parseArgs({
|
|
32
|
+
args: argv,
|
|
33
|
+
strict: true,
|
|
34
|
+
allowPositionals: true,
|
|
35
|
+
options: { shift: { type: "string", multiple: true }, from: { type: "string" }, to: { type: "string" }, help: { type: "boolean" } },
|
|
36
|
+
});
|
|
37
|
+
if (values.help) throw new UsageError("");
|
|
38
|
+
if (positionals.length < 2) throw new UsageError("give two or more releases");
|
|
39
|
+
const shifts = new Map<number, number>();
|
|
40
|
+
for (const text of values.shift ?? []) {
|
|
41
|
+
const match = /^(\d+):([+-]?\d+)$/.exec(text);
|
|
42
|
+
if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
|
|
43
|
+
shifts.set(Number(match[1]), Number(match[2]));
|
|
44
|
+
}
|
|
45
|
+
const releases: Release[] = positionals.map((path, index) => {
|
|
46
|
+
const { bytes } = readProjectFile(path);
|
|
47
|
+
if (bytes.length < 3) throw new UsageError(`${path} is too short for a PRG`);
|
|
48
|
+
const start = bytes.readUInt16LE(0) + (shifts.get(index + 1) ?? 0);
|
|
49
|
+
if (start < 0 || start + bytes.length - 3 > 0xffff) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
|
|
50
|
+
return { name: path, start, bytes: bytes.subarray(2) };
|
|
51
|
+
});
|
|
52
|
+
const comparison = compareReleases(releases, {
|
|
53
|
+
...(values.from === undefined ? {} : { from: address(values.from, "--from") }),
|
|
54
|
+
...(values.to === undefined ? {} : { to: address(values.to, "--to") }),
|
|
55
|
+
});
|
|
56
|
+
const compared = comparison.identicalBytes + comparison.differingBytes;
|
|
57
|
+
const identicalShare = compared === 0 ? 0 : comparison.identicalBytes / compared;
|
|
58
|
+
// A poor match at the load addresses can mean that the releases are placed differently.
|
|
59
|
+
const suggestions =
|
|
60
|
+
identicalShare < 0.5
|
|
61
|
+
? releases.slice(1).flatMap((release, index) => {
|
|
62
|
+
const best = suggestShift(releases[0]!, release);
|
|
63
|
+
return best === undefined || best.shift === 0 ? [] : [{ release: index + 2, shift: best.shift, matchingSequences: best.matches }];
|
|
64
|
+
})
|
|
65
|
+
: [];
|
|
66
|
+
return {
|
|
67
|
+
releases: releases.map((release) => ({ name: release.name, start: formatC64Address(release.start), end: formatC64Address(release.start + release.bytes.length - 1) })),
|
|
68
|
+
common: comparison.common === null ? null : { start: formatC64Address(comparison.common.start), end: formatC64Address(comparison.common.end) },
|
|
69
|
+
identicalBytes: comparison.identicalBytes,
|
|
70
|
+
differingBytes: comparison.differingBytes,
|
|
71
|
+
identicalShare: Number(identicalShare.toFixed(3)),
|
|
72
|
+
differences: comparison.differences.map((difference) => ({
|
|
73
|
+
start: formatC64Address(difference.start),
|
|
74
|
+
end: formatC64Address(difference.end),
|
|
75
|
+
values: Object.fromEntries(Object.entries(difference.values).map(([name, bytes]) => [name, hexBytes(bytes.slice(0, 32)) + (bytes.length > 32 ? " ..." : "")])),
|
|
76
|
+
groups: difference.groups,
|
|
77
|
+
})),
|
|
78
|
+
...(comparison.moreDifferences ? { moreDifferences: true } : {}),
|
|
79
|
+
...(suggestions.length > 0 ? { suggestedShifts: suggestions } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await runScript(() => run(process.argv.slice(2)), { usage: USAGE });
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-reverse-engineering
|
|
3
|
+
description: Use this skill to reverse engineer a C64 program or a part of it. The work goes from an unknown disk or file to documented understanding or a rebuilt program. It decides what to learn next and which specialist skill does that work. Use it for questions such as "what does this game do", "find the main loop", "understand $2100" or "rebuild this program".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# c64-reverse-engineering
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Coordinate the reverse engineering of a C64 program. Decide the next unknown to examine and the specialist skill that examines it.
|
|
11
|
+
|
|
12
|
+
This skill does not do the specialist work itself.
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
- The subject: a disk image or a program file in the project, or a program that runs in the emulator.
|
|
17
|
+
- The scope: the question or the target. Examples:
|
|
18
|
+
- identify the main loop
|
|
19
|
+
- understand the routine at `$2100`
|
|
20
|
+
- find the IRQ handler
|
|
21
|
+
- reconstruct the input system
|
|
22
|
+
- reverse engineer and rebuild the whole program
|
|
23
|
+
- The current project knowledge.
|
|
24
|
+
|
|
25
|
+
## Tools / execution path
|
|
26
|
+
|
|
27
|
+
This skill uses the other skills. It does not use the host runtime or the emulator directly.
|
|
28
|
+
|
|
29
|
+
## Workflow
|
|
30
|
+
|
|
31
|
+
1. Write down the scope. Stop when the scope is complete.
|
|
32
|
+
2. Read the current knowledge for the subject with the c64-knowledge skill. Do not examine again what the knowledge already explains.
|
|
33
|
+
3. Find the next unknown or blocker, and use the specialist for it:
|
|
34
|
+
|
|
35
|
+
| Unknown | Skill |
|
|
36
|
+
|---|---|
|
|
37
|
+
| The files on a disk image, or a file to extract | c64-disk |
|
|
38
|
+
| A BASIC program, and where it starts machine code | c64-basic |
|
|
39
|
+
| A program that can be packed or crunched | c64-unpacker |
|
|
40
|
+
| What the program does at run time: values, input, timing, screen | c64-emulator |
|
|
41
|
+
| The routines, code, data and references of the program | c64-static-analysis |
|
|
42
|
+
| The platform meaning of an address or a register | c64-memory-map |
|
|
43
|
+
| Differences between releases of the program | c64-provenance |
|
|
44
|
+
| Source code to build | c64-assembler |
|
|
45
|
+
| Proof that a rebuild behaves as the original | c64-testing |
|
|
46
|
+
|
|
47
|
+
4. Interpret the result. Keep observed facts apart from your conclusions.
|
|
48
|
+
5. Record each conclusion that the evidence supports with the c64-knowledge skill: names of routines and variables, data classifications, comments.
|
|
49
|
+
6. Go back to step 3 until the scope is complete.
|
|
50
|
+
7. For a rebuild scope: write the source, build it with the c64-assembler skill, and test it against the original with the c64-testing skill.
|
|
51
|
+
|
|
52
|
+
A typical order for an unknown program:
|
|
53
|
+
|
|
54
|
+
1. c64-disk: list the disk and extract the start file.
|
|
55
|
+
2. c64-basic: find the SYS address of the BASIC start.
|
|
56
|
+
3. c64-unpacker: find out if a packer compressed the program. Unpack it if necessary.
|
|
57
|
+
4. c64-emulator: run the program and observe it.
|
|
58
|
+
5. c64-static-analysis: find the routines, regions and references.
|
|
59
|
+
6. c64-knowledge and c64-memory-map: name the important routines and variables.
|
|
60
|
+
7. c64-static-analysis again: Ghidra now uses the names.
|
|
61
|
+
8. c64-assembler and c64-testing: rebuild and test when the scope asks for it.
|
|
62
|
+
|
|
63
|
+
## Knowledge
|
|
64
|
+
|
|
65
|
+
Read:
|
|
66
|
+
- the knowledge for the addresses, ranges and routines in the scope
|
|
67
|
+
- the history of a conclusion that is surprising or that a person disputes.
|
|
68
|
+
|
|
69
|
+
Automatic write:
|
|
70
|
+
- none directly.
|
|
71
|
+
|
|
72
|
+
Record durable conclusions with the c64-knowledge skill. Do not store:
|
|
73
|
+
- the state of the current investigation
|
|
74
|
+
- a name only because a routine needs a temporary name
|
|
75
|
+
- guesses.
|
|
76
|
+
|
|
77
|
+
## Result
|
|
78
|
+
|
|
79
|
+
A clear answer for the scope, with the durable conclusions in the project knowledge. For a rebuild scope, also a rebuilt program that passes its tests.
|
|
80
|
+
|
|
81
|
+
## Failure and conflicts
|
|
82
|
+
|
|
83
|
+
- If the evidence does not support a conclusion, keep the unknown and get more evidence. Do not make up a meaning.
|
|
84
|
+
- Before you replace current knowledge from the user or the LLM, read its history and the evidence for it.
|
|
85
|
+
- A conflict from the static analysis: find the correct answer at run time with the c64-emulator skill.
|
|
86
|
+
- A test that is INCONCLUSIVE does not show a defect. Find why the test could not compare.
|
|
87
|
+
|
|
88
|
+
## Handoffs
|
|
89
|
+
|
|
90
|
+
All specialist skills in the table above.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-static-analysis
|
|
3
|
+
description: Use this skill to find the structure of a C64 program without running it. DXA gives a fast first pass of code and data regions and a disassembly listing. Ghidra gives a deeper pass with routines, regions, references and decompiled code. The skill records the structure in the project knowledge and reports conflicts with the current knowledge. Do not use it for runtime questions or to name routines.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# c64-static-analysis
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Find the static structure of a C64 program. Record the structure in the project knowledge. Report conflicts with current knowledge.
|
|
11
|
+
|
|
12
|
+
## Inputs
|
|
13
|
+
|
|
14
|
+
- The program: a PRG file, relative to the project directory. Or 64 KiB of memory from `$0000`, for example a memory dump.
|
|
15
|
+
- Entry points: addresses where code starts, for example the SYS address from the c64-basic skill. Routine symbols in the knowledge are entry points too. Ghidra needs at least one. DXA finds the SYS of a BASIC start by itself.
|
|
16
|
+
- Optional: routines to decompile (Ghidra), or a file for the listing (DXA).
|
|
17
|
+
|
|
18
|
+
## Tools / execution path
|
|
19
|
+
|
|
20
|
+
The script reads the current knowledge and runs DXA or Ghidra on this machine. The host runtime is not necessary. The script records the result in `.c64-re-tools/knowledge.db`.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
node <skill>/scripts/analyze.ts extracted/game.prg --analyzer dxa --listing analysis/game.lst
|
|
24
|
+
node <skill>/scripts/analyze.ts extracted/game.prg --entry '$080d' --decompile '$080d'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Option | Meaning |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `--analyzer dxa` | Use DXA: a fast first pass. Without this option, the script uses Ghidra. |
|
|
30
|
+
| `--entry '$080d'` | An address where code starts. Use it again for more addresses. |
|
|
31
|
+
| `--listing analysis/game.lst` | DXA only: write the disassembly listing to this project file. |
|
|
32
|
+
| `--decompile '$0818'` | Ghidra only: decompile the routine at this address. Use it again for more routines, at most 32. |
|
|
33
|
+
| `--flat64k` | The file is 64 KiB of memory from `$0000`, not a PRG. |
|
|
34
|
+
|
|
35
|
+
The script prints one JSON object. Addresses are `$` and four hex digits.
|
|
36
|
+
|
|
37
|
+
Both analyzers use the knowledge from the user and the LLM as seeds:
|
|
38
|
+
|
|
39
|
+
- Each routine is an entry point.
|
|
40
|
+
- Each region that is not code stays data.
|
|
41
|
+
- Each name is a label. The listing, Ghidra and the decompiler show that name.
|
|
42
|
+
|
|
43
|
+
Facts that an analyzer found are not seeds. A new analysis can then correct or remove them.
|
|
44
|
+
|
|
45
|
+
## Workflow
|
|
46
|
+
|
|
47
|
+
1. Read the knowledge for the program with the c64-knowledge skill.
|
|
48
|
+
2. Run a first pass with `--analyzer dxa`. For a BASIC loader, DXA finds the SYS address. Read the listing for a quick view of the code.
|
|
49
|
+
3. Find the important routines. Give them names with the c64-knowledge skill.
|
|
50
|
+
4. Run a deeper pass with Ghidra. Give the entry points with `--entry`. Ask for decompiled code of the routines that you must understand.
|
|
51
|
+
5. Read the result:
|
|
52
|
+
- `changes` counts the symbols, regions and references that the analysis `added`, `changed`, `retired` or kept `unchanged`.
|
|
53
|
+
- DXA: `regions` lists the code and data regions, and `labels` lists the addresses that DXA named.
|
|
54
|
+
- Ghidra: `functions` lists the routines that Ghidra found, and `decompilations` gives C-like code for the requested routines.
|
|
55
|
+
- `conflicts` lists the places where the analyzer and the current knowledge disagree.
|
|
56
|
+
6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
|
|
57
|
+
|
|
58
|
+
The script records only structure: generated names, code and data regions, and references from Ghidra. The listing and the decompiled code are only for the current task. The script does not record them in the knowledge.
|
|
59
|
+
|
|
60
|
+
## Knowledge
|
|
61
|
+
|
|
62
|
+
Automatic write:
|
|
63
|
+
- code and data regions
|
|
64
|
+
- names that DXA generated (labels) and routine entry points with names that Ghidra generated
|
|
65
|
+
- references from Ghidra: call, jump, read and write.
|
|
66
|
+
|
|
67
|
+
The script never replaces a name or a region from the user or the LLM. A name that an analyzer only shows back stays with its owner. DXA and Ghidra do not replace the facts of each other.
|
|
68
|
+
|
|
69
|
+
A new analysis replaces the earlier facts of the same analyzer in the analyzed range. The history keeps the earlier facts. DXA labels stay until a person or the LLM removes them, because DXA names only the addresses that it sees used.
|
|
70
|
+
|
|
71
|
+
Do not store:
|
|
72
|
+
- the listing or decompiled code
|
|
73
|
+
- the function list as comments.
|
|
74
|
+
|
|
75
|
+
## Result
|
|
76
|
+
|
|
77
|
+
The changes to the knowledge and the conflicts. For DXA, also the regions, the labels and the path of the listing. For Ghidra, also the routines and the decompiled code.
|
|
78
|
+
|
|
79
|
+
The exit status is 0 for a result, also with conflicts. The exit status is 1 for an error. The exit status is 2 for a wrong argument.
|
|
80
|
+
|
|
81
|
+
## Failure and conflicts
|
|
82
|
+
|
|
83
|
+
- A conflict: the analyzer finds code where the knowledge has data, or data where the knowledge has code. The knowledge stays unchanged. Find the correct answer with the c64-emulator skill, then correct the knowledge with the c64-knowledge skill.
|
|
84
|
+
- A conflict with the problem "another address has this name": the knowledge has the generated name at a different address. The knowledge stays unchanged.
|
|
85
|
+
- `invalid-input` with a message about an entry point: no entry point is inside the program for Ghidra. Give one with `--entry`.
|
|
86
|
+
- `stale-revision`: the knowledge changed during the analysis. The script recorded nothing. Run the analysis again.
|
|
87
|
+
- `operation-failed` with a message about an incomplete result or listing: the analyzer did not give a complete result. The script recorded nothing. Tell the user.
|
|
88
|
+
- `installation-incomplete`: DXA or Ghidra is not installed on this machine. Tell the user the message.
|
|
89
|
+
- `notDecompiled`: Ghidra did not decompile these routines. Each entry has the reason. If the reason says that the decompiler did not start, tell the user the remedy in the reason. On macOS, Ghidra has no native decompiler until the user builds it: `./gradlew buildNatives` in the `support/gradle` directory of Ghidra.
|
|
90
|
+
|
|
91
|
+
## Handoffs
|
|
92
|
+
|
|
93
|
+
- Runtime behavior → c64-emulator
|
|
94
|
+
- Names and classifications → c64-knowledge
|
|
95
|
+
- The meaning of a C64 hardware address → c64-memory-map
|
|
96
|
+
- Source code for a rebuild → c64-assembler
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// The c64-static-analysis script: analyzes a project program with DXA (fast
|
|
2
|
+
// first pass) or Ghidra (deeper), which it runs itself, seeded from
|
|
3
|
+
// current knowledge, and imports the structural findings into
|
|
4
|
+
// .c64-re-tools/knowledge.db.
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { parseArgs } from "node:util";
|
|
9
|
+
|
|
10
|
+
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
11
|
+
import { WireFailure } from "#src/host-client/tools.ts";
|
|
12
|
+
import { readProjectFile } from "#src/host-client/transfer.ts";
|
|
13
|
+
import { analyze as analyzeWithDxa } from "#src/native/dxa.ts";
|
|
14
|
+
import { analyze as analyzeWithGhidra } from "#src/native/ghidra/analyze.ts";
|
|
15
|
+
import { localToolContext } from "#src/native/local.ts";
|
|
16
|
+
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
17
|
+
import { importFindings, runProvenance, type Analyzer, type ImportConflict, type ImportContext, type ImportResult, type NormalizedFindings } from "#src/knowledge/import.ts";
|
|
18
|
+
import { currentRevision } from "#src/knowledge/read.ts";
|
|
19
|
+
import { resolveProjectPath } from "#src/project.ts";
|
|
20
|
+
import { runScript, UsageError } from "#src/script.ts";
|
|
21
|
+
import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./findings.ts";
|
|
22
|
+
|
|
23
|
+
const USAGE = `analyze.ts <image> [options]
|
|
24
|
+
|
|
25
|
+
<image> a PRG file, relative to the project directory
|
|
26
|
+
--analyzer <name> dxa (fast first pass) or ghidra (deeper, the default)
|
|
27
|
+
--flat64k the image is 64 KiB of memory from $0000, not a PRG
|
|
28
|
+
--entry <address> an entry point, for example '$080d' (repeatable)
|
|
29
|
+
--decompile <address> ghidra: a routine to decompile, at most 32 (repeatable)
|
|
30
|
+
--listing <file> dxa: write the disassembly listing to this project file
|
|
31
|
+
|
|
32
|
+
Routine symbols in knowledge are entry points too. Ghidra needs at least one
|
|
33
|
+
entry point; dxa finds the SYS of a BASIC start by itself. The result is one
|
|
34
|
+
JSON object. Addresses are $ and four hex digits.`;
|
|
35
|
+
|
|
36
|
+
const MAX_LISTED_FUNCTIONS = 200;
|
|
37
|
+
|
|
38
|
+
/** A decompiler that did not start gets the remedy: Ghidra ships no native decompiler for macOS. */
|
|
39
|
+
function withRemedy(reason: string): string {
|
|
40
|
+
return reason.startsWith("the Ghidra decompiler did not start")
|
|
41
|
+
? `${reason} On macOS, build the native decompiler once: in <Ghidra>/support/gradle run ./gradlew buildNatives.`
|
|
42
|
+
: reason;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function addresses(values: string[] | undefined, option: string): number[] {
|
|
46
|
+
return (values ?? []).map((value) => {
|
|
47
|
+
try {
|
|
48
|
+
return parseC64Address(value);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new UsageError(`${option} must be $ followed by four hex digits, not ${value}`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One conflict: what the knowledge has, and what the analyzer found, under the analyzer's name. */
|
|
56
|
+
function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<string, unknown> {
|
|
57
|
+
if (conflict.category === "symbol") {
|
|
58
|
+
return {
|
|
59
|
+
at: formatC64Address(conflict.address),
|
|
60
|
+
knowledge: { name: conflict.current.name, kind: conflict.current.kind, origin: conflict.current.origin, at: formatC64Address(conflict.current.address) },
|
|
61
|
+
[analyzer]: conflict.finding,
|
|
62
|
+
problem: conflict.reason === "kind" ? "code and data disagree" : "another address has this name",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
range: { start: formatC64Address(conflict.start), end: formatC64Address(conflict.end) },
|
|
67
|
+
knowledge: conflict.current,
|
|
68
|
+
[analyzer]: conflict.finding,
|
|
69
|
+
problem: "code and data disagree",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Imports one analyzer's findings as one revision, refused when knowledge
|
|
75
|
+
* changed since the seeds were read. A refused import creates no knowledge.db.
|
|
76
|
+
* The revision records the image hash and the analyzer version.
|
|
77
|
+
*/
|
|
78
|
+
function record(findings: NormalizedFindings, revision: number, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion">) {
|
|
79
|
+
let imported: ImportResult;
|
|
80
|
+
try {
|
|
81
|
+
imported = withWrite((db) => importFindings(db, findings, { expectedRevision: revision, description, ...provenance }));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof KnowledgeError && error.code === "stale-revision") {
|
|
84
|
+
throw new KnowledgeError("stale-revision", "The knowledge changed during the analysis. Nothing was imported. Run the analysis again.");
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
revision: imported.revision,
|
|
90
|
+
changes: { symbols: imported.symbols, regions: imported.regions, references: imported.references },
|
|
91
|
+
conflicts: imported.conflicts.map((conflict) => showConflict(conflict, findings.analyzer)),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function run(argv: string[]): Promise<unknown> {
|
|
96
|
+
const { values, positionals } = parseArgs({
|
|
97
|
+
args: argv,
|
|
98
|
+
strict: true,
|
|
99
|
+
allowPositionals: true,
|
|
100
|
+
options: {
|
|
101
|
+
analyzer: { type: "string" },
|
|
102
|
+
flat64k: { type: "boolean" },
|
|
103
|
+
entry: { type: "string", multiple: true },
|
|
104
|
+
decompile: { type: "string", multiple: true },
|
|
105
|
+
listing: { type: "string" },
|
|
106
|
+
help: { type: "boolean" },
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
if (values.help) throw new UsageError("");
|
|
110
|
+
if (positionals.length !== 1) throw new UsageError("give one image");
|
|
111
|
+
const image = positionals[0]!;
|
|
112
|
+
const entries = addresses(values.entry, "--entry");
|
|
113
|
+
const decompile = addresses(values.decompile, "--decompile");
|
|
114
|
+
if (decompile.length > 32) throw new UsageError("--decompile takes at most 32 routines");
|
|
115
|
+
const analyzer = values.analyzer ?? "ghidra";
|
|
116
|
+
if (analyzer !== "dxa" && analyzer !== "ghidra") throw new UsageError(`--analyzer must be dxa or ghidra, not ${analyzer}`);
|
|
117
|
+
if (analyzer === "dxa" && decompile.length > 0) throw new UsageError("--decompile works only with --analyzer ghidra");
|
|
118
|
+
if (analyzer === "ghidra" && values.listing !== undefined) throw new UsageError("--listing works only with --analyzer dxa");
|
|
119
|
+
let listingPath: string | undefined;
|
|
120
|
+
if (values.listing !== undefined) {
|
|
121
|
+
try {
|
|
122
|
+
listingPath = resolveProjectPath(values.listing);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new UsageError(`--listing must be a path relative to the project directory that stays inside it, not ${values.listing}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const reader = openForRead();
|
|
129
|
+
const revision = currentRevision(reader);
|
|
130
|
+
const seeds = seedsFromKnowledge(reader, entries);
|
|
131
|
+
reader?.close();
|
|
132
|
+
|
|
133
|
+
const imageKind = values.flat64k ? "flat64k" : "prg";
|
|
134
|
+
const bytes = readProjectFile(image).bytes;
|
|
135
|
+
if (analyzer === "dxa") {
|
|
136
|
+
const { result, attachments } = await analyzeWithDxa({ imageKind, ...seeds }, bytes, localToolContext());
|
|
137
|
+
const listing = attachments[0]!.toString("utf8");
|
|
138
|
+
// The listing is written only after the import: a refused import leaves no new file.
|
|
139
|
+
const imported = record(dxaFindings(result), revision, `DXA analysis of ${image}`, runProvenance(bytes, result.toolVersion));
|
|
140
|
+
if (listingPath !== undefined) {
|
|
141
|
+
try {
|
|
142
|
+
mkdirSync(dirname(listingPath), { recursive: true });
|
|
143
|
+
writeFileSync(listingPath, listing);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const recorded = imported.revision === null ? "The knowledge did not change." : `The knowledge has the findings as revision ${imported.revision}.`;
|
|
146
|
+
throw new WireFailure("operation-failed", `The script did not write the listing to ${values.listing}: ${(error as Error).message}. ${recorded}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
analyzer: "dxa",
|
|
151
|
+
coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
|
|
152
|
+
...imported,
|
|
153
|
+
regions: result.regions.map((region) => ({ start: formatC64Address(region.start), end: formatC64Address(region.end), classification: region.classification })),
|
|
154
|
+
labels: result.labels.slice(0, MAX_LISTED_FUNCTIONS).map((label) => ({ address: formatC64Address(label.address), name: label.name })),
|
|
155
|
+
...(result.labels.length > MAX_LISTED_FUNCTIONS ? { moreLabels: result.labels.length - MAX_LISTED_FUNCTIONS } : {}),
|
|
156
|
+
...(listingPath === undefined ? {} : { listing: values.listing }),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { result } = await analyzeWithGhidra({ imageKind, ...seeds, decompile }, bytes, localToolContext());
|
|
161
|
+
const imported = record(ghidraFindings(result), revision, `Ghidra analysis of ${image}`, runProvenance(bytes, result.toolVersion));
|
|
162
|
+
return {
|
|
163
|
+
analyzer: "ghidra",
|
|
164
|
+
coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
|
|
165
|
+
...imported,
|
|
166
|
+
functions: result.functions.slice(0, MAX_LISTED_FUNCTIONS).map((fn) => ({ entry: formatC64Address(fn.entry), name: fn.name })),
|
|
167
|
+
...(result.functions.length > MAX_LISTED_FUNCTIONS ? { moreFunctions: result.functions.length - MAX_LISTED_FUNCTIONS } : {}),
|
|
168
|
+
decompilations: result.decompilations.map((item) => ({ entry: formatC64Address(item.entry), text: item.text, ...(item.truncated ? { truncated: true } : {}) })),
|
|
169
|
+
...((result.notDecompiled ?? []).length === 0
|
|
170
|
+
? {}
|
|
171
|
+
: { notDecompiled: result.notDecompiled!.map((item) => ({ entry: formatC64Address(item.entry), reason: withRemedy(item.reason) })) }),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await runScript(() => run(process.argv.slice(2)), { usage: USAGE, refusals: [KnowledgeError] });
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Knowledge seeds for an analyzer, and the mapping of a Ghidra result to the
|
|
2
|
+
// common normalized findings.
|
|
3
|
+
|
|
4
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
5
|
+
|
|
6
|
+
import type { NormalizedFindings } from "#src/knowledge/import.ts";
|
|
7
|
+
import { listRegions, listSymbols } from "#src/knowledge/read.ts";
|
|
8
|
+
import type { DxaResult, GhidraParams, GhidraResult } from "#src/native/types.ts";
|
|
9
|
+
|
|
10
|
+
export type Seeds = Pick<GhidraParams, "entryPoints" | "dataRanges" | "labels">;
|
|
11
|
+
|
|
12
|
+
const semantic = (row: { origin: string }) => row.origin === "user" || row.origin === "llm";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Seeds from current semantic knowledge: routines from the user or
|
|
16
|
+
* the LLM are entry points, their non-code regions are data, and their names
|
|
17
|
+
* are labels. Analyzer facts are never seeds: fed back, an analyzer's own
|
|
18
|
+
* earlier guess would force the same answer again, and a re-analysis could
|
|
19
|
+
* not retire it.
|
|
20
|
+
*/
|
|
21
|
+
export function seedsFromKnowledge(db: DatabaseSync | undefined, extraEntryPoints: number[] = []): Seeds {
|
|
22
|
+
const symbols = listSymbols(db).filter(semantic);
|
|
23
|
+
const entryPoints = [...new Set([...extraEntryPoints, ...symbols.filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address)])].sort((a, b) => a - b);
|
|
24
|
+
return {
|
|
25
|
+
entryPoints,
|
|
26
|
+
dataRanges: listRegions(db)
|
|
27
|
+
.filter((region) => semantic(region) && region.type !== "code")
|
|
28
|
+
.map((region) => ({ start: region.start, end: region.end })),
|
|
29
|
+
labels: symbols.map((symbol) => ({ address: symbol.address, name: symbol.name })),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ghidra's result as normalized findings. A function named by an echoed seed
|
|
35
|
+
* stays in the snapshot: its address holds the seed's semantic symbol, which
|
|
36
|
+
* the importer never replaces, so the name keeps its owner,
|
|
37
|
+
* and a code/data disagreement with that symbol is still reported.
|
|
38
|
+
*/
|
|
39
|
+
export function ghidraFindings(result: GhidraResult): NormalizedFindings {
|
|
40
|
+
// Ghidra can give one generated name to two functions, for example
|
|
41
|
+
// thunk_FUN_e434 to two thunks of one routine. A name
|
|
42
|
+
// must have one address, so a repeated name that is not a seed gets its address.
|
|
43
|
+
const uses = new Map<string, number>();
|
|
44
|
+
for (const fn of result.functions) uses.set(fn.name, (uses.get(fn.name) ?? 0) + 1);
|
|
45
|
+
const nameOf = (fn: GhidraResult["functions"][number]) =>
|
|
46
|
+
fn.nameSource !== "seed" && uses.get(fn.name)! > 1 ? `${fn.name}_${fn.entry.toString(16).padStart(4, "0")}` : fn.name;
|
|
47
|
+
return {
|
|
48
|
+
analyzer: "ghidra",
|
|
49
|
+
coverage: result.coverage,
|
|
50
|
+
authoritative: { symbols: result.completeness.functions, regions: result.completeness.regions, references: result.completeness.references },
|
|
51
|
+
symbols: result.functions.map((fn) => ({ address: fn.entry, name: nameOf(fn), kind: "routine" as const })),
|
|
52
|
+
regions: result.regions.map((region) => ({ start: region.start, end: region.end, type: region.classification === "code" ? ("code" as const) : ("bytes" as const) })),
|
|
53
|
+
references: result.references.map((reference) => ({ from: reference.from, to: reference.to, kind: reference.type })),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* DXA's result as normalized findings. A label in one of DXA's data
|
|
59
|
+
* regions becomes a symbol of kind data, any other label one of kind label.
|
|
60
|
+
* DXA names only referenced addresses, so labels are not authoritative, while
|
|
61
|
+
* its regions cover every byte and are.
|
|
62
|
+
*/
|
|
63
|
+
export function dxaFindings(result: DxaResult): NormalizedFindings {
|
|
64
|
+
const inData = (address: number) => result.regions.some((region) => region.classification === "data" && region.start <= address && address <= region.end);
|
|
65
|
+
return {
|
|
66
|
+
analyzer: "dxa",
|
|
67
|
+
coverage: result.coverage,
|
|
68
|
+
authoritative: { symbols: result.completeness.labels, regions: result.completeness.regions, references: false },
|
|
69
|
+
symbols: result.labels.map((label) => ({ address: label.address, name: label.name, kind: inData(label.address) ? ("data" as const) : ("label" as const) })),
|
|
70
|
+
regions: result.regions.map((region) => ({ start: region.start, end: region.end, type: region.classification === "code" ? ("code" as const) : ("bytes" as const) })),
|
|
71
|
+
references: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-testing
|
|
3
|
+
description: Use this skill to test if a rebuilt or changed C64 program behaves as the original. It runs one scenario on the original and then on the rebuild in an emulator. It compares memory, registers, chip state and the screen with explicit rules, and gives PASS, FAIL or INCONCLUSIVE. It also makes a checklist for a human playtest. Do not use it to find the cause of a difference.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# c64-testing
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Test if a rebuild or a changed program behaves as the original in a scenario. Give one result: PASS, FAIL or INCONCLUSIVE.
|
|
11
|
+
|
|
12
|
+
A PASS proves only the behavior that the scenario does and observes. It does not prove that the two programs are equal in all behavior.
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
- The original program and the rebuild: PRG files in the project.
|
|
17
|
+
- The symbols of each side. For the original, the project knowledge. For the rebuild, the JSON output of the c64-assembler skill in a file.
|
|
18
|
+
- A scenario file: the steps, the checkpoints and the observations. See [references/scenario-format.md](references/scenario-format.md).
|
|
19
|
+
|
|
20
|
+
## Tools / execution path
|
|
21
|
+
|
|
22
|
+
The script starts one emulator through the host runtime. It runs the scenario on the original and then on the rebuild. Before each side, it does a hard reset of the emulator. The CPU then stops at the reset vector.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
node <skill>/scripts/test.ts tests/moves-left.json
|
|
26
|
+
node <skill>/scripts/checklist.ts tests/moves-left.json --item 'FIRE starts the game.'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The test script prints one JSON object. The exit status is 0 for PASS, 1 for FAIL, 3 for INCONCLUSIVE and 2 for a bad call.
|
|
30
|
+
|
|
31
|
+
The checklist script prints a Markdown checklist for a human tester.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
1. Write the scenario file. Start the steps with a frame count, for example `{ "frames": 150 }`. Each side starts at the reset vector, and the C64 needs these frames to start.
|
|
36
|
+
2. Use symbol names, for example `player_x`, not fixed addresses. Each side finds the name in its own symbols. Use a `$` address only when both programs use the same address.
|
|
37
|
+
3. Make each checkpoint a condition, for example `runUntil` at a routine. Do not wait for a time.
|
|
38
|
+
4. Save the c64-assembler output of the rebuild to the symbol file: `assemble.ts ... > build/game.json`.
|
|
39
|
+
5. Run the test script.
|
|
40
|
+
6. Read the result:
|
|
41
|
+
- `PASS`: all observations are equal, or the differences are in the allowed tolerance.
|
|
42
|
+
- `FAIL`: `differences` lists each difference with the checkpoint, the item, the original value and the rebuild value.
|
|
43
|
+
- `INCONCLUSIVE`: `reason` tells why the script could not compare. This is not a failure of the program.
|
|
44
|
+
7. For a FAIL, examine the first difference with the c64-emulator skill.
|
|
45
|
+
|
|
46
|
+
## Knowledge
|
|
47
|
+
|
|
48
|
+
Read:
|
|
49
|
+
- the symbols of the original from the project knowledge.
|
|
50
|
+
|
|
51
|
+
Automatic write:
|
|
52
|
+
- none.
|
|
53
|
+
|
|
54
|
+
Do not store:
|
|
55
|
+
- test results
|
|
56
|
+
- screenshots
|
|
57
|
+
- playtest results.
|
|
58
|
+
|
|
59
|
+
## Result
|
|
60
|
+
|
|
61
|
+
PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that explain a FAIL.
|
|
62
|
+
|
|
63
|
+
## Failure and conflicts
|
|
64
|
+
|
|
65
|
+
- `invalid-input`: the scenario file has an error. The message names the step. Change the step and run the test again.
|
|
66
|
+
- INCONCLUSIVE, "did not reach the checkpoint": one side did not reach the checkpoint in the frame limit. Find the reason with the c64-emulator skill. Do not report a FAIL.
|
|
67
|
+
- INCONCLUSIVE, "has no symbol": one side has no symbol with that name. Add the name to the knowledge or to the rebuild source.
|
|
68
|
+
- INCONCLUSIVE, "no emulator" or "could not run": the host runtime or the emulator failed. Tell the user the message.
|
|
69
|
+
- A FAIL with only a `screen` difference: add `vicii` or `sprites` to the observation to find which chip state is different.
|
|
70
|
+
- A program that uses random numbers: set the random seed with a `write` step on both sides. If that is not possible, observe only values that do not depend on the random numbers.
|
|
71
|
+
|
|
72
|
+
Do not add a mask or a tolerance only to make a test pass. A tolerance must have a reason in the behavior of the program.
|
|
73
|
+
|
|
74
|
+
## Handoffs
|
|
75
|
+
|
|
76
|
+
- Find the cause of a difference → c64-emulator
|
|
77
|
+
- Find the code that causes it → c64-static-analysis
|
|
78
|
+
- Rebuild after a correction → c64-assembler
|
|
79
|
+
- Record a confirmed conclusion → c64-knowledge
|