@henols/c64-re-tools 0.2.2 → 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 (36) hide show
  1. package/package.json +2 -2
  2. package/skills/acme-build/SKILL.md +39 -23
  3. package/skills/acme-build/scripts/acme.mjs +159 -64
  4. package/skills/acme-build/template.a +1 -1
  5. package/skills/c64-disk-access/SKILL.md +156 -0
  6. package/skills/c64-disk-access/scripts/c1541.mjs +569 -0
  7. package/skills/c64-memory-mapping/SKILL.md +30 -23
  8. package/skills/c64-memory-mapping/scripts/driver.mjs +1 -1
  9. package/skills/c64-petcat/SKILL.md +87 -0
  10. package/skills/c64-petcat/scripts/petcat.mjs +221 -0
  11. package/skills/c64-program-recon/SKILL.md +93 -39
  12. package/skills/c64-program-recon/references/control-flow.md +12 -15
  13. package/skills/c64-program-recon/references/graphics.md +1 -1
  14. package/skills/c64-program-recon/references/observation-hazards.md +18 -16
  15. package/skills/c64-program-recon/references/reconstruction.md +1 -2
  16. package/skills/c64-program-recon/references/sound-and-input.md +6 -8
  17. package/skills/c64-program-recon/references/tool-selection.md +36 -17
  18. package/skills/c64-program-recon/scripts/packer-finding.mjs +165 -87
  19. package/skills/c64-program-recon/templates/memory-map.template.md +2 -2
  20. package/skills/c64-provenance-diff/SKILL.md +40 -5
  21. package/skills/c64-provenance-diff/scripts/diff-images.mjs +8 -8
  22. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +7 -5
  23. package/skills/c64-ram-capture/SKILL.md +112 -44
  24. package/skills/c64-ram-capture/scripts/compare.mjs +2 -2
  25. package/skills/c64-ram-capture/scripts/derive-transients.mjs +575 -0
  26. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +3 -3
  27. package/skills/c64-ram-capture/scripts/mcp-module.mjs +174 -0
  28. package/skills/c64-ram-capture/scripts/releases.mjs +1 -1
  29. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +147 -0
  30. package/skills/c64-ram-capture/scripts/watch-loads.mjs +15 -15
  31. package/skills/c64-ram-capture/templates/capture-record.template.md +44 -4
  32. package/skills/c64-ram-capture/transients/README.md +136 -0
  33. package/skills/routine-queue-walker/SKILL.md +114 -22
  34. package/skills/routine-queue-walker/scripts/completeness-report.mjs +463 -0
  35. package/skills/vice-wedge-triage/SKILL.md +96 -89
  36. package/skills/c64-ram-capture/scripts/d64-parse.mjs +0 -243
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // packer-finding.mjs -- the ONE place this project answers "which packer was
3
3
  // used on this binary", as a project-owned recon finding with an ordered
4
- // oracle chain and a hard, reasoned unknown (SURF-03).
4
+ // oracle chain and a hard, reasoned unknown.
5
5
  //
6
6
  // ---------------------------------------------------------------------------
7
7
  // WHY THIS FILE EXISTS
@@ -96,10 +96,102 @@
96
96
  // a measurement. Installing the identifier and running it against a genuinely
97
97
  // packed fixture is the experiment that would settle it; until then the
98
98
  // oracle-route test SKIPS with a visible reason and never reads as a pass.
