@henols/c64-re-tools 0.2.1 → 0.2.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/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +26 -0
- package/bin/cli.mjs +18 -7
- package/package.json +6 -4
- package/skills/acme-build/SKILL.md +83 -33
- package/skills/acme-build/scripts/acme.mjs +159 -64
- package/skills/acme-build/template.a +1 -1
- package/skills/c64-disk-access/SKILL.md +156 -0
- package/skills/c64-disk-access/scripts/c1541.mjs +569 -0
- package/skills/c64-memory-mapping/SKILL.md +419 -23
- package/skills/c64-memory-mapping/scripts/driver.mjs +1 -1
- package/skills/c64-petcat/SKILL.md +87 -0
- package/skills/c64-petcat/scripts/petcat.mjs +221 -0
- package/skills/c64-program-recon/SKILL.md +497 -92
- package/skills/c64-program-recon/references/control-flow.md +12 -15
- package/skills/c64-program-recon/references/graphics.md +1 -1
- package/skills/c64-program-recon/references/observation-hazards.md +18 -16
- package/skills/c64-program-recon/references/reconstruction.md +11 -6
- package/skills/c64-program-recon/references/sound-and-input.md +6 -8
- package/skills/c64-program-recon/references/tool-selection.md +37 -18
- package/skills/c64-program-recon/scripts/packer-finding.mjs +709 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +27 -13
- package/skills/c64-provenance-diff/SKILL.md +43 -8
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +9 -6
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +20 -8
- package/skills/c64-ram-capture/RELEASES.json.example +17 -0
- package/skills/c64-ram-capture/SKILL.md +147 -46
- package/skills/c64-ram-capture/scripts/compare.mjs +2 -2
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +575 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +3 -3
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +174 -0
- package/skills/c64-ram-capture/scripts/project-paths.mjs +1 -1
- package/skills/c64-ram-capture/scripts/releases.mjs +1 -1
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +147 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +19 -13
- package/skills/c64-ram-capture/templates/capture-record.template.md +44 -4
- package/skills/c64-ram-capture/transients/README.md +136 -0
- package/skills/routine-queue-walker/SKILL.md +365 -0
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +463 -0
- package/skills/vice-wedge-triage/SKILL.md +104 -97
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +0 -665
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +0 -243
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +0 -243
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +0 -133
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +0 -75
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +0 -339
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
# Memory map generation
|
|
2
2
|
|
|
3
|
-
**The memory map is GENERATED, not hand-authored
|
|
4
|
-
types and scopes written through the `
|
|
3
|
+
**The memory map is GENERATED, not hand-authored.** The store — labels, comments, block
|
|
4
|
+
types and scopes written through the `anno_*` tools described in `../SKILL.md` — is canonical. This
|
|
5
5
|
file used to be a fill-in-the-rows document; it is now the schema for the one input the generator
|
|
6
6
|
needs beyond the store itself, plus the confidence vocabulary that store comments carry.
|
|
7
7
|
|
|
8
8
|
Run the generator once findings are in the store:
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npx -y @henols/vice-mcp
|
|
12
|
-
node <plugin-root
|
|
11
|
+
npx -y @henols/vice-mcp anno render-memmap game.annostore --provenance sidecar.json
|
|
12
|
+
node <plugin-root>/src/mcp/vice/vice-proxy.ts anno render-memmap game.annostore --provenance sidecar.json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Add `--check` to compare the rendered file on disk against a fresh render — it exits non-zero and
|
|
16
|
-
prints the first differing line
|
|
17
|
-
|
|
18
|
-
the
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
prints the first differing line. Drift is reported when, and only when, one of these changed: the
|
|
17
|
+
rendered file itself (a hand edit); a store row (a range, a label, a comment, or a comment's
|
|
18
|
+
confidence grade); the provenance sidecar's bytes; the location of the store or the sidecar
|
|
19
|
+
**relative to the workspace root**; or the renderer. **Relocating the checkout is not drift** — the
|
|
20
|
+
same tree at a different absolute path renders the same bytes, because the banner records
|
|
21
|
+
workspace-relative locations rather than absolute ones.
|
|
22
|
+
|
|
23
|
+
There is no way to "fix" drift by editing the rendered file directly: the fix is always to re-run
|
|
24
|
+
the generator (or, if the sidecar itself is stale, correct it and re-run). That remedy is safe to
|
|
25
|
+
follow on any machine — because the banner records workspace-relative locations, re-running in a
|
|
26
|
+
different checkout or in a worktree does not rewrite the file with that machine's absolute paths.
|
|
27
|
+
The generated file carries a banner naming the store, the sidecar and a content digest — do not
|
|
28
|
+
strip it.
|
|
29
|
+
|
|
30
|
+
**One-time drift after upgrading, 2026-08-30.** A memory map rendered *before* 2026-08-30 will
|
|
31
|
+
report `drifted` on its first `--check` after this change, exactly once: the banner's `store:` and
|
|
32
|
+
`sidecar:` lines moved from absolute to workspace-relative spellings. Re-run the generator once and
|
|
33
|
+
commit the new banner, after which the file is stable across machines and worktrees. This is a
|
|
34
|
+
one-time, self-clearing banner correction — not a bug, and not a migration.
|
|
21
35
|
|
|
22
36
|
## The provenance sidecar
|
|
23
37
|
|
|
24
38
|
Some facts belong to the **run** (which capture, which `$01`, which video standard) rather than to
|
|
25
|
-
any address, and the store has no address-keyed shape for them
|
|
39
|
+
any address, and the store has no address-keyed shape for them. They are supplied to the
|
|
26
40
|
renderer as a small JSON sidecar, hand-authored from `c64-ram-capture`'s and `derive.mjs`'s own
|
|
27
41
|
outputs and validated by the renderer — a missing or malformed key is a named error listing every
|
|
28
42
|
problem at once, never a `<placeholder>` silently rendered into a published document.
|
|
@@ -64,8 +78,8 @@ literal values:
|
|
|
64
78
|
|
|
65
79
|
## Confidence vocabulary
|
|
66
80
|
|
|
67
|
-
Every comment written into the store through `
|
|
68
|
-
one of these five bracket tokens (the parser in `
|
|
81
|
+
Every comment written into the store through `anno_set_comment` that grades a finding leads with
|
|
82
|
+
one of these five bracket tokens (the parser in `anno-confidence.ts` throws on anything that is
|
|
69
83
|
close but not exact — a typo never silently degrades into an ungraded comment):
|
|
70
84
|
|
|
71
85
|
| Grade | Bracket token | Means |
|
|
@@ -80,4 +94,4 @@ Do not force an unknown range through a disassembler and record the output as co
|
|
|
80
94
|
decode of data is silently wrong and contaminates everything downstream.
|
|
81
95
|
|
|
82
96
|
**Do not promote a row by editing its grade.** Re-verify and restate the evidence with a fresh
|
|
83
|
-
`
|
|
97
|
+
`anno_set_comment` call, so the record of when something stopped being a guess survives.
|
|
@@ -15,7 +15,7 @@ either proves its own precondition or refuses to emit.
|
|
|
15
15
|
`ledger` will not write a verdict the earlier stages did not earn.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
D
|
|
18
|
+
D=src/skills/c64-provenance-diff/scripts/diff-images.mjs # from the repo root
|
|
19
19
|
|
|
20
20
|
node $D anchor-search # 1. prove the per-release offset [WRITES]
|
|
21
21
|
node $D diff # 2. N-way byte diff at that offset
|
|
@@ -25,7 +25,7 @@ node $D ledger # 4. regenerate recovery/PROVENANC
|
|
|
25
25
|
node $D diff --json # machine-readable, with per-range reasons
|
|
26
26
|
node $D diff --gap-tolerance 16 # coalescing width (default shown)
|
|
27
27
|
node $D anchor-search --reference <id> # pick the reference release
|
|
28
|
-
node
|
|
28
|
+
node src/skills/c64-provenance-diff/scripts/releases.mjs list # the release ids in play
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Pure Node over committed files — the `.bin` dumps, their `.map.json` manifests, and
|
|
@@ -126,9 +126,44 @@ The two seeds are where this goes wrong, and both failure modes are on record:
|
|
|
126
126
|
`io` (`$D000-$DFFF`) and `unused` (contiguous `$00`/`$FF` power-on runs) are
|
|
127
127
|
assigned at capture time and kept verbatim. Everything the trace reaches is `game`.
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
The `.bin` files are **never** edited or zeroed. Classification lives in
|
|
130
130
|
the manifests; the bytes stay verbatim evidence.
|
|
131
131
|
|
|
132
|
+
## Carrying the verdict into the rebuild
|
|
133
|
+
|
|
134
|
+
A verdict recorded here does not stay here. `anno export-asm`'s `--ledger` flag
|
|
135
|
+
reads THIS skill's generated `recovery/PROVENANCE.md` — never
|
|
136
|
+
re-deriving anything — and carries every covered range's Verdict and Confidence
|
|
137
|
+
into the exported ACME source as an inline comment on the block that range
|
|
138
|
+
overlaps:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
anno export-asm game.prg --store game.annostore --ledger recovery/PROVENANCE.md
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The `<image>` positional wants one of `.prg`/`.raw`/`.bin`, `--store` an
|
|
145
|
+
`.annostore`/`.store` file, and `--ledger` the `.md` this skill's own `ledger`
|
|
146
|
+
verb writes.
|
|
147
|
+
|
|
148
|
+
**The flag makes the verdict VISIBLE and decides nothing.** Every byte in
|
|
149
|
+
scope is still emitted, whatever the verdict says — a `CRACKER-PATCH` row does
|
|
150
|
+
not drop, filter or alter a single byte, it only makes the evidence readable
|
|
151
|
+
at the point of use. What gets reversed, kept or left out remains the
|
|
152
|
+
end-user's decision, never the tool's. `--ledger` is optional: omitting it
|
|
153
|
+
exports exactly as before, with no provenance comment anywhere in the output.
|
|
154
|
+
|
|
155
|
+
**When the operator, reading the ledger, decides a range genuinely should be
|
|
156
|
+
left out of the rebuild output** — a trainer patch, a cracktro block, anything
|
|
157
|
+
they choose — the round trip runs entirely on the `anno_*` MCP surface, never
|
|
158
|
+
by editing the ledger or the export: `anno_exclude_range` records the span
|
|
159
|
+
WITH the reason the operator gave, and `anno_include_range` takes the record
|
|
160
|
+
back if the decision changes. Recording an exclusion changes nothing about
|
|
161
|
+
which bytes the export emits — the exported block still carries every byte
|
|
162
|
+
of that span, now with a visible marker naming the exclusion and its reason,
|
|
163
|
+
so nothing is removed and no gap appears in the output. The ledger's verdict
|
|
164
|
+
is information the operator reads at this point; it is never wired as an
|
|
165
|
+
input to an automatic exclusion, here or anywhere else on this surface.
|
|
166
|
+
|
|
132
167
|
## A `CRACKER-PATCH` in `game` code is a trainer until proven otherwise
|
|
133
168
|
|
|
134
169
|
`count-patches` counts exactly one intersection — verdict `CRACKER-PATCH`, kind
|
|
@@ -234,10 +269,10 @@ addresses.
|
|
|
234
269
|
| Assembling | `acme-build` |
|
|
235
270
|
| **Whether a byte is original, cracker-changed, or unknown** | here |
|
|
236
271
|
|
|
237
|
-
|
|
238
|
-
find them**, graded with `Evidence:` and `Confidence:`. Promote
|
|
239
|
-
the new evidence, never by editing
|
|
240
|
-
|
|
272
|
+
Record findings that make RE faster in your own project notes **at the moment you
|
|
273
|
+
find them**, graded with `Evidence:` and `Confidence:`. Promote a finding by
|
|
274
|
+
re-logging it with the new evidence, never by editing an old grade in place — the
|
|
275
|
+
grade is only worth anything if it says what was actually known when it was written.
|
|
241
276
|
|
|
242
277
|
## Troubleshooting
|
|
243
278
|
|
|
@@ -254,4 +289,4 @@ through a GSD command (`/gsd-quick`).
|
|
|
254
289
|
| A range's `kind` looks wrong past its start | You resolved `kind` from `start`. Use `splitRangeByManifestKind`; coalescing does not respect kind boundaries. |
|
|
255
290
|
| A loader range disagrees with `NOTES.md` | `RELEASES.json`'s `loader_ranges` wins — it is earned from disassembly. Prose is how `$08F5` got misclassified. |
|
|
256
291
|
| Title-screen text shows up as cracktro | You used a bare printable-run scan. The vocabulary scan exists because `$4771-$4779` is the game's own text. |
|
|
257
|
-
| `unknown release "x" -- known releases: …` | `node
|
|
292
|
+
| `unknown release "x" -- known releases: …` | `node src/skills/c64-provenance-diff/scripts/releases.mjs list` for the valid ids. |
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// an already-committed file (a release's primary `.bin` dump, its
|
|
6
6
|
// `.map.json` range manifest, and `recovery/RELEASES.json`) and every tool
|
|
7
7
|
// is pure Node over those files -- nothing in this module contacts the
|
|
8
|
-
// emulator, ever (
|
|
8
|
+
// emulator, ever (zero third-party dependencies, `Buffer.indexOf` and
|
|
9
9
|
// `node:crypto` are sufficient).
|
|
10
10
|
//
|
|
11
11
|
// This is the step the objective calls "the one most able to produce
|
|
@@ -373,7 +373,7 @@ export function diffRanges(images, { gapTolerance = 16 } = {}) {
|
|
|
373
373
|
// one address's value would be both wrong for the range and would
|
|
374
374
|
// silently defeat collapsing (no two addresses would ever compare
|
|
375
375
|
// equal on evidence text, discovered live while running this tool
|
|
376
|
-
// against the real dumps
|
|
376
|
+
// against the real dumps).
|
|
377
377
|
rec = {
|
|
378
378
|
verdict: "ORIGINAL",
|
|
379
379
|
agreeing_releases: available.length,
|
|
@@ -519,7 +519,7 @@ function mergeGroup(group) {
|
|
|
519
519
|
// `swallowedGap` below. Deduplicated (via Set) so a coalesced range with
|
|
520
520
|
// many same-reason singleton addresses doesn't repeat identical
|
|
521
521
|
// boilerplate once per address -- found live while running this against
|
|
522
|
-
// the real dumps
|
|
522
|
+
// the real dumps.
|
|
523
523
|
const constituentNotes = [...new Set(nonOriginal.map((r) => r.evidence || r.reason).filter(Boolean))];
|
|
524
524
|
const swallowedGap = group.length > nonOriginal.length;
|
|
525
525
|
const note =
|
|
@@ -599,11 +599,11 @@ export function splitRangeByManifestKind(range, manifestRanges) {
|
|
|
599
599
|
|
|
600
600
|
/**
|
|
601
601
|
* Promote one manifest from `ranges-only` to `bucketed`: `unused`/`io`
|
|
602
|
-
* ranges are kept verbatim (
|
|
602
|
+
* ranges are kept verbatim (the byte-level classification already
|
|
603
603
|
* stands); every `unclassified` range is re-partitioned against the
|
|
604
604
|
* release's earned `loader_ranges` (never NOTES.md prose) and this image's
|
|
605
605
|
* own cracktro printable-run scan, with the remainder -- reached by the
|
|
606
|
-
* trace/entry point -- bucketed `game`.
|
|
606
|
+
* trace/entry point -- bucketed `game`. The underlying bytes are
|
|
607
607
|
* never edited; only the manifest's own `kind` field changes.
|
|
608
608
|
*/
|
|
609
609
|
export function bucketManifest(image, manifest, { loaderRanges, cracktroMinLength = 8 } = {}) {
|
|
@@ -614,7 +614,7 @@ export function bucketManifest(image, manifest, { loaderRanges, cracktroMinLengt
|
|
|
614
614
|
note: lr.note ?? "",
|
|
615
615
|
evidence: lr.evidence ?? "",
|
|
616
616
|
}));
|
|
617
|
-
// Keep every already-classified range verbatim (unused/io from
|
|
617
|
+
// Keep every already-classified range verbatim (unused/io from the
|
|
618
618
|
// byte-level pass, or -- on a re-run of an already-bucketed manifest --
|
|
619
619
|
// game/loader/cracktro from a prior run of this same function). Only
|
|
620
620
|
// "unclassified" is ever re-partitioned. Filtering "kept" down to just
|
|
@@ -693,6 +693,9 @@ export function renderLedger({ generatedRanges, gapTolerance, prose }) {
|
|
|
693
693
|
throw new Error(`renderLedger: refusing to emit -- generated tier stops at ${hex4(expected - 1)}, does not reach $FFFF`);
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
+
// NOTE: the embedded invocation path below is deliberately the
|
|
697
|
+
// CONSUMER's installed location (`.claude/skills/...`), not this repository's
|
|
698
|
+
// source-tree location (`src/skills/...`) -- pinned by diff-images.test.mjs and skill-consumer-paths.test.ts.
|
|
696
699
|
let generated = `<!-- GENERATED, DO NOT HAND-EDIT. Regenerate with: node .claude/skills/c64-provenance-diff/scripts/diff-images.mjs ledger --gap-tolerance ${gapTolerance} -->\n\n`;
|
|
697
700
|
generated += `| Start | End | Kind | Verdict | Confidence | Agreeing releases | Evidence / Reason |\n`;
|
|
698
701
|
generated += `|---|---|---|---|---|---|---|\n`;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Node/filesystem checks over `recovery/RELEASES.json` and the files it
|
|
5
5
|
// references, run entirely offline.
|
|
6
6
|
//
|
|
7
|
-
// This is the mechanical enforcement of
|
|
7
|
+
// This is the mechanical enforcement of the assumption_delta
|
|
8
8
|
// decision: the registry is release-CENTRIC (N releases, each a full field
|
|
9
9
|
// set, `canonical` demoted to a boolean on one entry), never
|
|
10
10
|
// canonical-image-centric again. A future plan that quietly reintroduces a
|
|
@@ -25,17 +25,29 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
25
25
|
const REPO_ROOT = projectRoot();
|
|
26
26
|
const RECOVERY_DIR = dataRoot();
|
|
27
27
|
const DISKS_DIR = disksRoot();
|
|
28
|
+
|
|
29
|
+
const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
|
|
30
|
+
|
|
28
31
|
// The parameterisation gate must cover EVERY module of the recovery pipeline, not
|
|
29
32
|
// just the ones sitting next to this file. When the six modules moved out of
|
|
30
33
|
// `tools/` into the two skills that use them (2026-08-04), a `HERE`-only scan
|
|
31
|
-
// silently stopped covering
|
|
32
|
-
//
|
|
34
|
+
// silently stopped covering the disk-image reader that used to live in the
|
|
35
|
+
// sibling skill's `scripts/` directory (since deleted)
|
|
36
|
+
// and `dump-artifacts.mjs` -- a static guard that keeps passing while
|
|
37
|
+
// checking less is worse than one that fails.
|
|
38
|
+
// 2026-08-22: the second entry used to be built project-root-relative,
|
|
39
|
+
// naming the skills tree's pre-relocation auto-discovery location by hand -- which
|
|
40
|
+
// stopped resolving the moment the skills tree moved to its current source-tree
|
|
41
|
+
// location. Rebuilt `HERE`-relative instead -- correct in both this dev checkout
|
|
42
|
+
// and a consumer's install, since a sibling skill's `scripts/` directory is always
|
|
43
|
+
// one level up and back down from this file's own location either way.
|
|
33
44
|
const SCAN_DIRS = [
|
|
34
|
-
HERE, //
|
|
35
|
-
resolve(
|
|
45
|
+
HERE, // src/skills/c64-provenance-diff/scripts
|
|
46
|
+
resolve(HERE, "..", "..", "c64-ram-capture", "scripts"),
|
|
36
47
|
];
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
for (const dir of SCAN_DIRS) {
|
|
49
|
+
if (!existsSync(dir)) die(`SCAN_DIRS entry does not exist: ${dir} -- a guard that keeps passing while checking less is worse than one that fails`);
|
|
50
|
+
}
|
|
39
51
|
|
|
40
52
|
function sha256File(path) {
|
|
41
53
|
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
@@ -131,7 +143,7 @@ function runBaseChecks(registry) {
|
|
|
131
143
|
for (const field of REQUIRED_DUMP_FILE_FIELDS) {
|
|
132
144
|
const value = d[field];
|
|
133
145
|
if (!value) {
|
|
134
|
-
errors.push(`release "${r.id}" dump "${d.label}": field "${field}" is not set (a dump is a four-file set
|
|
146
|
+
errors.push(`release "${r.id}" dump "${d.label}": field "${field}" is not set (a dump is a four-file set)`);
|
|
135
147
|
continue;
|
|
136
148
|
}
|
|
137
149
|
const filePath = join(REPO_ROOT, value);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"schema_notes": "Adding a release is one releases[] entry plus one invocation of tools/recover.mjs -- see c64-ram-capture/SKILL.md '## Release registry shape'.",
|
|
4
|
+
"releases": [
|
|
5
|
+
{
|
|
6
|
+
"id": "example-release",
|
|
7
|
+
"canonical": true,
|
|
8
|
+
"disk_image": "disks/example-release.d64",
|
|
9
|
+
"dumps": [
|
|
10
|
+
{
|
|
11
|
+
"label": "example-label",
|
|
12
|
+
"range_manifest": "recovery/example-release/dumps/example-release-example-label.map.json"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -14,12 +14,12 @@ failure and it is invisible in a hex dump. Two committed modules do that byte wo
|
|
|
14
14
|
and name the offending address when it is wrong.
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
S
|
|
18
|
-
|
|
17
|
+
S=src/skills/c64-ram-capture/scripts # from the repo root
|
|
18
|
+
A=$S/dump-artifacts.mjs
|
|
19
19
|
C=$S/compare.mjs L=$S/releases.mjs
|
|
20
|
+
T=$S/derive-transients.mjs V=$S/vsf-slice.mjs
|
|
21
|
+
TD=src/skills/c64-ram-capture/transients # the derived allow-lists live here
|
|
20
22
|
|
|
21
|
-
node $P directory --image path/to/image.d64 # what's on the disk (--json flags faked entries)
|
|
22
|
-
node $P bam --image path/to/image.d64 # disk name, DOS type, occupied track ranges
|
|
23
23
|
node $A assemble --chunks chunks.json # size + digest, writes nothing
|
|
24
24
|
node $A write-set --release <id> --label <label> \
|
|
25
25
|
--chunks chunks.json --raw raw.json # the four committed artifacts
|
|
@@ -28,10 +28,22 @@ node $L list # the valid --release ids
|
|
|
28
28
|
node $C digest dump.bin # sha256 + size, for the capture record
|
|
29
29
|
node $C compare a.bin b.bin # classify every difference, exit 1 on FAIL
|
|
30
30
|
node $C floor a.bin b.bin c.bin # drift floor across a capture set
|
|
31
|
+
|
|
32
|
+
node $T derive --release <id> --out $TD/<id>.json a.bin b.bin c.bin
|
|
33
|
+
node $T check --allow-list $TD/<id>.json a.bin b.bin
|
|
34
|
+
|
|
35
|
+
node $V slice run1.vsf --out run1.bin # flat 64K image from a .vsf snapshot
|
|
36
|
+
node $V digest run1.vsf # sha256 + size, writing no file
|
|
31
37
|
```
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
own `mcp__plugin_c64-re-tools_vice__*` calls. They contact nothing.
|
|
39
|
+
Every module above reads only committed files and the JSON **you** wrote from
|
|
40
|
+
your own `mcp__plugin_c64-re-tools_vice__*` calls. They contact nothing.
|
|
41
|
+
|
|
42
|
+
**Prerequisite: a resolvable project root.** `scripts/project-paths.mjs` uses
|
|
43
|
+
`C64RE_PROJECT_ROOT` when it is set, and otherwise walks up from the toolkit's
|
|
44
|
+
own location for the nearest ancestor directory containing a `.git` entry. A
|
|
45
|
+
scratch project has neither by default, so `git init` it first or set the
|
|
46
|
+
variable — the thrown error names both when this fails.
|
|
35
47
|
|
|
36
48
|
## The order
|
|
37
49
|
|
|
@@ -45,31 +57,9 @@ own `mcp__plugin_c64-re-tools_vice__*` calls. They contact nothing.
|
|
|
45
57
|
|
|
46
58
|
## Read the disk first
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
$ node $P directory --image demo.d64
|
|
53
|
-
PRG "DEMO GAME" first=5/0 blocks=5
|
|
54
|
-
|
|
55
|
-
$ node $P bam --image demo.d64
|
|
56
|
-
disk name: "DEMO DISK" id: 38 dos type: 2A
|
|
57
|
-
first dir sector: 18/1
|
|
58
|
-
occupied track ranges: 5
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Do not eyeball the directory for fakery — `--json` decides it. Every entry carries
|
|
62
|
-
`suspicious` plus `suspicious_reasons`, set when the block count is 0, when the
|
|
63
|
-
first track/sector falls outside the image, or when it points into a track the BAM
|
|
64
|
-
reports as entirely free. That last case is the signature of an entry claiming a
|
|
65
|
-
file never written to disk.
|
|
66
|
-
|
|
67
|
-
`scripts/d64-parse.test.mjs` proves the detector both **fires** on a synthetic
|
|
68
|
-
faked entry and stays silent on a well-formed one — a guard proven only silent is
|
|
69
|
-
not a guard. It also sweeps whatever real `.d64` corpus the project ships,
|
|
70
|
-
skipping when there is none. A non-null `chain_error` is the separate failure: a
|
|
71
|
-
directory chain that leaves the image or loops, reported instead of hanging.
|
|
72
|
-
**Confidence: HIGH** (synthetic fire-and-silence tests, plus a corpus sweep).
|
|
60
|
+
Disk-image structure — the directory, the block allocation map, a named file's
|
|
61
|
+
sector chain or raw bytes, and directory-fakery detection — is
|
|
62
|
+
`c64-disk-access`'s job now. Read the disk with it before booting anything.
|
|
73
63
|
|
|
74
64
|
## Boot a disk
|
|
75
65
|
|
|
@@ -78,6 +68,9 @@ directory chain that leaves the image or loops, reported instead of hanging.
|
|
|
78
68
|
3. `mcp__plugin_c64-re-tools_vice__vice_execution_run`.
|
|
79
69
|
4. `mcp__plugin_c64-re-tools_vice__vice_registers_get` and confirm the program counter has moved.
|
|
80
70
|
|
|
71
|
+
The broker sets the drive type at launch for every stock instance, so no drive setup
|
|
72
|
+
is needed before attaching.
|
|
73
|
+
|
|
81
74
|
If the program counter has not moved, type `LOAD"*",8,1` with
|
|
82
75
|
`mcp__plugin_c64-re-tools_vice__vice_keyboard_type`, run it, then type `RUN` and run it.
|
|
83
76
|
|
|
@@ -119,6 +112,17 @@ Read state before you resume, and resume exactly once at the end.
|
|
|
119
112
|
Hold keys down across a gate by releasing them at the trigger checkpoint in
|
|
120
113
|
step 3, never earlier.
|
|
121
114
|
|
|
115
|
+
**Fill the record's reproducibility key in the same step.**
|
|
116
|
+
`templates/capture-record.template.md`'s Identity table carries three rows that
|
|
117
|
+
are one key, not three facts: `binary sha256`, `argv digest` and `seed`. The
|
|
118
|
+
seed alone is **not** the key — measured, the same seed with a reordered argv
|
|
119
|
+
yielded a 76-byte-different image, so two captures whose argv digests differ are
|
|
120
|
+
different keys and must not be compared as a pair. A record with any of those
|
|
121
|
+
three blank is not a reproducible capture and the void protocol applies. The
|
|
122
|
+
table also carries a `capture route` row (`memory-read` or `snapshot`), and
|
|
123
|
+
**on the snapshot route the `$D000-$DFFF` volatility rule below does not
|
|
124
|
+
apply** — a difference there is a real difference.
|
|
125
|
+
|
|
122
126
|
`assemble` runs the same assertions and writes nothing, so it is the cheap check
|
|
123
127
|
on a set of chunks before committing them.
|
|
124
128
|
|
|
@@ -155,12 +159,12 @@ only after the provenance diff partitions loader from cracktro from game — see
|
|
|
155
159
|
|
|
156
160
|
## Find an entry point
|
|
157
161
|
|
|
158
|
-
1. Press past any "hit any key" gate
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
`
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
1. Press past any "hit any key" gate. **`vice_keyboard_matrix` is permanently unavailable** — the
|
|
163
|
+
binary monitor's `KEYBOARD_FEED` only injects PETSCII text into the KERNAL buffer and cannot
|
|
164
|
+
drive the raw matrix; see `docs/stock-hard-losses.md`. Use `vice_keyboard_type` /
|
|
165
|
+
`vice_keyboard_petscii` when the gate reads the KERNAL buffer, or `vice_joystick_set` when it
|
|
166
|
+
polls the matrix directly; buffer injection stays invisible to a program polling
|
|
167
|
+
`$DC00`/`$DC01` itself.
|
|
164
168
|
2. Step forward in batches with `mcp__plugin_c64-re-tools_vice__vice_execution_step`, reading
|
|
165
169
|
`mcp__plugin_c64-re-tools_vice__vice_registers_get` after each batch.
|
|
166
170
|
3. Stop when the program counter and the stack pointer both settle into a
|
|
@@ -247,8 +251,8 @@ were guaranteed. Region first, bit-count second.
|
|
|
247
251
|
`$E000-$FFFF` (RAM under KERNAL ROM when HIRAM = 0) is deliberately **not**
|
|
248
252
|
excluded. `$FAD8` and `$FC51` do differ across captures, but only two addresses
|
|
249
253
|
out of 8192 — too few for power-on garbage, and unexplained. They still fail, and
|
|
250
|
-
what writes them is an open question.
|
|
251
|
-
|
|
254
|
+
what writes them is an open question. Observed 2026-08-04; graded MEDIUM,
|
|
255
|
+
structural, not reproduced against a second release.
|
|
252
256
|
|
|
253
257
|
**Establish a drift floor** with `floor` across every capture of one checkpoint.
|
|
254
258
|
It reports each address that differed in any pairing, with the distinct values
|
|
@@ -262,9 +266,79 @@ Capture the power-on image as the very first action against a fresh machine, the
|
|
|
262
266
|
idle-capture twice more and run `floor` over the set. State the result as a
|
|
263
267
|
floor, not a complete set — more captures can only widen it.
|
|
264
268
|
|
|
269
|
+
## Derive a per-release transient allow-list
|
|
270
|
+
|
|
271
|
+
`scripts/derive-transients.mjs` is the named, repeatable derivation. **The
|
|
272
|
+
method is what carries forward between releases; no address set ever does.** Two
|
|
273
|
+
verbs:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
node $T derive --release <id> --out $TD/<id>.json run1.bin run2.bin run3.bin
|
|
277
|
+
node $T check --allow-list $TD/<id>.json runA.bin runB.bin
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`derive` takes **N ≥ 3** runs of the same release under the same protocol at the
|
|
281
|
+
same stop and writes the **union of addresses differing across every pairwise
|
|
282
|
+
comparison** — one entry per address, carrying which pairings it differed in,
|
|
283
|
+
the distinct bytes seen, and an empty attribution line for you to fill. Fewer
|
|
284
|
+
than three images is refused naming the count and the minimum; an image that is
|
|
285
|
+
not exactly 65536 bytes is refused naming the path and the length. It prints
|
|
286
|
+
`TRANSIENT_COUNT: <n>` at column 0, so a measurement gets transcribed rather
|
|
287
|
+
than paraphrased.
|
|
288
|
+
|
|
289
|
+
**Over the cap of 64 addresses the derivation is VOID:** non-zero exit, **no
|
|
290
|
+
artifact written**, and the message says what the overflow means — the stop is
|
|
291
|
+
not frame-exact. That is a fact to record, not a threshold to raise. `--cap`
|
|
292
|
+
only ever *narrows*; a value above 64 is refused by name, and the union is never
|
|
293
|
+
truncated to fit, because a truncated list makes every later comparison pass on
|
|
294
|
+
bytes nobody vetted. Overflow is a **measured** outcome: 0 differing addresses
|
|
295
|
+
at a frame-exact `READY` stop, 66 at a frame-anchored autostarted stop at
|
|
296
|
+
jitter 4000 ms, 300 at a wall-clock autostarted stop on a real release, 1242 at
|
|
297
|
+
a wall-clock `READY` stop with the determinism block applied.
|
|
298
|
+
|
|
299
|
+
Re-deriving over an existing artifact is **refused without `--force`**, with the
|
|
300
|
+
no-inheritance rule in the message: an inherited list cannot be distinguished
|
|
301
|
+
afterwards from an honestly derived one.
|
|
302
|
+
|
|
303
|
+
`check` re-checks one pair against an already-committed derivation without
|
|
304
|
+
re-deriving it, printing `CHECK_VERDICT: equivalent | not-equivalent` and exiting
|
|
305
|
+
1 when not equivalent. Note what it does **not** carry: no address range is a
|
|
306
|
+
volatile span, and there is no bit-count tolerance at any address — a one-bit
|
|
307
|
+
difference outside the list fails. Those two rules belong to `compare.mjs` and
|
|
308
|
+
are deliberately not inherited. `src/skills/c64-ram-capture/transients/README.md`
|
|
309
|
+
holds the artifact shape, the committed method and the cap's reasoning.
|
|
310
|
+
|
|
311
|
+
## Slice the image out of a snapshot instead of transcribing it
|
|
312
|
+
|
|
313
|
+
`scripts/vsf-slice.mjs` produces the flat 64K image by slicing a VICE `.vsf`
|
|
314
|
+
snapshot's memory module body. Two verbs:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
node $V slice run1.vsf --out run1.bin # writes exactly 65536 bytes
|
|
318
|
+
node $V digest run1.vsf # sha256 + size, writing no file
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Add `--json` to either for the same summary as one JSON object — it carries the
|
|
322
|
+
snapshot's memory-module minor, the observed body length, and the three CPU port
|
|
323
|
+
read-back values.
|
|
324
|
+
|
|
325
|
+
**The one thing that matters operationally: this route has no transcription
|
|
326
|
+
step, so the `$D000-$DFFF` volatility rule above does *not* apply to an image
|
|
327
|
+
produced this way.** That rule exists because `vice_memory_read` samples live
|
|
328
|
+
I/O. The snapshot array is RAM *under* I/O, not the register read view, so those
|
|
329
|
+
4096 addresses are ordinary RAM in a sliced image and a difference there is a
|
|
330
|
+
real difference. Do not carry the exclusion across from the memory-read route.
|
|
331
|
+
|
|
332
|
+
A malformed snapshot is **refused**, by name, naming the offending value and the
|
|
333
|
+
valid range — it is never truncated into a plausible short image. The `.vsf`
|
|
334
|
+
byte layout lives in exactly one place, `vsf-slice.ts` on the MCP side; this
|
|
335
|
+
script resolves it via `VICE_MCP_DIR`, the in-repo path, or the
|
|
336
|
+
`@henols/vice-mcp` package, and refuses naming every path it tried when no rung
|
|
337
|
+
resolves rather than falling back to a second copy of the layout.
|
|
338
|
+
|
|
265
339
|
## Feeding the memory map's provenance sidecar
|
|
266
340
|
|
|
267
|
-
`c64-program-recon`'s generated memory map (`vice-mcp
|
|
341
|
+
`c64-program-recon`'s generated memory map (`vice-mcp anno render-memmap`) takes a small provenance
|
|
268
342
|
sidecar as input, and this skill supplies one of its fields: `scripts/compare.mjs digest`'s `sha256`
|
|
269
343
|
and `size` become the sidecar's `captureSha256`, proving which image the rendered map describes. The
|
|
270
344
|
sidecar's other run-scoped keys (`port01`, `dd00`, `vicBank`, `screenRam`, `charsetOrBitmap`, `mode`,
|
|
@@ -279,6 +353,7 @@ This one owns the image and its identity. It does not restate what the others ca
|
|
|
279
353
|
|
|
280
354
|
| Need | Go to |
|
|
281
355
|
|---|---|
|
|
356
|
+
| A disk image's directory, BAM, sector chains, or directory-fakery detection | `c64-disk-access` |
|
|
282
357
|
| Which address to read next, and what the answer rules out | `c64-program-recon` |
|
|
283
358
|
| Every way a live read gives a wrong answer | `c64-program-recon` — `references/observation-hazards.md`. **Read before driving.** |
|
|
284
359
|
| What a specific address or bit means | `c64-memory-mapping` — `node … lookup '$D018'` |
|
|
@@ -287,6 +362,28 @@ This one owns the image and its identity. It does not restate what the others ca
|
|
|
287
362
|
| Whether the emulator is wedged, and whether it is safe to recycle | `vice-wedge-triage` |
|
|
288
363
|
| **A verified 64K image, or proving two captures equivalent** | here |
|
|
289
364
|
|
|
365
|
+
## Release registry shape
|
|
366
|
+
|
|
367
|
+
`scripts/releases.mjs` is the only module that reads a release id out of the
|
|
368
|
+
registry — every other module takes the id as an argument. Its shape:
|
|
369
|
+
|
|
370
|
+
| Field | Level | Required | For |
|
|
371
|
+
|---|---|---|---|
|
|
372
|
+
| `schema_version` | top-level | — | The registry format version. |
|
|
373
|
+
| `schema_notes` | top-level | — | Free-text stating the registry's N-readiness claim (`node $L schema-notes`). |
|
|
374
|
+
| `releases` | top-level | yes | The array of release entries below. |
|
|
375
|
+
| `id` | per-release | yes | The `--release` argument every other script takes. |
|
|
376
|
+
| `canonical` | per-release | — | A boolean on one entry, not "the canonical image" — there are N releases. |
|
|
377
|
+
| `disk_image` | per-release | yes | Project-relative path to the release's `.d64`. |
|
|
378
|
+
| `dumps` | per-release | **yes, as an array** | Per-capture records written by `write-set`. **Must be an array, never omitted** — `releases.mjs`'s `list` command reads `r.dumps.length` with no guard (`releases.mjs:98`), so a missing `dumps` throws `TypeError: Cannot read properties of undefined` instead of listing anything. An empty array (`[]`) is fine; an absent key is not. `releases.mjs` itself imposes no per-entry shape; `scripts/watch-loads.mjs` (a different reader) looks up an entry by `label` and reads its `range_manifest`, which the example below follows. |
|
|
379
|
+
|
|
380
|
+
The registry lives at `<project root>/recovery/RELEASES.json` by default —
|
|
381
|
+
override the whole path with `C64RE_REGISTRY`, or just the containing
|
|
382
|
+
directory with `C64RE_DATA_DIR`.
|
|
383
|
+
|
|
384
|
+
`src/skills/c64-ram-capture/RELEASES.json.example` is a copyable starting
|
|
385
|
+
point with every field above populated with placeholder values.
|
|
386
|
+
|
|
290
387
|
## References
|
|
291
388
|
|
|
292
389
|
What this skill ships, and the committed modules it leans on. No `references/`
|
|
@@ -295,14 +392,17 @@ split: the workflow fits in one file, which is the right call when it does.
|
|
|
295
392
|
| Path | Covers |
|
|
296
393
|
|---|---|
|
|
297
394
|
| `scripts/compare.mjs` | Difference classification and the drift floor. Pure logic over captures you already have — `node $C` with no arguments prints the rules. |
|
|
298
|
-
| `
|
|
299
|
-
| `scripts/
|
|
395
|
+
| `scripts/vsf-slice.mjs` | `slice` / `digest` — the flat 64K image sliced out of a `.vsf` snapshot with no transcription step. The layout lives in `vsf-slice.ts` on the MCP side; this wrapper resolves it and refuses by name when it cannot. Covered by `scripts/vsf-slice.test.mjs`. |
|
|
396
|
+
| `scripts/derive-transients.mjs` | `derive` / `check` — the per-release transient allow-list, derived from N ≥ 3 runs as the pairwise union, under a committed cap of 64 that **voids** rather than warns. Covered by `scripts/derive-transients.test.mjs`. |
|
|
397
|
+
| `transients/README.md` | The committed derivation method, the artifact shape, the cap's reasoning with its four measured reference points, and the rule that no address set is inherited between releases. Its `.gitignore` refuses every image byte form. |
|
|
398
|
+
| `templates/capture-record.template.md` | The per-capture record: identity including the three-row reproducibility key and the `capture route` row, machine state read in the same paused window, the void checklist, and the per-pairing comparison table. |
|
|
300
399
|
| `scripts/dump-artifacts.mjs` | `assemble` / `chip-state` / `manifest` / `write-set` — the guarded byte work, and the source of every `assembleImage:` message in the table below. |
|
|
400
|
+
| `RELEASES.json.example` | A copyable release-registry shape — see `## Release registry shape` above. |
|
|
301
401
|
|
|
302
|
-
|
|
303
|
-
find them**, graded with `Evidence:` and `Confidence:`. Promote
|
|
304
|
-
the new evidence, never by editing
|
|
305
|
-
|
|
402
|
+
Record findings that make RE faster in your own project notes **at the moment you
|
|
403
|
+
find them**, graded with `Evidence:` and `Confidence:`. Promote a finding by
|
|
404
|
+
re-logging it with the new evidence, never by editing an old grade in place — the
|
|
405
|
+
grade is only worth anything if it says what was actually known when it was written.
|
|
306
406
|
|
|
307
407
|
## Troubleshooting
|
|
308
408
|
|
|
@@ -320,3 +420,4 @@ through a GSD command (`/gsd-quick`).
|
|
|
320
420
|
| `--limit 0` printed nothing | Fixed 2026-08-04 — it now means unlimited. Re-pull the script if you see the old behaviour. |
|
|
321
421
|
| An epoch-drift error appeared mid-capture | The machine restarted under you. Void the run; do not salvage the artifacts. The next call succeeding does not undo it. |
|
|
322
422
|
| The emulator looks dead | `vice-wedge-triage` — and enumerate your own armed checkpoints before concluding anything. |
|
|
423
|
+
| `` `project-paths: could not locate the project root -- no `.git` found above ...` `` | No `.git` ancestor and no `C64RE_PROJECT_ROOT`. `git init` the project, or set the variable to its root. |
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
//
|
|
18
18
|
// $D000-$DFFF is this module's one departure from what SKILL.md said when it was
|
|
19
19
|
// written: that range is I/O, not RAM, so it can never be stable. See the VOLATILE
|
|
20
|
-
// table below
|
|
20
|
+
// table below for the evidence (observed 2026-08-04).
|
|
21
21
|
|
|
22
22
|
import { readFileSync } from "node:fs";
|
|
23
23
|
import { createHash } from "node:crypto";
|
|
@@ -36,7 +36,7 @@ const VOLATILE = [
|
|
|
36
36
|
// live hardware and two captures can never agree here. Added 2026-08-04 after
|
|
37
37
|
// every divergence across all six committed gameentry pairings landed either
|
|
38
38
|
// here ($D344, $D625, $D628) or in RAM under KERNAL ROM -- see
|
|
39
|
-
//
|
|
39
|
+
// Observed 2026-08-04. Confidence HIGH: structural.
|
|
40
40
|
[0xd000, 0xdfff],
|
|
41
41
|
];
|
|
42
42
|
|