@henols/c64-re-tools 0.2.1 → 0.2.2

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.
@@ -13,7 +13,7 @@ Build a network of confirmed facts. Once the vectors, the IRQ handler, the main
13
13
  tables are known, everything else classifies far more easily.
14
14
 
15
15
  ```bash
16
- D=.claude/skills/c64-program-recon/scripts/derive.mjs # from the repo root
16
+ D=src/skills/c64-program-recon/scripts/derive.mjs # from the repo root
17
17
 
18
18
  node $D vectors dump.bin # $01 + six vectors, which pair is live
19
19
  node $D vic --dd00 3E --d018 18 --d011 1B --d016 C8 # bank, screen, charset, mode
@@ -78,6 +78,42 @@ Differential experiments close the loop: patch a routine to `RTS` and see what s
78
78
  freeze and nothing else does, the routine's purpose is confirmed — far stronger evidence than
79
79
  reading the listing.
80
80
 
81
+ ## Step 0.5: is it packed, and by what?
82
+
83
+ Runs **between step 0 and step 1** — after scoping, before you go looking for an entry point.
84
+ Tracing a decruncher is the same wasted work as tracing a loader, and every label you write on a
85
+ packed image is thrown away the moment the real image is recovered.
86
+
87
+ ```bash
88
+ node src/skills/c64-program-recon/scripts/packer-finding.mjs game.prg # from the repo root
89
+ node src/skills/c64-program-recon/scripts/packer-finding.mjs game.prg --entropy 7.83
90
+ ```
91
+
92
+ Pass `--entropy` when you already have the number from `anno_get_binary_info`; otherwise the
93
+ script measures it from the file. It prints one JSON object. Read the `verdict`:
94
+
95
+ | Verdict | What it means | What to do |
96
+ | --- | --- | --- |
97
+ | `identified` | An external oracle stated the packer name, verbatim. `packer` holds it and `confidence` is `HIGH`. | Record the name as a finding. Then depack: run it in the emulator and capture RAM past the decrunch (`c64-ram-capture`). |
98
+ | `packed-unidentified` | Entropy is at or above the 7.5 packedness threshold and **no oracle named the packer**. `packer` is `null`. | Treat the image as packed. Depack the same way. Do not annotate these bytes and do not go hunting for a name. |
99
+ | `unpacked` | Entropy is below the threshold. Still not an identity claim — it says nothing about which packer, only that these bytes do not look compressed. | Continue to step 1 on this image. |
100
+ | `unknown` | No route produced an answer. `unavailableReason` always says why. | Continue, but record the unknown. Never write it up as "not packed". |
101
+
102
+ **A name is reported only when an external oracle stated one, and this project does not guess.**
103
+ No first-party route on this project's surface reports a packer name at all — the dated
104
+ investigation that established this, four independent ways, is written out in
105
+ `.planning/phases/19-absorbed-procedures-and-the-coverage-instrument/19-RESEARCH.md` §2, and the
106
+ dated decision that fixes the acceptance bar and its re-open trigger is recorded under
107
+ `19-DECISIONS.md` in that same directory (SURF-03). So there is no code path here that can write a packer name from entropy, from a
108
+ decompression address, or from a byte pattern. If you want a name and the finding does not give
109
+ you one, install an external identifier and point `UNP64` or `UNP64_PATH` at it — do not infer it.
110
+
111
+ **The entropy gate answers packedness, not identity.** High entropy tells you the bytes are
112
+ compressed (or encrypted, or genuinely random); it does not tell you by what. And the way a packed
113
+ image is actually opened up here is the run-and-capture route — run the program under the emulator
114
+ and capture RAM at a checkpoint past the decrunch — not an in-place unpack, which would destroy
115
+ the comments, labels and blocks the project already holds.
116
+
81
117
  ## Worked example — a real capture
82
118
 
83
119
  ```
@@ -121,29 +157,33 @@ Recon's findings are not memory-map prose written once and left to rot — they
121
157
  queryable annotation store, and the Markdown memory map is a *generated view* of that store (D-24),
122
158
  not something you hand-edit yourself.
123
159
 
124
- **Open or bootstrap the store**, then hand its path to every call that follows:
125
-
126
- ```bash
127
- npx -y @henols/vice-mcp r2000 bootstrap game.prg # npm install
128
- node <plugin-root>/.claude/mcp/vice/vice-proxy.ts r2000 bootstrap game.prg # in-repo/plugin
129
- ```
130
-
131
- Every `r2000_*` tool takes an explicit `project` path pointing at the resulting `.regen2000proj`
132
- (D-19) there is no ambient session state naming the store, so which project a call touched is
133
- always visible in the transcript.
160
+ **There is no bootstrap step, and no bootstrap verb.** The store is created by the first write to
161
+ it: name a `.annostore` path on any mutating call — `anno_set_label_name`, `anno_set_comment`,
162
+ `anno_set_data_type`, `anno_add_scope` — and it is created, committed and closed inside that call.
163
+ A read-only call against a path that does not exist yet is REFUSED by name rather than answering
164
+ against an empty store, so "I read nothing" and "there is nothing to read" stay distinguishable.
165
+
166
+ Every `anno_*` tool takes an explicit `store` path (D-19) — there is no ambient session state
167
+ naming the store, so which store a call touched is always visible in the transcript. Every call
168
+ that derives its answer from the program's **bytes** rather than from the annotations takes an
169
+ `image` path as well — `anno_get_binary_info`, `anno_read_region`, `anno_disassemble`,
170
+ `anno_get_cross_references`, `anno_search` and `anno_get_address_details`. The store holds
171
+ annotations and never bytes, so an omitted image would read as a plausible success against
172
+ whatever was recorded last. `image` is a `.prg` (2-byte little-endian load address plus payload) or
173
+ an exactly-65536-byte flat capture, dispatched **by extension first**, never by length.
134
174
 
