@henols/c64-re-tools 0.2.0 → 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.
Files changed (27) hide show
  1. package/README.md +1 -1
  2. package/THIRD-PARTY-NOTICES.md +26 -0
  3. package/bin/cli.mjs +18 -7
  4. package/package.json +6 -4
  5. package/skills/acme-build/SKILL.md +67 -47
  6. package/skills/acme-build/scripts/acme.mjs +2 -19
  7. package/skills/acme-build/template.a +17 -3
  8. package/skills/c64-memory-mapping/SKILL.md +426 -2
  9. package/skills/c64-program-recon/SKILL.md +479 -3
  10. package/skills/c64-program-recon/references/reconstruction.md +10 -4
  11. package/skills/c64-program-recon/references/tool-selection.md +2 -3
  12. package/skills/c64-program-recon/scripts/packer-finding.mjs +631 -0
  13. package/skills/c64-program-recon/templates/memory-map.template.md +86 -51
  14. package/skills/c64-provenance-diff/SKILL.md +4 -4
  15. package/skills/c64-provenance-diff/scripts/diff-images.mjs +3 -0
  16. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +14 -4
  17. package/skills/c64-ram-capture/RELEASES.json.example +17 -0
  18. package/skills/c64-ram-capture/SKILL.md +46 -2
  19. package/skills/c64-ram-capture/scripts/project-paths.mjs +1 -1
  20. package/skills/c64-ram-capture/scripts/watch-loads.mjs +6 -0
  21. package/skills/routine-queue-walker/SKILL.md +273 -0
  22. package/skills/vice-wedge-triage/SKILL.md +16 -0
  23. package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +0 -665
  24. package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +0 -243
  25. package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +0 -133
  26. package/skills/c64-ram-capture/scripts/test-corpus.mjs +0 -75
  27. package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +0 -339
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: c64-memory-mapping
3
- description: Look up what any C64 address means and turn raw 6502 disassembly into documented assembly, by resolving every address against the C64 memory map, KERNAL ROM routine list, canonical assembler symbols, and per-bit VIC-II/SID/CIA register tables. Use when asked to annotate or comment assembly, document a disassembly listing, or look up an address like $D020, $EA24 or $FFD2.
3
+ description: Look up what any C64 address means and turn raw 6502 disassembly into documented assembly, by resolving every address against the C64 memory map, KERNAL ROM routine list, canonical assembler symbols, and per-bit VIC-II/SID/CIA register tables. Use when asked to annotate or comment assembly against the published memory map, document a disassembly listing by resolving every address it touches, or look up an address like $D020, $EA24 or $FFD2.
4
4
  ---
5
5
 
6
6
  # C64 memory mapping & annotated disassembly
@@ -9,7 +9,7 @@ Look up what a C64 address means, and document a 6502 listing by resolving every
9
9
  address it touches. One script does both, offline, anywhere Node ≥18 runs:
10
10
 
