@henols/c64-re-tools 0.1.4

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 (35) hide show
  1. package/README.md +61 -0
  2. package/bin/cli.mjs +226 -0
  3. package/package.json +53 -0
  4. package/skills/acme-build/SKILL.md +224 -0
  5. package/skills/acme-build/scripts/acme.mjs +263 -0
  6. package/skills/acme-build/template.a +39 -0
  7. package/skills/c64-memory-mapping/SKILL.md +199 -0
  8. package/skills/c64-memory-mapping/memmap.json +8800 -0
  9. package/skills/c64-memory-mapping/scripts/driver.mjs +553 -0
  10. package/skills/c64-program-recon/SKILL.md +172 -0
  11. package/skills/c64-program-recon/references/control-flow.md +174 -0
  12. package/skills/c64-program-recon/references/graphics.md +73 -0
  13. package/skills/c64-program-recon/references/observation-hazards.md +118 -0
  14. package/skills/c64-program-recon/references/reconstruction.md +128 -0
  15. package/skills/c64-program-recon/references/sound-and-input.md +68 -0
  16. package/skills/c64-program-recon/references/tool-selection.md +55 -0
  17. package/skills/c64-program-recon/scripts/derive.mjs +364 -0
  18. package/skills/c64-program-recon/templates/memory-map.template.md +62 -0
  19. package/skills/c64-provenance-diff/SKILL.md +257 -0
  20. package/skills/c64-provenance-diff/scripts/diff-images.mjs +981 -0
  21. package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +665 -0
  22. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +383 -0
  23. package/skills/c64-ram-capture/SKILL.md +306 -0
  24. package/skills/c64-ram-capture/scripts/compare.mjs +258 -0
  25. package/skills/c64-ram-capture/scripts/d64-parse.mjs +243 -0
  26. package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +243 -0
  27. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +317 -0
  28. package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +133 -0
  29. package/skills/c64-ram-capture/scripts/project-paths.mjs +81 -0
  30. package/skills/c64-ram-capture/scripts/releases.mjs +109 -0
  31. package/skills/c64-ram-capture/scripts/test-corpus.mjs +75 -0
  32. package/skills/c64-ram-capture/scripts/watch-loads.mjs +575 -0
  33. package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +339 -0
  34. package/skills/c64-ram-capture/templates/capture-record.template.md +59 -0
  35. package/skills/vice-wedge-triage/SKILL.md +149 -0
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ // Where this toolkit's data lives, resolved portably.
3
+ //
4
+ // These modules ship as a bundled skill toolkit and may be installed at any
5
+ // depth in any project, so nothing here counts directory hops. Two rules:
6
+ //
7
+ // 1. The project root is found by walking UP for a `.git` marker. Counting
8
+ // hops from `import.meta.url` breaks the moment the toolkit is installed
9
+ // somewhere other than `.claude/skills/<skill>/scripts/`, and it breaks
10
+ // silently -- paths resolve to a plausible wrong place rather than erroring.
11
+ // 2. Every data location is overridable by environment variable, so a project
12
+ // that does not use this repo's `recovery/` + `disks/` layout can point the
13
+ // toolkit at its own without editing any module.
14
+ //
15
+ // Pure path arithmetic over the filesystem. Contacts nothing.
16
+ import { existsSync } from "node:fs";
17
+ import { fileURLToPath } from "node:url";
18
+ import { dirname, join, resolve, parse } from "node:path";
19
+
20
+ const HERE = dirname(fileURLToPath(import.meta.url));
21
+
22
+ /**
23
+ * Nearest ancestor directory containing a `.git` entry, starting from this
24
+ * file. Falls back to `C64RE_PROJECT_ROOT` when set, which also covers the
25
+ * case of running from an export with no git metadata at all.
26
+ */
27
+ export function projectRoot() {
28
+ if (process.env.C64RE_PROJECT_ROOT) return resolve(process.env.C64RE_PROJECT_ROOT);
29
+ let dir = HERE;
30
+ const { root } = parse(dir);
31
+ while (true) {
32
+ if (existsSync(join(dir, ".git"))) return dir;
33
+ if (dir === root) break;
34
+ dir = dirname(dir);
35
+ }
36
+ throw new Error(
37
+ "project-paths: could not locate the project root -- no `.git` found above " +
38
+ `${HERE}. Set C64RE_PROJECT_ROOT to the directory that holds your data dirs.`,
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Directory holding the release registry and the per-release dump directories.
44
+ * Defaults to `<project root>/recovery`; override with `C64RE_DATA_DIR`.
45
+ */
46
+ export function dataRoot() {
47
+ return process.env.C64RE_DATA_DIR
48
+ ? resolve(process.env.C64RE_DATA_DIR)
49
+ : join(projectRoot(), "recovery");
50
+ }
51
+
52
+ /**
53
+ * Directory holding the disk images a registry entry's `disk_image` is
54
+ * relative to. Defaults to the project root itself, because registry entries
55
+ * record project-relative paths like `disks/foo.d64`; override with
56
+ * `C64RE_DISKS_ROOT` when they are relative to something else.
57
+ */
58
+ export function disksRoot() {
59
+ return process.env.C64RE_DISKS_ROOT
60
+ ? resolve(process.env.C64RE_DISKS_ROOT)
61
+ : projectRoot();
62
+ }
63
+
64
+ /** The release registry file. Override the whole path with `C64RE_REGISTRY`. */
65
+ export function registryFile() {
66
+ return process.env.C64RE_REGISTRY
67
+ ? resolve(process.env.C64RE_REGISTRY)
68
+ : join(dataRoot(), "RELEASES.json");
69
+ }
70
+
71
+ /** One release's own data directory. */
72
+ export function releaseDataDir(id) {
73
+ return join(dataRoot(), id);
74
+ }
75
+
76
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
77
+ console.log(`project root: ${projectRoot()}`);
78
+ console.log(`data root: ${dataRoot()}`);
79
+ console.log(`disks root: ${disksRoot()}`);
80
+ console.log(`registry: ${registryFile()}`);
81
+ }
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ // N-way release registry accessor. This is the only module that reads a release
3
+ // identifier out of the registry -- every other module takes the id as an
4
+ // argument and never touches the registry file directly. `release` is the
5
+ // primary noun rather than "the canonical image": there are N releases, each
6
+ // owning a set of dumps, with `canonical` demoted to a boolean on one entry.
7
+ //
8
+ // Portable: the registry's location comes from `project-paths.mjs`, so a project
9
+ // with a different data layout points the toolkit at its own via
10
+ // `C64RE_DATA_DIR` / `C64RE_REGISTRY` rather than editing this file.
11
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
12
+ import { fileURLToPath } from "node:url";
13
+ import { resolve } from "node:path";
14
+
15
+ import { registryFile, releaseDataDir } from "./project-paths.mjs";
16
+
17
+ export const registryPath = registryFile();
18
+
19
+ const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
20
+
21
+ export function loadRegistry() {
22
+ if (!existsSync(registryPath)) {
23
+ throw new Error(`no registry at ${registryPath}`);
24
+ }
25
+ return JSON.parse(readFileSync(registryPath, "utf8"));
26
+ }
27
+
28
+ /**
29
+ * Persist the registry. JSON.stringify preserves each object's insertion
30
+ * (key) order, so a read-modify-write via upsertRelease keeps a stable key
31
+ * order automatically -- callers should not rebuild release objects from
32
+ * scratch with a different key order.
33
+ */
34
+ function saveRegistry(reg) {
35
+ writeFileSync(registryPath, JSON.stringify(reg, null, 2) + "\n");
36
+ }
37
+
38
+ /** The full entry for `id`, or throws with the known-id list on a miss. */
39
+ export function release(id) {
40
+ const reg = loadRegistry();
41
+ const r = reg.releases.find((r) => r.id === id);
42
+ if (!r) assertKnownRelease(id, reg);
43
+ return r;
44
+ }
45
+
46
+ /**
47
+ * The registry's own N-readiness documentation (01-03-PLAN.md's Task 2):
48
+ * a top-level `schema_notes` string, sibling to `schema_version` and
49
+ * `releases`, stating the mechanical claim that adding a release is one
50
+ * `releases[]` entry plus one invocation of `tools/recover.mjs`. Kept as a
51
+ * plain top-level field rather than a JSON comment (JSON has none) or a
52
+ * per-release field (it describes the registry's shape, not any one
53
+ * release). Rehearsed against the real validator in
54
+ * a release's own NOTES.md.
55
+ */
56
+ export function schemaNotes() {
57
+ return loadRegistry().schema_notes ?? null;
58
+ }
59
+
60
+ export function releaseDir(id) {
61
+ const reg = loadRegistry();
62
+ assertKnownRelease(id, reg);
63
+ return releaseDataDir(id);
64
+ }
65
+
66
+ /** Dies with the list of known ids on a miss -- see plan Layer 2. */
67
+ export function assertKnownRelease(id, reg) {
68
+ const registry = reg || loadRegistry();
69
+ const known = registry.releases.map((r) => r.id);
70
+ if (!known.includes(id)) {
71
+ throw new Error(`unknown release "${id}" -- known releases: ${known.join(", ")}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Read-modify-write: `fn` receives a shallow copy of the release entry and
77
+ * returns the replacement; the whole registry is then re-persisted with
78
+ * stable key order. This is the only sanctioned way to mutate an entry.
79
+ */
80
+ export function upsertRelease(id, fn) {
81
+ const reg = loadRegistry();
82
+ const idx = reg.releases.findIndex((r) => r.id === id);
83
+ if (idx === -1) {
84
+ throw new Error(`unknown release "${id}" -- known releases: ${reg.releases.map((r) => r.id).join(", ")}`);
85
+ }
86
+ reg.releases[idx] = fn({ ...reg.releases[idx] });
87
+ saveRegistry(reg);
88
+ return reg.releases[idx];
89
+ }
90
+
91
+ // -------------------------------------------------------------------- CLI
92
+
93
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
94
+ const [cmd, ...rest] = process.argv.slice(2);
95
+ if (cmd === "list") {
96
+ const reg = loadRegistry();
97
+ for (const r of reg.releases) {
98
+ console.log(`${r.id} canonical=${r.canonical} disk_image=${r.disk_image} dumps=${r.dumps.length}`);
99
+ }
100
+ } else if (cmd === "show") {
101
+ if (!rest[0]) die("usage: show <release-id>");
102
+ console.log(JSON.stringify(release(rest[0]), null, 2));
103
+ } else if (cmd === "schema-notes") {
104
+ console.log(schemaNotes() ?? "(no schema_notes field set)");
105
+ } else {
106
+ console.log(`usage: node ${fileURLToPath(import.meta.url)} <list|show <id>|schema-notes>`);
107
+ process.exit(cmd ? 1 : 0);
108
+ }
109
+ }
@@ -0,0 +1,75 @@
1
+ // Locates committed artifacts through the registry, so tests exercise whatever
2
+ // corpus the host project actually has instead of naming one project's files.
3
+ //
4
+ // The point is portability without losing coverage: a project with captures gets
5
+ // the real-artifact assertions; a project with none gets them skipped, not
6
+ // failed. Hardcoding `recovery/<some-release>/dumps/<some-label>.state.json`
7
+ // meant this toolkit's tests could only ever pass in the repo they were written
8
+ // in, which is the opposite of shippable.
9
+ //
10
+ // Test-support only. Pure filesystem reads; contacts nothing.
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ import { projectRoot } from "./project-paths.mjs";
15
+ import { loadRegistry } from "./releases.mjs";
16
+
17
+ /**
18
+ * First dump in the registry whose `field` names a file that exists, as
19
+ * `{ release, label, path }` -- or null when the registry is absent, empty, or
20
+ * names nothing on disk. Never throws: a missing registry is a skip, not a
21
+ * failure.
22
+ */
23
+ export function firstDumpArtifact(field) {
24
+ let reg;
25
+ try {
26
+ reg = loadRegistry();
27
+ } catch {
28
+ return null;
29
+ }
30
+ for (const r of reg.releases ?? []) {
31
+ for (const d of r.dumps ?? []) {
32
+ const value = d[field];
33
+ if (!value) continue;
34
+ const path = join(projectRoot(), value);
35
+ if (existsSync(path)) return { release: r.id, label: d.label, path };
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /** Every dump in the registry whose `field` names an existing file. */
42
+ export function allDumpArtifacts(field) {
43
+ let reg;
44
+ try {
45
+ reg = loadRegistry();
46
+ } catch {
47
+ return [];
48
+ }
49
+ const out = [];
50
+ for (const r of reg.releases ?? []) {
51
+ for (const d of r.dumps ?? []) {
52
+ const value = d[field];
53
+ if (!value) continue;
54
+ const path = join(projectRoot(), value);
55
+ if (existsSync(path)) out.push({ release: r.id, label: d.label, path });
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+
61
+ /** Parsed JSON for a `firstDumpArtifact` hit, or null. */
62
+ export function readJsonArtifact(field) {
63
+ const hit = firstDumpArtifact(field);
64
+ if (!hit) return null;
65
+ return { ...hit, json: JSON.parse(readFileSync(hit.path, "utf8")) };
66
+ }
67
+
68
+ /**
69
+ * node:test `skip` value: `false` to run, or a human-readable reason string.
70
+ * Pass the thing you looked for so a skipped run explains itself.
71
+ */
72
+ export function skipUnless(found, what) {
73
+ if (found && (!Array.isArray(found) || found.length > 0)) return false;
74
+ return `no ${what} found via this project's registry -- corpus-dependent check skipped`;
75
+ }