@henols/c64-re-tools 1.1.0 → 2.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -37
- package/distribution/package.ts +21 -0
- package/distribution/plugin.ts +122 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +76 -0
- package/skills/c64-assembler/scripts/assemble.ts +110 -0
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +66 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +93 -0
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +120 -0
- package/skills/c64-emulator/SKILL.md +134 -0
- package/skills/c64-knowledge/SKILL.md +166 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
- package/skills/c64-memory-map/SKILL.md +79 -0
- package/skills/c64-memory-map/scripts/memmap.ts +92 -0
- package/skills/c64-provenance/SKILL.md +110 -0
- package/skills/c64-provenance/scripts/compare.ts +152 -0
- package/skills/c64-provenance/scripts/provenance.ts +334 -0
- package/skills/c64-provenance/scripts/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/separate.ts +140 -0
- package/skills/c64-reverse-engineering/SKILL.md +144 -0
- package/skills/c64-source/SKILL.md +126 -0
- package/skills/c64-source/scripts/generate.ts +490 -0
- package/skills/c64-source/scripts/loader.ts +571 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +595 -0
- package/skills/c64-static-analysis/SKILL.md +119 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +75 -0
- package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +103 -0
- package/skills/c64-testing/references/scenario-format.md +116 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +493 -0
- package/skills/c64-testing/scripts/test.ts +65 -0
- package/skills/c64-unpacker/SKILL.md +135 -0
- package/skills/c64-unpacker/scripts/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/stages.ts +130 -0
- package/skills/c64-unpacker/scripts/unpack.ts +683 -0
- package/src/c64.ts +411 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +125 -0
- package/src/cli/status.ts +61 -0
- package/src/host/main.ts +134 -0
- package/src/host/server.ts +392 -0
- package/src/host/tools/c1541.ts +284 -0
- package/src/host/tools/index.ts +35 -0
- package/src/host/tools/petcat.ts +77 -0
- package/src/host/tools/status.ts +47 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +804 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +118 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +413 -0
- package/src/host/vice/screen.ts +184 -0
- package/src/host/vice/session.ts +1426 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +126 -0
- package/src/host-client/vice-session.ts +316 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +163 -0
- package/src/knowledge/import.ts +391 -0
- package/src/knowledge/read.ts +342 -0
- package/src/knowledge/schema.ts +229 -0
- package/src/knowledge/write.ts +663 -0
- package/src/mcp/main.ts +134 -0
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +316 -0
- package/src/mcp/tools/debug.ts +327 -0
- package/src/mcp/tools/execution.ts +117 -0
- package/src/mcp/tools/input.ts +72 -0
- package/src/mcp/tools/machine.ts +100 -0
- package/src/mcp/tools/media.ts +129 -0
- package/src/mcp/tools/memory.ts +250 -0
- package/src/mcp/tools/video.ts +271 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +179 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +173 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +274 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +113 -0
- package/src/native/types.ts +257 -0
- package/src/native/watchdog.ts +78 -0
- package/src/package.ts +21 -0
- package/src/platform.ts +441 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +259 -0
- package/src/protocol/tools.ts +266 -0
- package/src/protocol/vice.ts +1348 -0
- package/src/script.ts +60 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +194 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- 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
|
-
}
|