@henols/c64-re-tools 0.2.1 → 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 (46) hide show
  1. package/README.md +1 -1
  2. package/THIRD-PARTY-NOTICES.md +26 -0
  3. package/bin/cli.mjs +18 -7
  4. package/package.json +6 -4
  5. package/skills/acme-build/SKILL.md +83 -33
  6. package/skills/acme-build/scripts/acme.mjs +159 -64
  7. package/skills/acme-build/template.a +1 -1
  8. package/skills/c64-disk-access/SKILL.md +156 -0
  9. package/skills/c64-disk-access/scripts/c1541.mjs +569 -0
  10. package/skills/c64-memory-mapping/SKILL.md +419 -23
  11. package/skills/c64-memory-mapping/scripts/driver.mjs +1 -1
  12. package/skills/c64-petcat/SKILL.md +87 -0
  13. package/skills/c64-petcat/scripts/petcat.mjs +221 -0
  14. package/skills/c64-program-recon/SKILL.md +497 -92
  15. package/skills/c64-program-recon/references/control-flow.md +12 -15
  16. package/skills/c64-program-recon/references/graphics.md +1 -1
  17. package/skills/c64-program-recon/references/observation-hazards.md +18 -16
  18. package/skills/c64-program-recon/references/reconstruction.md +11 -6
  19. package/skills/c64-program-recon/references/sound-and-input.md +6 -8
  20. package/skills/c64-program-recon/references/tool-selection.md +37 -18
  21. package/skills/c64-program-recon/scripts/packer-finding.mjs +709 -0
  22. package/skills/c64-program-recon/templates/memory-map.template.md +27 -13
  23. package/skills/c64-provenance-diff/SKILL.md +43 -8
  24. package/skills/c64-provenance-diff/scripts/diff-images.mjs +9 -6
  25. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +20 -8
  26. package/skills/c64-ram-capture/RELEASES.json.example +17 -0
  27. package/skills/c64-ram-capture/SKILL.md +147 -46
  28. package/skills/c64-ram-capture/scripts/compare.mjs +2 -2
  29. package/skills/c64-ram-capture/scripts/derive-transients.mjs +575 -0
  30. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +3 -3
  31. package/skills/c64-ram-capture/scripts/mcp-module.mjs +174 -0
  32. package/skills/c64-ram-capture/scripts/project-paths.mjs +1 -1
  33. package/skills/c64-ram-capture/scripts/releases.mjs +1 -1
  34. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +147 -0
  35. package/skills/c64-ram-capture/scripts/watch-loads.mjs +19 -13
  36. package/skills/c64-ram-capture/templates/capture-record.template.md +44 -4
  37. package/skills/c64-ram-capture/transients/README.md +136 -0
  38. package/skills/routine-queue-walker/SKILL.md +365 -0
  39. package/skills/routine-queue-walker/scripts/completeness-report.mjs +463 -0
  40. package/skills/vice-wedge-triage/SKILL.md +104 -97
  41. package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +0 -665
  42. package/skills/c64-ram-capture/scripts/d64-parse.mjs +0 -243
  43. package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +0 -243
  44. package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +0 -133
  45. package/skills/c64-ram-capture/scripts/test-corpus.mjs +0 -75
  46. package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +0 -339
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ // petcat -> BASIC detokenization + SYS-handover driver. One capability:
3
+ // decode a BASIC program's stub to readable text and resolve the numeric
4
+ // address it hands over to machine code, when that address is a literal.
5
+ //
6
+ // Reached ONLY through the host-tool execution seam -- the project owner's
7
+ // rule of 2026-08-28 is that this script runs container-side, `petcat` lives
8
+ // host-side, and there is no container PATH to find it on. This file never spawns `petcat` itself; it constructs a
9
+ // TYPED request (`petcat.decode`) and reads the produced listing file back
10
+ // off the shared workspace tree, mirroring
11
+ // src/skills/c64-disk-access/scripts/c1541.mjs's own invokeSeam() shape
12
+ // verbatim.
13
+ //
14
+ // WHAT NOT TO DO:
15
+ // - Never spawn the conversion binary (`petcat`) directly from this
16
+ // script, even as a "just this once" fallback. A direct call works on
17
+ // the developer's own host and silently fails inside a container -- the
18
+ // exact failure this seam exists to remove.
19
+ // - Never guess an entry point when the seam reports a null one. A
20
+ // guessed address is spent on a disassembler downstream, and a wrong
21
+ // one is expensive there -- report the decline and its reason exactly
22
+ // as the seam gave them, never a fallback value.
23
+ import { dirname, relative, isAbsolute, resolve, sep } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { spawn } from "node:child_process";
26
+
27
+ import { resolveMcpModule, refusalMessage } from "../../c64-ram-capture/scripts/mcp-module.mjs";
28
+
29
+ const SELF = fileURLToPath(import.meta.url);
30
+ const HERE = dirname(SELF);
31
+
32
+ /** The MCP-side module this script reaches -- never imported statically
33
+ * (cross-package: this file ships in `@henols/c64-re-tools`, the seam client
34
+ * ships in `@henols/vice-mcp`), only located via the ladder and invoked with
35
+ * `process.execPath`, the interpreter already running this script, on an
36
+ * in-tree module -- not an external host binary. */
37
+ const HOST_TOOL_CLIENT_FILE = "host-tool-client.ts";
38
+
39
+ /**
40
+ * Invokes the host-tool execution seam for `tool`/`args`, rooted at
41
+ * `repoRoot` for THIS invocation's workspace-relative path resolution.
42
+ * Never rejects: a resolution failure, a spawn failure, or unparseable
43
+ * output all resolve to `{ ok: false, message }` -- the same shape a tool's
44
+ * own refusal uses, so a caller never needs a try/catch. Copied verbatim
45
+ * from acme.mjs's own invokeSeam() -- see this file's own header for why a
46
+ * shared import is not possible across the two npm packages.
47
+ */
48
+ function invokeSeam(tool, args, repoRoot) {
49
+ return new Promise((resolvePromise) => {
50
+ const resolved = resolveMcpModule(HOST_TOOL_CLIENT_FILE);
51
+ if (!resolved.ok) {
52
+ resolvePromise({ ok: false, message: refusalMessage(HOST_TOOL_CLIENT_FILE, resolved.rungs) });
53
+ return;
54
+ }
55
+
56
+ const cliArgs = [resolved.path, "run", "--tool", tool, "--args", JSON.stringify(args), "--repo-root", repoRoot];
57
+ let child;
58
+ try {
59
+ child = spawn(process.execPath, cliArgs, { stdio: ["ignore", "pipe", "pipe"] });
60
+ } catch (e) {
61
+ resolvePromise({ ok: false, message: e instanceof Error ? e.message : String(e) });
62
+ return;
63
+ }
64
+
65
+ let stdout = "";
66
+ let stderr = "";
67
+ child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
68
+ child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
69
+ child.on("error", (err) => resolvePromise({ ok: false, message: err.message }));
70
+ child.on("close", () => {
71
+ const lines = stdout.split("\n").filter((line) => line.trim() !== "");
72
+ const last = lines[lines.length - 1];
73
+ if (last === undefined) {
74
+ resolvePromise({ ok: false, message: `host-tool-client.ts produced no output${stderr ? ` (stderr: ${stderr})` : ""}` });
75
+ return;
76
+ }
77
+ try {
78
+ resolvePromise(JSON.parse(last));
79
+ } catch {
80
+ resolvePromise({ ok: false, message: `host-tool-client.ts produced non-JSON output: ${last}` });
81
+ }
82
+ });
83
+ });
84
+ }
85
+
86
+ /** The smallest common ancestor directory of two absolute paths -- computed,
87
+ * never a fixed guess, so the request's `--repo-root` for THIS invocation is
88
+ * always exactly big enough to contain both the image and the output
89
+ * directory, and no bigger. Copied verbatim from acme.mjs's own
90
+ * commonAncestorDir() -- see this file's own header for why a shared import
91
+ * is not possible. */
92
+ function commonAncestorDir(a, b) {
93
+ const partsA = resolve(a).split(sep);
94
+ const partsB = resolve(b).split(sep);
95
+ const common = [];
96
+ for (let i = 0; i < Math.min(partsA.length, partsB.length); i++) {
97
+ if (partsA[i] === partsB[i]) common.push(partsA[i]);
98
+ else break;
99
+ }
100
+ const joined = common.join(sep);
101
+ return joined === "" ? sep : joined;
102
+ }
103
+
104
+ /** `path.relative()`, except the "same directory" case yields `"."` rather
105
+ * than `""` -- the seam's `resolveWorkspacePath()` refuses an empty string,
106
+ * but accepts `"."` as a no-op relative reference to its own root. */
107
+ function toRel(root, abs) {
108
+ const r = relative(root, abs);
109
+ return r === "" ? "." : r;
110
+ }
111
+
112
+ // How to refer to this script in hints, from wherever we were run.
113
+ function selfPath() {
114
+ const r = relative(process.cwd(), SELF);
115
+ return !r || r.startsWith("..") || isAbsolute(r) ? SELF : r;
116
+ }
117
+
118
+ const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
119
+
120
+ // ------------------------------------------------------------- capabilities
121
+
122
+ /**
123
+ * Detokenizes `--image` (required) to readable BASIC text and resolves its
124
+ * `SYS` handover point, when it has one. `--out-dir` is optional, defaulting
125
+ * to the seam's own dirname(image) default exactly as acme.build's own
126
+ * outDir default does. Prints the seam's response verbatim as one line of
127
+ * JSON when `--json` is given -- the response IS the reportable shape
128
+ * (`{ ok, tool, exitStatus, results, stderrTail, entrypoint,
129
+ * entrypointReason }` / `{ ok: false, message }`), so no reshaping happens
130
+ * here.
131
+ */
132
+ async function runDecode(argv) {
133
+ const o = parseOpts(argv);
134
+ if (!o.image) die(`usage: decode --image <path.prg> [--out-dir <dir>] [--json]`);
135
+
136
+ const imageAbs = resolve(o.image);
137
+ const outDirAbs = o.outDir ? resolve(o.outDir) : dirname(imageAbs);
138
+
139
+ // Workspace-relative request construction (mirrors acme.mjs's own build()):
140
+ // the root for THIS invocation is the smallest ancestor containing both
141
+ // the image and the output directory.
142
+ const repoRoot = commonAncestorDir(dirname(imageAbs), outDirAbs);
143
+ const args = { image: toRel(repoRoot, imageAbs) };
144
+ if (o.outDir) args.outDir = toRel(repoRoot, outDirAbs);
145
+
146
+ const response = await invokeSeam("petcat.decode", args, repoRoot);
147
+ report(response, o);
148
+ process.exit(response.ok ? 0 : 1);
149
+ }
150
+
151
+ function report(response, { json }) {
152
+ if (json) {
153
+ console.log(JSON.stringify(response));
154
+ return;
155
+ }
156
+ if (!response.ok) {
157
+ console.error(`petcat call FAILED: ${response.message}`);
158
+ return;
159
+ }
160
+ // WHAT NOT TO DO (this file's own header): never guess an entry point --
161
+ // print exactly what the seam reported, decline included.
162
+ if (response.entrypoint !== null) {
163
+ console.log(`entry point: ${response.entrypoint} (${response.entrypointReason})`);
164
+ } else {
165
+ console.log(`entry point not resolved: ${response.entrypointReason}`);
166
+ }
167
+ for (const r of response.results ?? []) {
168
+ console.log(`${r.path} (${r.byteLength} bytes, sha256 ${r.sha256})`);
169
+ }
170
+ }
171
+
172
+ // ------------------------------------------------------------------ options
173
+
174
+ // Exported so petcat.test.mjs can cover this file's
175
+ // own CLI-option parsing as a pure unit, mirroring c1541.mjs's own exported
176
+ // parsers -- never spawns anything, never needs petcat installed.
177
+ export function parseOpts(argv) {
178
+ const o = { json: false };
179
+ for (let i = 0; i < argv.length; i++) {
180
+ const a = argv[i];
181
+ if (a === "--json") o.json = true;
182
+ else if (a === "--image") o.image = argv[++i];
183
+ else if (a === "--out-dir") o.outDir = argv[++i];
184
+ }
185
+ return o;
186
+ }
187
+
188
+ // --------------------------------------------------------------------- main
189
+ //
190
+ // The CLI dispatch below MUST be guarded to run only
191
+ // when this file is the actual entry point, not merely imported -- mirrors
192
+ // c1541.mjs's own entry-point guard verbatim (the "Rule 3
193
+ // fix, discovered mid-execution"), added there after an unguarded dispatch
194
+ // ran with the TEST RUNNER's own process.argv on every import of
195
+ // c1541.test.mjs, printing the usage banner and calling process.exit(0)
196
+ // before a single test() call ever registered. Nothing imports petcat.mjs as
197
+ // a module today (confirmed by grep across src/ and scripts/), so this was
198
+ // latent rather than live here -- but the next petcat.test.mjs that imports
199
+ // a pure helper from this file would reintroduce the exact bug c1541.mjs
200
+ // already found and fixed once. Same shape
201
+ // (`resolve(process.argv[1]) === fileURLToPath(import.meta.url)`), never a
202
+ // second guard shape invented for this sibling script.
203
+
204
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
205
+ const [cmd, ...rest] = process.argv.slice(2);
206
+ const VERBS = {
207
+ decode: (argv) => runDecode(argv),
208
+ };
209
+ if (!cmd || !VERBS[cmd]) {
210
+ console.log(`usage: node ${selfPath()} <command> [options]
211
+
212
+ decode --image <path.prg> [--out-dir <dir>] [--json] detokenize a BASIC program and resolve its SYS handover point
213
+
214
+ Never invokes petcat directly and never guesses an entry point -- a computed
215
+ SYS argument is reported as a named decline, never an address.
216
+
217
+ options: --image PATH --out-dir DIR --json`);
218
+ process.exit(cmd ? 1 : 0);
219
+ }
220
+ await VERBS[cmd](rest);
221
+ }