@henols/c64-re-tools 0.2.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/acme-build/SKILL.md +22 -22
- package/skills/c64-disk-access/SKILL.md +49 -46
- package/skills/c64-memory-mapping/SKILL.md +71 -70
- package/skills/c64-petcat/SKILL.md +11 -11
- package/skills/c64-program-recon/SKILL.md +109 -105
- package/skills/c64-program-recon/references/control-flow.md +13 -13
- package/skills/c64-program-recon/references/graphics.md +6 -6
- package/skills/c64-program-recon/references/observation-hazards.md +7 -7
- package/skills/c64-program-recon/references/reconstruction.md +6 -6
- package/skills/c64-program-recon/references/sound-and-input.md +8 -8
- package/skills/c64-program-recon/references/tool-selection.md +11 -11
- package/skills/c64-program-recon/templates/memory-map.template.md +16 -12
- package/skills/c64-provenance-diff/SKILL.md +43 -43
- package/skills/c64-ram-capture/SKILL.md +96 -45
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +525 -0
- package/skills/c64-ram-capture/templates/capture-record.template.md +11 -11
- package/skills/c64-ram-capture/transients/README.md +11 -10
- package/skills/routine-queue-walker/SKILL.md +205 -176
- package/skills/vice-wedge-triage/SKILL.md +48 -48
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Hazards: how observing a running C64 gives you the wrong answer
|
|
2
2
|
|
|
3
3
|
The entries here are graded higher than the method files, because most were established live on
|
|
4
|
-
this project at real cost. A wrong answer from a register table is cheap
|
|
4
|
+
this project at real cost. A wrong answer from a register table is cheap. A wrong answer from a
|
|
5
5
|
machine that changed *because you looked at it* discredits a whole session without announcing
|
|
6
6
|
itself.
|
|
7
7
|
|
|
@@ -78,18 +78,18 @@ Reading one while the game runs steals the event the game was about to service.
|
|
|
78
78
|
|
|
79
79
|
Prefer the whole-chip reads — `vice_vicii_get_state`, `vice_cia_get_state`, `vice_sid_get_state`
|
|
80
80
|
— over raw register reads. Whether the VICE monitor's own read path is side-effect-free is
|
|
81
|
-
**unverified**: treat it as
|
|
81
|
+
**unverified**: treat it as check-don't-assume rather than taking it on faith.
|
|
82
82
|
|
|
83
83
|
`vice_vicii_get_state`/`vice_cia_get_state` reads are `sidefx: false` with
|
|
84
84
|
no argument able to override it — **VERIFIED**, asserted on the wire body by a regression test.
|
|
85
85
|
Whether the emulator's own `MEM_GET` read path actually honours that flag for
|
|
86
86
|
`$D01E`/`$D01F`/`$DC0D`/`$DD0D` — i.e. whether it truly cannot clear them — is **ASSUMED**, with
|
|
87
|
-
no probe recorded in this repo
|
|
87
|
+
no probe recorded in this repo. Treat it as unverified, not as a proven guarantee.
|
|
88
88
|
`vice_sid_get_state` read-back is **permanently unavailable**: SID `$D400-$D418` is write-only
|
|
89
89
|
in hardware and the binary monitor has no SID command, so there is no route to recover it.
|
|
90
90
|
Writes to those addresses still work fine. See `docs/stock-hard-losses.md`. Also: an internal
|
|
91
91
|
field the register map cannot expose is marked `{ available: false, reason }` in the answer, never
|
|
92
|
-
a bare `0` — do not record a `0` from one of these fields as a measurement
|
|
92
|
+
a bare `0` — do not record a `0` from one of these fields as a measurement. Check `available`
|
|
93
93
|
first. A chip-state or sprite answer also **names the memory view it read** (`bank`, or
|
|
94
94
|
`registerBank`/`dataBank`), read through the emulator's own `io`/`ram` banks, so it stays valid
|
|
95
95
|
even while the program has I/O banked out ($01 driving the RAM/ROM/I-O switch). An answer with
|
|
@@ -105,11 +105,11 @@ Games and cracks poll the `$DC00`/`$DC01` matrix directly, bypassing the KERNAL
|
|
|
105
105
|
gate by releasing it at the trigger checkpoint, never earlier.
|
|
106
106
|
|
|
107
107
|
**`vice_keyboard_matrix` is permanently unavailable.** The binary monitor's `KEYBOARD_FEED` (0x72)
|
|
108
|
-
only injects PETSCII text into the KERNAL keyboard buffer
|
|
108
|
+
only injects PETSCII text into the KERNAL keyboard buffer. The emulator recomputes CIA port B from
|
|
109
109
|
its own keyboard array on every read, so there is no wire command that can drive the raw matrix —
|
|
110
110
|
this is unrecoverable, not merely unbuilt. See `docs/stock-hard-losses.md`. Use
|
|
111
111
|
`vice_keyboard_type` / `vice_keyboard_petscii` when the gate reads the KERNAL buffer, or
|
|
112
|
-
`vice_joystick_set` when it polls the matrix directly instead
|
|
112
|
+
`vice_joystick_set` when it polls the matrix directly instead. Either way, buffer injection is
|
|
113
113
|
invisible to a program polling `$DC00`/`$DC01` itself, so a matrix-polling gate must be driven by
|
|
114
114
|
the joystick or not at all.
|
|
115
115
|
|
|
@@ -140,4 +140,4 @@ real measured rate, so plan a live session to tolerate re-deriving a boot sequen
|
|
|
140
140
|
Never-written RAM drifts continuously, so two captures of the same checkpoint will not match
|
|
141
141
|
across all 64K. The recovery procedure is deterministic **for the program image**, not for 64K.
|
|
142
142
|
`c64-ram-capture` § Compare two captures carries the volatility regions and the drift
|
|
143
|
-
discriminator
|
|
143
|
+
discriminator. Use them rather than treating any difference as a divergence.
|
|
@@ -4,8 +4,8 @@ Do not wait until everything is understood. Stand up a buildable tree early, inc
|
|
|
4
4
|
unidentified bulk as binary, and replace regions with real source as they are confirmed. The tree
|
|
5
5
|
stays assemblable at every commit, so a regression has a small blast radius.
|
|
6
6
|
|
|
7
|
-
`acme-build` covers assembling
|
|
8
|
-
reconstruction
|
|
7
|
+
`acme-build` covers assembling. This file covers the shape of the source and what makes a
|
|
8
|
+
reconstruction right.
|
|
9
9
|
|
|
10
10
|
## Start with binary inclusion
|
|
11
11
|
|
|
@@ -43,9 +43,9 @@ at a checkpoint is not verified** — the constraint's own conclusion, and the r
|
|
|
43
43
|
design is part of the reconstruction work rather than something to bolt on afterwards.
|
|
44
44
|
|
|
45
45
|
**That bar is what buys you the freedom to rename routines, reorganise files, replace constants
|
|
46
|
-
with symbols and add macros**. **But** reorganising
|
|
46
|
+
with symbols and add macros**. **But** reorganising shifts addresses, which breaks self-modifying code and
|
|
47
47
|
timing-sensitive raster routines. Replay through the checkpoint set after EACH reorganisation, not
|
|
48
|
-
at the end of several: one
|
|
48
|
+
at the end of several: one shifted address per failing replay is a short diagnosis, ten is a
|
|
49
49
|
bisect.
|
|
50
50
|
|
|
51
51
|
A full 64K RAM capture is not a comparison surface: never-written RAM drifts continuously, so
|
|
@@ -91,7 +91,7 @@ Provisional names that carry their own uncertainty beat confident names that tur
|
|
|
91
91
|
| `Unknown_` | no reliable interpretation yet |
|
|
92
92
|
|
|
93
93
|
`Routine_43A2` → `Maybe_UpdatePlayer` → `UpdatePlayerPosition`, promoted only when behaviour is
|
|
94
|
-
confirmed. Avoid `AmazingCollisionRoutine`-style names entirely
|
|
94
|
+
confirmed. Avoid `AmazingCollisionRoutine`-style names entirely. They encode a guess as a fact.
|
|
95
95
|
|
|
96
96
|
Record the confidence next to the thing, in the same grammar the project uses everywhere else:
|
|
97
97
|
|
|
@@ -123,7 +123,7 @@ stream across full gameplay coverage is data, whatever the tracer guessed.
|
|
|
123
123
|
|
|
124
124
|
ACME's `--vicelabels` output is the `al C:xxxx .Name` format `vice_symbols_load` /
|
|
125
125
|
`vice_symbols_lookup` consume, so labels flow source → build → debugger without translation.
|
|
126
|
-
`acme-build` emits the `.vs` file on every build
|
|
126
|
+
`acme-build` emits the `.vs` file on every build. Load it after each one and your checkpoints carry
|
|
127
127
|
real names.
|
|
128
128
|
|
|
129
129
|
**The other direction — exporting the annotation store into that same format — is withdrawn as of
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SID and CIA: music, effects, the RNG, input, timing
|
|
2
2
|
|
|
3
3
|
Graded 2026-08-01, **MEDIUM**, doc-derived, except where marked. Per-register bit detail lives in
|
|
4
|
-
`c64-memory-mapping
|
|
4
|
+
`c64-memory-mapping`. This file carries the idioms and the order.
|
|
5
5
|
|
|
6
6
|
## SID — separating the player from the game logic
|
|
7
7
|
|
|
@@ -36,8 +36,8 @@ Confusing them is a frequent early error.
|
|
|
36
36
|
|
|
37
37
|
| | CIA#1 `$DC00` — keyboard, joysticks, **IRQ** | CIA#2 `$DD00` — VIC bank, serial, user port, **NMI** |
|
|
38
38
|
|---|---|---|
|
|
39
|
-
| Port A | `$DC00` keyboard **column** select
|
|
40
|
-
| Port B | `$DC01` keyboard **row** read
|
|
39
|
+
| Port A | `$DC00` keyboard **column** select. Joystick port 2 | `$DD00` VIC bank (bits 0-1, inverted). Serial ATN/CLK/DATA |
|
|
40
|
+
| Port B | `$DC01` keyboard **row** read. Joystick port 1 | `$DD01` user port / RS-232 |
|
|
41
41
|
| Timers | `$DC04-$DC07`, control at `$DC0E`/`$DC0F` | `$DD04-$DD07`, `$DD0E`/`$DD0F` — these drive **NMI** |
|
|
42
42
|
| Interrupt | `$DC0D` | `$DD0D`, same bit layout |
|
|
43
43
|
|
|
@@ -52,7 +52,7 @@ One that programs `$DC04-$DC07` and enables timer A runs its own timebase.
|
|
|
52
52
|
writing `$DD00` is usually talking to the drive.
|
|
53
53
|
- **`$DC0D`/`$DD0D` clear the interrupt flags on read**, the same shape as `$D01E`/`$D01F`. Reading
|
|
54
54
|
one steals an interrupt the game was about to service. Prefer `vice_cia_get_state`. The VICE
|
|
55
|
-
monitor's exact behaviour here is **unverified** —
|
|
55
|
+
monitor's exact behaviour here is **unverified** — check, don't assume. On stock,
|
|
56
56
|
`vice_cia_get_state` reports the **read** side of `$xx0D` as `interruptStatus` and marks the
|
|
57
57
|
write-side enable mask `unavailable` — the two share one address with different meanings, so a
|
|
58
58
|
reader looking for "which interrupts are enabled" is not silently handed the flags that have
|
|
@@ -62,13 +62,13 @@ One that programs `$DC04-$DC07` and enables timer A runs its own timebase.
|
|
|
62
62
|
afternoon.** Games and cracks bypass the KERNAL keyboard buffer and read the matrix directly.
|
|
63
63
|
Assume it until shown otherwise. **`vice_keyboard_matrix` is permanently unavailable** — the
|
|
64
64
|
binary monitor's `KEYBOARD_FEED` only injects PETSCII buffer text and cannot drive the raw
|
|
65
|
-
matrix
|
|
66
|
-
the gate reads the KERNAL buffer, or `vice_joystick_set` when it polls the matrix directly
|
|
67
|
-
|
|
65
|
+
matrix. See `docs/stock-hard-losses.md`. Use `vice_keyboard_type` / `vice_keyboard_petscii` when
|
|
66
|
+
the gate reads the KERNAL buffer, or `vice_joystick_set` when it polls the matrix directly.
|
|
67
|
+
Buffer injection stays invisible to a program polling `$DC00`/`$DC01` itself.
|
|
68
68
|
|
|
69
69
|
## Finding input handling from the observable side
|
|
70
70
|
|
|
71
71
|
Watch reads of `$DC00`/`$DC01` to find the input routine, then trace forward to what it stores.
|
|
72
72
|
The joystick bits are active-low: bit 4 is fire, bits 0-3 up/down/left/right. A routine that reads
|
|
73
|
-
`$DC01`, masks one bit and branches is the input decoder
|
|
73
|
+
`$DC01`, masks one bit and branches is the input decoder. The variable it writes is the one to
|
|
74
74
|
name first.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Which `mcp__plugin_c64-re-tools_vice__*` call answers which question
|
|
2
2
|
|
|
3
3
|
**This is not a restatement of the tool surface.** You already hold typed schemas for every
|
|
4
|
-
call
|
|
4
|
+
call. Read parameters off those. What the schemas cannot tell you is *which call to reach for
|
|
5
5
|
first*, and that ordering is the whole value here.
|
|
6
6
|
|
|
7
7
|
Curated 2026-08-01, doc-derived, **Confidence: MEDIUM** — the mapping is reasoned from the tool surface and this project's own
|
|
@@ -9,7 +9,7 @@ usage, not measured). Individual rows that have since been exercised live are ma
|
|
|
9
9
|
|
|
10
10
|
| Question | Call |
|
|
11
11
|
|---|---|
|
|
12
|
-
| Vectors, `$01`, `$D011`/`$D012`/`$D018`/`$D019`/`$D01A`, `$DC0D`/`$DD0D`, `$DD00` | `vice_memory_read` — highest-value first move
|
|
12
|
+
| Vectors, `$01`, `$D011`/`$D012`/`$D018`/`$D019`/`$D01A`, `$DC0D`/`$DD0D`, `$DD00` | `vice_memory_read` — highest-value first move. Answers most vector questions in one or two calls |
|
|
13
13
|
| What does the handler at this vector do? | `vice_disassemble` — the emulator's own decoder, not a dead listing |
|
|
14
14
|
| Is this really the main loop? | `vice_checkpoint_add` + `vice_run_until` + `vice_registers_get` — fires once per frame ⇒ proven |
|
|
15
15
|
| What code writes this? | `vice_watch_add` — finds **writers**. Best targets: `$D018`, VM+`$03F8`, `$D404` |
|
|
@@ -17,33 +17,33 @@ usage, not measured). Individual rows that have since been exercised live are ma
|
|
|
17
17
|
| Whole-chip SID state without the read hazards | `vice_sid_get_state` — **permanently unavailable**: SID `$D400-$D418` is write-only in hardware and the binary monitor has no SID command, so read-back cannot be recovered. Writes to those addresses still work fine over the memory-set primitive. See `docs/stock-hard-losses.md` |
|
|
18
18
|
| Decode sprite data | `vice_sprite_get` / `vice_sprite_inspect` |
|
|
19
19
|
| Find a known byte pattern | `vice_memory_search` |
|
|
20
|
-
| Carry labels across sessions | `vice_symbols_load` / `vice_symbols_lookup` — ACME `--vicelabels` emits the format they consume. The annotation store's own export into that format is **withdrawn as of 2026-08-29, and no phase currently owns its return
|
|
20
|
+
| Carry labels across sessions | `vice_symbols_load` / `vice_symbols_lookup` — ACME `--vicelabels` emits the format they consume. The annotation store's own export into that format is **withdrawn as of 2026-08-29, and no phase currently owns its return**. An earlier forecast named a numbered phase for it, and that forecast is superseded now |
|
|
21
21
|
| Is the machine wedged, or did it stop itself? | `vice_diagnose` — five-state verdict with its evidence (one state is `monitor_held_elsewhere`, because this monitor serves exactly one client at a time). **Reachable and proxy-intercepted as of 2026-08-04** (verified live). Triage tree: `vice-wedge-triage` |
|
|
22
22
|
| Replace a wedged instance | `vice_recycle` — destructive, requires a `reason`, and that reason is written into `.c64-re-tools/incidents/` **before** anything is killed. The reason *is* the evidence record |
|
|
23
|
-
| Read the restart epoch | **No tool does.** The proxy compares it around every forwarded call and raises drift itself
|
|
23
|
+
| Read the restart epoch | **No tool does.** The proxy compares it around every forwarded call and raises drift itself. A value comes from that error or from `vice_diagnose` |
|
|
24
24
|
|
|
25
25
|
## Delegate rather than restate
|
|
26
26
|
|
|
27
27
|
| Question | Go to |
|
|
28
28
|
|---|---|
|
|
29
29
|
| What does address X mean? | the `c64-memory-mapping` skill — `node … lookup '$D018'`. **Do not restate its tables.** |
|
|
30
|
-
| Is this byte original or cracker-
|
|
30
|
+
| Is this byte original or cracker-patched? | the `c64-provenance-diff` skill |
|
|
31
31
|
| A verified 64K image, or comparing two captures | the `c64-ram-capture` skill |
|
|
32
|
-
| Whole-program static disassembly with code/data separation | **`anno export-asm`** — withdrawn 2026-08-29, returned 2026-08-31
|
|
32
|
+
| Whole-program static disassembly with code/data separation | **`anno export-asm`** — withdrawn 2026-08-29, returned 2026-08-31. It settles correctness by assembling the output with a real ACME and diffing the bytes against the input. That oracle is test-only, so the verb itself writes source and runs no assembler. For a single routine, read one explicit range at a time with `anno_read_region` / `anno_disassemble` (4096-byte cap per call, refused rather than truncated above it). Record what you checked with `anno_set_data_type` |
|
|
33
33
|
|
|
34
34
|
## Runtime evidence versus the byte-derived guess
|
|
35
35
|
|
|
36
36
|
| Question | Call |
|
|
37
37
|
|---|---|
|
|
38
|
-
| Where the store's byte-derived block table (`anno_set_data_type`'s own ranges) disagrees with what the emulator was actually observed executing | **`anno evid-disagreements`** (also `anno_evid_disagreements`) — joins the typed ranges against the runtime-observed rows an `anno_evid_ingest` call already wrote
|
|
38
|
+
| Where the store's byte-derived block table (`anno_set_data_type`'s own ranges) disagrees with what the emulator was actually observed executing | **`anno evid-disagreements`** (also `anno_evid_disagreements`) — joins the typed ranges against the runtime-observed rows an `anno_evid_ingest` call already wrote. It reports disagreements first, agreement as a count only, and a never-observed count. A never-observed count is explicitly NOT evidence the address holds data |
|
|
39
39
|
| What evidence a store already holds, without re-running the program | `anno_evid_runs` — every run identity's observation count beside its denominator |
|
|
40
|
-
| Reset one run identity's evidence for a fresh re-measurement | `anno_evid_reset` — clears only that run identity's rows
|
|
40
|
+
| Reset one run identity's evidence for a fresh re-measurement | `anno_evid_reset` — clears only that run identity's rows. Pair it with `vice_memmap_zap` on the emulator side |
|
|
41
41
|
|
|
42
42
|
## What blocks this program's code from being moved
|
|
43
43
|
|
|
44
44
|
| Question | Call |
|
|
45
45
|
|---|---|
|
|
46
|
-
| Which constructions block relocating, rebasing or stripping part of this program | **`anno hazard-report`** (also `anno_hazard_report`) — enumerates movement-hazard findings derived from decoded bytes alone
|
|
46
|
+
| Which constructions block relocating, rebasing or stripping part of this program | **`anno hazard-report`** (also `anno_hazard_report`) — enumerates movement-hazard findings derived from decoded bytes alone. For example, a store or read-modify-write instruction may land its literal target on another instruction's opcode or operand byte. A later pass may then run different code or read a different value there. Each finding carries its own detection mechanism and a detection-strength token. It reports and touches NOTHING. It never relocates, strips or rebases anything, and it never emits a flag a caller could act on as an automatic relocation |
|
|
47
47
|
|
|
48
48
|
**A region this report does not flag is undecided or unflagged, never certified safe to move.** Every checked
|
|
49
49
|
region reports one of exactly three outcomes, and only one of them means a construction was actually found
|
|
@@ -58,9 +58,9 @@ moved.
|
|
|
58
58
|
|
|
59
59
|
**`vice_run_until`'s `timeout_ms` bounds the wait.** `timeout_ms` (default 30000, ceiling 600000)
|
|
60
60
|
bounds a run to an address the program never reaches, and a timed-out answer says the machine is
|
|
61
|
-
left
|
|
61
|
+
left stopped rather than looking like a wedged emulator. Prefer `vice_checkpoint_add` + a bounded
|
|
62
62
|
poll when the address is a hypothesis rather than a certainty — see `vice-wedge-triage` for the
|
|
63
|
-
full triage judgement and its live evidence
|
|
63
|
+
full triage judgement and its live evidence. Do not restate it here.
|
|
64
64
|
|
|
65
65
|
**`vice_diagnose` leaves the machine paused.** When it measures a cycle bracket it resumes the
|
|
66
66
|
machine once or twice and then leaves it **paused** — resuming is your own next call. And a
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Memory map generation
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**A generator writes the memory map. Nobody hand-authors it.** The store — labels, comments, block
|
|
4
4
|
types and scopes written through the `anno_*` tools described in `../SKILL.md` — is canonical. This
|
|
5
|
-
file used to be a fill-in-the-rows document
|
|
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:
|
|
@@ -13,15 +13,19 @@ node <plugin-root>/src/mcp/vice/vice-proxy.ts anno render-memmap game.annostore
|
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
prints the first differing line. `--check` reports drift when, and only when, one of these changed:
|
|
17
|
+
|
|
18
|
+
- the rendered file itself (a hand edit)
|
|
19
|
+
- a store row (a range, a label, a comment, or a comment's confidence grade)
|
|
20
|
+
- the provenance sidecar's bytes
|
|
21
|
+
- the location of the store or the sidecar **relative to the workspace root**
|
|
22
|
+
- the renderer
|
|
23
|
+
|
|
24
|
+
**Relocating the checkout is not drift** — the same tree at a different absolute path renders the
|
|
25
|
+
same bytes, because the banner records workspace-relative locations rather than absolute ones.
|
|
22
26
|
|
|
23
27
|
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,
|
|
28
|
+
the generator (or, if the sidecar itself is stale, fix it and re-run). That remedy is safe to
|
|
25
29
|
follow on any machine — because the banner records workspace-relative locations, re-running in a
|
|
26
30
|
different checkout or in a worktree does not rewrite the file with that machine's absolute paths.
|
|
27
31
|
The generated file carries a banner naming the store, the sidecar and a content digest — do not
|
|
@@ -38,7 +42,7 @@ one-time, self-clearing banner correction — not a bug, and not a migration.
|
|
|
38
42
|
Some facts belong to the **run** (which capture, which `$01`, which video standard) rather than to
|
|
39
43
|
any address, and the store has no address-keyed shape for them. They are supplied to the
|
|
40
44
|
renderer as a small JSON sidecar, hand-authored from `c64-ram-capture`'s and `derive.mjs`'s own
|
|
41
|
-
outputs and
|
|
45
|
+
outputs and confirmed by the renderer — a missing or malformed key is a named error listing every
|
|
42
46
|
problem at once, never a `<placeholder>` silently rendered into a published document.
|
|
43
47
|
|
|
44
48
|
| Key | Type | Where it comes from |
|
|
@@ -54,7 +58,7 @@ problem at once, never a `<placeholder>` silently rendered into a published docu
|
|
|
54
58
|
| `videoStandard` | `"PAL"` or `"NTSC"` | Known from the capture's origin/hardware context |
|
|
55
59
|
| `liveVectorPair` | string | `derive.mjs vectors` — the live vector pair (`$0314/$0315` or `$FFFE/$FFFF`) |
|
|
56
60
|
| `vectorHandler` | string | The address the live vector pair points at, confirmed live at a checkpoint |
|
|
57
|
-
| `rasterPositions` | string array, optional | One entry per observed `$D012` write on the way out of the live IRQ handler
|
|
61
|
+
| `rasterPositions` | string array, optional | One entry per observed `$D012` write on the way out of the live IRQ handler. `derive.mjs sprites` where sprite coordinates are relevant |
|
|
58
62
|
|
|
59
63
|
A fully-filled example, with plausible values in place of placeholders — copy this shape, never the
|
|
60
64
|
literal values:
|
|
@@ -93,5 +97,5 @@ close but not exact — a typo never silently degrades into an ungraded comment)
|
|
|
93
97
|
Do not force an unknown range through a disassembler and record the output as code. A linear
|
|
94
98
|
decode of data is silently wrong and contaminates everything downstream.
|
|
95
99
|
|
|
96
|
-
**Do not promote a row by editing its grade.** Re-
|
|
100
|
+
**Do not promote a row by editing its grade.** Re-confirm and restate the evidence with a fresh
|
|
97
101
|
`anno_set_comment` call, so the record of when something stopped being a guess survives.
|
|
@@ -42,7 +42,7 @@ Pure Node over committed files — the `.bin` dumps, their `.map.json` manifests
|
|
|
42
42
|
|
|
43
43
|
## Two verbs write to tracked files
|
|
44
44
|
|
|
45
|
-
`anchor-search` updates `recovery/RELEASES.json
|
|
45
|
+
`anchor-search` updates `recovery/RELEASES.json`. `ledger` rewrites
|
|
46
46
|
`recovery/PROVENANCE.md` and touches `RELEASES.json` too. So `git status` is
|
|
47
47
|
**expected** to be dirty after a run.
|
|
48
48
|
|
|
@@ -61,10 +61,10 @@ changed.
|
|
|
61
61
|
## Worked example — the real corpus
|
|
62
62
|
|
|
63
63
|
Two independently-cracked releases of one title, both captured at the same
|
|
64
|
-
post-loader entry trigger. **
|
|
65
|
-
`release-b
|
|
66
|
-
corpus, with only the ids renamed** — the tool has no
|
|
67
|
-
|
|
64
|
+
post-loader entry trigger. **This example shows the release ids below as
|
|
65
|
+
`release-a` and `release-b`.** **Every number is real output from a live run
|
|
66
|
+
against a two-release corpus, with only the ids renamed** — the tool has no
|
|
67
|
+
opinion about what you call a release.
|
|
68
68
|
|
|
69
69
|
```
|
|
70
70
|
$ node $D anchor-search
|
|
@@ -94,15 +94,15 @@ naming the alternatives it ruled out:
|
|
|
94
94
|
|
|
95
95
|
`UNKNOWN` with a rule-out list is the honest answer. Do not upgrade it to
|
|
96
96
|
`CRACKER-PATCH` because a byte differs. **Confidence: HIGH** — run live against the
|
|
97
|
-
committed corpus
|
|
97
|
+
committed corpus. `ledger` reproduced the committed
|
|
98
98
|
`generated_tier_sha256 dc7eb080…` byte-identically, so the classification is
|
|
99
99
|
deterministic.
|
|
100
100
|
|
|
101
101
|
**One qualifier on the 102, and it is the example's premise rather than its output:**
|
|
102
|
-
"independently-cracked"
|
|
103
|
-
determinism is HIGH
|
|
104
|
-
independence claim carries. See § *The independence precondition* below
|
|
105
|
-
quoting an `ORIGINAL` count as settled.
|
|
102
|
+
This example asserts "independently-cracked" at the top, and does not prove it.
|
|
103
|
+
The determinism is HIGH. The `ORIGINAL` verdicts inherit whatever confidence
|
|
104
|
+
that independence claim carries. See § *The independence precondition* below
|
|
105
|
+
before quoting an `ORIGINAL` count as settled.
|
|
106
106
|
|
|
107
107
|
## The five kinds and the three verdicts
|
|
108
108
|
|
|
@@ -112,13 +112,13 @@ quoting an `ORIGINAL` count as settled.
|
|
|
112
112
|
|
|
113
113
|
The two seeds are where this goes wrong, and both failure modes are on record:
|
|
114
114
|
|
|
115
|
-
- **`
|
|
116
|
-
disassembly evidence — **never from `NOTES.md` prose.** Reading a loader
|
|
117
|
-
out of prose
|
|
118
|
-
instruction,
|
|
119
|
-
- **`
|
|
120
|
-
printable-ASCII scan. A bare scan
|
|
121
|
-
|
|
115
|
+
- **`bucketManifest` seeds `loader` from `RELEASES.json`'s earned `loader_ranges`**
|
|
116
|
+
— live disassembly evidence — **never from `NOTES.md` prose.** Reading a loader
|
|
117
|
+
range out of prose once classified `$08F5`, a permanent joystick-poll
|
|
118
|
+
instruction, as loader code — the documented root cause.
|
|
119
|
+
- **`bucketManifest` seeds `cracktro` from a crack-credit *vocabulary* scan**, not
|
|
120
|
+
a bare printable-ASCII scan. A bare scan produced a real false positive against
|
|
121
|
+
a real corpus: **the game's own title-screen text** is printable ASCII
|
|
122
122
|
too, and it differed between the two releases. A bare scan called that cracker
|
|
123
123
|
credit. It is not — a differing string is not a cracker string, and it is
|
|
124
124
|
correctly left `UNKNOWN`.
|
|
@@ -127,7 +127,7 @@ The two seeds are where this goes wrong, and both failure modes are on record:
|
|
|
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
|
-
the manifests
|
|
130
|
+
the manifests. The bytes stay verbatim evidence.
|
|
131
131
|
|
|
132
132
|
## Carrying the verdict into the rebuild
|
|
133
133
|
|
|
@@ -147,7 +147,7 @@ verb writes.
|
|
|
147
147
|
|
|
148
148
|
**The flag makes the verdict VISIBLE and decides nothing.** Every byte in
|
|
149
149
|
scope is still emitted, whatever the verdict says — a `CRACKER-PATCH` row does
|
|
150
|
-
not drop, filter or
|
|
150
|
+
not drop, filter or change a single byte, it only makes the evidence readable
|
|
151
151
|
at the point of use. What gets reversed, kept or left out remains the
|
|
152
152
|
end-user's decision, never the tool's. `--ledger` is optional: omitting it
|
|
153
153
|
exports exactly as before, with no provenance comment anywhere in the output.
|
|
@@ -160,15 +160,15 @@ WITH the reason the operator gave, and `anno_include_range` takes the record
|
|
|
160
160
|
back if the decision changes. Recording an exclusion changes nothing about
|
|
161
161
|
which bytes the export emits — the exported block still carries every byte
|
|
162
162
|
of that span, now with a visible marker naming the exclusion and its reason,
|
|
163
|
-
so
|
|
164
|
-
is information the operator reads at this point
|
|
165
|
-
input to an automatic exclusion, here or anywhere else on this surface.
|
|
163
|
+
so the export removes nothing and no gap appears in the output. The ledger's
|
|
164
|
+
verdict is information the operator reads at this point. It is never wired as
|
|
165
|
+
an input to an automatic exclusion, here or anywhere else on this surface.
|
|
166
166
|
|
|
167
167
|
## A `CRACKER-PATCH` in `game` code is a trainer until proven otherwise
|
|
168
168
|
|
|
169
169
|
`count-patches` counts exactly one intersection — verdict `CRACKER-PATCH`, kind
|
|
170
170
|
`game`. That intersection has a name the pipeline never says out loud: a **trainer**.
|
|
171
|
-
A cracker
|
|
171
|
+
A cracker who changes bytes *inside game code* changes gameplay, and unlimited
|
|
172
172
|
lives, disabled collision or a frozen timer is the usual reason.
|
|
173
173
|
|
|
174
174
|
This matters because the three verdicts answer **who wrote a range**, not **what it
|
|
@@ -190,21 +190,21 @@ game as it shipped*.
|
|
|
190
190
|
|
|
191
191
|
### The independence precondition — this skill's own premise
|
|
192
192
|
|
|
193
|
-
`ORIGINAL` means "identical across two **independently**-cracked releases".
|
|
193
|
+
`ORIGINAL` means "identical across two **independently**-cracked releases". Remove the
|
|
194
194
|
word *independently* and the verdict is worthless: two releases sharing an ancestor
|
|
195
195
|
are identical everywhere the ancestor was, **including everywhere the ancestor's
|
|
196
196
|
cracker patched**. Establish the independence. Do not infer it from two releases
|
|
197
197
|
carrying different group names, different loaders, or different cracktros — those are
|
|
198
198
|
the cheapest things for a re-cracker to replace.
|
|
199
199
|
|
|
200
|
-
Until
|
|
200
|
+
Until you establish it, the diff is directional, and the direction is the trap:
|
|
201
201
|
|
|
202
202
|
- A diff **hit** is informative — something was patched.
|
|
203
203
|
- A diff **miss** is not, and it is the miss that reads as reassurance.
|
|
204
204
|
|
|
205
205
|
So `count-patches` reporting `0` is not evidence that no trainer exists. It is
|
|
206
206
|
evidence that no trainer exists **in one release and not the other**. With unproven
|
|
207
|
-
ancestry those are different claims, and only the second one
|
|
207
|
+
ancestry those are different claims, and the diff tested only the second one.
|
|
208
208
|
|
|
209
209
|
### The detector that does not depend on the diff
|
|
210
210
|
|
|
@@ -219,7 +219,7 @@ why it survives the shared-ancestor case:
|
|
|
219
219
|
way this hunt returns a false negative.
|
|
220
220
|
2. **Armed but never reached.** Code jumped to from a patched region that never
|
|
221
221
|
executes across full gameplay coverage is either dead crack scaffolding or a
|
|
222
|
-
trainer waiting on a trigger. Both need a verdict
|
|
222
|
+
trainer waiting on a trigger. Both need a verdict. Neither should be reproduced
|
|
223
223
|
without one.
|
|
224
224
|
3. **Trigger scanners.** Reads of the keyboard or joystick registers in code that is
|
|
225
225
|
not the game's own input handler, and comparisons against key codes inside a range
|
|
@@ -235,8 +235,8 @@ four signatures, at this coverage level" is an answer. "The diff was clean" is n
|
|
|
235
235
|
## Before you trust a verdict
|
|
236
236
|
|
|
237
237
|
- **Coverage is incomplete, and the ledger says so out loud.**
|
|
238
|
-
a load-coverage record is not a finished coverage claim until
|
|
239
|
-
|
|
238
|
+
a load-coverage record is not a finished coverage claim until you have actually
|
|
239
|
+
visited every game state. An on-demand-loaded
|
|
240
240
|
region — bytes that only appear after reaching a room or state nobody visited —
|
|
241
241
|
is by construction **absent from the primary dumps this diffs**. Every verdict is
|
|
242
242
|
scoped to "the addresses visible at the post-loader game-entry point", not to the
|
|
@@ -244,14 +244,14 @@ four signatures, at this coverage level" is an answer. "The diff was clean" is n
|
|
|
244
244
|
complete `LOADING.md` reopens it.
|
|
245
245
|
- **Never resolve a range's `kind` from its `start` address.** Coalescing groups on
|
|
246
246
|
*verdict* continuity, not *kind* continuity, so one range can span several kind
|
|
247
|
-
zones. `splitRangeByManifestKind` exists for this
|
|
248
|
-
a wide `ORIGINAL` range
|
|
249
|
-
|
|
247
|
+
zones. `splitRangeByManifestKind` exists for this. This bug appeared in a live
|
|
248
|
+
run: a wide `ORIGINAL` range ran straight through a `loader` sub-range nested
|
|
249
|
+
inside it. Resolving from `start` silently mislabels every address after
|
|
250
250
|
the first boundary.
|
|
251
251
|
- **`--gap-tolerance` is off-by-one sensitive by design.** A gap of identical bytes
|
|
252
|
-
*strictly shorter* than N coalesces
|
|
252
|
+
*strictly shorter* than N coalesces. A run of *exactly* N stays its own row.
|
|
253
253
|
- **More agreeing independent releases is the only thing that raises confidence.**
|
|
254
|
-
Two releases can establish `ORIGINAL
|
|
254
|
+
Two releases can establish `ORIGINAL`. They cannot establish intent. And *agreeing*
|
|
255
255
|
only counts once *independent* is proven — releases sharing an ancestor agree on
|
|
256
256
|
the ancestor's patches too, so unproven ancestry makes every `ORIGINAL` verdict
|
|
257
257
|
conditional rather than earned.
|
|
@@ -272,21 +272,21 @@ addresses.
|
|
|
272
272
|
Record findings that make RE faster in your own project notes **at the moment you
|
|
273
273
|
find them**, graded with `Evidence:` and `Confidence:`. Promote a finding by
|
|
274
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
|
|
275
|
+
grade is only worth anything if it says what you actually knew when you wrote it.
|
|
276
276
|
|
|
277
277
|
## Troubleshooting
|
|
278
278
|
|
|
279
|
-
| Symptom |
|
|
279
|
+
| Symptom | What to do |
|
|
280
280
|
|---|---|
|
|
281
281
|
| `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. |
|
|
282
|
-
| `git status` dirty after a run | Expected — two verbs write. Diff the two files
|
|
283
|
-
| `generated_tier_sha256` changed | The classification changed, not just a timestamp. Find the cause before committing
|
|
284
|
-
| `count-patches` reports 0 | Usually
|
|
282
|
+
| `git status` dirty after a run | Expected — two verbs write. Diff the two files. If only `proven_at`/`generated_at` moved, `git checkout --` them. |
|
|
283
|
+
| `generated_tier_sha256` changed | The classification changed, not just a timestamp. Find the cause before committing. The digest is the determinism check. |
|
|
284
|
+
| `count-patches` reports 0 | Usually accurate. 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. |
|
|
285
285
|
| 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. |
|
|
286
|
-
| Asked to
|
|
286
|
+
| Asked to prove a release has no trainer | You cannot prove 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. |
|
|
287
287
|
| 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. |
|
|
288
|
-
| Everything is `UNKNOWN` | Also usually
|
|
289
|
-
| A range's `kind` looks wrong past its start | You resolved `kind` from `start`. Use `splitRangeByManifestKind
|
|
290
|
-
| A loader range disagrees with `NOTES.md` | `RELEASES.json`'s `loader_ranges` wins
|
|
288
|
+
| Everything is `UNKNOWN` | Also usually accurate. `UNKNOWN` means "differs, no recognised signature, alternatives ruled out". Read the range's `reason` field. |
|
|
289
|
+
| A range's `kind` looks wrong past its start | You resolved `kind` from `start`. Use `splitRangeByManifestKind`. Coalescing does not respect kind boundaries. |
|
|
290
|
+
| A loader range disagrees with `NOTES.md` | `RELEASES.json`'s `loader_ranges` wins because live disassembly evidence earns it. Prose is how `$08F5` got misclassified. |
|
|
291
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. |
|
|
292
292
|
| `unknown release "x" -- known releases: …` | `node src/skills/c64-provenance-diff/scripts/releases.mjs list` for the valid ids. |
|