@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.
@@ -1,23 +1,37 @@
1
1
  # Memory map generation
2
2
 
3
3
  **The memory map is GENERATED, not hand-authored (D-24).** The store — labels, comments, block
4
- types and scopes written through the `r2000_*` tools described in `../SKILL.md` — is canonical. This
4
+ types and scopes written through the `anno_*` tools described in `../SKILL.md` — is canonical. This
5
5
  file used to be a fill-in-the-rows document; it is now the schema for the one input the generator
6
6
  needs beyond the store itself, plus the confidence vocabulary that store comments carry.
7
7
 
8
8
  Run the generator once findings are in the store:
9
9
 
10
10
  ```bash
11
- npx -y @henols/vice-mcp r2000 render-memmap game.regen2000proj --provenance sidecar.json
12
- node <plugin-root>/.claude/mcp/vice/vice-proxy.ts r2000 render-memmap game.regen2000proj --provenance sidecar.json
11
+ npx -y @henols/vice-mcp anno render-memmap game.annostore --provenance sidecar.json
12
+ node <plugin-root>/src/mcp/vice/vice-proxy.ts anno render-memmap game.annostore --provenance sidecar.json
13
13
  ```
14
14
 
15
15
  Add `--check` to compare the rendered file on disk against a fresh render — it exits non-zero and
16
- prints the first differing line on **either** a hand edit to the rendered file **or** a store change
17
- since it was last rendered. There is no way to "fix" drift by editing the rendered file directly:
18
- the fix is always to re-run the generator (or, if the sidecar itself is stale, correct it and
19
- re-run). The generated file carries a banner naming the store, the sidecar and a content digest
20
- do not strip it.
16
+ prints the first differing line. Drift is reported when, and only when, one of these changed: the
17
+ rendered file itself (a hand edit); a store row (a range, a label, a comment, or a comment's
18
+ confidence grade); the provenance sidecar's bytes; the location of the store or the sidecar
19
+ **relative to the workspace root**; or the renderer. **Relocating the checkout is not drift** the
20
+ same tree at a different absolute path renders the same bytes, because the banner records
21
+ workspace-relative locations rather than absolute ones.
22
+
23
+ There is no way to "fix" drift by editing the rendered file directly: the fix is always to re-run
24
+ the generator (or, if the sidecar itself is stale, correct it and re-run). That remedy is safe to
25
+ follow on any machine — because the banner records workspace-relative locations, re-running in a
26
+ different checkout or in a worktree does not rewrite the file with that machine's absolute paths.
27
+ The generated file carries a banner naming the store, the sidecar and a content digest — do not
28
+ strip it.
29
+
30
+ **One-time drift after upgrading, 2026-08-30.** A memory map rendered *before* 2026-08-30 will
31
+ report `drifted` on its first `--check` after this change, exactly once: the banner's `store:` and
32
+ `sidecar:` lines moved from absolute to workspace-relative spellings. Re-run the generator once and
33
+ commit the new banner, after which the file is stable across machines and worktrees. This is a
34
+ one-time, self-clearing banner correction — not a bug, and not a migration.
21
35
 
22
36
  ## The provenance sidecar
23
37
 
@@ -64,8 +78,8 @@ literal values:
64
78
 
65
79
  ## Confidence vocabulary
66
80
 