135
175
  **Write findings with the named tools, not a Markdown row:**
136
176
 
137
177
  | Tool | Use for |
138
178
  |---|---|
139
- | `r2000_set_label_name` | Naming a routine or table (`init_screen`, `sprite_table`) |
140
- | `r2000_set_data_type` | Classifying a block (`code`, `byte`, `address`, `petscii`, …) |
141
- | `r2000_add_scope` | Marking a handler's extent as a lexical scope |
142
- | `r2000_set_comment` | Recording the evidence — the carrier for the confidence grade below |
143
- | `r2000_batch_execute` | Bulk annotation, 5+ independent calls at once — a real memory map is dozens of labels/comments/block ranges, and batching is what makes that affordable under the per-call spawn-load-mutate-save-exit lifecycle |
179
+ | `anno_set_label_name` | Naming a routine or table (`init_screen`, `sprite_table`) |
180
+ | `anno_set_data_type` | Classifying a block (`code`, `byte`, `address`, `petscii`, …) |
181
+ | `anno_add_scope` | Marking a handler's extent as a lexical scope |
182
+ | `anno_set_comment` | Recording the evidence — the carrier for the confidence grade below |
183
+ | `anno_batch_execute` | Bulk annotation, 5+ independent calls at once — a real memory map is dozens of labels/comments/block ranges, and one batch is one open/commit/close instead of dozens. The store (and the image, when an inner call needs one) is named ONCE at the top level and every inner call inherits it. A malformed payload, an empty `calls` array, an uncurated inner name at any depth or an illegal label name refuses the **whole** batch by index and executes nothing; past that gate, execution runs to completion and each entry carries its own status, so an error entry inside a successful result means that one call did not work |
144
184
 
145
185
  **Grade with the confidence prefix.** Lead every evidence comment with exactly one of these five