99
- import { spawnSync } from "node:child_process";
100
- import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
101
- import { tmpdir } from "node:os";
102
- import { join } from "node:path";
99
+ import { execFileSync } from "node:child_process";
100
+ import { existsSync, readFileSync } from "node:fs";
101
+ import { basename, dirname, resolve } from "node:path";
102
+
103
+ import { resolveMcpModule, refusalMessage } from "../../c64-ram-capture/scripts/mcp-module.mjs";
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // The oracle's own child-process spawn.
107
+ // ---------------------------------------------------------------------------
108
+ // Both `probeUnp64()` and `runUnp64()` used to spawn `unp64` directly
109
+ // (`spawnSync`, and a `mkdtempSync()`-created scratch directory for the
110
+ // unpacked output). Both spawn sites are now behind the host-tool execution
111
+ // seam (`src/mcp/vice/host-tool.mts`'s `oracle.probe`/`oracle.run` allowlist
112
+ // entries) -- the project owner's rule of 2026-08-28 is that this script
113
+ // runs container-side, the oracle binary lives host-side, and there is no
114
+ // container PATH to find it on. Everything about the BINARY (locating it
115
+ // from `ORACLE_ENV_VARS`, the version-banner probe, the scratch output
116
+ // location, the argument array, the runtime bound, the standard-output cap)
117
+ // now lives in host-tool.mts; this file keeps everything about the FINDING
118
+ // (the name parser, the accepted character set, the caps below, the
119
+ // packedness threshold, and both functions' never-throw contract).
120
+
121
+ // SYNCHRONOUS ON PURPOSE: `execFileSync`, not the async `spawn` acme.mjs's
122
+ // own migration uses. `probeUnp64()`/`runUnp64()` are called synchronously,
123
+ // with no `await`, throughout this module's own colocated test file
124
+ // (`packer-finding.test.mjs`, unmodified by this migration) -- including at
125
+ // module scope (`const PROBED = probeUnp64();`). Converting them to
126
+ // async/Promise-returning functions would silently break every one of those
127
+ // call sites (a Promise is not the finding object the assertions expect),
128
+ // so the OUTER call into the seam's CLI wrapper must itself be synchronous.
129
+ // The asynchronous work (the actual child-process spawn of the oracle
130
+ // binary) still happens -- inside the SPAWNED subprocess, in
131
+ // host-tool.mts's own async `runHostTool()` -- `execFileSync` merely blocks
132
+ // this function until that subprocess exits, exactly as `spawnSync` used to
133
+ // block until `unp64` itself exited.
134
+ const HOST_TOOL_CLIENT_FILE = "host-tool-client.ts";
135
+
136
+ /**
137
+ * Synchronously invokes the host-tool execution seam for `tool`/`args`,
138
+ * optionally rooted at `repoRoot` for workspace-relative path resolution.
139
+ * NEVER throws: an unresolvable ladder, a spawn failure, a timeout, or
140
+ * unparseable output all return `{ ok: false, message }` -- the SAME shape
141
+ * a tool's own transport-level refusal uses, so callers translate a failure
142
+ * here identically to a `{ ok: false }` response from the seam itself.
143
+ */
144
+ function invokeSeamSync(tool, args, repoRoot) {
145
+ const resolved = resolveMcpModule(HOST_TOOL_CLIENT_FILE);
146
+ if (!resolved.ok) {
147
+ return { ok: false, message: refusalMessage(HOST_TOOL_CLIENT_FILE, resolved.rungs) };
148
+ }
149
+
150
+ const cliArgs = [resolved.path, "run", "--tool", tool, "--args", JSON.stringify(args)];
151
+ if (repoRoot) cliArgs.push("--repo-root", repoRoot);
152
+
153
+ let stdout;
154
+ try {
155
+ stdout = execFileSync(process.execPath, cliArgs, {
156
+ encoding: "utf8",
157
+ timeout: ORACLE_TIMEOUT_MS + 5_000,
158
+ shell: false,
159
+ windowsHide: true,
160
+ });
161
+ } catch (err) {
162
+ // execFileSync throws on a non-zero exit, a timeout, or a genuine spawn
163
+ // failure -- but a non-zero exit is the NORMAL signal for a tool-level
164
+ // `{ ok: false }` result (host-tool-client.ts's own CLI wrapper always
165
+ // prints its one JSON line before exiting non-zero), so recover it from
166
+ // the error object rather than treating every non-zero exit as a
167
+ // transport failure.
168
+ const recovered = typeof err.stdout === "string" ? err.stdout : err.stdout ? err.stdout.toString("utf8") : "";
169
+ if (recovered.trim() === "") {
170
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
171
+ }
172
+ stdout = recovered;
173
+ }
174
+
175
+ const lines = stdout.split("\n").filter((line) => line.trim() !== "");
176
+ const last = lines[lines.length - 1];
177
+ if (last === undefined) return { ok: false, message: "host-tool-client.ts produced no output" };
178
+ try {
179
+ return JSON.parse(last);
180
+ } catch {
181
+ return { ok: false, message: `host-tool-client.ts produced non-JSON output: ${last}` };
182
+ }
183
+ }
184
+
185
+ /** Splits an arbitrary (absolute or cwd-relative) file path into a workspace
186
+ * root + a plain relative name, so a single-file oracle.run request can
187
+ * satisfy the seam's workspace-relative path requirement (`resolveWorkspacePath()`
188
+ * in host-tool.mts refuses an absolute path outright) without needing the
189
+ * caller's actual project root at all -- the smallest possible root for a
190
+ * single file is its own containing directory. */
191
+ function toWorkspaceRelative(anyPath) {
192
+ const abs = resolve(anyPath);
193
+ return { repoRoot: dirname(abs), source: basename(abs) };
194
+ }
103
195
 
