@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11

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 (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -1,395 +0,0 @@
1
- #!/usr/bin/env node
2
- // The N-way release-registry invariant validator, and the release-name
3
- // parameterisation gate. Neither touches the emulator; both are pure
4
- // Node/filesystem checks over `recovery/RELEASES.json` and the files it
5
- // references, run entirely offline.
6
- //
7
- // This is the mechanical enforcement of the assumption_delta
8
- // decision: the registry is release-CENTRIC (N releases, each a full field
9
- // set, `canonical` demoted to a boolean on one entry), never
10
- // canonical-image-centric again. A future plan that quietly reintroduces a
11
- // privileged singular image, or hardcodes a release name into control flow,
12
- // fails loudly here instead of silently regressing the model.
13
- //
14
- // Every failure names the release, the file, and the field -- never a bare
15
- // "validation failed" -- per this plan's own instruction.
16
- import { readFileSync, readdirSync, existsSync } from "node:fs";
17
- import { createHash } from "node:crypto";
18
- import { fileURLToPath } from "node:url";
19
- import { dirname, join, resolve, relative, extname } from "node:path";
20
-
21
- import { loadRegistry, registryPath } from "../../c64-ram-capture/scripts/releases.mjs";
22
- import { projectRoot, dataRoot, disksRoot } from "../../c64-ram-capture/scripts/project-paths.mjs";
23
-
24
- const HERE = dirname(fileURLToPath(import.meta.url));
25
- const REPO_ROOT = projectRoot();
26
- const RECOVERY_DIR = dataRoot();
27
- const DISKS_DIR = disksRoot();
28
-
29
- const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
30
-
31
- // The parameterisation gate must cover EVERY module of the recovery pipeline, not
32
- // just the ones sitting next to this file. When the six modules moved out of
33
- // `tools/` into the two skills that use them (2026-08-04), a `HERE`-only scan
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,
39
- // naming the skills tree's pre-relocation auto-discovery location by hand -- which
40
- // stopped resolving the moment the skills tree moved to its current source-tree
41
- // location. Rebuilt `HERE`-relative instead -- correct in both this dev checkout
42
- // and a consumer's install, since a sibling skill's `scripts/` directory is always
43
- // one level up and back down from this file's own location either way.
44
- const SCAN_DIRS = [
45
- HERE, // src/skills/c64-provenance-diff/scripts
46
- resolve(HERE, "..", "..", "c64-ram-capture", "scripts"),
47
- ];
48
- for (const dir of SCAN_DIRS) {
49
- if (!existsSync(dir)) die(`SCAN_DIRS entry does not exist: ${dir} -- a guard that keeps passing while checking less is worse than one that fails`);
50
- }
51
-
52
- function sha256File(path) {
53
- return createHash("sha256").update(readFileSync(path)).digest("hex");
54
- }
55
-
56
- function rel(p) {
57
- return relative(REPO_ROOT, p);
58
- }
59
-
60
- // ---------------------------------------------------------------- validate
61
-
62
- const REQUIRED_DUMP_FILE_FIELDS = ["bin", "capture_record", "chip_state", "range_manifest"];
63
-
64
- // Directories under recovery/ that are NOT per-release directories and are
65
- // never expected to have a matching releases[] entry: `clean/` is the
66
- // canonical-image projection (checked separately, below), and `machine/`
67
- // holds machine-level (not release-level) evidence -- the power-on baseline
68
- // and decay-prone address set captured once per emulator, with no release
69
- // identity of its own (see tools/recover.mjs's `baseline`/`decay-reference`
70
- // verbs).
71
- const NON_RELEASE_DIRS = ["clean", "machine"];
72
-
73
- /**
74
- * Base invariants, checked on every `validate` run regardless of `--final`.
75
- * Returns a flat list of addressed error strings (each names the release,
76
- * file, and/or field involved) -- never a bare boolean.
77
- */
78
- function runBaseChecks(registry) {
79
- const errors = [];
80
- const releases = registry.releases;
81
-
82
- // -- directory <-> registry entry correspondence: no orphan on either side --
83
- const dirEntries = readdirSync(RECOVERY_DIR, { withFileTypes: true })
84
- .filter((d) => d.isDirectory() && !NON_RELEASE_DIRS.includes(d.name))
85
- .map((d) => d.name);
86
- const registryIds = releases.map((r) => r.id);
87
-
88
- for (const dirName of dirEntries) {
89
- if (!registryIds.includes(dirName)) {
90
- errors.push(`orphan directory recovery/${dirName}/ has no matching releases[] entry (known ids: ${registryIds.join(", ")})`);
91
- }
92
- }
93
- for (const id of registryIds) {
94
- if (!dirEntries.includes(id)) {
95
- errors.push(`releases[] entry "${id}" has no matching recovery/${id}/ directory`);
96
- }
97
- }
98
-
99
- // -- every release conforms to the same field set --
100
- if (releases.length > 0) {
101
- const canonicalFieldSet = Object.keys(releases[0]).sort().join(",");
102
- for (const r of releases) {
103
- const fieldSet = Object.keys(r).sort().join(",");
104
- if (fieldSet !== canonicalFieldSet) {
105
- errors.push(
106
- `release "${r.id}" has a different field set than release "${releases[0].id}" -- ` +
107
- `got [${Object.keys(r).sort().join(", ")}], expected [${Object.keys(releases[0]).sort().join(", ")}]`
108
- );
109
- }
110
- }
111
- }
112
-
113
- // -- at most one canonical:true, and the designation is a field, never a --
114
- // -- directory name or filename --
115
- const canonicalReleases = releases.filter((r) => r.canonical === true);
116
- if (canonicalReleases.length > 1) {
117
- errors.push(
118
- `more than one release carries canonical:true -- ${canonicalReleases.map((r) => r.id).join(", ")}. ` +
119
- `At most one release may be canonical.`
120
- );
121
- }
122
-
123
- // -- each release's disk_sha256 still matches the file under disks/ --
124
- for (const r of releases) {
125
- const diskPath = join(REPO_ROOT, r.disk_image);
126
- if (!existsSync(diskPath)) {
127
- errors.push(`release "${r.id}": disk_image "${r.disk_image}" does not exist on disk`);
128
- continue;
129
- }
130
- const actual = sha256File(diskPath);
131
- if (actual !== r.disk_sha256) {
132
- errors.push(
133
- `release "${r.id}": disk_sha256 field "${r.disk_sha256}" does not match the actual sha256 of ` +
134
- `${r.disk_image} ("${actual}") -- the evidence file may have been mutated`
135
- );
136
- }
137
- }
138
-
139
- // -- every dumps[] entry names four existing files; the bin's sha256 --
140
- // -- matches the file on disk --
141
- for (const r of releases) {
142
- for (const d of r.dumps ?? []) {
143
- for (const field of REQUIRED_DUMP_FILE_FIELDS) {
144
- const value = d[field];
145
- if (!value) {
146
- errors.push(`release "${r.id}" dump "${d.label}": field "${field}" is not set (a dump is a four-file set)`);
147
- continue;
148
- }
149
- const filePath = join(REPO_ROOT, value);
150
- if (!existsSync(filePath)) {
151
- errors.push(`release "${r.id}" dump "${d.label}": field "${field}" names "${value}", which does not exist`);
152
- }
153
- }
154
- if (d.bin && existsSync(join(REPO_ROOT, d.bin))) {
155
- const actual = sha256File(join(REPO_ROOT, d.bin));
156
- if (d.sha256 && actual !== d.sha256) {
157
- errors.push(
158
- `release "${r.id}" dump "${d.label}": recorded sha256 "${d.sha256}" does not match the actual ` +
159
- `sha256 of ${d.bin} ("${actual}")`
160
- );
161
- }
162
- }
163
- }
164
- }
165
-
166
- // -- `<data root>/clean/*.bin`, when present, is a PROJECTION: exactly one --
167
- // -- canonical release, and the file byte-identical to that release's --
168
- // -- primary dump. --
169
- //
170
- // The clean copy is DISCOVERED rather than named, so this works whatever a
171
- // project calls its projection. Naming one specific file meant the check
172
- // silently passed for every project that named it anything else -- a guard
173
- // that quietly stops guarding is worse than one that fails.
174
- const cleanDir = join(RECOVERY_DIR, "clean");
175
- const cleanBins = existsSync(cleanDir)
176
- ? readdirSync(cleanDir).filter((f) => f.toLowerCase().endsWith(".bin")).sort()
177
- : [];
178
-
179
- if (cleanBins.length > 1) {
180
- errors.push(
181
- `${rel(cleanDir)} holds ${cleanBins.length} .bin files (${cleanBins.join(", ")}) -- ` +
182
- `the clean projection must be a single file, otherwise there is no way to tell which one ` +
183
- `is the projection of the canonical release`
184
- );
185
- } else if (cleanBins.length === 1) {
186
- const cleanBinPath = join(cleanDir, cleanBins[0]);
187
- const cleanName = rel(cleanBinPath);
188
- if (canonicalReleases.length !== 1) {
189
- errors.push(
190
- `${cleanName} exists, but ${canonicalReleases.length} releases carry canonical:true ` +
191
- `(expected exactly 1) -- the projection has no single source to be a projection OF`
192
- );
193
- } else {
194
- const canonicalRelease = canonicalReleases[0];
195
- const primaryDump = (canonicalRelease.dumps ?? [])[0];
196
- if (!primaryDump || !primaryDump.bin || !existsSync(join(REPO_ROOT, primaryDump.bin))) {
197
- errors.push(
198
- `${cleanName} exists, but canonical release "${canonicalRelease.id}" has no ` +
199
- `primary dump with an existing .bin to compare against`
200
- );
201
- } else {
202
- const cleanHash = sha256File(cleanBinPath);
203
- const primaryHash = sha256File(join(REPO_ROOT, primaryDump.bin));
204
- if (cleanHash !== primaryHash) {
205
- errors.push(
206
- `${cleanName} (sha256 ${cleanHash}) is NOT byte-identical to canonical release ` +
207
- `"${canonicalRelease.id}"'s primary dump ${primaryDump.bin} (sha256 ${primaryHash}) -- ` +
208
- `the clean copy must be an exact projection, never an independent artifact`
209
- );
210
- }
211
- }
212
- }
213
- }
214
-
215
- return errors;
216
- }
217
-
218
- /** End-of-phase-only assertions, added by `validate --final`. */
219
- function runFinalChecks(registry) {
220
- const errors = [];
221
- const releases = registry.releases;
222
-
223
- for (const r of releases) {
224
- for (const d of r.dumps ?? []) {
225
- if (!d.range_manifest) continue; // already reported by runBaseChecks
226
- const manifestPath = join(REPO_ROOT, d.range_manifest);
227
- if (!existsSync(manifestPath)) continue; // already reported by runBaseChecks
228
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
229
- if (manifest.classification_state !== "bucketed") {
230
- errors.push(
231
- `release "${r.id}" dump "${d.label}": range_manifest ${d.range_manifest} has classification_state ` +
232
- `"${manifest.classification_state}", expected "bucketed" at the end of the phase`
233
- );
234
- }
235
- const unclassified = (manifest.ranges ?? []).filter((rg) => rg.kind === "unclassified");
236
- if (unclassified.length > 0) {
237
- errors.push(
238
- `release "${r.id}" dump "${d.label}": range_manifest ${d.range_manifest} still has ` +
239
- `${unclassified.length} range(s) with the transient kind "unclassified" -- must be fully bucketed`
240
- );
241
- }
242
- }
243
-
244
- if (!r.trigger || r.trigger.address == null) {
245
- errors.push(`release "${r.id}": trigger.address is null -- every release needs a non-null recorded trigger by the end of the phase`);
246
- }
247
- }
248
-
249
- const canonicalReleases = releases.filter((r) => r.canonical === true);
250
- if (canonicalReleases.length !== 1) {
251
- errors.push(
252
- `exactly one release must carry canonical:true at the end of the phase -- found ${canonicalReleases.length} ` +
253
- `(${canonicalReleases.map((r) => r.id).join(", ") || "none"})`
254
- );
255
- }
256
-
257
- return errors;
258
- }
259
-
260
- export function validateRegistry({ final = false } = {}) {
261
- const registry = loadRegistry();
262
- const errors = [...runBaseChecks(registry)];
263
- if (final) errors.push(...runFinalChecks(registry));
264
- return { ok: errors.length === 0, final, errors };
265
- }
266
-
267
- /** Validate a single release directory's shape against a loaded registry entry. */
268
- export function validateReleaseDir(id) {
269
- const registry = loadRegistry();
270
- const r = registry.releases.find((x) => x.id === id);
271
- if (!r) return { ok: false, errors: [`no releases[] entry for "${id}"`] };
272
- const dirPath = join(RECOVERY_DIR, id);
273
- if (!existsSync(dirPath)) return { ok: false, errors: [`recovery/${id}/ does not exist`] };
274
- const errors = [];
275
- for (const d of r.dumps ?? []) {
276
- for (const field of REQUIRED_DUMP_FILE_FIELDS) {
277
- if (!d[field]) errors.push(`release "${id}" dump "${d.label}": field "${field}" is not set`);
278
- }
279
- }
280
- return { ok: errors.length === 0, errors };
281
- }
282
-
283
- // ------------------------------------------------------- check-parameterisation
284
-
285
- function listMjsFiles(dir) {
286
- const out = [];
287
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
288
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
289
- const full = join(dir, entry.name);
290
- if (entry.isDirectory()) out.push(...listMjsFiles(full));
291
- else if (entry.isFile() && extname(entry.name) === ".mjs") out.push(full);
292
- }
293
- return out;
294
- }
295
-
296
- // Matches a registry release id used as the operand of a conditional
297
- // comparison or a switch/case label -- exactly the "branch on a release
298
- // identifier" antipattern this gate exists to catch. A bare string literal
299
- // elsewhere (a fixture path in a test, a usage example) does not match any
300
- // of these and is not flagged.
301
- function conditionalPatternsFor(id) {
302
- const q = `["'\`]${id}["'\`]`;
303
- return [
304
- new RegExp(`===\\s*${q}`),
305
- new RegExp(`${q}\\s*===`),
306
- new RegExp(`[^=!]==\\s*${q}`),
307
- new RegExp(`${q}\\s*==[^=]`),
308
- new RegExp(`!==\\s*${q}`),
309
- new RegExp(`${q}\\s*!==`),
310
- new RegExp(`case\\s+${q}\\s*:`),
311
- ];
312
- }
313
-
314
- // A CALL of the forbidden tool, as opposed to its name appearing in a
315
- // comment, a doc string, or (elsewhere in the project, never under tools/) a
316
- // deny-list array literal.
317
- const DENY_LIST_CALL_PATTERN = /\bcall(?:Tool)?\s*\(\s*["'`]vice_disk_list["'`]/;
318
-
319
- export function checkParameterisation({ toolsDir = SCAN_DIRS } = {}) {
320
- const registry = loadRegistry();
321
- const ids = registry.releases.map((r) => r.id);
322
- const dirs = (Array.isArray(toolsDir) ? toolsDir : [toolsDir]).filter((d) => existsSync(d));
323
- const files = dirs.flatMap((d) => listMjsFiles(d));
324
-
325
- const violations = [];
326
- const denyListCallViolations = [];
327
-
328
- for (const file of files) {
329
- const content = readFileSync(file, "utf8");
330
- const relFile = rel(file);
331
- for (const id of ids) {
332
- for (const pattern of conditionalPatternsFor(id)) {
333
- if (pattern.test(content)) {
334
- violations.push({ file: relFile, release: id, pattern: pattern.source });
335
- }
336
- }
337
- }
338
- if (DENY_LIST_CALL_PATTERN.test(content)) {
339
- denyListCallViolations.push(relFile);
340
- }
341
- }
342
-
343
- return {
344
- ok: violations.length === 0 && denyListCallViolations.length === 0,
345
- filesScanned: files.length,
346
- releaseIds: ids,
347
- violations,
348
- denyListCallViolations,
349
- };
350
- }
351
-
352
- // -------------------------------------------------------------------- CLI
353
-
354
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
355
- const [cmd, ...rest] = process.argv.slice(2);
356
- const jsonFlag = rest.includes("--json");
357
- const finalFlag = rest.includes("--final");
358
-
359
- function run() {
360
- if (cmd === "validate") {
361
- const result = validateRegistry({ final: finalFlag });
362
- if (jsonFlag) {
363
- console.log(JSON.stringify(result, null, 2));
364
- } else if (result.ok) {
365
- console.log(`validate${finalFlag ? " --final" : ""}: OK (registry ${rel(registryPath)})`);
366
- } else {
367
- console.error(`validate${finalFlag ? " --final" : ""}: FAILED with ${result.errors.length} error(s):`);
368
- for (const e of result.errors) console.error(` - ${e}`);
369
- }
370
- process.exitCode = result.ok ? 0 : 1;
371
- return;
372
- }
373
- if (cmd === "check-parameterisation") {
374
- const result = checkParameterisation();
375
- if (jsonFlag) {
376
- console.log(JSON.stringify(result, null, 2));
377
- } else {
378
- console.log(`check-parameterisation: scanned ${result.filesScanned} file(s) across the pipeline scripts dirs against release ids [${result.releaseIds.join(", ")}]`);
379
- for (const v of result.violations) {
380
- console.error(` - ${v.file}: release id "${v.release}" appears in a conditional (matched /${v.pattern}/)`);
381
- }
382
- for (const f of result.denyListCallViolations) {
383
- console.error(` - ${f}: calls the forbidden vice_disk_list tool directly`);
384
- }
385
- console.log(result.ok ? "check-parameterisation: OK" : "check-parameterisation: FAILED");
386
- }
387
- process.exitCode = result.ok ? 0 : 1;
388
- return;
389
- }
390
- console.log(`usage: node ${fileURLToPath(import.meta.url)} <validate [--final]|check-parameterisation> [--json]`);
391
- process.exitCode = cmd ? 1 : 0;
392
- }
393
-
394
- run();
395
- }
@@ -1,17 +0,0 @@
1
- {
2
- "schema_version": "1.0",
3
- "schema_notes": "Adding a release is one releases[] entry plus one invocation of tools/recover.mjs -- see c64-ram-capture/SKILL.md '## Release registry shape'.",
4
- "releases": [
5
- {
6
- "id": "example-release",
7
- "canonical": true,
8
- "disk_image": "disks/example-release.d64",
9
- "dumps": [
10
- {
11
- "label": "example-label",
12
- "range_manifest": "recovery/example-release/dumps/example-release-example-label.map.json"
13
- }
14
- ]
15
- }
16
- ]
17
- }