@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.
@@ -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; a wrong answer from a
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 verify-don't-assume rather than taking it on faith.
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; treat it as unverified, not as a proven guarantee.
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; check `available`
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; the emulator recomputes CIA port B from
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; either way, buffer injection is
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; use them rather than treating any difference as a divergence.
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; this file covers the shape of the source and what makes a
8
- reconstruction correct.
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 changes addresses, which breaks self-modifying code and
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 changed address per failing replay is a short diagnosis, ten is a
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; they encode a guess as a fact.
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; load it after each one and your checkpoints carry
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`; this file carries the idioms and the order.
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; 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 |
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** — verify, don't assume. On stock,
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; 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.
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; the variable it writes is the one to
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; read parameters off those. What the schemas cannot tell you is *which call to reach for
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; answers most vector questions in one or two calls |
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** — an earlier forecast naming a numbered phase for it is superseded |
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; a value comes from that error or from `vice_diagnose` |
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-changed? | the `c64-provenance-diff` skill |
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, settled 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) and record what you verified with `anno_set_data_type` |
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, reporting disagreements first, agreement as a count only, and a never-observed count that is explicitly NOT evidence the address holds data |
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; pair it with `vice_memmap_zap` on the emulator side |
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 (for example, a store or read-modify-write instruction whose literal target lands on another instruction's opcode or operand byte, changing what runs or what value is read on a later pass). Each finding carries its own detection mechanism and a detection-strength token. Reports and changes NOTHING: it never relocates, strips or rebases anything, and it never emits a flag a caller could act on as an automatic relocation |
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 halted rather than looking like a wedged emulator; prefer `vice_checkpoint_add` + a bounded
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; do not restate it here.
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
- **The memory map is GENERATED, not hand-authored.** The store — labels, comments, block
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; it is now the schema for the one input the generator
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. 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.
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, correct it and re-run). That remedy is safe to
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 validated by the renderer — a missing or malformed key is a named error listing every
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; `derive.mjs sprites` where sprite coordinates are relevant |
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-verify and restate the evidence with a fresh
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`; `ledger` rewrites
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. **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.
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; `ledger` reproduced the committed
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" 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.
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
- - **`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
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; the bytes stay verbatim evidence.
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 alter a single byte, it only makes the evidence readable
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 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.
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 changing bytes *inside game code* is altering gameplay, and unlimited
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". Delete the
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 it is established, the diff is directional, and the direction is the trap:
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 was tested.
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; neither should be reproduced
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 every game state
239
- has actually been visited. An on-demand-loaded
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, and the bug was found live:
248
- a wide `ORIGINAL` range was found running straight through a `loader` sub-range
249
- nested inside it. Resolving from `start` silently mislabels every address after
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; a run of *exactly* N stays its own row.
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`; they cannot establish intent. And *agreeing*
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 was actually known when it was written.
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 | Fix |
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; 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 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. |
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 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. |
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 correct. `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 — it is earned from disassembly. Prose is how `$08F5` got misclassified. |
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. |