104
196
  // ---------------------------------------------------------------------------
105
197
  // Vocabulary. Exactly four verdicts, frozen. Rule 4.
@@ -215,73 +307,78 @@ export function shannonEntropy(bytes) {
215
307
  // ---------------------------------------------------------------------------
216
308
 
217
309
  /**
218
- * Locates and probes the external packer identifier, WITHOUT touching any
219
- * input file.
220
- *
221
- * Resolution order: the two environment variables, then the bare command name
222
- * on the search path. A CONFIGURED path that does not exist on disk is
223
- * reported as absent and is deliberately not echoed back in the reason
224
- * (T-19-18: an attacker-influenceable value is never placed into a command,
225
- * and not into a message a later step might paste into one either).
310
+ * Turns a container-side environment record
311
+ * into a DIAGNOSTIC HINT, never a configuration value. This script's own
312
+ * filesystem is not the filesystem the oracle runs on -- host-tool.mts's
313
+ * `resolveOracleCommand()` decides the oracle's location from the HOST
314
+ * BROKER PROCESS'S OWN environment now, so a variable set in THIS
315
+ * (container-side) environment can only ever explain a possibly-surprising
316
+ * absent result, never select what actually runs.
226
317
  *
227
- * Never throws. A launch error, a non-zero status or a timeout are all
228
- * "absent", never a failure -- absence of the oracle is an expected state.
318
+ * Answers `null` when no oracle variable is set in `env`. Otherwise answers
319
+ * a non-empty string naming WHICH variable was set and stating that the
320
+ * seam consults the host broker process's own environment instead --
321
+ * NEVER interpolating the variable's value (T-19-18, unchanged by this
322
+ * migration).
229
323
  */
230
- export function probeUnp64(env = process.env) {
324
+ export function oracleConfigurationHint(env = process.env) {
231
325
  const source = env ?? {};
232
- let configuredVar = null;
233
- let configured = null;
234
326
  for (const name of ORACLE_ENV_VARS) {
235
327
  const value = source[name];
236
328
  if (typeof value === "string" && value.trim() !== "") {
237
- configuredVar = name;
238
- configured = value.trim();
239
- break;
329
+ return (
330
+ `${name} is set in this container-side environment, but it is not consulted: the host-tool ` +
331
+ "execution seam reads the oracle's location from the HOST BROKER PROCESS'S OWN environment, " +
332
+ `not this script's -- point ${name} at the oracle in the environment the host broker process sees`
333
+ );
240
334
  }
241
335
  }
336
+ return null;
337
+ }
242
338
 
243
- if (configured !== null && !existsSync(configured)) {
244
- return {
245
- available: false,
246
- command: null,
247
- version: null,
248
- reason:
249
- `the packer identifier configured through ${configuredVar} does not exist on disk -- ` +
250
- "treated as oracle-absent, and the configured value was not placed into any command",
251
- };
252
- }
253
-
254
- const command = configured ?? DEFAULT_ORACLE_COMMAND;
255
- const probe = spawnSync(command, ["--version"], {
256
- encoding: "utf8",
257
- timeout: ORACLE_TIMEOUT_MS,
258
- shell: false,
259
- windowsHide: true,
260
- });
339
+ /** Appends `hint` to `reason` when both are present, returns whichever of the
340
+ * two is non-null when only one is, and returns `null` when neither is. Kept
341
+ * as its own function so the "when absent, append the hint" rule in
342
+ * `probeUnp64()` below is one small, testable operation rather than inlined
343
+ * string-concatenation logic repeated at every call site. */
344
+ function appendHint(reason, hint) {
345
+ if (hint === null) return reason ?? null;
346
+ if (reason === null || reason === undefined) return hint;
347
+ return `${reason} ${hint}`;
348
+ }
261
349
 
262
- if (probe.error) {
263
- return {
264
- available: false,
265
- command: null,
266
- version: null,
267
- reason:
268
- configuredVar === null
269
- ? `no "${DEFAULT_ORACLE_COMMAND}" packer identifier was found on the search path`
270
- : `the packer identifier configured through ${configuredVar} could not be launched`,
271
- };
272
- }
350
+ /**
351
+ * Locates and probes the external packer identifier, WITHOUT touching any
352
+ * input file.
353
+ *
354
+ * This function decides NOTHING about the
355
+ * binary any more -- it sends the seam call UNCONDITIONALLY, with an empty
356
+ * argument object, whether or not a container-side oracle variable is set.
357
+ * There is no filesystem existence check on an oracle path here (the removed
358
+ * check answered the wrong question: this script's own filesystem is not the
359
+ * filesystem the oracle runs on). A container-side variable can only ever
360
+ * add a diagnostic hint to an ABSENT result -- see `oracleConfigurationHint()`
361
+ * -- never select what the host executes.
362
+ *
363
+ * Never throws. A launch error, a non-zero status or a timeout are all
364
+ * "absent", never a failure -- absence of the oracle is an expected state.
365
+ */
366
+ export function probeUnp64(env = process.env) {
367
+ const hint = oracleConfigurationHint(env);
368
+ const response = invokeSeamSync("oracle.probe", {});
273
369
 
274
- const banner = `${probe.stdout ?? ""}${probe.stderr ?? ""}`.trim();
275
- if (banner === "") {
276
- return {
277
- available: false,
278
- command: null,
279
- version: null,
280
- reason: "the packer identifier produced no version banner, so it was not accepted as an oracle",
281
- };
370
+ if (!response || response.ok !== true) {
371
+ const reason = (response && response.message) || "the packer identifier oracle.probe seam call failed";
372
+ return { available: false, command: null, version: null, reason: appendHint(reason, hint) };
282
373
  }
283
374
 
284
- return { available: true, command, version: banner.slice(0, 200), reason: null };
375
+ const available = response.available === true;
376
+ return {
377
+ available,
378
+ command: available ? response.command : null,
379
+ version: available ? response.version : null,
380
+ reason: available ? null : appendHint(response.reason, hint),
381
+ };
285
382
  }
286
383
 
287
384
  /**
@@ -307,35 +404,16 @@ export function runUnp64(probe, filePath) {
307
404
  return { ok: false, stdout: "", reason: "the input file does not exist" };
308
405
  }
309
406
 
310
- let scratch = null;
311
- try {
312
- scratch = mkdtempSync(join(tmpdir(), "packer-finding-"));
313
- const scratchOut = join(scratch, "unpacked.out");
314
- const run = spawnSync(probe.command, [filePath, scratchOut], {
315
- encoding: "utf8",
316
- timeout: ORACLE_TIMEOUT_MS,
317
- shell: false,
318
- windowsHide: true,
319
- maxBuffer: MAX_ORACLE_STDOUT_BYTES,
320
- });
321
- if (run.error) {
322
- return { ok: false, stdout: "", reason: "the oracle could not be run against the input file" };
323
- }
324
- return { ok: true, stdout: `${run.stdout ?? ""}`, reason: null };
325
- } catch {
326
- // A scratch directory that could not be created is an absent oracle, not
327
- // an error a recon pass should stop for.
328
- return { ok: false, stdout: "", reason: "a scratch directory for the oracle's output could not be created" };
329
- } finally {
330
- if (scratch !== null) {
331
- try {
332
- rmSync(scratch, { recursive: true, force: true });
333
- } catch {
334
- // Best effort. A leftover empty scratch directory is not worth
335
- // failing a read-only recon finding over.
336
- }
337
- }
407
+ // The scratch output location, the argument array, the runtime bound and
408
+ // the input file's absolute/relative form are all resolved host-side now;
409
+ // this file only ever hands the seam a workspace-relative `source`, rooted
410
+ // at the smallest root that can express it -- the file's own directory.
411
+ const { repoRoot, source } = toWorkspaceRelative(filePath);
412
+ const response = invokeSeamSync("oracle.run", { source }, repoRoot);
413
+ if (!response || typeof response.ok !== "boolean") {
414
+ return { ok: false, stdout: "", reason: (response && response.message) || "the oracle.run seam call failed" };
338
415
  }
416
+ return { ok: response.ok, stdout: typeof response.stdout === "string" ? response.stdout : "", reason: response.reason ?? null };
339
417
  }
340
418
 
341
419
  /**
@@ -1,6 +1,6 @@
1
1
  # Memory map generation
2
2
 
3
- **The memory map is GENERATED, not hand-authored (D-24).** The store — labels, comments, block
3
+ **The memory map is GENERATED, not hand-authored.** The store — labels, comments, block
4
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.
@@ -36,7 +36,7 @@ one-time, self-clearing banner correction — not a bug, and not a migration.
36
36
  ## The provenance sidecar
37
37
 
38
38
  Some facts belong to the **run** (which capture, which `$01`, which video standard) rather than to
39
- any address, and the store has no address-keyed shape for them (D-27). They are supplied to the
39
+ any address, and the store has no address-keyed shape for them. They are supplied to the
40
40
  renderer as a small JSON sidecar, hand-authored from `c64-ram-capture`'s and `derive.mjs`'s own
41
41
  outputs and validated by the renderer — a missing or malformed key is a named error listing every
42
42
  problem at once, never a `<placeholder>` silently rendered into a published document.
@@ -126,9 +126,44 @@ The two seeds are where this goes wrong, and both failure modes are on record:
126
126
  `io` (`$D000-$DFFF`) and `unused` (contiguous `$00`/`$FF` power-on runs) are
127
127
  assigned at capture time and kept verbatim. Everything the trace reaches is `game`.
128
128
 
129
- Per D-05 the `.bin` files are **never** edited or zeroed. Classification lives in
129
+ The `.bin` files are **never** edited or zeroed. Classification lives in
130
130
  the manifests; the bytes stay verbatim evidence.
131
131
 
132
+ ## Carrying the verdict into the rebuild
133
+
134
+ A verdict recorded here does not stay here. `anno export-asm`'s `--ledger` flag
135
+ reads THIS skill's generated `recovery/PROVENANCE.md` — never
136
+ re-deriving anything — and carries every covered range's Verdict and Confidence
137
+ into the exported ACME source as an inline comment on the block that range
138
+ overlaps:
139
+
140
+ ```
141
+ anno export-asm game.prg --store game.annostore --ledger recovery/PROVENANCE.md
142
+ ```
143
+
144
+ The `<image>` positional wants one of `.prg`/`.raw`/`.bin`, `--store` an
145
+ `.annostore`/`.store` file, and `--ledger` the `.md` this skill's own `ledger`
146
+ verb writes.
147
+
148
+ **The flag makes the verdict VISIBLE and decides nothing.** Every byte in
149
+ scope is still emitted, whatever the verdict says — a `CRACKER-PATCH` row does
150
+ not drop, filter or alter a single byte, it only makes the evidence readable
151
+ at the point of use. What gets reversed, kept or left out remains the
152
+ end-user's decision, never the tool's. `--ledger` is optional: omitting it
153
+ exports exactly as before, with no provenance comment anywhere in the output.
154
+
155
+ **When the operator, reading the ledger, decides a range genuinely should be
156
+ left out of the rebuild output** — a trainer patch, a cracktro block, anything
157
+ they choose — the round trip runs entirely on the `anno_*` MCP surface, never
158
+ by editing the ledger or the export: `anno_exclude_range` records the span
159
+ WITH the reason the operator gave, and `anno_include_range` takes the record
160
+ back if the decision changes. Recording an exclusion changes nothing about
161
+ which bytes the export emits — the exported block still carries every byte
162
+ of that span, now with a visible marker naming the exclusion and its reason,
163
+ so nothing is removed and no gap appears in the output. The ledger's verdict
164
+ is information the operator reads at this point; it is never wired as an
165
+ input to an automatic exclusion, here or anywhere else on this surface.
166
+
132
167
  ## A `CRACKER-PATCH` in `game` code is a trainer until proven otherwise
133
168
 
134
169
  `count-patches` counts exactly one intersection — verdict `CRACKER-PATCH`, kind
@@ -234,10 +269,10 @@ addresses.
234
269
  | Assembling | `acme-build` |
235
270
  | **Whether a byte is original, cracker-changed, or unknown** | here |
236
271
 
237
- Findings that make RE faster go in `.planning/RE-FINDINGS.md` **at the moment you
238
- find them**, graded with `Evidence:` and `Confidence:`. Promote by re-logging with
239
- the new evidence, never by editing a grade in place. File-changing work enters
240
- through a GSD command (`/gsd-quick`).
272
+ Record findings that make RE faster in your own project notes **at the moment you
273
+ find them**, graded with `Evidence:` and `Confidence:`. Promote a finding by
274
+ re-logging it with the new evidence, never by editing an old grade in place the
275
+ grade is only worth anything if it says what was actually known when it was written.
241
276
 
242
277
  ## Troubleshooting
243
278
 
@@ -5,7 +5,7 @@
5
5
  // an already-committed file (a release's primary `.bin` dump, its
6
6
  // `.map.json` range manifest, and `recovery/RELEASES.json`) and every tool
7
7
  // is pure Node over those files -- nothing in this module contacts the
8
- // emulator, ever (D-18: zero third-party dependencies, `Buffer.indexOf` and
8
+ // emulator, ever (zero third-party dependencies, `Buffer.indexOf` and
9
9
  // `node:crypto` are sufficient).
10
10
  //
11
11
  // This is the step the objective calls "the one most able to produce
@@ -373,7 +373,7 @@ export function diffRanges(images, { gapTolerance = 16 } = {}) {
373
373
  // one address's value would be both wrong for the range and would
374
374
  // silently defeat collapsing (no two addresses would ever compare
375
375
  // equal on evidence text, discovered live while running this tool
376
- // against the real dumps -- see .planning/RE-FINDINGS.md).
376
+ // against the real dumps).
377
377
  rec = {
378
378
  verdict: "ORIGINAL",
379
379
  agreeing_releases: available.length,
@@ -519,7 +519,7 @@ function mergeGroup(group) {
519
519
  // `swallowedGap` below. Deduplicated (via Set) so a coalesced range with
520
520
  // many same-reason singleton addresses doesn't repeat identical
521
521
  // boilerplate once per address -- found live while running this against
522
- // the real dumps (see .planning/RE-FINDINGS.md).
522
+ // the real dumps.
523
523
  const constituentNotes = [...new Set(nonOriginal.map((r) => r.evidence || r.reason).filter(Boolean))];
524
524
  const swallowedGap = group.length > nonOriginal.length;
525
525
  const note =
@@ -599,11 +599,11 @@ export function splitRangeByManifestKind(range, manifestRanges) {
599
599
 
600
600
  /**
601
601
  * Promote one manifest from `ranges-only` to `bucketed`: `unused`/`io`
602
- * ranges are kept verbatim (D-02's byte-level classification already
602
+ * ranges are kept verbatim (the byte-level classification already
603
603
  * stands); every `unclassified` range is re-partitioned against the
604
604
  * release's earned `loader_ranges` (never NOTES.md prose) and this image's
605
605
  * own cracktro printable-run scan, with the remainder -- reached by the
606
- * trace/entry point -- bucketed `game`. Per D-05 the underlying bytes are
606
+ * trace/entry point -- bucketed `game`. The underlying bytes are
607
607
  * never edited; only the manifest's own `kind` field changes.
608
608
  */
609
609
  export function bucketManifest(image, manifest, { loaderRanges, cracktroMinLength = 8 } = {}) {
@@ -614,7 +614,7 @@ export function bucketManifest(image, manifest, { loaderRanges, cracktroMinLengt
614
614
  note: lr.note ?? "",
615
615
  evidence: lr.evidence ?? "",
616
616
  }));
617
- // Keep every already-classified range verbatim (unused/io from D-02's
617
+ // Keep every already-classified range verbatim (unused/io from the
618
618
  // byte-level pass, or -- on a re-run of an already-bucketed manifest --
619
619
  // game/loader/cracktro from a prior run of this same function). Only
620
620
  // "unclassified" is ever re-partitioned. Filtering "kept" down to just
@@ -693,9 +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
696
+ // NOTE: the embedded invocation path below is deliberately the
697
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).
698
+ // source-tree location (`src/skills/...`) -- pinned by diff-images.test.mjs and skill-consumer-paths.test.ts.
699
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`;
700
700
  generated += `| Start | End | Kind | Verdict | Confidence | Agreeing releases | Evidence / Reason |\n`;
701
701
  generated += `|---|---|---|---|---|---|---|\n`;
@@ -4,7 +4,7 @@
4
4
  // Node/filesystem checks over `recovery/RELEASES.json` and the files it
5
5
  // references, run entirely offline.
6
6
  //
7
- // This is the mechanical enforcement of 01-01-PLAN.md's assumption_delta
7
+ // This is the mechanical enforcement of the assumption_delta
8
8
  // decision: the registry is release-CENTRIC (N releases, each a full field
9
9
  // set, `canonical` demoted to a boolean on one entry), never
10
10
  // canonical-image-centric again. A future plan that quietly reintroduces a
@@ -31,9 +31,11 @@ const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
31
31
  // The parameterisation gate must cover EVERY module of the recovery pipeline, not
32
32
  // just the ones sitting next to this file. When the six modules moved out of
33
33
  // `tools/` into the two skills that use them (2026-08-04), a `HERE`-only scan
34
- // silently stopped covering `d64-parse.mjs` and `dump-artifacts.mjs` -- a static
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,
34
+ // silently stopped covering the disk-image reader that used to live in the
35
+ // sibling skill's `scripts/` directory (since deleted)
36
+ // and `dump-artifacts.mjs` -- a static guard that keeps passing while
37
+ // checking less is worse than one that fails.
38
+ // 2026-08-22: the second entry used to be built project-root-relative,
37
39
  // naming the skills tree's pre-relocation auto-discovery location by hand -- which
38
40
  // stopped resolving the moment the skills tree moved to its current source-tree
39
41
  // location. Rebuilt `HERE`-relative instead -- correct in both this dev checkout
@@ -141,7 +143,7 @@ function runBaseChecks(registry) {
141
143
  for (const field of REQUIRED_DUMP_FILE_FIELDS) {
142
144
  const value = d[field];
143
145
  if (!value) {
144
- errors.push(`release "${r.id}" dump "${d.label}": field "${field}" is not set (a dump is a four-file set, per D-04/D-02)`);
146
+ errors.push(`release "${r.id}" dump "${d.label}": field "${field}" is not set (a dump is a four-file set)`);
145
147
  continue;
146
148
  }
147
149
  const filePath = join(REPO_ROOT, value);