@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.
- package/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +26 -0
- package/bin/cli.mjs +18 -7
- package/package.json +6 -4
- package/skills/acme-build/SKILL.md +83 -33
- package/skills/acme-build/scripts/acme.mjs +159 -64
- package/skills/acme-build/template.a +1 -1
- package/skills/c64-disk-access/SKILL.md +156 -0
- package/skills/c64-disk-access/scripts/c1541.mjs +569 -0
- package/skills/c64-memory-mapping/SKILL.md +419 -23
- package/skills/c64-memory-mapping/scripts/driver.mjs +1 -1
- package/skills/c64-petcat/SKILL.md +87 -0
- package/skills/c64-petcat/scripts/petcat.mjs +221 -0
- package/skills/c64-program-recon/SKILL.md +497 -92
- package/skills/c64-program-recon/references/control-flow.md +12 -15
- package/skills/c64-program-recon/references/graphics.md +1 -1
- package/skills/c64-program-recon/references/observation-hazards.md +18 -16
- package/skills/c64-program-recon/references/reconstruction.md +11 -6
- package/skills/c64-program-recon/references/sound-and-input.md +6 -8
- package/skills/c64-program-recon/references/tool-selection.md +37 -18
- package/skills/c64-program-recon/scripts/packer-finding.mjs +709 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +27 -13
- package/skills/c64-provenance-diff/SKILL.md +43 -8
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +9 -6
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +20 -8
- package/skills/c64-ram-capture/RELEASES.json.example +17 -0
- package/skills/c64-ram-capture/SKILL.md +147 -46
- package/skills/c64-ram-capture/scripts/compare.mjs +2 -2
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +575 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +3 -3
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +174 -0
- package/skills/c64-ram-capture/scripts/project-paths.mjs +1 -1
- package/skills/c64-ram-capture/scripts/releases.mjs +1 -1
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +147 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +19 -13
- package/skills/c64-ram-capture/templates/capture-record.template.md +44 -4
- package/skills/c64-ram-capture/transients/README.md +136 -0
- package/skills/routine-queue-walker/SKILL.md +365 -0
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +463 -0
- package/skills/vice-wedge-triage/SKILL.md +104 -97
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +0 -665
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +0 -243
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +0 -243
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +0 -133
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +0 -75
- 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
|
+
}
|