67
- Every comment written into the store through `r2000_set_comment` that grades a finding leads with
68
- one of these five bracket tokens (the parser in `r2000-confidence.ts` throws on anything that is
81
+ Every comment written into the store through `anno_set_comment` that grades a finding leads with
82
+ one of these five bracket tokens (the parser in `anno-confidence.ts` throws on anything that is
69
83
  close but not exact — a typo never silently degrades into an ungraded comment):
70
84
 
71
85
  | Grade | Bracket token | Means |
@@ -80,4 +94,4 @@ Do not force an unknown range through a disassembler and record the output as co
80
94
  decode of data is silently wrong and contaminates everything downstream.
81
95
 
82
96
  **Do not promote a row by editing its grade.** Re-verify and restate the evidence with a fresh
83
- `r2000_set_comment` call, so the record of when something stopped being a guess survives.
97
+ `anno_set_comment` call, so the record of when something stopped being a guess survives.
@@ -15,7 +15,7 @@ either proves its own precondition or refuses to emit.
15
15
  `ledger` will not write a verdict the earlier stages did not earn.
16
16
 
17
17
  ```bash
18
- D=.claude/skills/c64-provenance-diff/scripts/diff-images.mjs # from the repo root
18
+ D=src/skills/c64-provenance-diff/scripts/diff-images.mjs # from the repo root
19
19
 
20
20
  node $D anchor-search # 1. prove the per-release offset [WRITES]
21
21
  node $D diff # 2. N-way byte diff at that offset
@@ -25,7 +25,7 @@ node $D ledger # 4. regenerate recovery/PROVENANC
25
25
  node $D diff --json # machine-readable, with per-range reasons
26
26
  node $D diff --gap-tolerance 16 # coalescing width (default shown)
27
27
  node $D anchor-search --reference <id> # pick the reference release
28
- node .claude/skills/c64-provenance-diff/scripts/releases.mjs list # the release ids in play
28
+ node src/skills/c64-provenance-diff/scripts/releases.mjs list # the release ids in play
29
29
  ```
30
30
 
31
31
  Pure Node over committed files — the `.bin` dumps, their `.map.json` manifests, and
@@ -254,4 +254,4 @@ through a GSD command (`/gsd-quick`).
254
254
  | A range's `kind` looks wrong past its start | You resolved `kind` from `start`. Use `splitRangeByManifestKind`; coalescing does not respect kind boundaries. |
255
255
  | A loader range disagrees with `NOTES.md` | `RELEASES.json`'s `loader_ranges` wins — it is earned from disassembly. Prose is how `$08F5` got misclassified. |
256
256
  | Title-screen text shows up as cracktro | You used a bare printable-run scan. The vocabulary scan exists because `$4771-$4779` is the game's own text. |
257
- | `unknown release "x" -- known releases: …` | `node .claude/skills/c64-provenance-diff/scripts/releases.mjs list` for the valid ids. |
257
+ | `unknown release "x" -- known releases: …` | `node src/skills/c64-provenance-diff/scripts/releases.mjs list` for the valid ids. |
@@ -693,6 +693,9 @@ export function renderLedger({ generatedRanges, gapTolerance, prose }) {
693
693
  throw new Error(`renderLedger: refusing to emit -- generated tier stops at ${hex4(expected - 1)}, does not reach $FFFF`);
694
694
  }
695
695
 
696
+ // NOTE (plans 16-01, 16-10): the embedded invocation path below is deliberately the
697
+ // CONSUMER's installed location (`.claude/skills/...`), not this repository's
698
+ // source-tree location (`src/skills/...`) -- pinned by diff-images.test.mjs and skill-consumer-paths.test.ts (16-REVIEW.md CR-01).
696
699
  let generated = `<!-- GENERATED, DO NOT HAND-EDIT. Regenerate with: node .claude/skills/c64-provenance-diff/scripts/diff-images.mjs ledger --gap-tolerance ${gapTolerance} -->\n\n`;
697
700
  generated += `| Start | End | Kind | Verdict | Confidence | Agreeing releases | Evidence / Reason |\n`;
698
701
  generated += `|---|---|---|---|---|---|---|\n`;
@@ -25,17 +25,27 @@ const HERE = dirname(fileURLToPath(import.meta.url));
25
25
  const REPO_ROOT = projectRoot();
26
26
  const RECOVERY_DIR = dataRoot();
27
27
  const DISKS_DIR = disksRoot();
28
+
29
+ const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
30
+
28
31
  // The parameterisation gate must cover EVERY module of the recovery pipeline, not
29
32
  // just the ones sitting next to this file. When the six modules moved out of
30
33
  // `tools/` into the two skills that use them (2026-08-04), a `HERE`-only scan
31
34
  // silently stopped covering `d64-parse.mjs` and `dump-artifacts.mjs` -- a static
32
35
  // guard that keeps passing while checking less is worse than one that fails.
36
+ // 2026-08-22 (plan 16-01): the second entry used to be built project-root-relative,
37
+ // naming the skills tree's pre-relocation auto-discovery location by hand -- which
38
+ // stopped resolving the moment the skills tree moved to its current source-tree
39
+ // location. Rebuilt `HERE`-relative instead -- correct in both this dev checkout
40
+ // and a consumer's install, since a sibling skill's `scripts/` directory is always
41
+ // one level up and back down from this file's own location either way.
33
42
  const SCAN_DIRS = [
34
- HERE, // .claude/skills/c64-provenance-diff/scripts
35
- resolve(REPO_ROOT, ".claude", "skills", "c64-ram-capture", "scripts"),
43
+ HERE, // src/skills/c64-provenance-diff/scripts
44
+ resolve(HERE, "..", "..", "c64-ram-capture", "scripts"),
36
45
  ];
37
-
38
- const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
46
+ for (const dir of SCAN_DIRS) {
47
+ if (!existsSync(dir)) die(`SCAN_DIRS entry does not exist: ${dir} -- a guard that keeps passing while checking less is worse than one that fails`);
48
+ }
39
49
 
40
50
  function sha256File(path) {
41
51
  return createHash("sha256").update(readFileSync(path)).digest("hex");
@@ -0,0 +1,17 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "schema_notes": "Adding a release is one releases[] entry plus one invocation of tools/recover.mjs -- see c64-ram-capture/SKILL.md '## Release registry shape'.",
4
+ "releases": [
5
+ {
6
+ "id": "example-release",
7
+ "canonical": true,
8
+ "disk_image": "disks/example-release.d64",
9
+ "dumps": [
10
+ {
11
+ "label": "example-label",
12
+ "range_manifest": "recovery/example-release/dumps/example-release-example-label.map.json"
13
+ }
14
+ ]
15
+ }
16
+ ]
17
+ }
@@ -14,7 +14,7 @@ failure and it is invisible in a hex dump. Two committed modules do that byte wo
14
14
  and name the offending address when it is wrong.
15
15
 
16
16
  ```bash
17
- S=.claude/skills/c64-ram-capture/scripts # from the repo root
17
+ S=src/skills/c64-ram-capture/scripts # from the repo root
18
18
  P=$S/d64-parse.mjs A=$S/dump-artifacts.mjs
19
19
  C=$S/compare.mjs L=$S/releases.mjs
20
20
 
@@ -33,6 +33,12 @@ node $C floor a.bin b.bin c.bin # drift floor across a capture
33
33
  All three modules read only committed files and the JSON **you** wrote from your
34
34
  own `mcp__plugin_c64-re-tools_vice__*` calls. They contact nothing.
35
35
 
36
+ **Prerequisite: a resolvable project root.** `scripts/project-paths.mjs` uses
37
+ `C64RE_PROJECT_ROOT` when it is set, and otherwise walks up from the toolkit's
38
+ own location for the nearest ancestor directory containing a `.git` entry. A
39
+ scratch project has neither by default, so `git init` it first or set the
40
+ variable — the thrown error names both when this fails.
41
+
36
42
  ## The order
37
43
 
38
44
  | # | Phase | Settles |
@@ -78,6 +84,9 @@ directory chain that leaves the image or loops, reported instead of hanging.
78
84
  3. `mcp__plugin_c64-re-tools_vice__vice_execution_run`.
79
85
  4. `mcp__plugin_c64-re-tools_vice__vice_registers_get` and confirm the program counter has moved.
80
86
 
87
+ The broker sets the drive type at launch for every stock instance, so no drive setup
88
+ is needed before attaching.
89
+
81
90
  If the program counter has not moved, type `LOAD"*",8,1` with
82
91
  `mcp__plugin_c64-re-tools_vice__vice_keyboard_type`, run it, then type `RUN` and run it.
83
92
 
@@ -264,7 +273,7 @@ floor, not a complete set — more captures can only widen it.
264
273
 
265
274
  ## Feeding the memory map's provenance sidecar
266
275
 
267
- `c64-program-recon`'s generated memory map (`vice-mcp r2000 render-memmap`) takes a small provenance
276
+ `c64-program-recon`'s generated memory map (`vice-mcp anno render-memmap`) takes a small provenance
268
277
  sidecar as input, and this skill supplies one of its fields: `scripts/compare.mjs digest`'s `sha256`
269
278
  and `size` become the sidecar's `captureSha256`, proving which image the rendered map describes. The
270
279
  sidecar's other run-scoped keys (`port01`, `dd00`, `vicBank`, `screenRam`, `charsetOrBitmap`, `mode`,
@@ -287,6 +296,28 @@ This one owns the image and its identity. It does not restate what the others ca
287
296
  | Whether the emulator is wedged, and whether it is safe to recycle | `vice-wedge-triage` |
288
297
  | **A verified 64K image, or proving two captures equivalent** | here |
289
298
 
299
+ ## Release registry shape
300
+
301
+ `scripts/releases.mjs` is the only module that reads a release id out of the
302
+ registry — every other module takes the id as an argument. Its shape:
303
+
304
+ | Field | Level | Required | For |
305
+ |---|---|---|---|
306
+ | `schema_version` | top-level | — | The registry format version. |
307
+ | `schema_notes` | top-level | — | Free-text stating the registry's N-readiness claim (`node $L schema-notes`). |
308
+ | `releases` | top-level | yes | The array of release entries below. |
309
+ | `id` | per-release | yes | The `--release` argument every other script takes. |
310
+ | `canonical` | per-release | — | A boolean on one entry, not "the canonical image" — there are N releases. |
311
+ | `disk_image` | per-release | yes | Project-relative path to the release's `.d64`. |
312
+ | `dumps` | per-release | **yes, as an array** | Per-capture records written by `write-set`. **Must be an array, never omitted** — `releases.mjs`'s `list` command reads `r.dumps.length` with no guard (`releases.mjs:98`), so a missing `dumps` throws `TypeError: Cannot read properties of undefined` instead of listing anything. An empty array (`[]`) is fine; an absent key is not. `releases.mjs` itself imposes no per-entry shape; `scripts/watch-loads.mjs` (a different reader) looks up an entry by `label` and reads its `range_manifest`, which the example below follows. |
313
+
314
+ The registry lives at `<project root>/recovery/RELEASES.json` by default —
315
+ override the whole path with `C64RE_REGISTRY`, or just the containing
316
+ directory with `C64RE_DATA_DIR`.
317
+
318
+ `src/skills/c64-ram-capture/RELEASES.json.example` is a copyable starting
319
+ point with every field above populated with placeholder values.
320
+
290
321
  ## References
291
322
 
292
323
  What this skill ships, and the committed modules it leans on. No `references/`
@@ -298,6 +329,7 @@ split: the workflow fits in one file, which is the right call when it does.
298
329
  | `templates/capture-record.template.md` | The per-capture record: identity, machine state read in the same paused window, the void checklist, and the per-pairing comparison table. |
299
330
  | `scripts/d64-parse.mjs` | `.d64` directory, BAM, and `--json` fakery detection. Fixture-tested against both real images by `scripts/d64-parse.test.mjs`. |
300
331
  | `scripts/dump-artifacts.mjs` | `assemble` / `chip-state` / `manifest` / `write-set` — the guarded byte work, and the source of every `assembleImage:` message in the table below. |
332
+ | `RELEASES.json.example` | A copyable release-registry shape — see `## Release registry shape` above. |
301
333
 
302
334
  Findings that make RE faster go in `.planning/RE-FINDINGS.md` **at the moment you
303
335
  find them**, graded with `Evidence:` and `Confidence:`. Promote by re-logging with
@@ -320,3 +352,4 @@ through a GSD command (`/gsd-quick`).
320
352
  | `--limit 0` printed nothing | Fixed 2026-08-04 — it now means unlimited. Re-pull the script if you see the old behaviour. |
321
353
  | An epoch-drift error appeared mid-capture | The machine restarted under you. Void the run; do not salvage the artifacts. The next call succeeding does not undo it. |
322
354
  | The emulator looks dead | `vice-wedge-triage` — and enumerate your own armed checkpoints before concluding anything. |
355
+ | `` `project-paths: could not locate the project root -- no `.git` found above ...` `` | No `.git` ancestor and no `C64RE_PROJECT_ROOT`. `git init` the project, or set the variable to its root. |
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // 1. The project root is found by walking UP for a `.git` marker. Counting
8
8
  // hops from `import.meta.url` breaks the moment the toolkit is installed
9
- // somewhere other than `.claude/skills/<skill>/scripts/`, and it breaks
9
+ // somewhere other than `src/skills/<skill>/scripts/`, and it breaks
10
10
  // silently -- paths resolve to a plausible wrong place rather than erroring.
11
11
  // 2. Every data location is overridable by environment variable, so a project
12
12
  // that does not use this repo's `recovery/` + `disks/` layout can point the
@@ -445,6 +445,12 @@ function renderReleaseSection(id, log) {
445
445
  * string templating over already-fetched data -- nothing here reads a file
446
446
  * or contacts anything; the CLI `render` verb below is what reads the
447
447
  * hit-log files and writes the result.
448
+ *
449
+ * The two script paths named in the output below are deliberately the
450
+ * consumer's installed location, not this repository's source tree -- this
451
+ * string is written into a `recovery/LOADING.md` a consumer keeps. Plan
452
+ * 16-04's blanket sweep rewrote them once (16-REVIEW.md CR-01 class); pinned
453
+ * by the test below plus the class-level registry in skill-consumer-paths.test.ts.
448
454
  */
449
455
  export function renderLoading(entries) {
450
456
  let out = "# `recovery/LOADING.md` -- the on-demand-load detection record\n\n";
@@ -0,0 +1,273 @@
1
+ ---
2
+ name: routine-queue-walker
3
+ description: Drive an existing C64 annotation store's backlog of undocumented routines and auto-named symbols to closure — build the candidate queue from labels and comments, work it one entry at a time against explicit addresses, rebuild it after every pass, and report every leftover. Use when asked to annotate every remaining routine in a project, document all undocumented subroutines left in an annotation project, rename the leftover auto-generated labels, clear a backlog of unnamed symbols, drive an annotation pass to completion, or list what is still unannotated after a pass.
4
+ ---
5
+
6
+ # Walking the routine and symbol queue to closure
7
+
8
+ **Do not start annotating whatever is in front of you.** The expensive failure
9
+ here is not slow work — it is a pass that *looks* finished while a hundred
10
+ `p_XXXX` labels are still nameless and nobody wrote down which ones. Build the
11
+ queue first, from data, then walk it to the end.
12
+
13
+ This playbook assumes block classification has already happened and an
14
+ annotation store already exists. If you do not yet know what the program is —
15
+ where it starts, which vector is live, which regions are code — stop and run
16
+ `c64-program-recon` first. That skill answers *what is this program*; this one
17
+ answers *what is still undocumented in it, and how do I finish*.
18
+
19
+ ## The one rule that makes this different from upstream's version
20
+
21
+ **Work the queue one entry at a time.** Not as a throughput compromise — as an
22
+ accurate model of the store underneath. One `.annostore` is one writer: every
23
+ mutating call opens it, commits and closes inside the call, and every one of
24
+ them accepts an optional `base_revision` compare-and-swap that REFUSES a write
25
+ computed against a revision the store has already moved past. Fanning several
26
+ writers at one store therefore buys **zero** extra throughput and costs
27
+ correctness: the losers come back as named stale-revision refusals you then
28
+ have to re-derive and replay. Reading fan-out — several agents *thinking* over
29
+ already-fetched answers — is fine, and its value is reasoning bandwidth, never
30
+ I/O.
31
+
32
+ **Every call names its own store.** There is no ambient "current store" on this
33
+ surface: pass `store` (a `.annostore` path) on every call, and pass `image` as
34
+ well on every call that derives its answer from the program's bytes rather than
35
+ from the annotations — `anno_get_binary_info`, `anno_read_region`,
36
+ `anno_disassemble`, `anno_get_cross_references`, `anno_search` and
37
+ `anno_get_address_details`. The store holds annotations and never bytes, so an
38
+ omitted image would read as a plausible success against whatever was recorded
39
+ last.
40
+
41
+ ## Phase 0 — context, and the packed-binary gate
42
+
43
+ 1. Call `anno_get_binary_info`. Keep `origin`, `size`, `system`, `filename`,
44
+ `description` and `may_contain_undocumented_opcodes` — every later step
45
+ quotes them.
46
+ 2. Read the returned `entropy` against the threshold of **7.5** carried in that
47
+ tool's own description. At or above it, the bytes are very likely packed.
48
+ 3. If the binary looks packed, **stop and say so.** Do not annotate a packed
49
+ image: you would be documenting a decruncher, and every label you write is
50
+ thrown away the moment the real image is recovered. This project's route to
51
+ an unpacked image is `c64-ram-capture` — run the program in the emulator and
52
+ capture RAM at a checkpoint past the decrunch — plus the packer-identity
53
+ finding in `c64-program-recon`, which names the packer when an oracle can.
54
+ Come back with the captured image and start again at Phase 0.
55
+
56
+ Upstream's in-place `unpack_binary` step is deliberately not carried: it is
57
+ destructive (it clears the comments, labels and blocks already in the store)
58
+ and this project has a non-destructive route to the same answer.
59
+
60
+ ## Phase 1 — make sure blocks are classified
61
+
62
+ Region classification is a prerequisite for everything below: a routine
63
+ candidate is only meaningful once the bytes around it are known to be code.
64
+
65
+ 1. Follow `src/skills/c64-memory-mapping/SKILL.md` for the classification pass
66
+ and for what each region type means.
67
+ 2. Do that pass yourself, in one sitting. It is a single long walk over the
68
+ whole binary, not a queue of independent items.
69
+
70
+ ## Phase 2 — the routine queue
71
+
72
+ ### 2.1 Build the candidate list
73
+
74
+ A routine counts as **already documented** when its entry address carries a
75
+ line comment. That is the only test; do not guess from the label name.
76
+
77
+ 1. Call `anno_get_symbols` for all labels — user, system and external, with
78
+ an explicit `max_results` above the program's label count (`max_results` is
79
+ REQUIRED on this surface and has no default, so a truncated answer is always
80
+ a ceiling you chose). Keep the answer; Phase 3 reuses it.
81
+ 2. Call `anno_get_comments`, again with an explicit `max_results`. Keep that
82
+ too — the true match count rides beside the list, so truncation is a fact
83
+ you are told rather than one you infer.
84
+ 3. Keep a label as a routine candidate when any of these holds:
85
+ - its name starts with `s_` (an auto-generated subroutine label);
86
+ - it sits in a code region and is the target of at least one `JSR`
87
+ cross-reference (`anno_get_cross_references`);
88
+ - it is a `p_XXXX` label sitting **inside a code region**. These come from
89
+ split lo/hi immediate loads and from address tables, and they are almost
90
+ always chained raster-IRQ handlers, hardware- or shadow-vector handlers,
91
+ or jump-table and callback targets. Treat every one of them as a
92
+ candidate rather than pattern-matching specific vector addresses;
93
+ - it is the label named exactly `start`.
94
+ 4. Drop every candidate that already carries a line comment.
95
+ 5. What is left is the routine queue.
96
+ 6. **Order it with `start` first** when `start` is in it. The entry point sets
97
+ the context every other routine is read against.
98
+
99
+ ### 2.2 Walk it
100
+
101
+ - **Always work from an explicit address** — `$XXXX`, or the decimal
102
+ equivalent. Never from "wherever we are"; there is no editor cursor in this
103
+ project's route, and upstream's own text forbids relying on one anyway. Read
104
+ the routine's bytes with `anno_read_region` over the explicit range.
105
+ - Take **one** entry at a time, to completion, before starting the next.
106
+ - For each entry, do the full job: rename the label (`anno_set_label_name`),
107
+ add a header line comment describing what the routine does and what it
108
+ leaves in the registers and memory, add side comments on the instructions
109
+ that carry the meaning (`anno_set_comment`), and record anything you are
110
+ unsure about rather than smoothing it over.
111
+ - Record per entry: the address, the old label, the new label, a one-line
112
+ summary, and any uncertainty. That record is the report in Phase 4.
113
+
114
+ ### 2.3 Refresh point
115
+
116
+ When the queue is empty, read the store's revision with `anno_save_project`.
117
+ **It performs no write, and it exists to say so:** every mutating verb on this
118
+ surface has already committed and fsynced its own write by the time it
119
+ returned, so there is nothing for an explicit save to flush. Record the
120
+ revision — it is the checkpoint this pass is measured from, and the
121
+ `base_revision` a later compare-and-swap write would quote. Everything after
122
+ this point re-reads the store, because Phase 2 has just changed the label names
123
+ Phase 3 filters on.
124
+
125
+ ## Phase 3 — the symbol queue
126
+
127
+ ### 3.1 Build the candidate list
128
+
129
+ A symbol counts as **already documented** when it has a name a human chose, or
130
+ when it is a well-known system address (hardware register, KERNAL entry point,
131
+ OS variable).
132
+
133
+ 1. Call `anno_get_symbols` **again** — Phase 2 renamed things.
134
+ 2. Keep every label whose name still matches an auto-generated pattern:
135
+ `zpp_XX`, `zpf_XX`, `zpa_XX` in the zero page; `p_XXXX`, `f_XXXX`, `a_XXXX`
136
+ and `e_XXXX` outside it.
137
+ 3. Exclude: `s_XXXX` (Phase 2 handled those), `b_XXXX` (branch targets, not
138
+ data symbols), and any `p_XXXX` inside a code region (also Phase 2's).
139
+ 4. What is left is the symbol queue.
140
+
141
+ ### 3.2 Walk it
142
+
143
+ Same discipline as Phase 2: explicit address, one entry at a time, to
144
+ completion. For each symbol, use `anno_get_cross_references` to find who
145
+ touches it — a symbol's meaning is what its callers do with it — then rename it
146
+ and comment it. Classify it plainly: flag, counter, pointer, state variable,
147
+ buffer, table.
148
+
149
+ **No premature halting.** The symbol queue is routinely far larger than the
150
+ routine queue — fifty, a hundred entries is normal. Do not truncate it, do not
151
+ skip "secondary" symbols, and do not stop early because it is long. Feeding the
152
+ whole queue through is the job. Stopping early and labelling the remainder
153
+ "skipped for review" is a failed pass, not a completed one — unless the
154
+ remainder is reported explicitly, in full, under Phase 4's leftovers table.
155
+
156
+ For naming conventions and for what any given hardware or KERNAL address
157
+ means, follow `src/skills/c64-memory-mapping/SKILL.md` rather than guessing.
158
+
159
+ ### 3.3 Refresh point
160
+
161
+ Read the revision again with `anno_save_project` and record it. No write is
162
+ performed; the writes already landed.
163
+
164
+ ## Phase 4 — save and report
165
+
166
+ 1. Read the revision one last time with `anno_save_project` and quote it in
167
+ the report, so the pass is attributable to an exact store state.
168
+ 2. Write the report. Four sections, all of them required:
169
+
170
+ **Regions.** How many regions are classified, grouped by type, plus anything
171
+ notable — text at a fixed address, a jump table, a sprite block.
172
+
173
+ **Routines.**
174
+
175
+ | Address | Old label | New label | What it does |
176
+ | ------- | --------- | --------- | ------------ |
177
+ | `$C000` | `s_C000` | `init_screen` | Clears screen RAM, sets the border colour |
178
+
179
+ **Symbols.**
180
+
181
+ | Address | Old label | New label | Classification |
182
+ | ------- | --------- | --------- | -------------- |
183
+ | `$02` | `zpp_02` | `ptr_screen` | Zero-page indirect pointer |
184
+
185
+ **Leftovers — uncertain, skipped, or still unannotated.** This section is not
186
+ optional and it is not allowed to be empty when the queues were not emptied.
187
+ List every routine and every symbol that was left undone, with its address and
188
+ the reason. Never report "no uncertain areas" or "nothing left" while a single
189
+ `f_XXXX` or `a_XXXX` label is still auto-named or a queued routine is still
190
+ uncommented — those must be listed by name for a human to pick up.
191
+
192
+ ## Phase 5 — measure the pass instead of asserting it finished
193
+
194
+ A report that says "all routines documented" is a claim about the report, not
195
+ about the program. Measure it. From the repository root:
196
+
197
+ ```
198
+ node src/mcp/vice/vice-proxy.ts anno coverage game.prg --store game.annostore
199
+ ```
200
+
201
+ **`--store` is REQUIRED and is a second path, not a spelling of the first.**
202
+ `<program>` supplies the payload bytes and the load origin; `--store` names the
203
+ annotation store holding the labels, comments and typed ranges. The store holds
204
+ annotations and never bytes, so the verb refuses to guess either path from the
205
+ other.
206
+
207
+ **Dated note, 2026-08-30 — the positional is a program IMAGE, and the command
208
+ above is now correct against the shipped verb.** `<program>` is a `.prg` (a
209
+ 2-byte little-endian load address followed by the payload) or an
210
+ **exactly-65536-byte** flat capture with a `.raw` or `.bin` extension — the two
211
+ forms every other verb and tool on this surface already reads, and the two
212
+ `c64-ram-capture` produces. The intermediate project-file format this verb
213
+ previously required has **no producer left in this repo**; it is still accepted
214
+ so an existing project file keeps working, but nothing here writes one, so do
215
+ not go looking for a step that produces it.
216
+
217
+ Dispatch is by **file extension first, length second**. A short flat capture is
218
+ therefore refused by name — `a flat 64K capture must be exactly 65536 bytes` —
219
+ rather than misread as a `.prg` whose first two payload bytes become the load
220
+ address. If you get that refusal, the capture is truncated; re-capture it, do
221
+ not rename it.
222
+
223
+ Add `--out coverage.json` to keep the machine-readable report, `--force` to
224
+ overwrite one, and `--sample N` to widen the reproducibility sample. The verb
225
+ reads the same store every call in this playbook writes to, and exits **0 even
226
+ when the numbers are bad** — a low measurement is a result, not a failure.
227
+ Non-zero means a caller error, an image it could not read, or a store it could
228
+ not read at all.
229
+
230
+ **Run it three times:** once before Phase 2, so the pass has a starting point
231
+ to be compared against; once at Phase 2.3's refresh point; and once at the end,
232
+ after Phase 4's final save. The last run is what goes in the report.
233
+
234
+ **Read the three numbers against each other. Never quote one of them alone.**
235
+ There is deliberately no single "percent documented" figure, because one
236
+ combined number lets a weak measure hide behind a strong one and makes the
237
+ claim unfalsifiable:
238
+
239
+ - **A high user fraction beside a large unreached count means the wrong things
240
+ were named.** Every label got a human name, but most of the image was never
241
+ reached by the descent walk from any seed — the queue was worked over the
242
+ easily-visible part of the program and the rest was never entered. Go back to
243
+ Phase 0 and find more entry points (chained IRQ vectors, dispatch tables),
244
+ not more labels.
245
+ - **A large divergence means the store and the bytes disagree about what is
246
+ code.** Bytes the census reached as instructions that the store does not call
247
+ `Code` are places where Phase 1's classification is behind the actual control
248
+ flow. The reverse direction (the store calls it `Code`, the census never
249
+ reached it) is ordinary on an image with unreachable filler — read it, do not
250
+ chase it.
251
+ - **A low distinct-comment ratio means the comments are filler.** Fifty
252
+ addresses carrying the same sentence counts once, not fifty times. That is
253
+ the number that catches a pass which renamed everything and explained
254
+ nothing.
255
+
256
+ Anything the per-measure findings list names belongs in Phase 4's leftovers
257
+ table, by address. A finding is a named defect in one named measure — it is
258
+ never a rating, and there is no number to report as "the coverage".
259
+
260
+ ## When something fails
261
+
262
+ - A failed call is not a reason to drop a queue entry. Log the address, the
263
+ call and the error, put the entry back on the queue, and carry on with the
264
+ next one. Report every one of those in the leftovers table.
265
+ - A refused write is not silent and must not be treated as one. A
266
+ stale-revision refusal (a `base_revision` that the store has moved past), an
267
+ illegal label name, or a scope that overlaps an existing one all come back
268
+ REFUSED and named, with nothing written. Re-read, re-derive and replay that
269
+ one entry; never widen the range or drop the `base_revision` to make the
270
+ refusal go away.
271
+ - Never invent an answer to make a queue entry go away. An honest "this looks
272
+ like a table, callers unclear" in the leftovers table is worth more than a
273
+ confident wrong label that the next reader has to un-learn.
@@ -167,14 +167,14 @@ connection but never answers is contention, not a hung emulator — and the brok
167
167
  knows whether it holds a lease on that port, which is the thing a human or agent can actually go
168
168
  check instead of guessing. Named causes, so a reader knows where to look: a hand-run `nc` session
169
169
  left open against the port, a second Claude Code session driving the same instance, VICE's own
170
- `-remotemonitor`, and any other 6502 debugger that dials in including regenerator2000's own
171
- `--vice` flag. **This plugin's own regenerator2000 route can never be one of them:** the launch
172
- path refuses `--vice` by construction (no caller-supplied argv passthrough exists to inject it in
173
- the first place) *and* by a scan that throws if the flag is ever present in the final argv not by
174
- documentation alone (`R2000-01`, plan 10-01) so a user chasing a silent emulator can rule this
175
- project's own r2000 integration out immediately, rather than suspecting it. The standing advice
176
- does not change: contention is **never** a reason to recycle — the instance is healthy, merely
177
- claimed elsewhere.
170
+ `-remotemonitor`, and any other 6502 debugger that dials in. **This plugin's own annotation route
171
+ can never be one of them:** the `anno_*` tool surface and the `anno` CLI are pure store-and-image
172
+ readers they open a SQLite annotation store and decode bytes out of a file on disk, and there is
173
+ no emulator connection anywhere on that path to contend for the port. That is a structural
174
+ property of what those calls do, not a documented promise, so a user chasing a silent emulator can
175
+ rule this project's own annotation tooling out immediately rather than suspecting it. The standing
176
+ advice does not change: contention is **never** a reason to recycle — the instance is healthy,
177
+ merely claimed elsewhere.
178
178
 
179
179
  ## The manual fallback, when `vice_diagnose` cannot answer
180
180