@henols/c64-re-tools 1.1.0 → 2.0.0-rc.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -37
  3. package/distribution/package.ts +36 -0
  4. package/distribution/plugin.ts +120 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +73 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +103 -0
  8. package/skills/c64-basic/SKILL.md +67 -0
  9. package/skills/c64-basic/scripts/basic.ts +46 -0
  10. package/skills/c64-disk/SKILL.md +79 -0
  11. package/skills/c64-disk/scripts/disk.ts +65 -0
  12. package/skills/c64-emulator/SKILL.md +124 -0
  13. package/skills/c64-knowledge/SKILL.md +102 -0
  14. package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
  15. package/skills/c64-memory-map/SKILL.md +76 -0
  16. package/skills/c64-memory-map/scripts/memmap.ts +81 -0
  17. package/skills/c64-memory-map/scripts/platform.ts +424 -0
  18. package/skills/c64-provenance/SKILL.md +76 -0
  19. package/skills/c64-provenance/scripts/compare.ts +92 -0
  20. package/skills/c64-provenance/scripts/provenance.ts +83 -0
  21. package/skills/c64-reverse-engineering/SKILL.md +90 -0
  22. package/skills/c64-static-analysis/SKILL.md +96 -0
  23. package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
  24. package/skills/c64-static-analysis/scripts/findings.ts +73 -0
  25. package/skills/c64-testing/SKILL.md +79 -0
  26. package/skills/c64-testing/references/scenario-format.md +76 -0
  27. package/skills/c64-testing/scripts/checklist.ts +51 -0
  28. package/skills/c64-testing/scripts/scenario.ts +473 -0
  29. package/skills/c64-testing/scripts/test.ts +55 -0
  30. package/skills/c64-unpacker/SKILL.md +82 -0
  31. package/skills/c64-unpacker/scripts/evidence.ts +135 -0
  32. package/skills/c64-unpacker/scripts/unpack.ts +216 -0
  33. package/src/c64.ts +281 -0
  34. package/src/cli/ap-sdk.ts +44 -0
  35. package/src/cli/cleanup.ts +194 -0
  36. package/src/cli/main.ts +122 -0
  37. package/src/cli/status.ts +34 -0
  38. package/src/host/main.ts +121 -0
  39. package/src/host/server.ts +409 -0
  40. package/src/host/tools/c1541.ts +255 -0
  41. package/src/host/tools/index.ts +31 -0
  42. package/src/host/tools/petcat.ts +70 -0
  43. package/src/host/tools/status.ts +30 -0
  44. package/src/host/tools/version.ts +41 -0
  45. package/src/host/vice/adapter.ts +761 -0
  46. package/src/host/vice/binary-monitor.ts +389 -0
  47. package/src/host/vice/chips.ts +108 -0
  48. package/src/host/vice/connect.ts +34 -0
  49. package/src/host/vice/process.ts +402 -0
  50. package/src/host/vice/screen.ts +183 -0
  51. package/src/host/vice/session.ts +1316 -0
  52. package/src/host/vice/text-monitor.ts +223 -0
  53. package/src/host-client/connect.ts +241 -0
  54. package/src/host-client/tools.ts +89 -0
  55. package/src/host-client/transfer.ts +100 -0
  56. package/src/host-client/vice-session.ts +254 -0
  57. package/src/knowledge/database.ts +132 -0
  58. package/src/knowledge/history.ts +128 -0
  59. package/src/knowledge/import.ts +344 -0
  60. package/src/knowledge/read.ts +191 -0
  61. package/src/knowledge/schema.ts +140 -0
  62. package/src/knowledge/write.ts +348 -0
  63. package/src/mcp/main.ts +109 -0
  64. package/src/mcp/server.ts +307 -0
  65. package/src/mcp/tools/debug.ts +237 -0
  66. package/src/mcp/tools/execution.ts +116 -0
  67. package/src/mcp/tools/input.ts +69 -0
  68. package/src/mcp/tools/machine.ts +97 -0
  69. package/src/mcp/tools/media.ts +106 -0
  70. package/src/mcp/tools/memory.ts +203 -0
  71. package/src/mcp/tools/video.ts +260 -0
  72. package/src/native/acme.ts +116 -0
  73. package/src/native/discover.ts +141 -0
  74. package/src/native/dxa.ts +166 -0
  75. package/src/native/ghidra/analyze.ts +86 -0
  76. package/src/native/ghidra/index.ts +157 -0
  77. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  78. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  79. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  80. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  81. package/src/native/ghidra/scripts/C64Export.java +242 -0
  82. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  83. package/src/native/local.ts +34 -0
  84. package/src/native/processes.ts +350 -0
  85. package/src/native/run.ts +172 -0
  86. package/src/native/staging.ts +83 -0
  87. package/src/native/status.ts +101 -0
  88. package/src/native/types.ts +228 -0
  89. package/src/native/watchdog.ts +78 -0
  90. package/src/project.ts +39 -0
  91. package/src/protocol/framing.ts +132 -0
  92. package/src/protocol/messages.ts +252 -0
  93. package/src/protocol/tools.ts +247 -0
  94. package/src/protocol/vice.ts +1235 -0
  95. package/src/script.ts +59 -0
  96. package/src/time.ts +15 -0
  97. package/src/trace.ts +162 -0
  98. package/THIRD-PARTY-NOTICES.md +0 -26
  99. package/bin/cli.mjs +0 -283
  100. package/skills/acme-build/SKILL.md +0 -260
  101. package/skills/acme-build/scripts/acme.mjs +0 -341
  102. package/skills/acme-build/template.a +0 -53
  103. package/skills/c64-disk-access/SKILL.md +0 -159
  104. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  105. package/skills/c64-memory-mapping/SKILL.md +0 -631
  106. package/skills/c64-memory-mapping/memmap.json +0 -8800
  107. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  108. package/skills/c64-petcat/SKILL.md +0 -87
  109. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  110. package/skills/c64-program-recon/SKILL.md +0 -706
  111. package/skills/c64-program-recon/references/control-flow.md +0 -180
  112. package/skills/c64-program-recon/references/graphics.md +0 -79
  113. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  114. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  115. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  116. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  117. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  118. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  119. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  120. package/skills/c64-provenance-diff/SKILL.md +0 -292
  121. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  122. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  123. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  124. package/skills/c64-ram-capture/SKILL.md +0 -474
  125. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  126. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  127. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  128. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  129. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  130. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  131. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  132. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  133. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  134. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  135. package/skills/c64-ram-capture/transients/README.md +0 -137
  136. package/skills/routine-queue-walker/SKILL.md +0 -394
  137. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  138. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -1,581 +0,0 @@
