@henols/c64-re-tools 0.2.1 → 0.2.3

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