@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.
- package/package.json +2 -2
- package/skills/acme-build/SKILL.md +39 -23
- package/skills/acme-build/scripts/acme.mjs +159 -64
- package/skills/acme-build/template.a +1 -1
- package/skills/c64-disk-access/SKILL.md +156 -0
- package/skills/c64-disk-access/scripts/c1541.mjs +569 -0
- package/skills/c64-memory-mapping/SKILL.md +30 -23
- package/skills/c64-memory-mapping/scripts/driver.mjs +1 -1
- package/skills/c64-petcat/SKILL.md +87 -0
- package/skills/c64-petcat/scripts/petcat.mjs +221 -0
- package/skills/c64-program-recon/SKILL.md +93 -39
- package/skills/c64-program-recon/references/control-flow.md +12 -15
- package/skills/c64-program-recon/references/graphics.md +1 -1
- package/skills/c64-program-recon/references/observation-hazards.md +18 -16
- package/skills/c64-program-recon/references/reconstruction.md +1 -2
- package/skills/c64-program-recon/references/sound-and-input.md +6 -8
- package/skills/c64-program-recon/references/tool-selection.md +36 -17
- package/skills/c64-program-recon/scripts/packer-finding.mjs +165 -87
- package/skills/c64-program-recon/templates/memory-map.template.md +2 -2
- package/skills/c64-provenance-diff/SKILL.md +40 -5
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +8 -8
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +7 -5
- package/skills/c64-ram-capture/SKILL.md +112 -44
- package/skills/c64-ram-capture/scripts/compare.mjs +2 -2
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +575 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +3 -3
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +174 -0
- package/skills/c64-ram-capture/scripts/releases.mjs +1 -1
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +147 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +15 -15
- package/skills/c64-ram-capture/templates/capture-record.template.md +44 -4
- package/skills/c64-ram-capture/transients/README.md +136 -0
- package/skills/routine-queue-walker/SKILL.md +114 -22
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +463 -0
- package/skills/vice-wedge-triage/SKILL.md +96 -89
- 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
|
|
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 {
|
|
100
|
-
import { existsSync,
|
|
101
|
-
import {
|
|
102
|
-
|
|
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
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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
|
-
*
|
|
228
|
-
*
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
find them**, graded with `Evidence:` and `Confidence:`. Promote
|
|
239
|
-
the new evidence, never by editing
|
|
240
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
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`.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
35
|
-
//
|
|
36
|
-
//
|
|
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
|
|
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);
|