@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,463 @@
1
+ #!/usr/bin/env node
2
+ // completeness-report.mjs -- renders the per-fixture decomposition
3
+ // completeness report, and OWNS the GATE: this script's own process exit
4
+ // code is routine-queue-walker's numeric stop condition -- 0 only when every measure below
5
+ // clears its own bar, 1 the instant any one of them does not, naming which
6
+ // measure and which address failed.
7
+ //
8
+ // WHY THIS FILE EXISTS: criterion 2's disagreement query and criterion 1's
9
+ // zero-`Undefined` census must be answered TOGETHER, from ONE real store, or
10
+ // a completeness claim can hide a weak measure behind a strong one -- exactly
11
+ // the failure `printCoverageReport()`/`printEvidDisagreementsReport()` were
12
+ // each already built to avoid, one verb over. `routine-queue-walker` already
13
+ // exists to drive an annotation store's backlog to closure and report every
14
+ // leftover; this script supplies the numeric stop condition it currently
15
+ // lacks.
16
+ //
17
+ // WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: rendering the per-fixture
18
+ // decomposition-completeness report, REFUSING to render at all without the
19
+ // disagreement input (a required output-schema field only that input can
20
+ // populate), and computing the GATE's pass/fail verdict
21
+ // and exit code from the rendered measures.
22
+ //
23
+ // WHAT NOT TO DO:
24
+ // - Never derive a completeness measure from the store's block-type
25
+ // listing directly in THIS file. `anno-coverage.ts`'s own trap 1 forbids
26
+ // it there, and extending or re-implementing that module here is
27
+ // equally forbidden -- this script never reads a store; it renders the fifth CLI
28
+ // verb's own `--json` answer, which already did the reading.
29
+ // - Never render without the disagreement-query input. A missing input is
30
+ // refused by name (`MissingDisagreementInputError`), never defaulted to
31
+ // an empty array -- an omitted query and a query that found nothing must
32
+ // never look the same.
33
+ // - Never print a percentage, rate or combined figure. Every count in this
34
+ // report carries its own denominator, exactly like
35
+ // `printEvidDisagreementsReport()`'s own discipline.
36
+ // - Never re-implement `evid-reconcile.ts`'s four-bucket join, or rename
37
+ // any of its field names. This script only ever reads
38
+ // `disagreementInput`'s fields verbatim, as `anno decomp-completeness
39
+ // --json` already named them.
40
+ // - Never restate any of the four split-table `SPLIT_DATA_TYPES` spellings
41
+ // as a literal string in THIS file (a mechanical grep guard over this
42
+ // exact file is a standing acceptance criterion). The "table" naming
43
+ // is computed on the VERB side (`anno-cli.ts`'s own
44
+ // `renderedType` field, read from `anno-types.ts`'s `isSplitDataType()`)
45
+ // and this script only ever renders `renderedType` verbatim.
46
+ // - Never soften a gate failure into a bulletin. `computeGateFailures()`
47
+ // below is the ONE place a measure becomes a pass/fail verdict; a
48
+ // softened refusal here is exactly what planted controls 1 and 2 (task
49
+ // 2) exist to catch going RED.
50
+ // - Never carry a second copy of the MCP-module resolution ladder. It
51
+ // lives ONE place, `../../c64-ram-capture/scripts/mcp-module.mjs`, and is
52
+ // imported from there.
53
+ import { spawnSync } from "node:child_process";
54
+
55
+ import { resolveMcpModule, refusalMessage, TARGET_PACKAGE } from "../../c64-ram-capture/scripts/mcp-module.mjs";
56
+
57
+ /** The MCP-side entry point this script forwards to -- the SAME "node
58
+ * vice-proxy.ts anno <verb>" invocation `routine-queue-walker/SKILL.md`'s
59
+ * skill's own closing `anno coverage` call already uses (no broker, no
60
+ * container-out seam -- the store is `node:sqlite` in-process, and this
61
+ * script's own job is orchestration, never a store read of its own). */
62
+ const TARGET_FILE = "vice-proxy.ts";
63
+
64
+ /** Thrown by `renderCompletenessReport()` when `report.disagreementInput` is
65
+ * absent, or present but missing a complete `runIdentity` -- mechanism
66
+ * 2's own required output-schema field. The message always names the
67
+ * `--disagreements` flag literally, so a caller reading only the thrown
68
+ * message still knows what to pass. */
69
+ export class MissingDisagreementInputError extends Error {
70
+ constructor(message) {
71
+ super(message);
72
+ this.name = "MissingDisagreementInputError";
73
+ }
74
+ }
75
+
76
+ /**
77
+ * The frozen survivor prefix set, MIRRORED from
78
+ * `src/mcp/vice/anno-cli.ts`'s own frozen set -- see
79
+ * docs/phase45-wave0-measurements.md for the MEASURED label population this
80
+ * was frozen against. Exported here, separately from the verb's own copy,
81
+ * because this script's own tests must be able to assert on the predicate in
82
+ * isolation, without a live store or a subprocess -- and because this
83
+ * script's own header forbids it from reading a store directly, so it
84
+ * cannot import the verb's copy through anything but a duplicate literal.
85
+ *
86
+ * WHAT NOT TO DO: if the frozen set in `anno-cli.ts` ever changes, this copy
87
+ * moves in the SAME commit, or the two renderers silently disagree about
88
+ * what "survivor" means. Never restate `AUTO_NAME_PREFIX_RE`'s eleven
89
+ * prefixes as their own literal strings here -- this predicate matches
90
+ * against a caller-SUPPLIED name (from the verb's own `survivors` answer),
91
+ * never derives a name from a store itself, so there is no store-derived
92
+ * value to keep in sync beyond this one regex pair.
93
+ */
94
+ const AUTO_NAME_PREFIX_RE = /^(zpf_|f_|zpa_|a_|p_|zpp_|e_|j_|s_|b_|r_)/;
95
+ const SURVIVOR_EXTRA_RE = /^(?:l_[0-9a-f]{4}|(?:FUN|LAB)_[0-9a-f]{4}|l[0-9a-f]{3,4})$/;
96
+
97
+ /** True iff `name` is a survivor under the frozen set. ASCII case-sensitive:
98
+ * `l_0810` IS a survivor, `L_0810` is NOT -- `anno-coverage.test.ts`'s own
99
+ * `L_` exclusion precedent, restated for this phase's own prefix set. */
100
+ export function isSurvivorName(name) {
101
+ return AUTO_NAME_PREFIX_RE.test(name) || SURVIVOR_EXTRA_RE.test(name);
102
+ }
103
+
104
+ /**
105
+ * The precedence rule, mirrored here (see this file's own
106
+ * header on why a mirror rather than an import) so this script's own test
107
+ * tier can assert the PRECEDENCE explicitly, not merely pass through a
108
+ * verb-computed value. `anno-cli.ts`'s `typedByFor()` is the authoritative
109
+ * copy that actually runs against a real store; this one exists only to be
110
+ * unit-tested in isolation, exactly like `isSurvivorName()` above. Evidence
111
+ * beats inference: `observed-executing` (a real execute observation exists
112
+ * inside the range) beats `authored` (an `AUTHORED_PROVENANCE_COMMENT_PREFIX`
113
+ * comment exists and there is no observation) beats `byte-derived` (neither).
114
+ * A range that is BOTH observed and authored renders `observed-executing`.
115
+ */
116
+ export function typedByFor(hasObservation, hasAuthoredComment) {
117
+ if (hasObservation) return "observed-executing";
118
+ if (hasAuthoredComment) return "authored";
119
+ return "byte-derived";
120
+ }
121
+
122
+ const PURPOSE_ELEMENT_KEYS = ["function", "inputs", "outputs", "sideEffects"];
123
+
124
+ /**
125
+ * Normalises a raw `anno decomp-completeness --json` answer into the report
126
+ * shape this module renders and gates. Pure: no filesystem, no subprocess, no
127
+ * store. Throws a plain `Error` (never `MissingDisagreementInputError`, which
128
+ * is `renderCompletenessReport()`'s own refusal) when `answer` is not even a
129
+ * plausible answer object -- a caller error, distinct from a missing
130
+ * disagreement input.
131
+ *
132
+ * Every NEW field defaults to the CONSERVATIVE (gate-failing or vacuity-
133
+ * naming) shape when absent, never to a shape that would silently pass --
134
+ * The same refuse-by-name discipline, applied to every field, not only the
135
+ * original disagreement input.
136
+ */
137
+ export function buildCompletenessReport(answer) {
138
+ if (typeof answer !== "object" || answer === null) {
139
+ throw new Error("buildCompletenessReport: expected a decomp-completeness --json answer object, got " + JSON.stringify(answer));
140
+ }
141
+ const disagreementInput = answer.disagreementInput;
142
+ const disagreementResolution =
143
+ answer.disagreementResolution && typeof answer.disagreementResolution === "object"
144
+ ? {
145
+ rows: Array.isArray(answer.disagreementResolution.rows) ? answer.disagreementResolution.rows : [],
146
+ unresolvedCount: answer.disagreementResolution.unresolvedCount ?? (disagreementInput?.disagreementCount ?? 0),
147
+ denominator: answer.disagreementResolution.denominator ?? (disagreementInput?.denominator ? disagreementInput.disagreementCount : 0),
148
+ }
149
+ : {
150
+ rows: [],
151
+ // Conservative default: an answer that carries a real disagreement
152
+ // count but no resolution census at all is treated as ENTIRELY
153
+ // unresolved, never as a silent pass -- the same discipline
154
+ // applies to the disagreement input itself, applied here to its
155
+ // resolution.
156
+ unresolvedCount: disagreementInput?.disagreementCount ?? 0,
157
+ denominator: disagreementInput?.disagreementCount ?? 0,
158
+ };
159
+ return {
160
+ store: answer.store,
161
+ fixture: answer.fixture,
162
+ executionDisposition: answer.executionDisposition,
163
+ notExecutedReason: answer.notExecutedReason ?? null,
164
+ byteCensus: answer.byteCensus,
165
+ survivors: Array.isArray(answer.survivors) ? answer.survivors : [],
166
+ rangeProvenance: Array.isArray(answer.rangeProvenance) ? answer.rangeProvenance : [],
167
+ entryPoints: Array.isArray(answer.entryPoints) ? answer.entryPoints : [],
168
+ referencedAddresses:
169
+ answer.referencedAddresses && typeof answer.referencedAddresses === "object"
170
+ ? {
171
+ resolved: Array.isArray(answer.referencedAddresses.resolved) ? answer.referencedAddresses.resolved : [],
172
+ declined: Array.isArray(answer.referencedAddresses.declined) ? answer.referencedAddresses.declined : [],
173
+ unresolved: Array.isArray(answer.referencedAddresses.unresolved) ? answer.referencedAddresses.unresolved : [],
174
+ denominator: answer.referencedAddresses.denominator ?? 0,
175
+ }
176
+ : { resolved: [], declined: [], unresolved: [], denominator: 0 },
177
+ disagreementInput,
178
+ disagreementResolution,
179
+ };
180
+ }
181
+
182
+ function addr(a) {
183
+ return typeof a === "number" ? `$${a.toString(16).padStart(4, "0")}` : String(a);
184
+ }
185
+
186
+ /**
187
+ * Renders `report` as text, copying `printEvidDisagreementsReport()`'s own
188
+ * rendering discipline exactly: every measure under its own heading,
189
+ * disagreements first, `denominator` beside every count, an explicit
190
+ * sentence stating what absence does NOT prove, and never a percentage,
191
+ * rate or combined figure.
192
+ *
193
+ * THROWS `MissingDisagreementInputError` when
194
+ * `report.disagreementInput` is absent, or present but its own
195
+ * `runIdentity` is neither `null` nor a complete
196
+ * `{ imageSha256, argvDigest, seed }` object. An empty `disagreements`
197
+ * ARRAY alone is not enough to refuse -- that is a real, non-vacuous "zero
198
+ * disagreements" answer; what is refused is the ABSENCE of the input
199
+ * itself.
200
+ *
201
+ * `identity === null` is a THIRD,
202
+ * legitimate value here, mirroring anno-cli.ts's own
203
+ * `validateDisagreementDocumentShape()`/match-check -- the real answer `anno
204
+ * evid-disagreements --json` produces for a store with zero observed runs
205
+ * (a non-executed fixture). By the time a report reaches this
206
+ * function, `anno decomp-completeness`'s own server-side check has already
207
+ * proven that null against the store's own evid-runs table (refusing a
208
+ * null identity on a store that DOES carry real runs) -- this function
209
+ * never re-derives that proof, only trusts an already-validated report.
210
+ */
211
+ export function renderCompletenessReport(report) {
212
+ const input = report?.disagreementInput;
213
+ const identity = input?.runIdentity;
214
+ const identityIsWellFormed =
215
+ identity === null ||
216
+ (typeof identity === "object" && identity !== null && typeof identity.imageSha256 === "string" && typeof identity.argvDigest === "string" && typeof identity.seed === "string");
217
+ if (input === undefined || input === null || !identityIsWellFormed) {
218
+ throw new MissingDisagreementInputError(
219
+ "renderCompletenessReport: no disagreement input is present on this report -- refusing to render. " +
220
+ "Pass --disagreements to `anno decomp-completeness` (the JSON `anno evid-disagreements --json` wrote " +
221
+ "for the SAME store); an omitted query and a query that found nothing must never render the same report.",
222
+ );
223
+ }
224
+
225
+ const lines = [];
226
+ lines.push(`decomposition completeness: ${report.store ?? "(unknown store)"}`);
227
+ lines.push(` FIXTURE: ${report.fixture ?? "(unknown fixture)"}`);
228
+ if (report.executionDisposition === "not-executed") {
229
+ lines.push(` NOT EXECUTED: ${report.notExecutedReason ?? "(no reason recorded)"}`);
230
+ } else {
231
+ lines.push(" EXECUTED: this fixture was run under the reproducible-run protocol.");
232
+ }
233
+ lines.push("");
234
+
235
+ const census = report.byteCensus ?? { byType: {}, undefinedCount: 0, denominator: 0 };
236
+ lines.push(` BYTE CENSUS (denominator ${census.denominator ?? 0})`);
237
+ for (const [type, count] of Object.entries(census.byType ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
238
+ lines.push(` ${type}: ${count} of ${census.denominator ?? 0}`);
239
+ }
240
+ lines.push(` undefined: ${census.undefinedCount ?? 0} of ${census.denominator ?? 0}`);
241
+ for (const gap of Array.isArray(census.undefinedRanges) ? census.undefinedRanges : []) {
242
+ lines.push(` UNDEFINED: ${addr(gap.start)}-${addr(gap.endInclusive)}`);
243
+ }
244
+ lines.push("");
245
+
246
+ const survivors = report.survivors ?? [];
247
+ lines.push(` SURVIVORS (${survivors.length})`);
248
+ if (survivors.length === 0) {
249
+ lines.push(" none");
250
+ } else {
251
+ for (const s of survivors) {
252
+ lines.push(` ${addr(s.address)} ${s.name}`);
253
+ }
254
+ }
255
+ lines.push("");
256
+
257
+ lines.push(` DISAGREEMENTS (${input.disagreementCount ?? 0} of ${input.denominator ?? 0})`);
258
+ const disagreements = Array.isArray(input.disagreements) ? input.disagreements : [];
259
+ if (disagreements.length === 0) {
260
+ lines.push(" none");
261
+ } else {
262
+ for (const d of disagreements) {
263
+ const banks = Array.isArray(d.sourceBanks) ? d.sourceBanks.join(",") : "";
264
+ lines.push(` ${addr(d.address)} byte-derived=${d.byteDerived} runtime=${d.runtime} banks=${banks}`);
265
+ }
266
+ }
267
+ lines.push(` AGREEMENT: ${input.agreementCount ?? 0} of ${input.denominator ?? 0}`);
268
+ lines.push(
269
+ ` NO OBSERVATION: ${input.blockCoveredNeverObservedCount ?? 0} of ${input.denominator ?? 0} -- an address never observed ` +
270
+ "executing proves NOTHING about what it is; absence is not evidence for or against any classification.",
271
+ );
272
+ const resolution = report.disagreementResolution ?? { rows: [], unresolvedCount: 0, denominator: 0 };
273
+ const acceptedCount = resolution.rows.length - resolution.unresolvedCount;
274
+ lines.push(
275
+ ` DISAGREEMENT RESOLUTION: ${acceptedCount} accepted, ${resolution.unresolvedCount} unresolved of ${resolution.denominator} -- ` +
276
+ "criterion 2's own gate: a nonzero unresolved count BLOCKS rather than being reported beside a pass.",
277
+ );
278
+ lines.push("");
279
+
280
+ const rangeProvenance = report.rangeProvenance ?? [];
281
+ lines.push(` RANGE PROVENANCE (${rangeProvenance.length} range(s))`);
282
+ if (rangeProvenance.length === 0) {
283
+ lines.push(" none");
284
+ } else {
285
+ for (const row of rangeProvenance) {
286
+ lines.push(` ${addr(row.start)}-${addr(row.endInclusive)} ${row.renderedType} typedBy: ${row.typedBy}`);
287
+ }
288
+ }
289
+ lines.push("");
290
+
291
+ const entryPoints = report.entryPoints ?? [];
292
+ const fullyDocumented = entryPoints.filter(
293
+ (e) => e.hasName && PURPOSE_ELEMENT_KEYS.every((k) => e.purposeElements && e.purposeElements[k]),
294
+ ).length;
295
+ lines.push(` ENTRY POINTS (${fullyDocumented} of ${entryPoints.length})`);
296
+ if (entryPoints.length === 0) {
297
+ lines.push(" none -- a zero-entry-point count is a fact about the candidate set, never evidence of completeness.");
298
+ } else {
299
+ for (const e of entryPoints) {
300
+ const missing = PURPOSE_ELEMENT_KEYS.filter((k) => !(e.purposeElements && e.purposeElements[k]));
301
+ lines.push(
302
+ ` ${addr(e.address)} ${e.name ?? "(unnamed)"} hasName=${Boolean(e.hasName)}` +
303
+ (missing.length > 0 ? ` MISSING: ${missing.join(", ")}` : " purpose comment complete"),
304
+ );
305
+ }
306
+ }
307
+ lines.push("");
308
+
309
+ const refs = report.referencedAddresses ?? { resolved: [], declined: [], unresolved: [], denominator: 0 };
310
+ lines.push(` REFERENCED NON-HARDWARE ADDRESSES (${refs.resolved.length} resolved of ${refs.denominator})`);
311
+ if (refs.denominator === 0) {
312
+ lines.push(" none -- a zero-referenced-address count is a fact about the candidate set, never evidence of completeness.");
313
+ } else {
314
+ lines.push(` RESOLVED: ${refs.resolved.length === 0 ? "none" : refs.resolved.map(addr).join(", ")}`);
315
+ lines.push(` DECLINED: ${refs.declined.length === 0 ? "none" : refs.declined.map((d) => `${addr(d.address)} (${d.reason})`).join(", ")}`);
316
+ lines.push(` UNRESOLVED: ${refs.unresolved.length === 0 ? "none" : refs.unresolved.map(addr).join(", ")}`);
317
+ }
318
+ lines.push("");
319
+
320
+ lines.push(
321
+ " Read every figure above against the others, never combined into one -- together they name what this " +
322
+ "store's block table covers, never what the program actually is.",
323
+ );
324
+
325
+ const failures = computeGateFailures(report);
326
+ lines.push("");
327
+ if (failures.length === 0) {
328
+ lines.push(" GATE: PASS -- every measure above cleared its own bar.");
329
+ } else {
330
+ lines.push(` GATE: FAIL (${failures.length})`);
331
+ for (const f of failures) lines.push(` - ${f}`);
332
+ }
333
+
334
+ return lines.join("\n");
335
+ }
336
+
337
+ /**
338
+ * THE GATE (the numeric stop condition; criterion 2's own words: a
339
+ * nonzero unresolved count BLOCKS rather than being reported beside a
340
+ * pass). Returns an array of human-readable failure strings, each naming
341
+ * the offending address where one exists; an empty array means the gate
342
+ * passes. Never throws -- a malformed report renders its own absence as a
343
+ * failure (see the individual guards below) rather than crashing the report
344
+ * that exists to surface exactly this kind of gap.
345
+ *
346
+ * ALL FIVE gate conditions, restated from the plan this implements:
347
+ * 1. `byteCensus.undefinedCount === 0`
348
+ * 2. `disagreementResolution.unresolvedCount === 0`
349
+ * 3. `survivors` is empty
350
+ * 4. every `entryPoints` row has `hasName` true and all four
351
+ * `purposeElements` true
352
+ * 5. `referencedAddresses.unresolved` is empty
353
+ */
354
+ export function computeGateFailures(report) {
355
+ const failures = [];
356
+
357
+ const undefinedCount = report?.byteCensus?.undefinedCount ?? 0;
358
+ if (undefinedCount !== 0) {
359
+ const gaps = Array.isArray(report?.byteCensus?.undefinedRanges) ? report.byteCensus.undefinedRanges : [];
360
+ const named = gaps.length > 0 ? gaps.map((g) => (g.start === g.endInclusive ? addr(g.start) : `${addr(g.start)}-${addr(g.endInclusive)}`)).join(", ") : "(address not reported)";
361
+ failures.push(`byte census: ${undefinedCount} Undefined byte(s) remain at ${named} (must be 0)`);
362
+ }
363
+
364
+ const survivors = Array.isArray(report?.survivors) ? report.survivors : [];
365
+ if (survivors.length > 0) {
366
+ for (const s of survivors) failures.push(`survivor auto-name at ${addr(s.address)} (${s.name}) still sits in a code region`);
367
+ }
368
+
369
+ const entryPoints = Array.isArray(report?.entryPoints) ? report.entryPoints : [];
370
+ for (const e of entryPoints) {
371
+ if (!e.hasName) {
372
+ failures.push(`entry point ${addr(e.address)} has no authored name`);
373
+ continue;
374
+ }
375
+ const missing = PURPOSE_ELEMENT_KEYS.filter((k) => !(e.purposeElements && e.purposeElements[k]));
376
+ if (missing.length > 0) {
377
+ failures.push(`entry point ${addr(e.address)} is missing purpose-comment element(s): ${missing.join(", ")}`);
378
+ }
379
+ }
380
+
381
+ const refs = report?.referencedAddresses ?? { unresolved: [] };
382
+ for (const a of Array.isArray(refs.unresolved) ? refs.unresolved : []) {
383
+ failures.push(`referenced address ${addr(a)} is neither named nor declined`);
384
+ }
385
+
386
+ const resolution = report?.disagreementResolution ?? { unresolvedCount: 0 };
387
+ const unresolvedCount = resolution.unresolvedCount ?? 0;
388
+ if (unresolvedCount !== 0) {
389
+ failures.push(`${unresolvedCount} disagreement(s) remain unresolved (no DISAGREEMENT-ACCEPTED comment)`);
390
+ }
391
+
392
+ return failures;
393
+ }
394
+
395
+ /**
396
+ * Forwards `["anno", "decomp-completeness", ...argv, "--json"]` to the
397
+ * resolved MCP-side `vice-proxy.ts`, parses its stdout as JSON, and returns
398
+ * `buildCompletenessReport()`'s own normalised shape. Never rejects: an
399
+ * unresolved MCP module, a non-zero exit, or unparsable stdout all resolve
400
+ * to a thrown `Error` with the seam's own refusal text (or, per this
401
+ * script's never-throw posture at the CLI boundary, `main()` below catches
402
+ * it and reports it as an exit code) -- this function itself may throw,
403
+ * since it is the in-process API a test or another script calls directly.
404
+ */
405
+ export function fetchCompletenessReport(argv) {
406
+ const resolved = resolveMcpModule(TARGET_FILE);
407
+ if (!resolved.ok) {
408
+ throw new Error(
409
+ `completeness-report.mjs: ${refusalMessage(TARGET_FILE, resolved.rungs)}\n` +
410
+ `${TARGET_FILE} is where the fifth anno CLI verb lives (${TARGET_PACKAGE}). Refusing rather than ` +
411
+ "reading a store directly here -- a second copy of that read is exactly the divergence this script's own header forbids.",
412
+ );
413
+ }
414
+ const fullArgv = ["anno", "decomp-completeness", ...argv, "--json"];
415
+ const run = spawnSync(process.execPath, [resolved.path, ...fullArgv], { encoding: "utf8" });
416
+ if (run.error) {
417
+ throw new Error(`completeness-report.mjs: could not run ${resolved.path}: ${run.error.message}`);
418
+ }
419
+ if (run.signal) {
420
+ throw new Error(`completeness-report.mjs: ${resolved.path} was killed by ${run.signal}`);
421
+ }
422
+ if (run.status !== 0) {
423
+ throw new Error(`completeness-report.mjs: anno decomp-completeness exited ${run.status}: ${run.stderr || run.stdout}`);
424
+ }
425
+ let parsed;
426
+ try {
427
+ parsed = JSON.parse(run.stdout);
428
+ } catch (err) {
429
+ throw new Error(`completeness-report.mjs: anno decomp-completeness --json did not print valid JSON: ${err.message}`);
430
+ }
431
+ return buildCompletenessReport(parsed);
432
+ }
433
+
434
+ /** CLI entry point: `node completeness-report.mjs --store FILE --disagreements FILE --manifest FILE`.
435
+ * Forwards argv verbatim to the resolved verb, renders the result, and
436
+ * returns a process exit code -- never calls `process.exit()` itself, so
437
+ * `main()` stays testable in-process. A thrown `MissingDisagreementInputError`
438
+ * is reported with its own message and nothing more (the refusal IS the
439
+ * report); any other thrown error is reported the same way, verbatim,
440
+ * never swallowed. On a SUCCESSFULLY RENDERED report, the exit code is THE
441
+ * GATE's own verdict (`computeGateFailures()`), never a bare 0 -- this is
442
+ * the numeric stop condition, and softening it here is exactly the
443
+ * regression planted controls 1/2 (task 2) exist to catch. */
444
+ export function main(argv) {
445
+ let report;
446
+ try {
447
+ report = fetchCompletenessReport(argv);
448
+ } catch (err) {
449
+ console.error(err instanceof Error ? err.message : String(err));
450
+ return 1;
451
+ }
452
+ try {
453
+ console.log(renderCompletenessReport(report));
454
+ return computeGateFailures(report).length === 0 ? 0 : 1;
455
+ } catch (err) {
456
+ console.error(err instanceof Error ? err.message : String(err));
457
+ return 1;
458
+ }
459
+ }
460
+
461
+ if (import.meta.url === `file://${process.argv[1]}`) {
462
+ process.exitCode = main(process.argv.slice(2));
463
+ }