@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.
- package/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +26 -0
- package/bin/cli.mjs +18 -7
- package/package.json +6 -4
- package/skills/acme-build/SKILL.md +51 -17
- package/skills/c64-memory-mapping/SKILL.md +409 -20
- package/skills/c64-program-recon/SKILL.md +437 -86
- package/skills/c64-program-recon/references/reconstruction.md +10 -4
- package/skills/c64-program-recon/references/tool-selection.md +2 -2
- package/skills/c64-program-recon/scripts/packer-finding.mjs +631 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +25 -11
- package/skills/c64-provenance-diff/SKILL.md +3 -3
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +3 -0
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +14 -4
- package/skills/c64-ram-capture/RELEASES.json.example +17 -0
- package/skills/c64-ram-capture/SKILL.md +35 -2
- package/skills/c64-ram-capture/scripts/project-paths.mjs +1 -1
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +6 -0
- package/skills/routine-queue-walker/SKILL.md +273 -0
- package/skills/vice-wedge-triage/SKILL.md +8 -8
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +0 -665
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +0 -243
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +0 -133
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +0 -75
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +0 -339
|
@@ -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 `
|
|
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
|
|
12
|
-
node <plugin-root
|
|
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
|
|
17
|
-
|
|
18
|
-
the
|
|
19
|
-
|
|
20
|
-
|
|
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 `
|
|
68
|
-
one of these five bracket tokens (the parser in `
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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
|
|
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, //
|
|
35
|
-
resolve(
|
|
43
|
+
HERE, // src/skills/c64-provenance-diff/scripts
|
|
44
|
+
resolve(HERE, "..", "..", "c64-ram-capture", "scripts"),
|
|
36
45
|
];
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
project's own
|
|
176
|
-
does not change: contention is **never** a reason to recycle — the instance is healthy,
|
|
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
|
|