146
- bracket tokens (quoted verbatim from `r2000-confidence.ts`, the parser's own source of truth):
186
+ bracket tokens (quoted verbatim from `anno-confidence.ts`, the parser's own source of truth):
147
187
 
148
188
  `[confirmed-code]` (confirmed code), `[probable-code]` (probable code), `[confirmed-data]`
149
189
  (confirmed data), `[probable-data]` (probable data), `[unknown]` (unknown).
@@ -151,93 +191,164 @@ bracket tokens (quoted verbatim from `r2000-confidence.ts`, the parser's own sou
151
191
  A typo in the bracket token — wrong case, an underscore, a plural, stray whitespace — **fails
152
192
  loudly**; it does not silently degrade into an ungraded comment. As with `RE-FINDINGS.md`, do not
153
193
  promote a row by editing its grade in place: re-verify and restate the evidence with a fresh
154
- `r2000_set_comment` call, so the record of when something stopped being a guess survives.
155
-
156
- **Query instead of re-deriving.** `r2000_get_symbols`, `r2000_get_comments`, `r2000_get_blocks` and
157
- `r2000_get_cross_references` answer straight from the store. `r2000_search_disassembly` searches
158
- labels, comments and instructions together but `max_results` is **REQUIRED** on this surface,
159
- because regenerator2000's own default is 50 and silently truncates a full-program pass. The query
160
- this whole workflow exists to make cheap:
161
-
162
- > "Show me everything still `[unknown]`" → `r2000_search_disassembly` with `query: "[unknown]"` and
194
+ `anno_set_comment` call, so the record of when something stopped being a guess survives.
195
+
196
+ **Query instead of re-deriving.** `anno_get_symbols`, `anno_get_comments` and `anno_get_blocks`
197
+ answer straight from the store; `anno_get_cross_references` and `anno_search` derive their answers
198
+ from the image bytes plus the store's typed ranges, so they take `image` too. `anno_search` searches
199
+ three corpora together label names, comment text, and the instruction text rendered from every
200
+ range typed `code` **byte-exact and case-sensitive**, with each corpus named in the answer
201
+ alongside how many entries it held, so a genuine zero over a real corpus stays distinguishable from
202
+ a corpus this surface does not have.
203
+
204
+ `max_results` is **REQUIRED, with no default,** on every one of those reads. That is deliberate: an
205
+ implicit default silently truncates a full-program pass, and here the true match count rides beside
206
+ the truncated list, so truncation is a fact you are told rather than one you infer. The query this
207
+ whole workflow exists to make cheap:
208
+
209
+ > "Show me everything still `[unknown]`" → `anno_search` with `query: "[unknown]"` and
163
210
  > an explicit `max_results` set above your program's comment count.
164
211
 
165
- (The composite address-details lookup is deliberately not on this surface D-32, a 64K-project
166
- defect filed upstream its answer is reachable as a combination of the tools above.)
212
+ `anno_get_blocks` is also the read route for the store's other structural annotations: pass
213
+ `include: ["scopes", "enums", "enum_usage"]` to get scope spans (which `anno_remove_scope` must
214
+ match exactly), every project enum with its variants, and every address-to-enum association.
215
+
216
+ `anno_get_address_details` composes everything known about ONE address — the labels bound there,
217
+ the comments there, the typed range covering it, and the cross-references reaching it. **The
218
+ composition is disclosed:** the body carries `composed_client_side` and a `composed_from` list
219
+ naming all four sources, so a composition is never mistaken for something the store held whole.
167
220
 
168
221
  ### Take names to the running machine, and bring live findings back
169
222
 
170
- The store and the running emulator are not two independent destinations for a name — writing one
171
- into the store and discovering one live are two legs of **one loop**, in this order, matching how
172
- `R2000-14`/`R2000-15` were actually proven (see Phase 11's live walkthrough,
173
- `evidence/criterion4/WALKTHROUGH.md`):
174
-
175
- 1. **Export what the store already knows.** `r2000 export-lbl <project>` writes `al C:xxxx .Name`
176
- lines that `stock-symbols.ts`'s own parser accepts — the verb reads the written file back
177
- through that same parser before it reports success, never trusting a regenerator2000 exit code
178
- alone.
179
-
180
- ```bash
181
- npx -y @henols/vice-mcp r2000 export-lbl game.regen2000proj # npm install
182
- node <plugin-root>/.claude/mcp/vice/vice-proxy.ts r2000 export-lbl game.regen2000proj # in-repo/plugin
183
- ```
184
-
185
- 2. **Load it into the running machine — `vice_symbols_load`, exactly once.** Load that `.lbl` file
186
- into the live emulator with `vice_symbols_load`. Call it **exactly once** per regenerated file:
187
- it REPLACES the machine's symbol table rather than merging into it, so loading an older export a
188
- second time after the store has moved on would silently discard the newer names.
189
- 3. **Discover something live the static pass could not, then write it to the store first.**
190
- Disassembling or reading the running machine (`vice_disassemble`, a checkpoint hit, …) can turn
191
- up a name the static store never had. Write it with `r2000_set_label_name` *before* regenerating
192
- anything — the store is the merge point (D-29), not your own notes.
193
- 4. **Regenerate the whole `.lbl` and bring it back with `import-lbl`, never an incremental patch.**
194
- `r2000 import-lbl <project> <lbl>` imports an externally-produced `.lbl` file into the project,
195
- and reports whether the import was **disk-verified** re-read from disk in a fresh process,
196
- never trusted from the child's own success text alone.
197
-
198
- ```bash
199
- npx -y @henols/vice-mcp r2000 import-lbl game.regen2000proj discovered.lbl # npm install
200
- node <plugin-root>/.claude/mcp/vice/vice-proxy.ts r2000 import-lbl game.regen2000proj discovered.lbl # in-repo/plugin
201
- ```
202
-
203
- Two traps: `export-lbl` exports **USER** labels only — the auto-generated `a_D011`/`e_FFD2`
204
- externals never appear in the written file. And both verbs require an EXISTING `.regen2000proj`;
205
- neither one bootstraps a project from a raw input.
206
-
207
- `r2000 gen-enums` — turning register writes into named enum variants — is documented in
208
- `c64-memory-mapping`, alongside the `memmap.json` bit table it consumes.
223
+ **Dated withdrawal, 2026-08-29 the `.lbl` round trip is WITHDRAWN, and as of 2026-08-31 no phase
224
+ currently owns its return.** The two CLI verbs that carried it, `export-lbl` and `import-lbl`, are
225
+ gone from this surface: both were delivery paths into the retired static analyser. This notice
226
+ previously forecast that a numbered phase would rebuild them alongside the ACME export route; that
227
+ forecast was **wrong and is corrected here rather than deleted**. The phase that rebuilt the ACME
228
+ export route covered that route only no requirement and no success criterion of it mentioned the
229
+ `.lbl` round tripso the round trip still has no route and **no phase currently owns its return**.
230
+ Do not reach for these verbs here: they do not exist, and an invocation fails with an unknown-verb
231
+ error and no explanation of why.
232
+
233
+ The **loop itself is not withdrawn**, only its two automated legs, and the discipline it encodes is
234
+ what to keep doing by hand for as long as they stay gone:
235
+
236
+ 1. **The store is the merge point (D-29), not your own notes.** A name discovered live —
237
+ disassembling the running machine, a checkpoint hit — is written into the store with
238
+ `anno_set_label_name` *first*, before it is carried anywhere else.
239
+ 2. **`vice_symbols_load` REPLACES the machine's symbol table rather than merging into it.** Call it
240
+ **exactly once** per generated `.lbl` file. Loading an older file a second time, after the store
241
+ has moved on, silently discards the newer names.
242
+ 3. **Regenerate whole, never patch incrementally.** The round trip regenerated the entire `.lbl`
243
+ from the store, and any rebuild of it must do the same; a hand-written incremental patch
244
+ reintroduces exactly the drift the single merge point exists to prevent.
245
+
246
+ Two traps that survive the withdrawal and are part of the specification whoever eventually rebuilds
247
+ this will read: the export carried **USER** labels only auto-generated `a_D011`/`e_FFD2` externals
248
+ never appeared in the written fileand neither direction ever created a store from a raw input.
249
+
250
+ `gen-enums` — turning register writes into named enum variants — is **withdrawn on the same terms,
251
+ and no phase currently owns its return either**. The same superseded forecast named a numbered phase
252
+ for it; that phase's requirements covered the ACME export oracle only. What `gen-enums` consumed,
253
+ the `memmap.json` bit table, is documented in `c64-memory-mapping` along with the withdrawal and the
254
+ by-hand route that stays open.
209
255
 
210
256
  **Generate the memory map; do not hand-author it.** Fill in the provenance sidecar (schema and a
211
257
  filled example live in `templates/memory-map.template.md`), then:
212
258
 
213
259
  ```bash
214
- npx -y @henols/vice-mcp r2000 render-memmap game.regen2000proj --provenance sidecar.json
215
- node <plugin-root>/.claude/mcp/vice/vice-proxy.ts r2000 render-memmap game.regen2000proj --provenance sidecar.json
260
+ npx -y @henols/vice-mcp anno render-memmap game.annostore --provenance sidecar.json
261
+ node <plugin-root>/src/mcp/vice/vice-proxy.ts anno render-memmap game.annostore --provenance sidecar.json
216
262
  ```
217
263
 
218
- Add `--check` to detect drift either a hand edit to the rendered file, or a store change since it
219
- was last rendered. The rendered file carries a generated-file banner; treat it like every other
220
- generated artifact in this repo and never hand-edit it.
264
+ Add `--check` to detect drift. It is reported when, and only when, one of these changed: the
265
+ rendered file itself (a hand edit); a store row (a range, a label, a comment, or a comment's
266
+ confidence grade); the provenance sidecar's bytes; the location of the store or the sidecar
267
+ **relative to the workspace root**; or the renderer. **Relocating the checkout is not drift** — the
268
+ same tree at a different absolute path renders the same bytes, because the banner records
269
+ workspace-relative locations. The rendered file carries a generated-file banner; treat it like every
270
+ other generated artifact in this repo and never hand-edit it.
271
+
272
+ **Dated correction, 2026-08-30 — the paragraph above used to name TWO drift causes, and a third
273
+ existed.** Before gap-closure round 2 the banner recorded the store and the sidecar by their
274
+ ABSOLUTE paths, so the checkout's own location was a silent third cause: an identical store,
275
+ sidecar and rendered file reported `drifted` the moment the tree sat at a different absolute path,
276
+ while `render-memmap` printed the same `render_digest` in both. Plan 29-18 removed that cause by
277
+ recording workspace-relative locations, so the cause set above is the one the shipped verb has. The
278
+ old two-cause wording is superseded rather than merely reworded, and this note says so because a
279
+ reader meeting it in history needs to know which claim was live when.
280
+
281
+ **One-time drift after upgrading, 2026-08-30.** A memory map rendered *before* that change reports
282
+ `drifted` on its first `--check` afterwards, exactly once, because the banner's recorded locations
283
+ changed from absolute to workspace-relative spellings. Re-run the generator and commit the new
284
+ banner. This repository has no committed rendered `memory-map.md` — only the template — so nothing
285
+ here regresses; the sentence is written for **consuming projects**, which do have one.
286
+
287
+ **This playbook itself has a generated twin, and it is not the one to edit.**
288
+ `installer/skills/c64-program-recon/` is a gitignored COPY of this directory, rebuilt from it by
289
+ `installer/scripts/sync-skills.mjs` on the installer package's `prepack` and by
290
+ `npm --prefix installer run sync-skills`. Edit THIS file; never edit the twin. A hand-edit there is
291
+ overwritten by the next sync and is not independently covered either — the gates that scan the
292
+ shipped tree run the sync before they scan it, so a change made only in the twin is erased before it
293
+ is ever measured. A change made here is SHIPPED only once that sync has run.
294
+
295
+ **Dated correction, 2026-08-30 — `render-memmap` reads the annotation store directly, and the note
296
+ that used to stand here was WRONG when it shipped.** Phase 29 plan 29-12 rebuilt this verb over the
297
+ Phase 28 annotation store on `D-17`'s authority: its positional is an EXISTING `.annostore`, opened
298
+ with `mustExist` — an absent store is refused by name rather than created — and nothing on the path
299
+ it reaches consults the retired external analyser. The pre-store project file the earlier note named
300
+ has no producer left in this repository, so there is no route back to the old spelling. That earlier
301
+ note asserted in the PRESENT TENSE that this verb still read a project file; it was already false
302
+ when it shipped, and it is DELETED here rather than amended, so a reader comparing two dated claims
303
+ can tell which one to believe.
221
304
 
222
305
  ## Static disassembly
223
306
 
224
- Turning a `.prg` or a flat 64K image into ACME source, offline, is not part of this
225
- skill's own method it is a separate route:
307
+ **Dated withdrawal 2026-08-29, dated return 2026-08-31 whole-program ACME export was WITHDRAWN
308
+ and has come back as `anno export-asm`, behind a real-ACME byte-diff oracle.** The notice is kept
309
+ rather than deleted because the withdrawal explains the shape of what returned. The removed verb
310
+ turned a `.prg` or a flat 64K image into ACME source offline and settled its own correctness with a
311
+ transcript parser; what returned is not a rename of it. It is rebuilt over the **annotation store**,
312
+ and its correctness is settled by **assembling the output with a real ACME and diffing the bytes
313
+ against the input** — never by an exit code and never by a string match on the exporter's own
314
+ output.
226
315
 
227
316
  ```bash
228
- npx -y @henols/vice-mcp r2000 export-asm game.prg # npm installs
229
- node <plugin-root>/.claude/mcp/vice/vice-proxy.ts r2000 export-asm game.prg # in-repo/plugin
317
+ npx -y @henols/vice-mcp anno export-asm game.prg --store game.annostore --out game.a
318
+ node <plugin-root>/src/mcp/vice/vice-proxy.ts anno export-asm game.prg --store game.annostore
230
319
  ```
231
320
 
232
- This is **static**, over a file on disk `vice_disassemble` (the live-RAM route
233
- this skill's own table above uses) reads a running emulator's RAM at a checkpoint
234
- instead. The two are complementary: reach for the static route before the emulator
235
- is even running, and for `vice_disassemble` once you have a live checkpoint to
236
- decode from.
237
-
238
- Extracting from a `.d64` image: name the file inside the image explicitly. The
239
- tool lists the directory and refuses rather than guess (D-02) a guess could
240
- analyse a cracktro or loader stub instead of the game.
321
+ `<image>` and `--store` are **two separate arguments and neither is derived from the other**: the
322
+ image supplies the bytes, the store supplies the names, typed ranges and comments. `--out` defaults
323
+ to a `.a` beside the **store** rather than beside the image, and an existing destination is refused
324
+ rather than overwritten unless you pass `--force`.
325
+
326
+ **It writes source and runs no assembler**, and says so in its own second output line
327
+ (`this file has NOT been assembled`). The real-ACME byte-diff is a **test-only** oracle in this
328
+ repository's test suite, absent from the published package and unreachable at runtime so a clean
329
+ run is evidence that source was written, not an assembler verdict. `acme-build` carries the full
330
+ statement of that split.
331
+
332
+ Two routes remain for reading a single routine, and they are the ones the rest of this playbook
333
+ already uses:
334
+
335
+ - **`anno_read_region`** and **`anno_disassemble`** render one routine or table at an **explicit**
336
+ inclusive range, decoded fresh from the image bytes on every call and written nowhere. That is
337
+ the static route, bounded on purpose: the combined byte count is capped at **4096 bytes**
338
+ (`ANNO_READ_REGION_MAX_BYTES`), and a wider request is REFUSED by name rather than truncated,
339
+ because a full-64K disassembly dumped into an agent's context is exactly the hazard the cap
340
+ exists to prevent.
341
+ - **`vice_disassemble`** is the live-RAM route this skill's own table above uses: it reads a
342
+ running emulator's RAM at a checkpoint.
343
+
344
+ The two are complementary — reach for the static reads before the emulator is even running, and for
345
+ `vice_disassemble` once you have a live checkpoint to decode from.
346
+
347
+ Extracting a program from a `.d64` image is a separate capability that this repository still does
348
+ not have, and — correcting an earlier note that assigned it to the same numbered phase as the ACME
349
+ export oracle — **no phase currently owns it**. Whenever it is built it must name the file inside
350
+ the image explicitly and refuse rather than guess (D-02), because a guess could analyse a cracktro
351
+ or loader stub instead of the game.
241
352
 
242
353
  ## Before you touch the emulator
243
354
 
@@ -253,6 +364,246 @@ two lines are the part you cannot afford to load lazily.
253
364
  `ping` still reporting `running`, an identical PC — because the machine genuinely never moved.
254
365
  Two cheap reads settle it, and neither needs `vice_execution_run`.
255
366
 
367
+ ## Documenting one routine, end to end
368
+
369
+ The table at the top of this page finds *where* the structure is. This section
370
+ is what you do once you have picked one routine out of it and want it
371
+ documented properly in the annotation store.
372
+
373
+ **Scope, so three skills do not fight over the same job.** This procedure
374
+ handles **one routine, at one explicit address**. Building the backlog of every
375
+ undocumented routine in a project and draining it to closure is
376
+ `routine-queue-walker`'s job — it calls into this procedure once per queue
377
+ entry. Classifying the *regions* around the routine, and naming the data
378
+ symbols it touches, is the absorbed pair in `c64-memory-mapping`.
379
+
380
+ ### 1. Context first
381
+
382
+ `anno_get_binary_info` for `system`, `filename`, `description` and
383
+ `may_contain_undocumented_opcodes`.
384
+
385
+ - `system` names the target machine and therefore which memory map, hardware
386
+ registers and ROM entry points are in play.
387
+ - `filename` and `description` identify the software. This is how you recognise
388
+ a stock component instead of re-deriving it — a Hubbard-style music driver,
389
+ an Exomizer decrunch stub — and how genre informs a guess
390
+ (`check_collision` is a plausible routine in a shooter).
391
+ - With `may_contain_undocumented_opcodes: true`, expect `LAX`, `SAX`, `SLO`,
392
+ `DCP`, `ISC`. These are real instructions, not disassembly errors; do not
393
+ stop reading at one.
394
+
395
+ ### 2. Bounds, from an explicit address
396
+
397
+ **Always start from an address you were given or derived** — `$XXXX` or its
398
+ decimal equivalent. There is no editor cursor in this project's route, and
399
+ upstream's own text forbids relying on one in any case.
400
+
401
+ Find the start (the entry point or its label) and the end (`RTS`, `RTI`, or a
402
+ `JMP`). Two shapes to expect:
403
+
404
+ - A routine ending in `JMP shared_epilogue` still **ends there** — that is a
405
+ tail call, and the target's body is a different routine.
406
+ - A routine with no return at all may **fall through** into the next one. Use
407
+ the cross-references and the flow to decide where the boundary is, and say in
408
+ the comment that it falls through.
409
+
410
+ ### 3. Read the range
411
+
412
+ `anno_read_region` over the routine's explicit range, naming the `store` and
413
+ the `image`, with `view` **omitted** — the disassembly view is that parameter's
414
+ documented default, so the call needs no `view` at all here.
415
+
416
+ The combined byte count is capped at **4096 bytes** per call
417
+ (`ANNO_READ_REGION_MAX_BYTES`) and a request above it is refused by name
418
+ rather than silently truncated. A routine longer than that — rare, but real in
419
+ a decruncher or a level builder — is read as **consecutive ranges**. Read them
420
+ in order; do not raise the cap to swallow the whole program, because the cap is
421
+ what keeps a "read this routine" call from becoming a whole-program export.
422
+
423
+ Then read the flow, not just the instructions:
424
+
425
+ - Does it loop? Where does the loop terminate?
426
+ - Does it call other routines, or ROM entry points?
427
+ - Does it touch hardware registers?
428
+
429
+ Recurring shapes worth recognising on sight:
430
+
431
+ | Pattern | Almost always |
432
+ |---|---|
433
+ | `SEI` … `CLI` bracketing | IRQ setup or teardown |
434
+ | `LDA`/`STA` with `DEX`/`DEY`/`BNE` | Memory copy or fill |
435
+ | Bit shifts plus `ADC`/`SBC` chains | Maths, or a decompressor |
436
+ | Reads an I/O address then branches | Hardware polling |
437
+ | Writes to `$0314`/`$FFFE` | Interrupt vector installation |
438
+ | Writes to `$D400`–`$D418` | Music or SFX driver tick |
439
+ | Reads `$DC00`/`$DC01` | Joystick or keyboard polling |
440
+
441
+ ### 4. Who calls it
442
+
443
+ `anno_get_cross_references` on the entry point. The caller is often more
444
+ decisive than the body:
445
+
446
+ - Called from an init block → a setup routine, runs once.
447
+ - Called from the main loop → a per-frame update.
448
+ - Called from the IRQ → must be fast; likely a music tick or a raster update,
449
+ and its zero-page usage is IRQ-relative.
450
+ - **No callers at all** → not necessarily dead. It may be a dispatch target
451
+ reached through a jump table; check the nearby data blocks for an address
452
+ table pointing at it.
453
+
454
+ ### 5. What data it touches
455
+
456
+ For every address the routine reads or writes:
457
+
458
+ 1. `lookup` it first (see `c64-memory-mapping`). A hardware register or KERNAL
459
+ entry point is answered outright and needs no further work.
460
+ 2. Otherwise `anno_get_cross_references` on that address, and read the shape:
461
+ - Written once, in init → a constant or a config value.
462
+ - Written *and* read by several routines → shared state, a global.
463
+ - In the zero page and used as `($addr),Y` → an indirect pointer.
464
+ 3. **Enums.** If the accessed addresses or the immediate values form a logical
465
+ set — state constants, joystick direction bits, colour codes — check for an
466
+ existing project, global or system enum that matches, and apply it with
467
+ `anno_apply_enum_usage` at the accessing instruction. If none matches but
468
+ the set is clean, define one with `anno_create_project_enum` (give it a
469
+ real `description`) and then apply it everywhere it fits. This is what turns
470
+ `lda #$1b` into something a reader understands.
471
+
472
+ **The pointer-formatting step this project does not have.** A routine that sets
473
+ up a pointer or a vector does it with a pair of immediate loads — `LDA #<target
474
+ / STA ptr`, `LDA #>target / STA ptr+1`. Upstream calls `set_immediate_format`
475
+ on each of the two instruction addresses, with `low_byte` / `high_byte` and the
476
+ target, so the pair renders as one symbol reference. **That call is not exposed
477
+ on this project's surface.** `BUILD-03` ("every branch, `JSR`/`JMP` and data
478
+ reference goes through a symbol, so code can move") is the requirement that
479
+ supplies its criterion, and the per-call disposition sits in the manifest named
480
+ in the attribution header above. Until then, recombine the two bytes yourself
481
+ and put the reconstructed target in a side comment on both instructions, so the
482
+ vector setup is readable even though the store cannot format it.
483
+
484
+ ### 6. Synthesise, then document
485
+
486
+ Four things, and they are the four things the comment block carries: **purpose**
487
+ (one sentence), **inputs** (registers and memory used as arguments), **outputs**
488
+ (registers and memory modified), **side effects** (hardware, screen, sound).
489
+
490
+ Rename the label with `anno_set_label_name`, then put a multi-line `"line"`
491
+ comment above the first instruction with `anno_set_comment`, in this exact
492
+ shape — the separator is both the first and the last line:
493
+
494
+ ```
495
+ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
496
+ <what the routine does>
497
+
498
+ Inputs: <registers or memory used as arguments, or "None">
499
+ Outputs: <registers or memory modified, or "None">
500
+ Side Effects: <hardware changes, screen updates, etc., or "None">
501
+ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
502
+ ```
503
+
504
+ Then add `"side"` comments to the instructions that carry the meaning — what a
505
+ register holds here, why this branch is taken, what this address represents.
506
+ This is the part that makes the listing readable for the next person, and it is
507
+ the part most often skipped. Grade evidence comments with the confidence prefix
508
+ documented earlier on this page.
509
+
510
+ ### 7. Report
511
+
512
+ - **Purpose** in one sentence.
513
+ - **Inputs / outputs / side effects** as determined above.
514
+ - **Evidence** — the instructions or cross-references that decided it.
515
+ - **Actions taken** — what was renamed, which line comment was added, which
516
+ instructions got side comments, which enums were defined or applied.
517
+ - **Uncertain areas** — every instruction or address whose purpose is still
518
+ unclear, by address. A routine report with no uncertain areas on a real game
519
+ is usually a report that stopped looking.
520
+
521
+ ### What goes wrong
522
+
523
+ | Symptom | What it actually is |
524
+ |---|---|
525
+ | No `RTS`/`JMP`/`RTI` at the apparent end | Deliberate fall-through. Check whether the next label is independently called. |
526
+ | `JMP some_routine` as the last instruction | A tail call. This routine ends there; the target is a separate routine. |
527
+ | Several routines converging on one `RTS` | A shared epilogue. It belongs to none of them; note it in each comment. |
528
+ | No callers, but the routine is clearly live | Reached through a jump table. Look for an address table pointing at it. |
529
+ | Disassembly appears to break mid-routine | Undocumented opcodes. Check the binary-info hint and keep reading. |
530
+ | Zero-page usage contradicts the main program's | The routine runs from the IRQ. Its context is IRQ-relative. |
531
+
532
+ ## REFERENCE-ONLY: decoding Commodore BASIC tokens
533
+
534
+ **This capability is DEFERRED under `FUT-01` and this skill does not claim
535
+ it.** The reason is empirical, not a shortage of effort: a commercial C64 title
536
+ captured after its loader has run almost universally reduces to a one-line
537
+ `SYS` stub, so a token decoder would spend its life decoding `10 SYS 2064` and
538
+ nothing else. The material below is carried as **reference text only** — read
539
+ it if you hit the rare program that really does carry a tokenised BASIC
540
+ program, and note that none of its trigger phrases appear in any skill's
541
+ `description:` frontmatter, so it cannot fire on its own.
542
+
543
+ If a future milestone lifts `FUT-01`, this section is the starting point rather
544
+ than a fresh research task.
545
+
546
+ ### Line anatomy
547
+
548
+ A tokenised BASIC program is a linked list in memory. Each line is:
549
+
550
+ 1. **Bytes 0–1 — next-line pointer.** The address where the *next* line
551
+ begins, little-endian (`24 04` → `$0424`).
552
+ 2. **Bytes 2–3 — line number**, 16-bit little-endian (`0A 00` → `10`).
553
+ 3. **Bytes 4–N — the tokens**, running until a `$00` terminator.
554
+ 4. **End of program** when a line's next-line pointer is `$00 $00`.
555
+
556
+ Read the range with `anno_read_region`, `view: "hexdump"`, over an explicit
557
+ start and end address — subject to the same 4096-byte ceiling as every other
558
+ range read on this surface. Then walk the pointer chain from the first line
559
+ until the pointer is `$00 $00`.
560
+
561
+ ### Keyword tokens (BASIC V2)
562
+
563
+ Bytes with the high bit set, `$80` through `$CB`, are keywords:
564
+
565
+ | Hex | Keyword | Hex | Keyword | Hex | Keyword | Hex | Keyword |
566
+ | --- | --- | --- | --- | --- | --- | --- | --- |
567
+ | `$80` | `END` | `$93` | `LOAD` | `$A6` | `SPC(` | `$B9` | `POS` |
568
+ | `$81` | `FOR` | `$94` | `SAVE` | `$A7` | `THEN` | `$BA` | `SQR` |
569
+ | `$82` | `NEXT` | `$95` | `VERIFY` | `$A8` | `NOT` | `$BB` | `RND` |
570
+ | `$83` | `DATA` | `$96` | `DEF` | `$A9` | `STEP` | `$BC` | `LOG` |
571
+ | `$84` | `INPUT#` | `$97` | `POKE` | `$AA` | `+` | `$BD` | `EXP` |
572
+ | `$85` | `INPUT` | `$98` | `PRINT#` | `$AB` | `-` | `$BE` | `COS` |
573
+ | `$86` | `DIM` | `$99` | `PRINT` | `$AC` | `*` | `$BF` | `SIN` |
574
+ | `$87` | `READ` | `$9A` | `CONT` | `$AD` | `/` | `$C0` | `TAN` |
575
+ | `$88` | `LET` | `$9B` | `LIST` | `$AE` | `^` | `$C1` | `ATN` |
576
+ | `$89` | `GOTO` | `$9C` | `CLR` | `$AF` | `AND` | `$C2` | `PEEK` |
577
+ | `$8A` | `RUN` | `$9D` | `CMD` | `$B0` | `OR` | `$C3` | `LEN` |
578
+ | `$8B` | `IF` | `$9E` | `SYS` | `$B1` | `>` | `$C4` | `STR$` |
579
+ | `$8C` | `RESTORE` | `$9F` | `OPEN` | `$B2` | `=` | `$C5` | `VAL` |
580
+ | `$8D` | `GOSUB` | `$A0` | `CLOSE` | `$B3` | `<` | `$C6` | `ASC` |
581
+ | `$8E` | `RETURN` | `$A1` | `GET` | `$B4` | `SGN` | `$C7` | `CHR$` |
582
+ | `$8F` | `REM` | `$A2` | `NEW` | `$B5` | `INT` | `$C8` | `LEFT$` |
583
+ | `$90` | `STOP` | `$A3` | `TAB(` | `$B6` | `ABS` | `$C9` | `RIGHT$` |
584
+ | `$91` | `ON` | `$A4` | `TO` | `$B7` | `USR` | `$CA` | `MID$` |
585
+ | `$92` | `WAIT` | `$A5` | `FN` | `$B8` | `FRE` | `$CB` | `GO` |
586
+
587
+ Bytes between `$20` and `$7F` are literal PETSCII characters — strings,
588
+ variable names, numbers.
589
+
590
+ ### What a decoding pass would write
591
+
592
+ Per line, batched through `anno_batch_execute`:
593
+
594
+ 1. `anno_set_data_type` `address` over bytes 0–1 (the next-line pointer).
595
+ 2. `anno_set_data_type` `word` over bytes 2–3 (the line number).
596
+ 3. `anno_set_data_type` `byte` from byte 4 through the `$00` terminator,
597
+ inclusive.
598
+ 4. `anno_set_comment` `"side"` at byte 0, carrying the reconstructed line —
599
+ `10 REM LODE RUNNER`.
600
+
601
+ Then jump to the next-line pointer and repeat until it reads `$00 $00`, and
602
+ finally mark that `$00 $00` terminator itself as `word`. Nothing needs to be
603
+ "saved": every one of those writes committed and fsynced inside its own call.
604
+ `anno_save_project` performs **no write at all** — it reports the store's current
605
+ revision, which is what to quote when you write the pass up.
606
+
256
607
  ## Which skill does what
257
608
 
258
609
  This one is the route between the stations. It does not restate what the others carry.
@@ -262,7 +613,7 @@ This one is the route between the stations. It does not restate what the others
262
613
  | A verified 64K image, or comparing two captures | `c64-ram-capture` |
263
614
  | What a specific address or bit means | `c64-memory-mapping` — `node … lookup '$D018'` |
264
615
  | Assembling | `acme-build` |
265
- | Static disassembly of a `.prg` or flat image | `vice-mcp r2000 export-asm` (see above) |
616
+ | Static disassembly of a `.prg` or flat image | **`anno export-asm`** withdrawn 2026-08-29, returned 2026-08-31 behind a real-ACME byte-diff oracle that is test-only, so the verb writes source and assembles nothing. Read one range at a time with `anno_read_region` for a single routine (see above) |
266
617
  | Whether a byte is original or cracker-changed | `c64-provenance-diff` |
267
618
  | The emulator stopped moving — wedged, self-trapped, or respawned | `vice-wedge-triage` |
268
619
  | **Which address to read next, and what the answer rules out** | here |
@@ -122,7 +122,13 @@ stream across full gameplay coverage is data, whatever the tracer guessed.
122
122
 
123
123
  ## Labels round-trip through VICE
124
124
 
125
- ACME's `--vicelabels` output and regenerator2000's exported label files share one format, which
126
- `vice_symbols_load` / `vice_symbols_lookup` consume. Labels therefore flow
127
- disassembler → source → build → debugger without translation. `acme-build` emits the `.vs` file on
128
- every build; load it after each one and your checkpoints carry real names.
125
+ ACME's `--vicelabels` output is the `al C:xxxx .Name` format `vice_symbols_load` /
126
+ `vice_symbols_lookup` consume, so labels flow source → build → debugger without translation.
127
+ `acme-build` emits the `.vs` file on every build; load it after each one and your checkpoints carry
128
+ real names.
129
+
130
+ **The other direction — exporting the annotation store into that same format — is withdrawn as of
131
+ 2026-08-29, and no phase currently owns its return** (an earlier forecast naming a numbered phase
132
+ for it is superseded: that phase covered the ACME export oracle only). So a name discovered live
133
+ goes into the store with `anno_set_label_name` first (the store is the merge point) and reaches the
134
+ emulator only through a `.lbl` you produce yourself.