11
11
  ```bash
12
- D=.claude/skills/c64-memory-mapping/scripts/driver.mjs # relative to the repo root
12
+ D=src/skills/c64-memory-mapping/scripts/driver.mjs # relative to the repo root
13
13
 
14
14
  node $D lookup '$D011' '$FFD2' # what lives at an address
15
15
  node $D annotate --file game.asm # document a listing or .asm file
@@ -186,6 +186,430 @@ were expecting. Because it mutates the repo, `memmap` belongs behind a GSD
186
186
  command (`/gsd-quick`), per this project's GSD Workflow Enforcement rule — it is
187
187
  not a read-only lookup like `lookup` and `annotate`.
188
188
 
189
+ ## Feeding the enum generator
190
+
191
+ `memmap.json`'s structured `bits` entries are the source of the curated register bit-name table used
192
+ to generate program-specific enums for this project's annotation store: register
193
+ writes disassemble as `lda #D011_YSCROLL3_ROW25_SCREENON_TEXT` instead of a bare `#$1b`. The
194
+ generator is `src/mcp/vice/anno-regbits-gen.ts`; its committed output is
195
+ `src/mcp/vice/anno-regbits.json`; and that output is **digest-pinned** to `memmap.json` — a
196
+ `node anno-regbits-gen.ts` run compares its own fresh build against the committed file, and CI fails
197
+ if `memmap.json` changed without a re-run.
198
+
199
+ **The honest gap:** only 29 of this file's 959 entries carry a structured `bits` array. `$D015`,
200
+ `$D017`, `$D01A` and `$D01B`–`$D01D` — the sprite-plane bitmask registers a real game writes
201
+ constantly — are **not** among those 29, so the enum generator supplies them from its own curated
202
+ override table (`OVERRIDES` in `anno-regbits-gen.ts`), not from this file. Widening `memmap.json`'s
203
+ `io` parser (or repairing the OCR damage already present in some `bits` prose, e.g. a letter `O` for
204
+ the digit `0`) so those registers get a real structured entry here is separate work belonging to this
205
+ skill, not the generator.
206
+
207
+ **Installing those bit names into a program's own disassembly — dated withdrawal, 2026-08-29.** The
208
+ table above only builds `anno-regbits.json`. Turning a specific program's register *writes* into
209
+ named enum variants was the `gen-enums` CLI verb, and **that verb is WITHDRAWN from this surface, and
210
+ no phase currently owns its return.** This notice previously forecast that it would come
211
+ back as a rebuild over the annotation store alongside the ACME export route; the ACME export route
212
+ did come back on 2026-08-31, but the phase that rebuilt it covered that route only — no requirement
213
+ and no success criterion of it mentioned `gen-enums`. That forecast is corrected here rather than
214
+ deleted. Do not invoke the verb — it does not exist, and the invocation fails with an unknown-verb
215
+ error and nothing to explain it.
216
+
217
+ What it did, so the rebuild has a specification and so a reader knows what is missing: it read the
218
+ program's own disassembly, created one enum variant per DISTINCT value actually written at each
219
+ matching immediate-load address (named from the curated table above), and printed
220
+ total/paired/unpaired register-store counts plus a per-enum variant count.
221
+
222
+ **The by-hand route is open in the meantime, and it is the same one this skill already documents
223
+ for any other enum.** `anno_create_project_enum` defines the variants and `anno_apply_enum_usage`
224
+ binds one to the accessing instruction's address — see "Name it" and "Document it" below. That is
225
+ manual where `gen-enums` was bulk, but it writes exactly the same rows into the store.
226
+
227
+ ## Classifying every region of an annotation project
228
+
229
+ Everything above answers *what does this published address mean*. This section
230
+ answers a different question over the same map: **given a loaded binary in an
231
+ annotation project, what is each region of it — code, or one of eight kinds of
232
+ data?**
233
+
234
+ **This is the static answer, taken from bytes on disk.** `c64-program-recon`
235
+ answers the *live* code-versus-data question — its step 5 is "what the PC
236
+ actually visits across full coverage", and an execution trace beats every
237
+ static heuristic on this page. Run the live pass when you have a running
238
+ machine; run this one when all you have is a file, and treat a later trace as
239
+ the thing that overrules it.
240
+
241
+ When a binary is first loaded, the auto-analyzer traces reachable code from the
242
+ entry point and marks it **Code**. Everything else is **Undefined** — not
243
+ "data", just unexplored. The job here is to walk the Undefined regions, work
244
+ out what each one actually is, and set it.
245
+
246
+ ### The one mistake that matters more than the rest
247
+
248
+ **Never disassemble a region without concrete proof that it executes.** Random
249
+ data routinely disassembles into plausible-looking instruction sequences, and
250
+ that is *not* evidence of code — it is a property of the 6502's dense opcode
251
+ map. A region earns the Code type only when at least one of these holds:
252
+
253
+ - **It is a `JSR`/`JMP` target.** Already-analysed code contains `JSR $addr` or
254
+ `JMP $addr` landing in it. Check with `anno_get_cross_references` — it names
255
+ the `image` as well as the `store`, takes a REQUIRED `max_results`, and unions
256
+ three sources on every call without caching any of them: the instructions
257
+ decoded fresh out of every range typed `code`, the typed split ADDRESS tables
258
+ (the `_address` forms produce cross-references and the `_word` forms do not),
259
+ and the stored rows, which exist because a computed dispatch cannot be
260
+ recovered from bytes at all.
261
+ - **It is a branch target** of an already-analysed `BNE`/`BEQ`/`BCC`/`BCS`/
262
+ `BPL`/`BMI`/`BVC`/`BVS`.
263
+ - **It is a vector or handler**: its address appears in a vector table
264
+ (`$FFFA`–`$FFFF`, `$0314`–`$0319`), in an `address` or split-address block,
265
+ or in a jump table reached by `JMP ($addr)`.
266
+ - **A human says so explicitly.**
267
+
268
+ None of those? Leave it **Undefined**, or classify it as data — even when the
269
+ bytes disassemble cleanly. "It looked like code" is how a sprite sheet becomes
270
+ four hundred lines of fiction.
271
+
272
+ ### The order of the passes
273
+
274
+ Work the Undefined blocks in four passes, in this order. Do not interleave
275
+ them; each pass makes the next one cheaper.
276
+
277
+ 1. **Provably-reachable code** — read from the entry point of each region that
278
+ meets the proof bar above with `anno_disassemble`, then type the range you
279
+ actually verified as `code` with `anno_set_data_type`. **Reading and typing
280
+ are two calls on this surface, and that is deliberate:** `anno_disassemble`
281
+ decodes fresh from the image bytes and writes nothing at all, so nothing is
282
+ ever classified as code by a decoder's guess — the boundary you record is
283
+ the one you read and judged.
284
+ 2. **Text** — PETSCII and screencode strings.
285
+ 3. **Tables** — byte, word, address and split (lo/hi, hi/lo) tables.
286
+ 4. **Whatever is left** — decide data, or leave it Undefined for a human.
287
+ **Never** speculatively disassemble in this pass; by definition nothing here
288
+ met the proof bar.
289
+
290
+ ### Scope, and reading a region
291
+
292
+ 1. `anno_get_binary_info` first. Keep `origin`, `size`, `system`, `filename`,
293
+ `description` and `may_contain_undocumented_opcodes`.
294
+ - `system` names the target machine. On a C64 the rest of this skill *is*
295
+ the memory map you need — `lookup` any address a region touches before
296
+ guessing at it.
297
+ - `filename` and `description` are the software context. A known title, a
298
+ known music driver or a known packer changes what a region is likely to
299
+ be.
300
+ - `may_contain_undocumented_opcodes: true` means illegal opcodes (`LAX`,
301
+ `SAX`, `SLO`, `DCP`, `ISC`) may appear. **Do not misclassify those as
302
+ data** — they are valid instructions. The flag is a human's hint, not a
303
+ guarantee: some programs use them with the flag false.
304
+ 2. `anno_get_blocks` to see what is already classified, and focus on the
305
+ Undefined entries. `max_results` is REQUIRED with no default; pass a ceiling
306
+ above the range count you expect and compare the returned count against it.
307
+ 3. Read each candidate region twice, through `anno_read_region` (which names
308
+ both the `store` and the `image`): `view:
309
+ "hexdump"` shows the byte patterns, and **omitting `view`** gives the
310
+ disassembly view — that is the parameter's documented default — which shows
311
+ how the region would decode. The combined byte count is capped at **4096 bytes** per call
312
+ (`ANNO_READ_REGION_MAX_BYTES`), and a request above the cap is refused by
313
+ name rather than truncated — so walk a large binary in consecutive ranges.
314
+ Chunks of **256–512 bytes** are the practical working size for
315
+ classification; a 4096-byte hexdump is more than can be read carefully in
316
+ one pass.
317
+
318
+ ### Applying the classification
319
+
320
+ - **Code**: `anno_disassemble` from the entry-point address to READ, then
321
+ `anno_set_data_type` with `"code"` to RECORD the range you verified.
322
+ `anno_disassemble` performs no write, so nothing is classified until you say
323
+ so. Type only as far as you actually followed the flow — the end of a routine
324
+ at its `RTS`/`RTI`/`JMP`, not "to the end of the region" — because the typed
325
+ `code` ranges are what `anno_get_cross_references` and `anno_search` decode
326
+ instructions out of later.
327
+ - **Data**: batch the `anno_set_data_type` calls through
328
+ `anno_batch_execute`. A real classification pass is dozens of ranges, and one
329
+ batch is one open/commit/close rather than dozens.
330
+ - **A wrong classification is not a disaster and does not need undoing.**
331
+ `anno_set_data_type` is idempotent over a range: set the correct type again
332
+ over the same range and the previous one is gone, and an identical repeat
333
+ succeeds reporting `changed: false`. (Upstream reaches for an undo call here;
334
+ this project does not expose one, and does not need to.)
335
+ - **Read the disclosures on a successful retype; they are not errors and they
336
+ are never dropped.** `contradictedComments` names comments whose recorded
337
+ confidence now contradicts the type you just applied, and
338
+ `reinterpretedSplitTables` names every split table the write FRAGMENTED, with
339
+ the entry-address pairs it read before and the pairs each surviving remainder
340
+ reads now. A split table's entries re-pair as a function of the row's start
341
+ **and** its length, so a fragment decodes to different 16-bit values than the
342
+ ones a human recorded.
343
+ - A split layout REFUSES an odd byte count — the low half and the high half must
344
+ be the same length.
345
+ - Re-read `anno_get_blocks` after each batch to confirm what actually landed.
346
+ `max_results` is REQUIRED on that read and has no default; the true match
347
+ count is returned beside the list.
348
+
349
+ Example of a valid data-only batch, then the code regions separately:
350
+
351
+ ```
352
+ anno_batch_execute:
353
+ - anno_set_data_type start=2304 end=2367 data_type="byte"
354
+ - anno_set_data_type start=2368 end=2431 data_type="petscii"
355
+ then: anno_disassemble address=2049 # read, writes nothing
356
+ then: anno_set_data_type start=2049 end=2303 data_type="code"
357
+ then: anno_get_blocks max_results=500 # refresh
358
+ ```
359
+
360
+ ### The block types
361
+
362
+ | Block type | `data_type` | When |
363
+ |---|---|---|
364
+ | **Code** | `code` | Provably-executed instructions. Read the extent with `anno_disassemble` first — it writes nothing — then record exactly what you verified. |
365
+ | Byte | `byte` | Raw 8-bit data: sprites, bitmaps, charsets, lookup tables, variables, unknowns |
366
+ | Word | `word` | 16-bit little-endian values: 16-bit variables, math constants, SID frequencies |
367
+ | Address | `address` | 16-bit LE pointers — jump tables, vector lists. Creates cross-references |
368
+ | PETSCII text | `petscii` | PETSCII strings: messages, prompts, anything bound for `$FFD2` |
369
+ | Screencode text | `screencode` | Text written straight to screen RAM (`$0400`–`$07E7`) |
370
+ | Lo/Hi address | `lo_hi_address` | Split address table, low bytes first. Even byte count required |
371
+ | Hi/Lo address | `hi_lo_address` | Split address table, high bytes first. Even byte count required |
372
+ | Lo/Hi word | `lo_hi_word` | Split word table, low half first — e.g. a SID frequency table |
373
+ | Hi/Lo word | `hi_lo_word` | Split word table, high half first |
374
+ | External file | `external_file` | Large blobs to export as-is: SID tunes, bitmaps, charsets |
375
+ | Undefined | `undefined` | Reset to unknown. The honest answer for a region you cannot place |
376
+
377
+ ### Recognising each kind
378
+
379
+ **Byte data** — regular patterns that form no valid instruction sequence;
380
+ addressed by `LDA addr,X` / `LDA addr,Y` table lookups; sprite data in 63-byte
381
+ (padded to 64) units, usually grouped; bitmap data in 8-byte character cells;
382
+ colour data confined to `$00`–`$0F`; or random-looking bytes between two code
383
+ blocks whose disassembly is nonsense.
384
+
385
+ **Word data** — byte pairs forming meaningful 16-bit values (screen addresses,
386
+ timer values); loaded low-then-high by adjacent `LDA addr` / `LDA addr+1`.
387
+
388
+ **Address tables** — byte pairs that read as little-endian addresses landing
389
+ *inside* the binary; reached by `JMP ($addr)` or indexed indirect reads. Jump
390
+ tables, dispatch tables and vector lists all live here.
391
+
392
+ **Split lo/hi (or hi/lo) tables** — two equal halves, one of plausible low
393
+ bytes and one of plausible high bytes, referenced separately:
394
+ `LDA lo,X / STA ptr / LDA hi,X / STA ptr+1 / JMP (ptr)`. Recombine the halves
395
+ and check the addresses are real. Lo/Hi (low half first) is the commoner form
396
+ on the 6502. **The total byte count must be even and the halves equal** — an
397
+ odd count means the boundary is in the wrong place.
398
+
399
+ **PETSCII text** — bytes in `$20`–`$7E` (unshifted) or `$C0`–`$DF` (shifted),
400
+ often recognisably English since PETSCII shares `$20`–`$5F` with ASCII;
401
+ terminated by `$00`, `$0D`, or a high-bit sentinel; reached by `$FFD2` (CHROUT)
402
+ or `$AB1E` (BASIC STROUT). `GAME OVER`, `PRESS FIRE`, menus, credits.
403
+
404
+ **Screencode text** — bytes in `$00`–`$3F` where `$00` is `@` and `$01` is `A`;
405
+ copied directly to `$0400`–`$07E7`. `LDA data,X / STA $0400,X` is the
406
+ give-away. A full screen dump is exactly 1000 bytes.
407
+
408
+ **External file** — a large contiguous non-code block matching a known format:
409
+ a `PSID`/`RSID` header, a 2048-byte charset (256 chars × 8 bytes), sprite data
410
+ in multiples of 64, or a bitmap. Export it rather than annotate it.
411
+
412
+ **PETSCII is not screencode.** If it is copied to `$0400`, it is screencode; if
413
+ it is passed to CHROUT, it is PETSCII. Getting this backwards produces text
414
+ that renders as garbage in exactly one of the two places.
415
+
416
+ ### The adjacent-table limitation, and how it was closed
417
+
418
+ **Dated limitation, recorded 2026-08-24; CLOSED 2026-08-29 when the store
419
+ changed underneath it.** The old store auto-merged two adjacent regions of the
420
+ *same* type into one block, so two byte tables side by side — or the two halves
421
+ of a split table sitting next to each other — lost their boundary on read, and
422
+ the answer upstream reached for was a `toggle_splitter` call this project never
423
+ exposed.
424
+
425
+ **The annotation store does not do that.** It never joins two rows of its own
426
+ accord: adjacent same-type ranges stay two rows, and `anno_get_blocks` reports
427
+ them as two. A range you name that *spans* several existing rows still collapses
428
+ to the one row you asked for — that is you asking, not the store deciding — and
429
+ partial overlaps split and preserve the addresses outside your range rather than
430
+ swallowing them.
431
+
432
+ So the working rules are now the ordinary ones:
433
+
434
+ - **Do** rely on the block listing to tell two adjacent same-type tables apart.
435
+ It can, provided you typed them as two calls rather than one spanning call.
436
+ - **Do** still record the boundary in the annotations as well — an
437
+ `anno_set_label_name` at the start of the second table and a line comment on
438
+ both naming the extent you determined. A name and an evidence line survive a
439
+ later retype; a row boundary does not.
440
+ - **Do not** carry the old over-merge caveat into a report taken from this
441
+ store. It was true of the retired one and is not true here.
442
+
443
+ ### Labelling, and the report
444
+
445
+ Name what you classified — `anno_set_label_name` on entry points, tables and
446
+ strings — and comment it with `anno_set_comment` (`"line"` above,
447
+ `"side"` beside). For the conventions to name things *by*, and for the
448
+ comment-block format to use on a subroutine, follow the absorbed routine
449
+ procedure in `src/skills/c64-program-recon/SKILL.md`.
450
+
451
+ Then report, and mean it:
452
+
453
+ - Total blocks by type.
454
+ - Notable findings — "three PETSCII strings", "a lo/hi jump table at `$1200`".
455
+ - **Every region still uncertain or still Undefined, by address.** This is the
456
+ part that makes the pass reusable. A classification report with no uncertain
457
+ regions on a real game is almost always a report that stopped looking.
458
+ - The store revision, read with `anno_save_project`. That verb performs **no
459
+ write** — every classification call above already committed and fsynced its
460
+ own — so quoting the revision is how the report is pinned to an exact store
461
+ state rather than to "after the pass".
462
+
463
+ ### What goes wrong
464
+
465
+ | Symptom | What it actually is |
466
+ |---|---|
467
+ | A region disassembles beautifully but has no incoming reference | Data. Decodability is not evidence; leave it Undefined. |
468
+ | Disassembly full of impossible branches or `BRK` (`$00`) floods | Data misread as code. |
469
+ | Odd-looking instructions, but real `JSR`/`JMP` cross-references land here | Probably code using undocumented opcodes. Check the `may_contain_undocumented_opcodes` hint. |
470
+ | A split table's addresses recombine to nonsense | The half boundary is misplaced, or the table is hi/lo rather than lo/hi. |
471
+ | Two tables you classified separately show up as one block | The adjacent-table limitation above. Not your error. |
472
+ | Text renders as garbage on screen but fine through CHROUT | It is PETSCII, typed as screencode — or the reverse. |
473
+
474
+ ## What a symbol in the store actually represents
475
+
476
+ `lookup` at the top of this page answers what a **published** address means —
477
+ a hardware register, a KERNAL entry point, an OS variable. That answer comes
478
+ from four tables and holds for every program.
479
+
480
+ This section is the other half: **what a program's *own* address represents.**
481
+ No table can tell you, because the meaning was decided by the program's code.
482
+ When `lookup` returns a region-only answer — the dominant case for a game's own
483
+ code and variables, as noted above — this is the procedure that gets you a
484
+ name.
485
+
486
+ ### 1. Target and context
487
+
488
+ - **Always start from an explicit address**, `$XXXX` or its decimal
489
+ equivalent. There is no editor cursor in this project's route, and upstream's
490
+ own text forbids relying on one anyway. `anno_get_address_details` composes
491
+ the symbol, comments, block type and cross-references for one explicit
492
+ address in a single call.
493
+ - `anno_get_binary_info` for `system`, `filename`, `description` and
494
+ `may_contain_undocumented_opcodes`. `filename` and `description` are how a
495
+ symbol gets a *domain* name — `lap_counter` in a racing game, `lives` in a
496
+ platformer — instead of a generic one. With undocumented opcodes in play,
497
+ remember that `LAX`, `SAX` and `DCP` have real read/write side effects that
498
+ belong in the data-flow picture.
499
+
500
+ ### 2. Gather the usage
501
+
502
+ `anno_get_cross_references` on the address — naming the `store`, the `image` and
503
+ a REQUIRED `max_results` — returns everywhere it is touched. Read the
504
+ instruction at each site, because the instruction is the evidence:
505
+
506
+ - **Writes**: `STA`, `STX`, `STY`
507
+ - **Reads**: `LDA`, `LDX`, `LDY`, `BIT`, `CMP`, `CPX`, `CPY`, `ADC`, `SBC`
508
+ - **Read-modify-write**: `INC`, `DEC`, `ASL`, `LSR`, `ROL`, `ROR`
509
+
510
+ **Zero cross-references is a result, not a dead end.** Three explanations, in
511
+ order of likelihood:
512
+
513
+ 1. It is reached **indirectly**. Check whether it is in the zero page
514
+ (`$00`–`$FF`) and whether nearby code uses `($addr),Y` or `($addr,X)`. An
515
+ indirect pointer's *target* has no direct reference by construction.
516
+ 2. It is a **well-known system address** the disassembler does not cross-
517
+ reference. Run `lookup` on it — that is exactly the case the four tables
518
+ above cover.
519
+ 3. It is genuinely **dead**: unused variable, or code no longer reached. Say so
520
+ in the report rather than inventing a purpose.
521
+
522
+ ### 3. Place it
523
+
524
+ **A hardware register?** `lookup` it. If one of the four tables names it, take
525
+ the published name and the per-bit breakdown with it — that reading rests on
526
+ hardware and holds for any program.
527
+
528
+ **Inside a well-known global block?** Screen RAM (`$0400`–`$07E7`), colour RAM
529
+ (`$D800`–`$DBFF`), a sprite pointer at VM+`$03F8`. **Do not skip these as
530
+ "obvious".** Name them systematically from the base plus the offset —
531
+ `SCREEN_ROW03_COL12` — so contiguous structures read as structures instead of a
532
+ field of auto-generated offsets.
533
+
534
+ **An external ROM or system routine?** An `e_` prefix, or an address in KERNAL
535
+ space (`$E000`–`$FFFF`), or a standard shadow vector. `lookup` gives the
536
+ routine's published name; rename to the conventional form — `$FFD2` becomes
537
+ `KERNAL_CHROUT`, `$EA31` becomes `SYSTEM_IRQ_HANDLER`.
538
+
539
+ **A 16-bit pointer?** Below `$0100`, and used with indirect-indexed `($xx),Y`
540
+ or indexed-indirect `($xx,X)`. Rename to `ptr_`/`vec_` form and comment what it
541
+ points *to*, which is the thing the name cannot carry.
542
+
543
+ **A flag or bitmask?** Only ever `$00`/`$01` or `$00`/`$FF`; tested with `BIT`
544
+ or `LDA`/`BEQ`. Name it as a predicate — `is_active`, `has_collided`. When the
545
+ individual bits carry separate meanings, that is an enum: define it with
546
+ `anno_create_project_enum` (`$01 = ACTIVE`, `$02 = COLLIDED`, `$04 =
547
+ VISIBLE`) and apply it with `anno_apply_enum_usage` so every bitmask test
548
+ reads as words rather than hex.
549
+
550
+ **A counter or index?** `INC`/`DEC` inside a loop, compared against a limit
551
+ with `CPX`/`CPY`/`CMP`. `loop_idx`, `sprite_count`, `delay_timer`.
552
+
553
+ **A state variable?** Several distinct values, often feeding a dispatch
554
+ (`ASL` / `TAX` / `JMP (table,X)`). Name it `game_state` or `current_mode` — and
555
+ these are the best enum candidates of all. Look for an existing enum first;
556
+ define one (`0 = INIT`, `1 = TITLE`, `2 = GAMEPLAY`, `3 = GAME_OVER`) with a
557
+ real `description` if none matches, then apply it to every instruction reading
558
+ or writing the variable.
559
+
560
+ ### 4. Name it
561
+
562
+ | Symbol kind | Convention | Example |
563
+ |---|---|---|
564
+ | Zero-page variable | `zp_` prefix | `zp_player_lives`, `zp_delay_timer` |
565
+ | Zero-page pointer | `zp_ptr_` prefix | `zp_ptr_screen`, `zp_ptr_dest` |
566
+ | RAM variable | `snake_case` | `score_hi`, `current_level` |
567
+ | Pointer / vector | `ptr_` / `vec_` prefix | `ptr_screen`, `vec_irq` |
568
+ | Hardware register | `UPPER_SNAKE` | `VIC_SPR0_X`, `SID_FREQ_LO1` |
569
+ | Constant / address | `UPPER_SNAKE` | `SCREEN_RAM`, `CHR_ROM_BASE` |
570
+ | Routine entry point | `snake_case` | `init_screen`, `draw_sprite` |
571
+ | External ROM call | `KERNAL_` / `OS_` | `KERNAL_CHROUT`, `KERNAL_CLRCHN` |
572
+
573
+ > **The zero-page rule overrides all of the above.** An address at or below
574
+ > `$FF` **must** carry the `zp_` prefix — a zero-page pointer becomes
575
+ > `zp_ptr_`, a zero-page flag becomes `zp_is_active`, and a zero-page OS
576
+ > variable becomes `zp_`-prefixed too. The prefix makes the addressing mode
577
+ > visible at every use site, which is the whole point.
578
+
579
+ Apply it with `anno_set_label_name`.
580
+
581
+ ### 5. Document it
582
+
583
+ - `anno_set_comment` `"line"` at the definition: the range it occupies, its
584
+ purpose, its bitfield layout if it has one.
585
+ - `anno_set_comment` `"side"` at the interesting *uses*: why this read, why
586
+ this write. "Reset life counter" beside a `STA` is worth more than any name.
587
+ - Define and apply enums where the values form a set (above).
588
+
589
+ **The pointer-formatting step this project does not have.** Where a pointer is
590
+ initialised by immediate loads of a target's low and high bytes — `LDA #<target
591
+ / STA ptr / LDA #>target / STA ptr+1` — upstream calls `set_immediate_format`
592
+ twice to turn both immediates into a single readable symbol reference. **That
593
+ call is not exposed on this project's surface.** `BUILD-03` ("every branch,
594
+ `JSR`/`JMP` and data reference goes through a symbol, so code can move") is the
595
+ requirement that supplies its criterion, and the per-call disposition is
596
+ recorded in the manifest named in the attribution header above. Until then,
597
+ reconstruct the target by hand and put it in a side comment on both
598
+ instructions — `; low byte of ptr_sprite_table ($C240)` — so the pointer is
599
+ still readable even though the store cannot format it.
600
+
601
+ ### 6. Report
602
+
603
+ - **Address** and its current label.
604
+ - **Classification**: flag, counter, pointer, hardware register, state
605
+ variable, dead.
606
+ - **Evidence**: the specific cross-references or usage patterns that decided
607
+ it. A classification with no evidence line is a guess wearing a name.
608
+ - **Actions taken**: what was renamed, what was commented, which enums were
609
+ defined or applied.
610
+ - **Uncertainty**: if `anno_get_cross_references` returned nothing, say which
611
+ of the three explanations in step 2 you could and could not rule out.
612
+
189
613
  ## Troubleshooting
190
614
 
191
615
  | Symptom | Fix |