1
- #!/usr/bin/env node
2
- // The on-demand-load detector's pure logic (01-04 Task 1). Every function
3
- // here takes already-fetched data as an argument or reads a committed file
4
- // -- nothing in this module contacts the emulator, ever. The single
5
- // permitted route to the emulator is the executing agent's own
6
- // `mcp__plugin_c64-re-tools_vice__*` tool calls (see .claude/CLAUDE.md "Emulator Access");
7
- // arming, resuming, polling, disassembling and reading memory all happen in
8
- // the agent's own turn, and the observations land in a committed hit-log
9
- // JSON (`recovery/<release>/dumps/<release>-loading-hits.json`) that this
10
- // module reads back. The import-purity guard test in
11
- // tools/watch-loads.test.mjs is the mechanical statement of that boundary:
12
- // every import specifier in this file resolves to a `node:` built-in or a
13
- // sibling file inside tools/, so this module cannot acquire an outside
14
- // dependency without the guard failing.
15
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
16
- import { createHash } from "node:crypto";
17
- import { fileURLToPath } from "node:url";
18
- import { dirname, join, resolve, relative } from "node:path";
19
-
20
- import { loadRegistry, release as getReleaseEntry, upsertRelease } from "./releases.mjs";
21
- import { projectRoot, dataRoot } from "./project-paths.mjs";
22
-
23
- const HERE = dirname(fileURLToPath(import.meta.url));
24
- const REPO_ROOT = projectRoot();
25
- const RECOVERY_DIR = dataRoot();
26
-
27
- const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
28
-
29
- function rel(p) {
30
- return relative(REPO_ROOT, p);
31
- }
32
-
33
- // ------------------------------------------------------------ address math
34
-
35
- /** Parse `"$08B1"`, `"0x8b1"`, a decimal string, or a number into an integer. */
36
- export function addrNum(a) {
37
- if (typeof a === "number") return a;
38
- if (typeof a === "string") {
39
- const s = a.trim();
40
- if (s.startsWith("$")) return parseInt(s.slice(1), 16);
41
- if (/^0x/i.test(s)) return parseInt(s, 16);
42
- const n = Number(s);
43
- if (Number.isNaN(n)) throw new Error(`addrNum: cannot parse address from "${a}"`);
44
- return n;
45
- }
46
- throw new Error(`addrNum: cannot parse address from ${JSON.stringify(a)}`);
47
- }
48
-
49
- /** Format an integer as a canonical `$XXXX` 4-hex-digit address string. */
50
- export function hex4(n) {
51
- return "$" + (n & 0xffff).toString(16).toUpperCase().padStart(4, "0");
52
- }
53
-
54
- // -------------------------------------------------------------- hit-log I/O
55
-
56
- function hitLogPath(releaseId) {
57
- return join(RECOVERY_DIR, releaseId, "dumps", `${releaseId}-loading-hits.json`);
58
- }
59
-
60
- /** Read and JSON.parse a release's committed boundary hit-log artifact. */
61
- export function readHitLog(releaseId) {
62
- const p = hitLogPath(releaseId);
63
- if (!existsSync(p)) {
64
- throw new Error(`readHitLog: no hit log at ${rel(p)} for release "${releaseId}"`);
65
- }
66
- return JSON.parse(readFileSync(p, "utf8"));
67
- }
68
-
69
- /**
70
- * Structural validation of a hit-log's boundary-artifact shape: every
71
- * `armed` entry must carry the `checkpoint_num` the arming call returned,
72
- * and `teardown.checkpoints_remaining` must be present -- both are the
73
- * "the delete call's own word is never the proof" invariant made mechanical.
74
- */
75
- export function validateHitLog(log) {
76
- const errors = [];
77
- for (const a of log.armed ?? []) {
78
- if (a.checkpoint_num === undefined || a.checkpoint_num === null) {
79
- errors.push(`armed sentinel "${a.name}" is missing checkpoint_num`);
80
- }
81
- }
82
- if (!log.teardown || log.teardown.checkpoints_remaining === undefined || log.teardown.checkpoints_remaining === null) {
83
- errors.push("teardown.checkpoints_remaining is not recorded");
84
- }
85
- return { ok: errors.length === 0, errors };
86
- }
87
-
88
- // --------------------------------------------------------------- WATCH_SET
89
-
90
- function loadManifestForRelease(rel) {
91
- const dump = (rel.dumps ?? []).find((d) => d.label === "run1");
92
- if (!dump || !dump.range_manifest) {
93
- throw new Error(`WATCH_SET: release "${rel.id}" has no run1 dump with a range_manifest recorded`);
94
- }
95
- const manifestPath = join(REPO_ROOT, dump.range_manifest);
96
- return JSON.parse(readFileSync(manifestPath, "utf8"));
97
- }
98
-
99
- /**
100
- * Resolve the two-tier sentinel set for one release from registry data and
101
- * that release's run1 range manifest -- never hardcoded. `stopping` tier is
102
- * one sentinel per `loader_ranges` entry (the loader-reentry sentinels that
103
- * must never fire again after the dump point). `counting` tier is
104
- * one sentinel per never-populated range in the run1 manifest, plus one
105
- * register sentinel on CIA2 port A ($DD00), which carries both the VIC
106
- * bank-select bits and the bit-banged serial-bus lines a KERNAL-bypassing
107
- * raw-sector loader toggles directly -- the primary on-demand-load sentinel
108
- * precisely because such a loader leaves no KERNAL vector activity to watch
109
- * instead.
110
- */
111
- export function WATCH_SET(releaseId, { registry, manifest } = {}) {
112
- const reg = registry ?? loadRegistry();
113
- const rel = reg.releases.find((r) => r.id === releaseId);
114
- if (!rel) {
115
- throw new Error(`WATCH_SET: unknown release "${releaseId}" -- known releases: ${reg.releases.map((r) => r.id).join(", ")}`);
116
- }
117
- const loaderRanges = rel.loader_ranges ?? [];
118
- if (loaderRanges.length === 0) {
119
- throw new Error(
120
- `WATCH_SET: release "${releaseId}" has no loader_ranges recorded -- derive and record loader_ranges ` +
121
- `(earn them live against a disassembly, per plan Task 2 step 1) before resolving a watch set; a set ` +
122
- `with no re-entry sentinel in it is not a two-tier set`
123
- );
124
- }
125
- const map = manifest ?? loadManifestForRelease(rel);
126
- const neverPopulated = (map.ranges ?? []).filter((r) => r.kind === "unused");
127
-
128
- const sentinels = [];
129
- for (const lr of loaderRanges) {
130
- const start = addrNum(lr.start);
131
- const end = addrNum(lr.end);
132
- sentinels.push({
133
- name: `loader:${hex4(start)}-${hex4(end)}`,
134
- kind: "loader-reentry",
135
- tier: "stopping",
136
- type: "exec",
137
- start,
138
- end,
139
- reason: lr.note ?? "loader-reentry range: must never fire again after the dump point",
140
- evidence: lr.evidence ?? "",
141
- });
142
- }
143
- for (const nr of neverPopulated) {
144
- const start = addrNum(nr.start);
145
- const end = addrNum(nr.end);
146
- sentinels.push({
147
- name: `unused:${hex4(start)}-${hex4(end)}`,
148
- kind: "never-populated",
149
- tier: "counting",
150
- type: "write",
151
- start,
152
- end,
153
- reason: nr.note ?? "never-populated range in the run1 capture -- any write during gameplay is a candidate",
154
- evidence: nr.note ?? "",
155
- });
156
- }
157
- sentinels.push({
158
- name: "reg:$DD00",
159
- kind: "register",
160
- tier: "counting",
161
- type: "write",
162
- start: 0xdd00,
163
- end: 0xdd00,
164
- reason:
165
- "CIA2 port A -- VIC-II bank-select bits (0-1) plus the bit-banged serial-bus lines (ATN/CLOCK/DATA, bits 3-5) " +
166
- "a KERNAL-bypassing raw-sector loader toggles directly; the primary on-demand-load sentinel because such a " +
167
- "loader leaves no KERNAL vector activity to watch instead",
168
- evidence:
169
- "c64-memory-mapping skill memmap: $DD00 bits 0-1 select the VIC bank (00=bank3 $C000-$FFFF ... 11=bank0 " +
170
- "$0000-$3FFF); bits 3-5 are the serial bus ATN OUT/CLOCK OUT/DATA OUT lines",
171
- });
172
- return sentinels;
173
- }
174
-
175
- // ----------------------------------------------------------- attributeAddress
176
-
177
- /**
178
- * Resolve `addr` to exactly one sentinel's name. Validates the *whole*
179
- * sentinel set for overlap/duplication on every call -- a configuration
180
- * error is a property of the set, not of the one address being queried, so
181
- * it must be caught regardless of which address happens to be asked about.
182
- * Abutting ranges (one range's `end` immediately followed by the next
183
- * range's `start`) are never flagged: they are adjacent, not overlapping.
184
- */
185
- export function attributeAddress(addr, sentinels) {
186
- const a = addrNum(addr);
187
- for (let i = 0; i < sentinels.length; i++) {
188
- for (let j = i + 1; j < sentinels.length; j++) {
189
- const s1 = sentinels[i];
190
- const s2 = sentinels[j];
191
- const s1s = addrNum(s1.start);
192
- const s1e = addrNum(s1.end);
193
- const s2s = addrNum(s2.start);
194
- const s2e = addrNum(s2.end);
195
- if (s1s <= s2e && s2s <= s1e) {
196
- throw new Error(
197
- `attributeAddress: overlapping or duplicate sentinel ranges "${s1.name}" (${hex4(s1s)}-${hex4(s1e)}) and ` +
198
- `"${s2.name}" (${hex4(s2s)}-${hex4(s2e)}) -- refusing to resolve a winner by precedence`
199
- );
200
- }
201
- }
202
- }
203
- const matches = sentinels.filter((s) => a >= addrNum(s.start) && a <= addrNum(s.end));
204
- if (matches.length === 0) {
205
- return { matched: false, name: null, address: a };
206
- }
207
- return { matched: true, name: matches[0].name, address: a };
208
- }
209
-
210
- // ----------------------------------------------------------------- reportHits
211
-
212
- /**
213
- * Total order over a hit log: cycle ascending, then address ascending, then
214
- * sentinel name ascending -- so two hits sharing both cycle and address
215
- * still have one defined position, and re-reporting an unchanged log is
216
- * byte-identical. Accepts either a bare array of hit records or the whole
217
- * boundary-artifact object (reading its `.hits` field); an absent/empty
218
- * `hits` array reports as an empty result rather than throwing.
219
- */
220
- export function reportHits(hitLog) {
221
- const hits = Array.isArray(hitLog) ? hitLog : hitLog?.hits ?? [];
222
- return [...hits].sort((a, b) => {
223
- const ca = a.cycle ?? 0;
224
- const cb = b.cycle ?? 0;
225
- if (ca !== cb) return ca - cb;
226
- const aa = addrNum(a.address);
227
- const ab = addrNum(b.address);
228
- if (aa !== ab) return aa - ab;
229
- return String(a.sentinel ?? "").localeCompare(String(b.sentinel ?? ""));
230
- });
231
- }
232
-
233
- // ------------------------------------------------------------------ idleGate
234
-
235
- /**
236
- * The mechanical half of the idle check. Passes only when every
237
- * `stopping`-tier sentinel recorded exactly zero hits AND the recorded
238
- * `cycles_advanced` is greater than zero -- a machine that did not execute
239
- * proves nothing, whatever the hit counts say. Otherwise names the
240
- * violating sentinels with their counts.
241
- */
242
- export function idleGate(calibration) {
243
- const cyclesAdvanced = calibration?.cycles_advanced;
244
- const sentinels = calibration?.sentinels ?? [];
245
- const violations = sentinels
246
- .filter((s) => s.tier === "stopping" && s.hits !== 0)
247
- .map((s) => ({ name: s.name, hits: s.hits }));
248
- const cyclesOk = typeof cyclesAdvanced === "number" && cyclesAdvanced > 0;
249
- const reasons = [];
250
- if (!cyclesOk) {
251
- reasons.push(`cycles_advanced (${cyclesAdvanced}) is not greater than zero -- a machine that did not execute proves nothing`);
252
- }
253
- if (violations.length > 0) {
254
- reasons.push(
255
- `stopping-tier sentinel(s) recorded non-zero idle hits: ${violations.map((v) => `${v.name}=${v.hits}`).join(", ")}`
256
- );
257
- }
258
- return { ok: cyclesOk && violations.length === 0, cycles_advanced: cyclesAdvanced, violations, reasons };
259
- }
260
-
261
- // ---------------------------------------------------------------- classifyHit
262
-
263
- /**
264
- * Returns `unattributed` unless the hit record carries a non-empty program
265
- * counter, backtrace and disassembly -- only then does it return the
266
- * recorded classification (`gameplay-write` or `load-candidate`). An
267
- * unattributed hit is reported as exactly that, never as a bare count.
268
- */
269
- export function classifyHit(hit) {
270
- const hasPc = hit?.pc !== undefined && hit?.pc !== null && hit?.pc !== "";
271
- const hasBacktrace = Array.isArray(hit?.backtrace) ? hit.backtrace.length > 0 : !!hit?.backtrace;
272
- const hasDisassembly = !!hit?.disassembly;
273
- if (!hasPc || !hasBacktrace || !hasDisassembly) return "unattributed";
274
- if (hit.classification === "gameplay-write" || hit.classification === "load-candidate") {
275
- return hit.classification;
276
- }
277
- return "unattributed";
278
- }
279
-
280
- // ------------------------------------------------------------ screenSignature
281
-
282
- /**
283
- * Hash the 1000 bytes of screen matrix the agent read (hex string in,
284
- * digest out) together with the sprite-enable register value. Screenshots
285
- * are human-audit artifacts and are never hashed here or anywhere else in
286
- * this project: an encoder can emit different bytes for pixel-identical
287
- * images, and this project deliberately installs no image-decoding library
288
- * to decode-then-hash instead.
289
- */
290
- export function screenSignature(screenMatrixHex, spriteEnable) {
291
- const buf = Buffer.from(screenMatrixHex, "hex");
292
- if (buf.length !== 1000) {
293
- throw new Error(`screenSignature: expected 1000 bytes of screen matrix hex, got ${buf.length} bytes`);
294
- }
295
- const digest = createHash("sha256").update(buf).digest("hex");
296
- return { digest, sprite_enable: spriteEnable ?? null };
297
- }
298
-
299
- // --------------------------------------------------------------- recordWatchSet
300
-
301
- /** Persist a resolved sentinel set into the registry under `watch_set`. */
302
- export function recordWatchSet(releaseId, watchSet) {
303
- return upsertRelease(releaseId, (r) => ({ ...r, watch_set: watchSet }));
304
- }
305
-
306
- // ----------------------------------------------------------------- renderLoading
307
-
308
- function escapeCell(text) {
309
- return String(text ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
310
- }
311
-
312
- function fmtRange(s) {
313
- return `${hex4(addrNum(s.start))}-${hex4(addrNum(s.end))}`;
314
- }
315
-
316
- function renderReleaseSection(id, log) {
317
- const hits = reportHits(log);
318
- const count = hits.length;
319
- let s = `## Release: ${id}\n\n`;
320
-
321
- s += `**Load-event count:**\n\n${count}\n\n`;
322
-
323
- if (log.run_status === "blocked") {
324
- if (count === 0) {
325
- s += `> **⚠ THIS IS NOT AN EVIDENCED ZERO.** The count above is \`0\` only because no live ` +
326
- `emulator work reached completion for this release this run -- it is a bare absence of ` +
327
- `attempted measurement, not a null result earned by an idle calibration on a machine proven ` +
328
- `to have executed. ${log.run_status_note ?? ""}\n\n`;
329
- } else {
330
- s += `> **⚠ THIS IS A PARTIAL RESULT, NOT A COMPLETED COVERAGE CLAIM.** The count above (\`${count}\`) ` +
331
- `reflects genuinely attributed hits from the portion of the play-through that did complete before this ` +
332
- `run was blocked -- it is not evidence that no further load events exist beyond what was reached. ` +
333
- `${log.run_status_note ?? ""}\n\n`;
334
- }
335
- }
336
-
337
- s += `**Route:** the executing agent's own \`mcp__plugin_c64-re-tools_vice__*\` tool calls -- machine ${log.machine ?? "unknown"}, ` +
338
- `video standard ${log.video_standard ?? "unknown"}, VICE server version ${log.vice_version ?? "unknown"}.\n\n`;
339
-
340
- s += `### Armed set\n\n`;
341
- s += "| Sentinel | Kind | Tier | Type | Range | Reason | Evidence | Idle hits |\n";
342
- s += "|---|---|---|---|---|---|---|---|\n";
343
- for (const a of log.armed ?? []) {
344
- s += `| ${a.name} | ${a.kind} | ${a.tier} | ${a.type} | ${fmtRange(a)} | ${escapeCell(a.reason)} | ${escapeCell(a.evidence)} | ${a.idle_hits ?? ""} |\n`;
345
- }
346
- s += "\n";
347
-
348
- s += `### Idle calibration\n\n`;
349
- const cal = log.idle_calibration ?? {};
350
- s += `Cycles advanced during the no-input idle window: **${cal.cycles_advanced ?? "unrecorded"}**.\n\n`;
351
- s += "| Sentinel | Tier | Range | Idle hits |\n|---|---|---|---|\n";
352
- for (const sn of cal.sentinels ?? []) {
353
- s += `| ${sn.name} | ${sn.tier} | ${sn.start !== undefined ? fmtRange(sn) : ""} | ${sn.hits} |\n`;
354
- }
355
- s += "\n";
356
-
357
- s += `### Counting-tier probe\n\n`;
358
- const probe = log.counting_tier_probe ?? {};
359
- s += `Observed hit count: ${probe.hit_count ?? "unrecorded"}. Execution stopped during the probe: ${probe.execution_stopped ?? "unrecorded"}.\n\n`;
360
- if (probe.fallback_taken) {
361
- s += `**Fallback taken:** the counting tier could not count without stopping. ${probe.fallback_note ?? ""}\n\n`;
362
- }
363
-
364
- s += `### Coverage reached\n\n`;
365
- s += "| Milestone | Reached | Screen signature | Cycles advanced | Retries | Screenshot |\n|---|---|---|---|---|---|\n";
366
- for (const m of log.milestones ?? []) {
367
- s += `| ${m.name} | ${m.reached ? "yes" : "no"} | ${m.screen_signature ?? ""} | ${m.cycles_advanced ?? ""} | ${m.retries ?? 0} | ${m.screenshot ?? ""} |\n`;
368
- }
369
- s += "\n";
370
-
371
- s += `### States not reached\n\n`;
372
- const notReachedMilestones = (log.milestones ?? []).filter((m) => !m.reached);
373
- const scopeBoundary = log.scope_not_attempted ?? [];
374
- if (notReachedMilestones.length === 0 && scopeBoundary.length === 0) {
375
- s += "(nothing recorded as not reached -- if this looks wrong, the record is incomplete, not the coverage)\n\n";
376
- } else {
377
- for (const m of notReachedMilestones) {
378
- s += `- **${m.name}**: not reached. ${m.not_reached_reason ?? ""}\n`;
379
- }
380
- for (const item of scopeBoundary) {
381
- s += `- ${item}\n`;
382
- }
383
- s += "\n";
384
- }
385
-
386
- s += `### Attributed hits\n\n`;
387
- if (hits.length === 0) {
388
- s += "(no hits recorded above the idle floor)\n\n";
389
- } else {
390
- s += "| Cycle | Address | Sentinel | Tier | Classification | Evidence |\n|---|---|---|---|---|---|\n";
391
- for (const h of hits) {
392
- const cls = h.classification ?? classifyHit(h);
393
- s += `| ${h.cycle} | ${h.address} | ${h.sentinel} | ${h.tier ?? ""} | ${cls} | ${escapeCell(h.disassembly ?? "")} |\n`;
394
- }
395
- s += "\n";
396
- }
397
-
398
- const loadCandidates = hits.filter((h) => (h.classification ?? classifyHit(h)) === "load-candidate");
399
- s += `### Supplementary dumps\n\n`;
400
- if (loadCandidates.length === 0) {
401
- s += "None -- no hit was classified `load-candidate` for this release.\n\n";
402
- } else {
403
- for (const h of loadCandidates) {
404
- s += `- Hit at ${h.address} (cycle ${h.cycle}): supplementary dump \`${h.supplementary_dump ?? "unrecorded"}\`, ` +
405
- `registry ref \`${h.load_event_ref ?? "unrecorded"}\`. Reproducibility bar: a single capture, decided in ` +
406
- `the claim is about an observed moment rather than a stable state -- if loaded content is later ` +
407
- `absorbed into the canonical image, this region must be re-captured at the primary dumps' ` +
408
- `three-run bar before it can be treated as a round-trip diff target.\n`;
409
- }
410
- s += "\n";
411
- }
412
-
413
- s += `### Hand-off to the exhaustive trace\n\n`;
414
- s += "The registry's `watch_set` entries for this release are the re-armable specification: whoever runs the " +
415
- "exhaustive all-chambers trace re-arms the same set by issuing the same `mcp__plugin_c64-re-tools_vice__vice_checkpoint_add` " +
416
- "calls, and interprets what it observes with this module's pure `attributeAddress`, " +
417
- "`reportHits` and `classifyHit` functions. This is a hand-off of data and procedure, not an executable -- describe " +
418
- "agent-performed arming with acceptance criteria over a committed record " +
419
- "rather than over an exit code. A late hit there reopens this document.\n\n";
420
-
421
- s += `### Input sequence notes\n\n`;
422
- s += (log.input_notes ?? "(no input notes recorded)") + "\n\n";
423
- s += "This is plain notes, not a `verify/scripts/` artifact -- the replay tooling owns the real " +
424
- "input-script format; these notes are a seed for it, not a pre-empting specification.\n\n";
425
-
426
- s += `### Teardown proof\n\n`;
427
- const teardown = log.teardown ?? {};
428
- s += `Checkpoints remaining after teardown, from an explicit \`mcp__plugin_c64-re-tools_vice__vice_checkpoint_list\` enumeration: ` +
429
- `**${teardown.checkpoints_remaining ?? "unrecorded"}** (enumerated at ${teardown.enumerated_at ?? "unrecorded"}).\n\n`;
430
-
431
- if ((log.identity_changes ?? []).length > 0) {
432
- s += `### Identity changes\n\n`;
433
- for (const c of log.identity_changes) {
434
- s += `- ${JSON.stringify(c)}\n`;
435
- }
436
- s += "\n";
437
- }
438
-
439
- return s;
440
- }
441
-
442
- /**
443
- * Render `recovery/LOADING.md` from a list of `{ id, log }` entries, each
444
- * `log` being one release's validated boundary hit-log artifact. Pure
445
- * string templating over already-fetched data -- nothing here reads a file
446
- * or contacts anything; the CLI `render` verb below is what reads the
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. A blanket
452
- * path sweep rewrote them to the source tree once already; pinned
453
- * by the test below plus the class-level registry in skill-consumer-paths.test.ts.
454
- */
455
- export function renderLoading(entries) {
456
- let out = "# `recovery/LOADING.md` -- the on-demand-load detection record\n\n";
457
- out +=
458
- "This document is the absence-as-evidence record: per release, the armed set with its justification, the " +
459
- "idle calibration result, the coverage reached with a mechanical arrival proof per milestone, the states not " +
460
- "reached, the attributed hits, and the teardown enumeration. Every measurement below was fetched by the " +
461
- "executing agent's own `mcp__plugin_c64-re-tools_vice__*` tool calls; `.claude/skills/c64-ram-capture/scripts/watch-loads.mjs` and `.claude/skills/c64-ram-capture/scripts/dump-artifacts.mjs` hold " +
462
- "only the pure logic that resolves, attributes, orders and renders it -- neither module contacted the " +
463
- "emulator.\n\n";
464
- for (const { id, log } of entries) {
465
- out += renderReleaseSection(id, log);
466
- }
467
- return out;
468
- }
469
-
470
- // -------------------------------------------------------------------- CLI
471
-
472
- function optValue(rest, name) {
473
- const i = rest.indexOf(`--${name}`);
474
- return i === -1 ? undefined : rest[i + 1];
475
- }
476
-
477
- const VERBS = {
478
- resolve(rest) {
479
- const releaseId = optValue(rest, "release");
480
- if (!releaseId) die("usage: resolve --release <id> [--json]");
481
- const watchSet = WATCH_SET(releaseId);
482
- recordWatchSet(releaseId, watchSet);
483
- const result = { release: releaseId, count: watchSet.length, watch_set: watchSet };
484
- if (rest.includes("--json")) {
485
- console.log(JSON.stringify(result, null, 2));
486
- } else {
487
- console.log(`${releaseId}: resolved ${watchSet.length} sentinel(s)`);
488
- for (const s of watchSet) console.log(` ${s.name} tier=${s.tier} type=${s.type} ${fmtRange(s)}`);
489
- }
490
- },
491
-
492
- attribute(rest) {
493
- const releaseId = optValue(rest, "release");
494
- const addrArg = optValue(rest, "addr");
495
- if (!releaseId || !addrArg) die("usage: attribute --release <id> --addr <address> [--json]");
496
- const relEntry = getReleaseEntry(releaseId);
497
- const sentinels = relEntry.watch_set && relEntry.watch_set.length ? relEntry.watch_set : WATCH_SET(releaseId);
498
- const result = attributeAddress(addrArg, sentinels);
499
- if (rest.includes("--json")) {
500
- console.log(JSON.stringify(result, null, 2));
501
- } else {
502
- console.log(result.matched ? `${addrArg} -> ${result.name}` : `${addrArg} -> unmatched`);
503
- }
504
- },
505
-
506
- report(rest) {
507
- const releaseId = optValue(rest, "release");
508
- if (!releaseId) die("usage: report --release <id> [--json]");
509
- const log = readHitLog(releaseId);
510
- const hits = reportHits(log);
511
- const result = { release: releaseId, count: hits.length, hits };
512
- if (rest.includes("--json")) {
513
- console.log(JSON.stringify(result, null, 2));
514
- } else {
515
- console.log(`${releaseId}: ${hits.length} hit(s)`);
516
- for (const h of hits) {
517
- console.log(` cycle=${h.cycle} addr=${h.address} sentinel=${h.sentinel} classification=${h.classification ?? classifyHit(h)}`);
518
- }
519
- }
520
- },
521
-
522
- "check-idle"(rest) {
523
- const releaseId = optValue(rest, "release");
524
- if (!releaseId) die("usage: check-idle --release <id> [--json]");
525
- const log = readHitLog(releaseId);
526
- if (!log.idle_calibration) die(`check-idle: release "${releaseId}" hit log has no idle_calibration recorded`);
527
- const gate = idleGate(log.idle_calibration);
528
- const result = { release: releaseId, ...gate, sentinels: log.idle_calibration.sentinels ?? [] };
529
- if (rest.includes("--json")) {
530
- console.log(JSON.stringify(result, null, 2));
531
- } else {
532
- console.log(`${releaseId}: cycles_advanced=${gate.cycles_advanced} ok=${gate.ok}`);
533
- for (const s of result.sentinels) console.log(` ${s.name}: tier=${s.tier} ${s.start !== undefined ? fmtRange(s) : ""} hits=${s.hits}`);
534
- if (!gate.ok) for (const r of gate.reasons) console.error(` - ${r}`);
535
- }
536
- process.exitCode = gate.ok ? 0 : 1;
537
- },
538
-
539
- signature(rest) {
540
- const hex = optValue(rest, "hex");
541
- const spriteEnable = optValue(rest, "sprite-enable");
542
- if (!hex) die("usage: signature --hex <1000-byte-hex> [--sprite-enable <n>] [--json]");
543
- const result = screenSignature(hex, spriteEnable !== undefined ? Number(spriteEnable) : null);
544
- if (rest.includes("--json")) {
545
- console.log(JSON.stringify(result, null, 2));
546
- } else {
547
- console.log(`${result.digest} sprite_enable=${result.sprite_enable}`);
548
- }
549
- },
550
-
551
- render(rest) {
552
- const reg = loadRegistry();
553
- const only = optValue(rest, "release");
554
- const releaseIds = only ? [only] : reg.releases.map((r) => r.id);
555
- const entries = [];
556
- for (const id of releaseIds) {
557
- const p = hitLogPath(id);
558
- if (!existsSync(p)) continue;
559
- entries.push({ id, log: JSON.parse(readFileSync(p, "utf8")) });
560
- }
561
- const markdown = renderLoading(entries);
562
- const outPath = join(RECOVERY_DIR, "LOADING.md");
563
- writeFileSync(outPath, markdown);
564
- const result = { path: rel(outPath), releases: entries.map((e) => e.id) };
565
- if (rest.includes("--json")) {
566
- console.log(JSON.stringify(result, null, 2));
567
- } else {
568
- console.log(`wrote ${rel(outPath)} for releases: ${result.releases.join(", ")}`);
569
- }
570
- },
571
- };
572
-
573
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
574
- const [cmd, ...rest] = process.argv.slice(2);
575
- if (!cmd || !VERBS[cmd]) {
576
- console.log(`usage: node ${fileURLToPath(import.meta.url)} <resolve|attribute|report|check-idle|signature|render> [--release <id>] [--json]`);
577
- process.exitCode = cmd ? 1 : 0;
578
- } else {
579
- VERBS[cmd](rest);
580
- }
581
- }
@@ -1,99 +0,0 @@
1
- # Capture record — `<release>-<checkpoint>-run<N>`
2
-
3
- One record per capture. The reader records every field **in the same step as the
4
- capture**, before resuming the machine — a value read later describes a
5
- different machine.
6
-
7
- Every `node scripts/…` command cited below is relative to **this skill's own
8
- directory** (`src/skills/c64-ram-capture/`), not to the repository root. From
9
- the repo root, use the `$S`-derived variables `SKILL.md`'s quick-reference
10
- block defines — `node $V slice …` rather than `node scripts/vsf-slice.mjs
11
- slice …`. Stated once here because five rows use the shorthand, and
12
- `scripts/` resolves from neither cwd by accident.
13
-
14
- ## Identity
15
-
16
- | Field | Value | How obtained |
17
- |---|---|---|
18
- | image path | `recovery/<release>/dumps/<name>.bin` | — |
19
- | size | `65536` bytes | must be exact. Anything else is not a full image |
20
- | sha256 | `<64 hex chars>` | `node scripts/compare.mjs digest <name>.bin` |
21
- | binary sha256 | `<64 hex chars>` | `sha256sum <release>.d64` (or `.prg`), or `node scripts/compare.mjs digest <release>.d64`. This identifies the release by its **bytes**, never by its filename |
22
- | argv digest | `<64 hex chars>` | `argvDigest()` in `src/mcp/vice/capture-predicate.ts` — sha256 over the **exact spawn argv array joined by a single NUL byte**. **Order-sensitive by construction**, which is the whole reason it is a field of its own. It refuses an empty argv array rather than digesting the empty string, so a blank here is a blank and never a plausible-looking digest |
23
- | seed | `4242` | the `-seed` value the broker set when it launched the instance — `STOCK_DETERMINISM_SEED` in `src/mcp/vice/broker-launch.mts`. Cite the constant, not a loose number |
24
- | capture route | `memory-read` \| `snapshot` | `memory-read` = the sixteen `vice_memory_read` calls, transcribed. `snapshot` = `node scripts/vsf-slice.mjs slice <run>.vsf --out <run>.bin`. **This row decides which volatility rule applies to this record** — see the closing note |
25
- | checkpoint / trigger address | `$____` | the address armed for this capture |
26
- | release | the registry id this capture belongs to | — |
27
- | run | `<N>` of `<total>` | three runs is this project's minimum for a verified capture |
28
-
29
- ### The reproducibility key is the triple, not the seed
30
-
31
- **`(binary sha256, argv digest, seed)`.** All three rows, together. Two captures
32
- share a reproducibility key only when all three fields are exactly equal.
33
-
34
- **MEASURED: the same seed with a reordered argv yielded a 76-byte-different
35
- image.** So the seed alone is not the key, and a record keyed on it claims a
36
- reproducibility it does not have. Two captures whose argv digests differ are
37
- **different keys and must not be compared as a pair**, even at the same seed —
38
- including when the difference is a single reordered element, because the digest
39
- is order-sensitive and a reordered argv is a different launch.
40
-
41
- **A record with any of the three fields blank is not a reproducible capture.**
42
- The void protocol below applies to it: it is not a capture with a gap in its
43
- paperwork, it is a capture whose identity cannot be established.
44
-
45
- ## Machine state at the capture instant
46
-
47
- Read these *before* resuming, in the same paused window as the memory reads.
48
-
49
- | Field | Value | Source |
50
- |---|---|---|
51
- | `$01` (processor port) | `$__` `%________` | `vice_memory_read` — decides which vectors are live |
52
- | video standard | PAL \| NTSC | `vice_vicii_get_state` |
53
- | registers (PC, A, X, Y, SP, flags) | | `vice_registers_get` |
54
- | epoch-drift errors during the capture | `none` | the proxy raises these itself, before and after every forwarded call — no tool reads the epoch on demand |
55
- | checkpoints armed at exit | `0` | `vice_checkpoint_list` — accept only this enumeration as proof |
56
-
57
- ## Verdict
58
-
59
- - [ ] Size is exactly 65536 bytes.
60
- - [ ] All three reproducibility-key fields — `binary sha256`, `argv digest`, `seed` — hold a value. A blank in any one of them fails this box.
61
- - [ ] The `capture route` row holds a value, so a reader knows which volatility rule this record is under.
62
- - [ ] No epoch-drift error appeared at any point during the capture.
63
- - [ ] `vice_checkpoint_list` reported zero checkpoints before resuming.
64
- - [ ] Machine resumed exactly once, at the end.
65
-
66
- If any box stays unchecked, void the run: rename each artifact to
67
- `<name>.VOID-<UTC timestamp>`, write a sibling note giving the reason and — when a
68
- drift error was the cause — both epoch values quoted from that error's own text,
69
- and keep the voided artifacts on disk.
70
-
71
- ## Comparison against sibling runs
72
-
73
- `node scripts/compare.mjs compare <a>.bin <b>.bin` for each pairing, and
74
- `node scripts/compare.mjs floor <a>.bin <b>.bin <c>.bin` across the set.
75
-
76
- | Pairing | volatile | drift (1 bit) | divergence (2+ bits) | verdict |
77
- |---|---|---|---|---|
78
- | run1 vs run2 | | | | |
79
- | run1 vs run3 | | | | |
80
- | run2 vs run3 | | | | |
81
-
82
- Record the drift floor address count, and state it as a floor rather than a
83
- complete set — more captures of the same checkpoint can only widen it.
84
-
85
- ### `$D000-$DFFF`: the rule depends on the `capture route` row
86
-
87
- This exclusion is a property of **how you transcribed the image**, not of the
88
- C64. Read the `capture route` row and apply the matching half. Getting this
89
- backwards costs a good capture in one direction and hides 4096 addresses of real
90
- divergence in the other.
91
-
92
- | `capture route` | Rule for `$D000-$DFFF` |
93
- |---|---|
94
- | `memory-read` | **Any divergence inside `$D000-$DFFF` is not a divergence.** On this route, `vice_memory_read` reads the range and samples live I/O. The VIC's registers repeat every `$40` across `$D000-$D3FF`, and the SID's registers repeat every `$40` across `$D400-$D7FF`. Two reads can never agree there. Classify it as volatile, say so in the record, and do not void a good capture over it. |
95
- | `snapshot` | **The exclusion does not apply, and a divergence there is a real divergence.** A `.vsf`-sliced image is the `C64MEM` array — `mem_ram[]`. This array is RAM *under* I/O, not the register read view, so those 4096 addresses are ordinary RAM in a sliced image. There is no transcription step on this route to sample anything. Do not carry the memory-read exclusion across. |
96
-
97
- Both halves are live: records taken on the memory-read route still exist and
98
- still need the first row. What is not permitted is applying either half without
99
- reading which route produced the image.