@henols/c64-re-tools 0.1.4
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 +61 -0
- package/bin/cli.mjs +226 -0
- package/package.json +53 -0
- package/skills/acme-build/SKILL.md +224 -0
- package/skills/acme-build/scripts/acme.mjs +263 -0
- package/skills/acme-build/template.a +39 -0
- package/skills/c64-memory-mapping/SKILL.md +199 -0
- package/skills/c64-memory-mapping/memmap.json +8800 -0
- package/skills/c64-memory-mapping/scripts/driver.mjs +553 -0
- package/skills/c64-program-recon/SKILL.md +172 -0
- package/skills/c64-program-recon/references/control-flow.md +174 -0
- package/skills/c64-program-recon/references/graphics.md +73 -0
- package/skills/c64-program-recon/references/observation-hazards.md +118 -0
- package/skills/c64-program-recon/references/reconstruction.md +128 -0
- package/skills/c64-program-recon/references/sound-and-input.md +68 -0
- package/skills/c64-program-recon/references/tool-selection.md +55 -0
- package/skills/c64-program-recon/scripts/derive.mjs +364 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +62 -0
- package/skills/c64-provenance-diff/SKILL.md +257 -0
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +981 -0
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +665 -0
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +383 -0
- package/skills/c64-ram-capture/SKILL.md +306 -0
- package/skills/c64-ram-capture/scripts/compare.mjs +258 -0
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +243 -0
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +243 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +317 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +133 -0
- package/skills/c64-ram-capture/scripts/project-paths.mjs +81 -0
- package/skills/c64-ram-capture/scripts/releases.mjs +109 -0
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +75 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +575 -0
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +339 -0
- package/skills/c64-ram-capture/templates/capture-record.template.md +59 -0
- package/skills/vice-wedge-triage/SKILL.md +149 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-provenance-diff
|
|
3
|
+
description: Decide whether a byte in a cracked C64 release is original game code or something a cracker changed, by diffing two or more independently-cracked releases at an anchor-proven offset. Use when asked to diff two releases or disk images, work out which bytes the cracker patched, tell loader or cracktro code from game code, prove a byte is original, establish provenance or confidence for a memory range, regenerate the provenance ledger, or run anchor-search, count-patches or diff-images. Also use when asked whether a crack added a trainer or cheat, whether a patch changes gameplay rather than loading, whether a rebuild would inherit a cracker's gameplay alteration, or whether two releases are genuinely independent rather than sharing an ancestor.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deciding what a cracker changed
|
|
7
|
+
|
|
8
|
+
**A byte that differs between two releases is not a cracker patch.** It is a byte
|
|
9
|
+
that differs. This pipeline exists because the gap between those two statements is
|
|
10
|
+
where confident nonsense gets manufactured — `scripts/diff-images.mjs`'s own header
|
|
11
|
+
calls it "the step most able to produce confident nonsense". Every stage below
|
|
12
|
+
either proves its own precondition or refuses to emit.
|
|
13
|
+
|
|
14
|
+
**Run the four verbs in order.** `diff` is meaningless without a proven offset, and
|
|
15
|
+
`ledger` will not write a verdict the earlier stages did not earn.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
D=.claude/skills/c64-provenance-diff/scripts/diff-images.mjs # from the repo root
|
|
19
|
+
|
|
20
|
+
node $D anchor-search # 1. prove the per-release offset [WRITES]
|
|
21
|
+
node $D diff # 2. N-way byte diff at that offset
|
|
22
|
+
node $D count-patches # 3. CRACKER-PATCH addresses in game code
|
|
23
|
+
node $D ledger # 4. regenerate recovery/PROVENANCE.md [WRITES]
|
|
24
|
+
|
|
25
|
+
node $D diff --json # machine-readable, with per-range reasons
|
|
26
|
+
node $D diff --gap-tolerance 16 # coalescing width (default shown)
|
|
27
|
+
node $D anchor-search --reference <id> # pick the reference release
|
|
28
|
+
node .claude/skills/c64-provenance-diff/scripts/releases.mjs list # the release ids in play
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Pure Node over committed files — the `.bin` dumps, their `.map.json` manifests, and
|
|
32
|
+
`recovery/RELEASES.json`. It contacts nothing.
|
|
33
|
+
|
|
34
|
+
## The order
|
|
35
|
+
|
|
36
|
+
| # | Verb | Proves | Refuses to |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| 1 | `anchor-search` | A single global offset per release, from long distinctive byte runs located with `Buffer.indexOf` | Accept a **majority** vote — every usable anchor must agree, or there is no offset |
|
|
39
|
+
| 2 | `diff` | Which ranges differ, coalesced on verdict continuity | Diff at an assumed offset |
|
|
40
|
+
| 3 | `count-patches` | How many addresses are `CRACKER-PATCH` **and** `game`-kind | Count a patch outside game code |
|
|
41
|
+
| 4 | `ledger` | The generated tier of `recovery/PROVENANCE.md` | Emit rather than launder an assumption |
|
|
42
|
+
|
|
43
|
+
## Two verbs write to tracked files
|
|
44
|
+
|
|
45
|
+
`anchor-search` updates `recovery/RELEASES.json`; `ledger` rewrites
|
|
46
|
+
`recovery/PROVENANCE.md` and touches `RELEASES.json` too. So `git status` is
|
|
47
|
+
**expected** to be dirty after a run.
|
|
48
|
+
|
|
49
|
+
What matters is *what* changed. A clean re-run produces a **timestamp-only** diff —
|
|
50
|
+
`proven_at` and `generated_at`. Anything else is a real change:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git diff -- recovery/RELEASES.json recovery/PROVENANCE.md
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If the only `-`/`+` pairs are those two fields, revert the churn and move on. If
|
|
57
|
+
`offset`, `anchor_count`, `anchors_agreeing` or `generated_tier_sha256` moved, stop
|
|
58
|
+
and find out why before committing — that is the pipeline telling you the evidence
|
|
59
|
+
changed.
|
|
60
|
+
|
|
61
|
+
## Worked example — the real corpus
|
|
62
|
+
|
|
63
|
+
Two independently-cracked releases of one title, both captured at the same
|
|
64
|
+
post-loader entry trigger. **The release ids below are shown as `release-a` and
|
|
65
|
+
`release-b`; every number is real output from a live run against a two-release
|
|
66
|
+
corpus, with only the ids renamed** — the tool has no opinion about what a
|
|
67
|
+
release is called.
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
$ node $D anchor-search
|
|
71
|
+
release-a -> release-b: ok=true offset=0 (all 7 usable anchor(s) agree on offset 0)
|
|
72
|
+
|
|
73
|
+
$ node $D diff
|
|
74
|
+
diff: 204 range(s), gap_tolerance=16, coalesced=260
|
|
75
|
+
|
|
76
|
+
$ node $D count-patches
|
|
77
|
+
release-a: 0
|
|
78
|
+
release-b: 0
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**204 differing ranges and zero cracker patches.** The verdict tally from
|
|
82
|
+
`diff --json` is `{"UNKNOWN": 102, "ORIGINAL": 102}` — nothing reached
|
|
83
|
+
`CRACKER-PATCH` at all. That is the pipeline working, not failing.
|
|
84
|
+
|
|
85
|
+
Read it as: 102 ranges are identical across two independently-cracked releases, so
|
|
86
|
+
they are `ORIGINAL` with real evidence behind the word. The other 102 differ, match
|
|
87
|
+
no cracker signature, and are therefore `UNKNOWN` — and each carries a `reason`
|
|
88
|
+
naming the alternatives it ruled out:
|
|
89
|
+
|
|
90
|
+
> differs across 2 release(s) (release-a, release-b) with no recognised cracker
|
|
91
|
+
> signature … not a revision difference … not a `.d64` read error … not a packer
|
|
92
|
+
> artifact … not relocation (the anchor-proven offset for this pair is recorded
|
|
93
|
+
> above and used here).
|
|
94
|
+
|
|
95
|
+
`UNKNOWN` with a rule-out list is the honest answer. Do not upgrade it to
|
|
96
|
+
`CRACKER-PATCH` because a byte differs. **Confidence: HIGH** — run live against the
|
|
97
|
+
committed corpus; `ledger` reproduced the committed
|
|
98
|
+
`generated_tier_sha256 dc7eb080…` byte-identically, so the classification is
|
|
99
|
+
deterministic.
|
|
100
|
+
|
|
101
|
+
**One qualifier on the 102, and it is the example's premise rather than its output:**
|
|
102
|
+
"independently-cracked" is asserted at the top of this example, not proven by it. The
|
|
103
|
+
determinism is HIGH; the `ORIGINAL` verdicts inherit whatever confidence that
|
|
104
|
+
independence claim carries. See § *The independence precondition* below before
|
|
105
|
+
quoting an `ORIGINAL` count as settled.
|
|
106
|
+
|
|
107
|
+
## The five kinds and the three verdicts
|
|
108
|
+
|
|
109
|
+
`bucketManifest` promotes a manifest from `ranges-only` to `bucketed`, assigning
|
|
110
|
+
`game` / `loader` / `cracktro` / `io` / `unused`. Verdicts are `ORIGINAL`,
|
|
111
|
+
`CRACKER-PATCH`, `UNKNOWN`, carrying `HIGH` or `MEDIUM-HIGH` confidence.
|
|
112
|
+
|
|
113
|
+
The two seeds are where this goes wrong, and both failure modes are on record:
|
|
114
|
+
|
|
115
|
+
- **`loader` is seeded from `RELEASES.json`'s earned `loader_ranges`** — live
|
|
116
|
+
disassembly evidence — **never from `NOTES.md` prose.** Reading a loader range
|
|
117
|
+
out of prose is the documented root cause of `$08F5`, a permanent joystick-poll
|
|
118
|
+
instruction, once being classified as loader code.
|
|
119
|
+
- **`cracktro` is seeded from a crack-credit *vocabulary* scan**, not a bare
|
|
120
|
+
printable-ASCII scan. A bare scan was tried and produced a real false positive
|
|
121
|
+
against a real corpus: **the game's own title-screen text** is printable ASCII
|
|
122
|
+
too, and it differed between the two releases. A bare scan called that cracker
|
|
123
|
+
credit. It is not — a differing string is not a cracker string, and it is
|
|
124
|
+
correctly left `UNKNOWN`.
|
|
125
|
+
|
|
126
|
+
`io` (`$D000-$DFFF`) and `unused` (contiguous `$00`/`$FF` power-on runs) are
|
|
127
|
+
assigned at capture time and kept verbatim. Everything the trace reaches is `game`.
|
|
128
|
+
|
|
129
|
+
Per D-05 the `.bin` files are **never** edited or zeroed. Classification lives in
|
|
130
|
+
the manifests; the bytes stay verbatim evidence.
|
|
131
|
+
|
|
132
|
+
## A `CRACKER-PATCH` in `game` code is a trainer until proven otherwise
|
|
133
|
+
|
|
134
|
+
`count-patches` counts exactly one intersection — verdict `CRACKER-PATCH`, kind
|
|
135
|
+
`game`. That intersection has a name the pipeline never says out loud: a **trainer**.
|
|
136
|
+
A cracker changing bytes *inside game code* is altering gameplay, and unlimited
|
|
137
|
+
lives, disabled collision or a frozen timer is the usual reason.
|
|
138
|
+
|
|
139
|
+
This matters because the three verdicts answer **who wrote a range**, not **what it
|
|
140
|
+
does**. A relocated loader stub and a life-decrement patched to a `NOP` both come
|
|
141
|
+
back `CRACKER-PATCH`. So give every patch a **function verdict** alongside its origin
|
|
142
|
+
verdict:
|
|
143
|
+
|
|
144
|
+
| Function | Means | Why it matters |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `loader` | raw-sector loading, decrunch, relocation, drive code | an obstacle to get past, not a subject |
|
|
147
|
+
| `cracktro` | intro, scroller, music, the crack's own presentation | not the object of study |
|
|
148
|
+
| `gameplay` | reads or writes game state — **a trainer** | any rebuild copying these bytes inherits it |
|
|
149
|
+
| `unknown` | not yet attributed | scrutinise before trusting |
|
|
150
|
+
|
|
151
|
+
**A rebuild reconstructed from these bytes inherits a `gameplay` patch silently**, and
|
|
152
|
+
behaviour-only verification will not catch it: the baselines come from the same
|
|
153
|
+
cracked image, so the rebuild and its reference agree *while both differ from the
|
|
154
|
+
game as it shipped*.
|
|
155
|
+
|
|
156
|
+
### The independence precondition — this skill's own premise
|
|
157
|
+
|
|
158
|
+
`ORIGINAL` means "identical across two **independently**-cracked releases". Delete the
|
|
159
|
+
word *independently* and the verdict is worthless: two releases sharing an ancestor
|
|
160
|
+
are identical everywhere the ancestor was, **including everywhere the ancestor's
|
|
161
|
+
cracker patched**. Establish the independence. Do not infer it from two releases
|
|
162
|
+
carrying different group names, different loaders, or different cracktros — those are
|
|
163
|
+
the cheapest things for a re-cracker to replace.
|
|
164
|
+
|
|
165
|
+
Until it is established, the diff is directional, and the direction is the trap:
|
|
166
|
+
|
|
167
|
+
- A diff **hit** is informative — something was patched.
|
|
168
|
+
- A diff **miss** is not, and it is the miss that reads as reassurance.
|
|
169
|
+
|
|
170
|
+
So `count-patches` reporting `0` is not evidence that no trainer exists. It is
|
|
171
|
+
evidence that no trainer exists **in one release and not the other**. With unproven
|
|
172
|
+
ancestry those are different claims, and only the second one was tested.
|
|
173
|
+
|
|
174
|
+
### The detector that does not depend on the diff
|
|
175
|
+
|
|
176
|
+
A signature hunt over the canonical image, read against the coverage map rather than
|
|
177
|
+
against another release. None of it needs a second release at all, which is precisely
|
|
178
|
+
why it survives the shared-ancestor case:
|
|
179
|
+
|
|
180
|
+
1. **Writes to a consequence counter from an unexpected site.** Once the memory map
|
|
181
|
+
names what the game decrements on failure — lives, timer, health — every writer
|
|
182
|
+
that is not the game's own is a candidate. Search **every addressing form that can
|
|
183
|
+
reach the address**, indexed included. An absolute-mode-only search is the standard
|
|
184
|
+
way this hunt returns a false negative.
|
|
185
|
+
2. **Armed but never reached.** Code jumped to from a patched region that never
|
|
186
|
+
executes across full gameplay coverage is either dead crack scaffolding or a
|
|
187
|
+
trainer waiting on a trigger. Both need a verdict; neither should be reproduced
|
|
188
|
+
without one.
|
|
189
|
+
3. **Trigger scanners.** Reads of the keyboard or joystick registers in code that is
|
|
190
|
+
not the game's own input handler, and comparisons against key codes inside a range
|
|
191
|
+
already marked `CRACKER-PATCH`.
|
|
192
|
+
4. **`NOP` sleds and inverted branches.** The cheapest trainer is a patched-out check
|
|
193
|
+
— `EA EA EA` where a `JSR` or a decrement was, or a `BEQ`↔`BNE` flip on a collision
|
|
194
|
+
or life test. These show as a few bytes inside otherwise-original code: the pattern
|
|
195
|
+
most easily dismissed as noise, and the one that matters most.
|
|
196
|
+
|
|
197
|
+
**A negative is a result, and must state its own limits.** "No trainer found, by these
|
|
198
|
+
four signatures, at this coverage level" is an answer. "The diff was clean" is not.
|
|
199
|
+
|
|
200
|
+
## Before you trust a verdict
|
|
201
|
+
|
|
202
|
+
- **Coverage is incomplete, and the ledger says so out loud.**
|
|
203
|
+
a load-coverage record is not a finished coverage claim until every game state
|
|
204
|
+
has actually been visited. An on-demand-loaded
|
|
205
|
+
region — bytes that only appear after reaching a room or state nobody visited —
|
|
206
|
+
is by construction **absent from the primary dumps this diffs**. Every verdict is
|
|
207
|
+
scoped to "the addresses visible at the post-loader game-entry point", not to the
|
|
208
|
+
whole running game. This is exactly why the ledger is regenerable: a more
|
|
209
|
+
complete `LOADING.md` reopens it.
|
|
210
|
+
- **Never resolve a range's `kind` from its `start` address.** Coalescing groups on
|
|
211
|
+
*verdict* continuity, not *kind* continuity, so one range can span several kind
|
|
212
|
+
zones. `splitRangeByManifestKind` exists for this, and the bug was found live:
|
|
213
|
+
a wide `ORIGINAL` range was found running straight through a `loader` sub-range
|
|
214
|
+
nested inside it. Resolving from `start` silently mislabels every address after
|
|
215
|
+
the first boundary.
|
|
216
|
+
- **`--gap-tolerance` is off-by-one sensitive by design.** A gap of identical bytes
|
|
217
|
+
*strictly shorter* than N coalesces; a run of *exactly* N stays its own row.
|
|
218
|
+
- **More agreeing independent releases is the only thing that raises confidence.**
|
|
219
|
+
Two releases can establish `ORIGINAL`; they cannot establish intent. And *agreeing*
|
|
220
|
+
only counts once *independent* is proven — releases sharing an ancestor agree on
|
|
221
|
+
the ancestor's patches too, so unproven ancestry makes every `ORIGINAL` verdict
|
|
222
|
+
conditional rather than earned.
|
|
223
|
+
|
|
224
|
+
## Which skill does what
|
|
225
|
+
|
|
226
|
+
This one answers "is this byte original?". It does not capture images or read
|
|
227
|
+
addresses.
|
|
228
|
+
|
|
229
|
+
| Need | Go to |
|
|
230
|
+
|---|---|
|
|
231
|
+
| A verified 64K image, or proving two captures equivalent | `c64-ram-capture` |
|
|
232
|
+
| Which address to read next, and what the answer rules out | `c64-program-recon` |
|
|
233
|
+
| What a specific address or bit means | `c64-memory-mapping` — `node … lookup '$D018'` |
|
|
234
|
+
| Assembling, or a first-pass dead listing | `acme-build` |
|
|
235
|
+
| **Whether a byte is original, cracker-changed, or unknown** | here |
|
|
236
|
+
|
|
237
|
+
Findings that make RE faster go in `.planning/RE-FINDINGS.md` **at the moment you
|
|
238
|
+
find them**, graded with `Evidence:` and `Confidence:`. Promote by re-logging with
|
|
239
|
+
the new evidence, never by editing a grade in place. File-changing work enters
|
|
240
|
+
through a GSD command (`/gsd-quick`).
|
|
241
|
+
|
|
242
|
+
## Troubleshooting
|
|
243
|
+
|
|
244
|
+
| Symptom | Fix |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `anchor-search` reports `ok=false` | Anchors disagreed, so there is no single offset. Do **not** pick the majority — the images are not the same fully-loaded state, or one capture is bad. Re-capture rather than force it. |
|
|
247
|
+
| `git status` dirty after a run | Expected — two verbs write. Diff the two files; if only `proven_at`/`generated_at` moved, `git checkout --` them. |
|
|
248
|
+
| `generated_tier_sha256` changed | The classification changed, not just a timestamp. Find the cause before committing; the digest is the determinism check. |
|
|
249
|
+
| `count-patches` reports 0 | Usually correct. It counts `CRACKER-PATCH` **and** `game`-kind addresses; with two releases and no signature match, nothing qualifies. Check the `diff --json` tally before treating it as a bug. But do not read it as "no trainer" — see the next two rows. |
|
|
250
|
+
| Asked whether the crack added a trainer | `count-patches` is the diff-side answer: `CRACKER-PATCH` ∧ `game` **is** the trainer count. It is necessary and not sufficient — it cannot see a trainer both releases carry. Run the signature hunt too. |
|
|
251
|
+
| Asked to confirm a release has no trainer | You cannot confirm that from a diff alone, and saying so is the answer. A clean diff only rules out a trainer in *one* release and not the other; with unproven ancestry that is a weaker claim than it sounds. Report the signature-hunt result with its coverage limits. |
|
|
252
|
+
| Two releases agree everywhere suspicious | Suspect shared ancestry before concluding `ORIGINAL`. Different group names, loaders and cracktros are the cheapest things for a re-cracker to swap and prove nothing about independence. |
|
|
253
|
+
| Everything is `UNKNOWN` | Also usually correct. `UNKNOWN` means "differs, no recognised signature, alternatives ruled out". Read the range's `reason` field. |
|
|
254
|
+
| A range's `kind` looks wrong past its start | You resolved `kind` from `start`. Use `splitRangeByManifestKind`; coalescing does not respect kind boundaries. |
|
|
255
|
+
| 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
|
+
| 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 .claude/skills/c64-provenance-diff/scripts/releases.mjs list` for the valid ids. |
|