@henols/vice-mcp 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/anno-cli.ts +156 -158
- package/anno-confidence.ts +2 -2
- package/anno-derive.ts +6 -6
- package/anno-details.ts +4 -4
- package/anno-export-asm.ts +100 -101
- package/anno-graphics.ts +16 -16
- package/anno-hazard-report.ts +2 -2
- package/anno-import.ts +15 -15
- package/anno-index.ts +8 -8
- package/anno-join.ts +35 -35
- package/anno-memmap-render.ts +22 -21
- package/anno-provenance-ledger.ts +4 -4
- package/anno-regbits-gen.ts +13 -13
- package/anno-store-export.ts +11 -11
- package/anno-store.ts +139 -144
- package/anno-symbols.ts +7 -7
- package/anno-types.ts +55 -55
- package/package.json +1 -1
- package/resources/broker-control.mjs +85 -92
- package/resources/broker-epoch.mjs +6 -7
- package/resources/broker-kill.mjs +29 -30
- package/resources/broker-launch.mjs +352 -370
- package/resources/broker-state.mjs +9 -10
- package/resources/host-tool.mjs +636 -664
- package/resources/vice-broker.mjs +189 -191
- package/vice-broker-client.ts +98 -100
package/anno-cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// anno-cli.ts -- the thin CLI ergonomics layer over the annotation store
|
|
3
|
-
//
|
|
2
|
+
// anno-cli.ts -- the thin CLI ergonomics layer over the annotation store.
|
|
3
|
+
// Reached as `vice-mcp anno <verb>` because that bin is the only
|
|
4
4
|
// surface that resolves identically across the Claude Code plugin route and
|
|
5
5
|
// both npm-installer routes: `installer/bin/cli.mjs`'s `viceServerEntry()`
|
|
6
6
|
// always launches this server via `npx` in BOTH npm-installer modes, and
|
|
@@ -8,18 +8,17 @@
|
|
|
8
8
|
// consuming project for some other filesystem-path-resolving design to find.
|
|
9
9
|
//
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
|
-
// FIVE VERBS. THAT IS THE WHOLE SURFACE
|
|
12
|
-
// time
|
|
13
|
-
//
|
|
14
|
-
// query, so a planted test has
|
|
15
|
-
// states to compare rather than
|
|
16
|
-
// only
|
|
17
|
-
//
|
|
18
|
-
// report's ONLY data path into a real store
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// decision, not a silent reopening of it).
|
|
11
|
+
// FIVE VERBS. THAT IS THE WHOLE SURFACE -- narrowed to two, then grown back
|
|
12
|
+
// one verb at a time as each earned a real route over this project's own
|
|
13
|
+
// annotation store: a third verb landed first; a fourth, `evid-disagreements`,
|
|
14
|
+
// followed as the CLI route for the disagreement query, so a planted test has
|
|
15
|
+
// three RENDERED, textually-distinguishable states to compare rather than
|
|
16
|
+
// only the MCP tool's JSON, which a test can only inspect structurally; a
|
|
17
|
+
// fifth, `decomp-completeness`, followed as the CLI route for the
|
|
18
|
+
// decomposition-completeness report's ONLY data path into a real store. Each
|
|
19
|
+
// raise of the count DELIBERATELY SUPERSEDES the prior "THAT IS THE WHOLE
|
|
20
|
+
// SURFACE" framing rather than silently reopening it -- this is the second
|
|
21
|
+
// raise over that framing, not the first.
|
|
23
22
|
// ---------------------------------------------------------------------------
|
|
24
23
|
// This file used to carry eight. Six were removed in one commit because they
|
|
25
24
|
// were delivery paths for the retired external analyser this project used to
|
|
@@ -43,15 +42,14 @@
|
|
|
43
42
|
// requirement and no success criterion of the phase that rebuilt
|
|
44
43
|
// `export-asm` covers any of them, and NO PHASE CURRENTLY OWNS THEM, so
|
|
45
44
|
// the symbol round trip still has NO route at all. That is recorded as a
|
|
46
|
-
// withdrawal in
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// place
|
|
50
|
-
//
|
|
51
|
-
// cannot contradict each other.
|
|
45
|
+
// withdrawal in this project's own capability record rather than left for
|
|
46
|
+
// a reader to discover by running it. The exact wording of those
|
|
47
|
+
// withdrawal notices across both skill trees is kept in exactly one
|
|
48
|
+
// place; this file states the code fact and does not restate their text,
|
|
49
|
+
// so the two edits cannot contradict each other.
|
|
52
50
|
//
|
|
53
51
|
// WHAT NOT TO DO, named concretely:
|
|
54
|
-
// - Never auto-pick an input when the caller does not name one
|
|
52
|
+
// - Never auto-pick an input when the caller does not name one. A
|
|
55
53
|
// silent auto-pick would happily analyse a cracktro or loader stub's
|
|
56
54
|
// bytes instead of the actual game -- precisely the failure
|
|
57
55
|
// `c64-provenance-diff` exists to prevent elsewhere in this project.
|
|
@@ -67,9 +65,9 @@
|
|
|
67
65
|
// confinement escape waiting to be written.
|
|
68
66
|
//
|
|
69
67
|
// THIS PARAGRAPH WAS FALSE WHEN IT WAS FIRST WRITTEN, and that is why it
|
|
70
|
-
// now names the mechanism that keeps it.
|
|
71
|
-
//
|
|
72
|
-
//
|
|
68
|
+
// now names the mechanism that keeps it. An independent verification pass
|
|
69
|
+
// reproduced three escapes on this very tree, on arguments the shipped
|
|
70
|
+
// playbooks tell an agent to compose in a
|
|
73
71
|
// Bash invocation: `render-memmap --out` and `coverage --out` reached
|
|
74
72
|
// `writeFileSync` as raw caller strings (the first silently replacing a
|
|
75
73
|
// pre-existing file OUTSIDE the workspace root and exiting 0), and
|
|
@@ -95,7 +93,7 @@
|
|
|
95
93
|
// noticing.
|
|
96
94
|
//
|
|
97
95
|
// AND WHAT IT DOES NOT CHECK, stated in terms so the limit can be closed
|
|
98
|
-
// deliberately rather than discovered
|
|
96
|
+
// deliberately rather than discovered: it does not associate a
|
|
99
97
|
// particular argument with a particular call site. "Six arguments each
|
|
100
98
|
// confined once" and "five confined with one of them confined twice" read
|
|
101
99
|
// the same to it. That association needs per-argument dataflow through
|
|
@@ -119,16 +117,16 @@
|
|
|
119
117
|
//
|
|
120
118
|
// Import nothing from `hostpath.ts` or `containerpath.ts`. Every path this
|
|
121
119
|
// CLI handles is already container-side, and translating any of these
|
|
122
|
-
// arguments would be the mirror image of
|
|
123
|
-
// where a client-side-derived path was wrongly translated a
|
|
124
|
-
// This absence is asserted structurally by
|
|
125
|
-
//
|
|
120
|
+
// arguments would be the mirror image of a screenshot-path trap this project
|
|
121
|
+
// hit before, where a client-side-derived path was wrongly translated a
|
|
122
|
+
// second time. This absence is asserted structurally by
|
|
123
|
+
// `hostpath-consumers.test.ts`, not merely stated here.
|
|
126
124
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
127
125
|
import { basename, dirname, extname, join, sep } from "node:path";
|
|
128
126
|
import { fileURLToPath } from "node:url";
|
|
129
127
|
|
|
130
128
|
import { renderMemoryMap, checkRenderedMemoryMap } from "./anno-memmap-render.ts";
|
|
131
|
-
// The ACME source emitter
|
|
129
|
+
// The ACME source emitter. It reads the store and the image and
|
|
132
130
|
// returns text plus counts; it starts no assembler and knows nothing about
|
|
133
131
|
// one. `acme-verify.ts` -- the module that DOES spawn ACME -- is deliberately
|
|
134
132
|
// NOT imported here and must never be: it is test-only (it is absent from
|
|
@@ -136,7 +134,7 @@ import { renderMemoryMap, checkRenderedMemoryMap } from "./anno-memmap-render.ts
|
|
|
136
134
|
// would drag it into the published closure `check-npm-packages.mjs` walks.
|
|
137
135
|
import { exportAsmTree } from "./anno-export-asm.ts";
|
|
138
136
|
import type { ExportAsmTreeResult } from "./anno-export-asm.ts";
|
|
139
|
-
// The coverage instrument
|
|
137
|
+
// The coverage instrument. It declares its own input shapes
|
|
140
138
|
// and never reads a store, a file or a tool on its own behalf -- a caller
|
|
141
139
|
// fetches and hands the data in, which is exactly what makes the store
|
|
142
140
|
// re-point below a CALLER-side change and nothing more.
|
|
@@ -147,14 +145,14 @@ import type { CoverageReport, LoadedProject, AnnoComment, AnnoCrossReference, An
|
|
|
147
145
|
import type { BlockEntry } from "./block-class.ts";
|
|
148
146
|
import { openStore, closeStore, listLabels, listComments, listRanges, listExecObservations, listObservedRuns, listXrefs } from "./anno-store.ts";
|
|
149
147
|
import type { AnnoStoreHandle } from "./anno-store.ts";
|
|
150
|
-
// The shared 6502/6510 decoder
|
|
151
|
-
// entry-point and referenced-address censuses
|
|
148
|
+
// The shared 6502/6510 decoder. `decomp-completeness`'s
|
|
149
|
+
// entry-point and referenced-address censuses walk the
|
|
152
150
|
// SAME code-range decode `anno_disassemble` and `anno-enum-gen.ts`'s
|
|
153
151
|
// `fetchRegisterSearchRows()` already use -- never a second decoder, never a
|
|
154
152
|
// regex over rendered text.
|
|
155
153
|
import { decode } from "./disasm-decoder.ts";
|
|
156
154
|
import type { Instruction } from "./disasm-decoder.ts";
|
|
157
|
-
// The disagreement query's own pure join
|
|
155
|
+
// The disagreement query's own pure join. This
|
|
158
156
|
// is the SAME reconcileObservedExecution() the anno_evid_disagreements MCP
|
|
159
157
|
// tool calls -- reached here directly (a static import, never lazy) because
|
|
160
158
|
// this module IS the CLI, not a startup-cost-sensitive MCP server entry
|
|
@@ -166,7 +164,7 @@ import { reconcileObservedExecution } from "./evid-reconcile.ts";
|
|
|
166
164
|
import { buildHazardReport } from "./anno-hazard-report.ts";
|
|
167
165
|
import type { HazardReport } from "./anno-hazard-report.ts";
|
|
168
166
|
import type { EvidReconciliation } from "./evid-reconcile.ts";
|
|
169
|
-
// The derived half of
|
|
167
|
+
// The derived half of the cross-reference union: cross-references are DERIVED from the bytes
|
|
170
168
|
// plus the store's typed ranges plus the few rows that cannot be recovered
|
|
171
169
|
// from bytes at all. There is exactly one definition of that union and this
|
|
172
170
|
// file calls it rather than restating it.
|
|
@@ -174,9 +172,9 @@ import { crossReferencesTo } from "./anno-derive.ts";
|
|
|
174
172
|
import { storePathWithinWorkspace, isSplitDataType } from "./anno-types.ts";
|
|
175
173
|
import type { CommentRow, LabelRow, RangeRow, DataType } from "./anno-types.ts";
|
|
176
174
|
import { repoRoot } from "./repo-root.ts";
|
|
177
|
-
//
|
|
175
|
+
// The three comment-text conventions, declared once in
|
|
178
176
|
// anno-store-export.ts and imported everywhere they are matched -- never
|
|
179
|
-
// restated as a second literal
|
|
177
|
+
// restated as a second literal.
|
|
180
178
|
import { DECLINE_COMMENT_PREFIX, DISAGREEMENT_ACCEPTED_COMMENT_PREFIX, AUTHORED_PROVENANCE_COMMENT_PREFIX } from "./anno-store-export.ts";
|
|
181
179
|
|
|
182
180
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -189,7 +187,7 @@ usage (plugin/in-repo): ${PLUGIN_INVOCATION}
|
|
|
189
187
|
verbs:
|
|
190
188
|
render-memmap <store> --provenance FILE [--out FILE] [--force] [--check]
|
|
191
189
|
Generates the Markdown memory map from an annotation store plus a
|
|
192
|
-
validated provenance sidecar (
|
|
190
|
+
validated provenance sidecar (the store is canonical; this
|
|
193
191
|
output is a GENERATED VIEW -- never hand-edit it). Without --check,
|
|
194
192
|
writes --out (default: memory-map.md beside the STORE -- in the
|
|
195
193
|
store's own directory), refusing to overwrite an existing file there
|
|
@@ -213,7 +211,7 @@ verbs:
|
|
|
213
211
|
|
|
214
212
|
coverage <image> --store FILE [--out FILE] [--force] [--sample N]
|
|
215
213
|
Measures how far a program has actually been reverse-engineered
|
|
216
|
-
|
|
214
|
+
through anno-coverage.ts. <image> supplies the
|
|
217
215
|
PAYLOAD BYTES and the load origin; --store names the ANNOTATION STORE
|
|
218
216
|
holding the labels, comments and typed ranges. Those are two separate
|
|
219
217
|
files on purpose: the store holds annotations and never bytes, so a
|
|
@@ -222,14 +220,14 @@ verbs:
|
|
|
222
220
|
<image> is dispatched IN THIS ORDER, and the order is load-bearing:
|
|
223
221
|
first, a .raw or .bin is read as a flat capture BY EXTENSION, before
|
|
224
222
|
any length check, so a truncated capture is refused BY NAME instead
|
|
225
|
-
of falling through to the .prg parser (
|
|
223
|
+
of falling through to the .prg parser (a 4096-byte .raw once
|
|
226
224
|
had its first two bytes read as a load address and reported a
|
|
227
225
|
complete-looking measurement); then any file that is NOT a .prg and
|
|
228
226
|
is exactly 65536 bytes is read as a flat capture, which is the one
|
|
229
227
|
branch that does dispatch on byte length; then a .prg, whose first
|
|
230
228
|
two bytes are the load address. The retired JSON project form
|
|
231
229
|
survives as a TRAILING LEGACY branch, reached only when none of
|
|
232
|
-
those matched -- its only producer was deleted
|
|
230
|
+
those matched -- its only producer was deleted, and it is kept
|
|
233
231
|
solely so an existing file on disk is not broken.
|
|
234
232
|
Prints three separately named measures -- the structural byte census,
|
|
235
233
|
the two label figures, and the sampled reproducibility result -- plus
|
|
@@ -279,7 +277,7 @@ verbs:
|
|
|
279
277
|
room.
|
|
280
278
|
--ledger names c64-provenance-diff's generated recovery/PROVENANCE.md.
|
|
281
279
|
Supplying it makes the export carry each covered range's recorded
|
|
282
|
-
Verdict and Confidence as inline comments
|
|
280
|
+
Verdict and Confidence as inline comments. It is OPTIONAL:
|
|
283
281
|
omitting it exports exactly as before. The flag changes COMMENT TEXT
|
|
284
282
|
ONLY -- it never changes which bytes or which blocks are emitted, and a
|
|
285
283
|
range the supplied ledger does not cover is refused by name rather than
|
|
@@ -318,15 +316,15 @@ verbs:
|
|
|
318
316
|
|
|
319
317
|
decomp-completeness --store FILE --disagreements FILE --manifest FILE [--json]
|
|
320
318
|
The decomposition-closure completeness answer for ONE
|
|
321
|
-
per-fixture store
|
|
319
|
+
per-fixture store. Three REQUIRED arguments, none defaulted from
|
|
322
320
|
another: --store names the annotation store; --disagreements names the
|
|
323
321
|
JSON "anno evid-disagreements --store <same store> --json" wrote for
|
|
324
|
-
THIS store's own run; --manifest names the execution manifest
|
|
322
|
+
THIS store's own run; --manifest names the execution manifest
|
|
325
323
|
recording which committed fixtures were actually run. Omitting ANY of
|
|
326
324
|
the three refuses BY NAME with exit 1 -- there is no default and no
|
|
327
325
|
empty-array substitute for a missing disagreement input, because an
|
|
328
326
|
omitted query and a query that found nothing must never render the
|
|
329
|
-
same report
|
|
327
|
+
same report.
|
|
330
328
|
The supplied --disagreements document is refused, by name, when it is
|
|
331
329
|
missing any EvidReconciliation field, and when its own recorded
|
|
332
330
|
runIdentity (image_sha256/argv_digest/seed) matches no row in the
|
|
@@ -342,9 +340,9 @@ verbs:
|
|
|
342
340
|
candidate queue uses), the fixture's own execution disposition read
|
|
343
341
|
from --manifest (a NOT EXECUTED fixture renders that fact by name,
|
|
344
342
|
never a clean bill of health), and the disagreement input verbatim.
|
|
345
|
-
Never prints a percentage, rate or combined figure
|
|
346
|
-
|
|
347
|
-
rendered report.
|
|
343
|
+
Never prints a percentage, rate or combined figure -- the same rule
|
|
344
|
+
this CLI applies to every verb's own report. --json prints the raw
|
|
345
|
+
JSON answer instead of the rendered report.
|
|
348
346
|
Requires an EXISTING annotation store, an EXISTING --disagreements
|
|
349
347
|
document and an EXISTING --manifest file; creates none and writes
|
|
350
348
|
nothing.
|
|
@@ -370,7 +368,7 @@ verbs:
|
|
|
370
368
|
|
|
371
369
|
Every verb requires inputs that already exist. None creates a project, a
|
|
372
370
|
store or a sidecar, and none derives one path from another -- this CLI
|
|
373
|
-
never guesses
|
|
371
|
+
never guesses.
|
|
374
372
|
`;
|
|
375
373
|
|
|
376
374
|
function errMsg(err: unknown): string {
|
|
@@ -378,7 +376,7 @@ function errMsg(err: unknown): string {
|
|
|
378
376
|
}
|
|
379
377
|
|
|
380
378
|
/**
|
|
381
|
-
*
|
|
379
|
+
* The ONE declared verb-to-accepted-options fact in this
|
|
382
380
|
* file. Every option every verb's own code actually reads is listed here --
|
|
383
381
|
* ground truth, not merely what USAGE happens to say.
|
|
384
382
|
*
|
|
@@ -396,8 +394,8 @@ function errMsg(err: unknown): string {
|
|
|
396
394
|
*
|
|
397
395
|
* `export-asm` deliberately carries NO assembler-facing option. It writes
|
|
398
396
|
* source and runs no assembler, so there is no binary to name, no exit status
|
|
399
|
-
* to surface and no flag that could imply either. `--ledger`
|
|
400
|
-
*
|
|
397
|
+
* to surface and no flag that could imply either. `--ledger` does not weaken
|
|
398
|
+
* that claim: it is an EVIDENCE-CARRYING
|
|
401
399
|
* INPUT, exactly like `--store`, never an assembler-facing option -- it names
|
|
402
400
|
* a file to READ, not a way to run or configure an assembler.
|
|
403
401
|
*/
|
|
@@ -411,7 +409,7 @@ export const VERB_OPTIONS: Readonly<Record<string, readonly string[]>> = Object.
|
|
|
411
409
|
});
|
|
412
410
|
|
|
413
411
|
/**
|
|
414
|
-
* The one shared refusal check
|
|
412
|
+
* The one shared refusal check generalises to every verb (the same
|
|
415
413
|
* closed-option-set posture, applied uniformly rather than verb by verb).
|
|
416
414
|
* Scans `rest` for any `--flag`-shaped token not in `verb`'s accepted set
|
|
417
415
|
* from `VERB_OPTIONS` and returns a one-line refusal naming the flag and the
|
|
@@ -421,8 +419,8 @@ export const VERB_OPTIONS: Readonly<Record<string, readonly string[]>> = Object.
|
|
|
421
419
|
* message). Never throws -- this file's never-throw posture applies here
|
|
422
420
|
* too.
|
|
423
421
|
*
|
|
424
|
-
* THE LOOKUP IS AN OWN-PROPERTY READ, AND THAT IS THE WHOLE POINT (
|
|
425
|
-
*
|
|
422
|
+
* THE LOOKUP IS AN OWN-PROPERTY READ, AND THAT IS THE WHOLE POINT (fixed
|
|
423
|
+
* 2026-08-31, after a real crash reproduced it). `VERB_OPTIONS` is an object literal, so it
|
|
426
424
|
* inherits from `Object.prototype`; a bare `VERB_OPTIONS[verb]` resolved
|
|
427
425
|
* `hasOwnProperty`, `toString`, `constructor`, `valueOf` and `__proto__` to
|
|
428
426
|
* TRUTHY inherited FUNCTIONS. Those sailed past the `if (!accepted) return
|
|
@@ -461,17 +459,17 @@ export function checkAcceptedOptions(verb: string, rest: string[]): string | und
|
|
|
461
459
|
* passed `--force`. Called by the TWO verbs that write a single output FILE
|
|
462
460
|
* -- `cmdRenderMemmap()` (non-`--check` branch only; `--check` never writes)
|
|
463
461
|
* and `cmdCoverage()` -- so overwrite safety is uniform across both rather
|
|
464
|
-
* than one verb accreting a check the other lacks
|
|
462
|
+
* than one verb accreting a check the other lacks.
|
|
465
463
|
*
|
|
466
464
|
* "SHARED BY EVERY VERB THAT WRITES AN OUTPUT FILE" IS WHAT THIS DOC USED TO
|
|
467
465
|
* SAY, AND IT WAS NOT TRUE. `render-memmap` wrote an output file and had
|
|
468
466
|
* neither `--force` in its option set nor a call to this function anywhere on
|
|
469
|
-
* its path;
|
|
467
|
+
* its path; an independent review reproduced it destroying a pre-existing file
|
|
470
468
|
* silently, exit code 0. The claim is now stated as the TWO call sites it
|
|
471
469
|
* actually has, because a count is checkable where "every" is not.
|
|
472
470
|
*
|
|
473
471
|
* "THE TWO CALL SITES" IS WHAT THIS SENTENCE SAID UNTIL 2026-08-31, AFTER
|
|
474
|
-
* `cmdExportAsm()` BECAME THE THIRD
|
|
472
|
+
* `cmdExportAsm()` BECAME THE THIRD. The paragraph directly
|
|
475
473
|
* above had been updated to name all three; this one, whose entire point is
|
|
476
474
|
* that a COUNT is checkable where "every" is not, was left carrying a stale
|
|
477
475
|
* count -- the failure mode it exists to argue against, reproduced in
|
|
@@ -480,10 +478,10 @@ export function checkAcceptedOptions(verb: string, rest: string[]): string | und
|
|
|
480
478
|
* confinement seam, so the next verb to write an output file cannot leave this
|
|
481
479
|
* number behind again.
|
|
482
480
|
*
|
|
483
|
-
* BACK DOWN TO TWO,
|
|
484
|
-
* from a FILE to a DIRECTORY
|
|
481
|
+
* BACK DOWN TO TWO, after `export-asm`'s `--out` was promoted
|
|
482
|
+
* from a FILE to a DIRECTORY. A directory's overwrite question --
|
|
485
483
|
* does this directory already hold something, and may `force` replace it --
|
|
486
|
-
* is `exportAsmTree()`'s own output-directory contract
|
|
484
|
+
* is `exportAsmTree()`'s own output-directory contract,
|
|
487
485
|
* never this function's single-file question, so `cmdExportAsm()` dropped its
|
|
488
486
|
* call here rather than reshaping a file-shaped check to fit a directory. The
|
|
489
487
|
* count this doc states, and the count `anno-cli.test.ts` checks mechanically,
|
|
@@ -506,7 +504,7 @@ function refuseOverwrite(outPath: string, force: boolean | undefined, verbLabel:
|
|
|
506
504
|
|
|
507
505
|
/**
|
|
508
506
|
* THE ONE "is this token a value, or the next flag?" TEST, shared by all THREE
|
|
509
|
-
* option parsers below (
|
|
507
|
+
* option parsers below (fixed 2026-08-31, after a real failure reproduced it).
|
|
510
508
|
*
|
|
511
509
|
* The `*MissingValue` mechanism exists precisely to avoid "silently swallowing
|
|
512
510
|
* the next token" when an option is given without its value. Until this
|
|
@@ -545,7 +543,7 @@ interface RenderMemmapParsedArgs {
|
|
|
545
543
|
}
|
|
546
544
|
|
|
547
545
|
/** Fixed, closed option set for render-memmap -- exactly `--provenance`,
|
|
548
|
-
* `--out`, `--force` and `--check`. Per
|
|
546
|
+
* `--out`, `--force` and `--check`. Per this file's closed-option-set posture (do not silently
|
|
549
547
|
* accept a flag a verb does not implement, or a flag missing its value), any
|
|
550
548
|
* OTHER `--flag`-shaped token is refused as `unknownOption`, and
|
|
551
549
|
* `--provenance`/`--out` with no value (or a flag-shaped "value") is refused
|
|
@@ -597,25 +595,25 @@ function parseRenderMemmapArgs(rest: string[]): RenderMemmapParsedArgs {
|
|
|
597
595
|
|
|
598
596
|
/**
|
|
599
597
|
* `render-memmap <store> --provenance FILE [--out FILE] [--force] [--check]`
|
|
600
|
-
* --
|
|
598
|
+
* -- the generated-view verb, via `anno-memmap-render.ts`'s
|
|
601
599
|
* `renderMemoryMap()`/`checkRenderedMemoryMap()`. Never writes a file when
|
|
602
600
|
* `--check` is given -- that mode only reads and reports.
|
|
603
601
|
*
|
|
604
602
|
* ALL THREE OF THIS VERB'S PATHS ARE CONFINED, and the reason each one is
|
|
605
603
|
* named here rather than left to a reader to infer is that two of them were
|
|
606
|
-
* NOT, and shipped that way.
|
|
607
|
-
*
|
|
604
|
+
* NOT, and shipped that way. An independent verification pass
|
|
605
|
+
* reproduced both on this tree:
|
|
608
606
|
*
|
|
609
607
|
* - `--out` reached `writeFileSync` as the RAW caller string. Pointed
|
|
610
608
|
* outside the workspace root it exited 0, printed `wrote /tmp/.../
|
|
611
609
|
* PRECIOUS.md` and replaced that pre-existing file's bytes. `--force`
|
|
612
610
|
* was not in this verb's option set at all, so `refuseOverwrite()` --
|
|
613
611
|
* whose own doc claims the safety is uniform across every verb that
|
|
614
|
-
* writes an output file -- was never reached from here
|
|
612
|
+
* writes an output file -- was never reached from here.
|
|
615
613
|
* - `--provenance` reached `readFileSync` as the RAW caller string, making
|
|
616
614
|
* it an arbitrary-file read oracle; the sidecar parse failure then
|
|
617
615
|
* interpolated Node's own parse error, which carries a snippet of the
|
|
618
|
-
* file, so the oracle DISCLOSED CONTENT
|
|
616
|
+
* file, so the oracle DISCLOSED CONTENT. Confining it here also
|
|
619
617
|
* confines it for `anno-memmap-render.ts`, which reads it with no check
|
|
620
618
|
* of its own.
|
|
621
619
|
*
|
|
@@ -635,7 +633,7 @@ function parseRenderMemmapArgs(rest: string[]): RenderMemmapParsedArgs {
|
|
|
635
633
|
* fails when the inventory and the surface disagree in either direction, or
|
|
636
634
|
* when this file's confinement call sites number fewer than the inventory's
|
|
637
635
|
* entries. It does not associate a particular argument with a particular call
|
|
638
|
-
* site
|
|
636
|
+
* site, so six arguments confined once each and five confined with one
|
|
639
637
|
* of them confined twice read the same to it; that limit is named here rather
|
|
640
638
|
* than papered over. A header that asserts a property must point at the
|
|
641
639
|
* mechanism that keeps it, and must claim no more than the mechanism checks.
|
|
@@ -699,7 +697,7 @@ async function cmdRenderMemmap(rest: string[]): Promise<number> {
|
|
|
699
697
|
return 1;
|
|
700
698
|
}
|
|
701
699
|
|
|
702
|
-
//
|
|
700
|
+
// The sidecar is confined BEFORE the existence check, so a path
|
|
703
701
|
// outside the workspace root never reaches the filesystem at all -- not as
|
|
704
702
|
// an `existsSync` probe (which is itself an oracle: it answers "does this
|
|
705
703
|
// file exist" for any path the process can stat) and not as the
|
|
@@ -718,7 +716,7 @@ async function cmdRenderMemmap(rest: string[]): Promise<number> {
|
|
|
718
716
|
return 1;
|
|
719
717
|
}
|
|
720
718
|
|
|
721
|
-
//
|
|
719
|
+
// The default is applied FIRST and the result confined AFTER, so the
|
|
722
720
|
// derived path and a caller-supplied one are confined by the same rule --
|
|
723
721
|
// rather than the default being trusted because this verb computed it.
|
|
724
722
|
let outPath: string;
|
|
@@ -751,7 +749,7 @@ async function cmdRenderMemmap(rest: string[]): Promise<number> {
|
|
|
751
749
|
return 1;
|
|
752
750
|
}
|
|
753
751
|
|
|
754
|
-
//
|
|
752
|
+
// The second half of that same fix. `--check` never writes, so the overwrite refusal
|
|
755
753
|
// belongs on THIS branch only -- and it runs against the CONFINED path, so
|
|
756
754
|
// the file it protects is the file that would actually be written.
|
|
757
755
|
if (!refuseOverwrite(outPath, force, "render-memmap")) {
|
|
@@ -768,7 +766,7 @@ async function cmdRenderMemmap(rest: string[]): Promise<number> {
|
|
|
768
766
|
try {
|
|
769
767
|
writeFileSync(outPath, rendered.markdown);
|
|
770
768
|
} catch (err) {
|
|
771
|
-
//
|
|
769
|
+
// The same shape as bootstrapProject()'s write above,
|
|
772
770
|
// one verb over -- an ordinary write failure (missing parent directory,
|
|
773
771
|
// permissions, full disk) must not throw past this verb's own
|
|
774
772
|
// never-throw contract.
|
|
@@ -795,7 +793,7 @@ interface CoverageParsedArgs {
|
|
|
795
793
|
}
|
|
796
794
|
|
|
797
795
|
/** Fixed, closed option set for coverage -- exactly `--store`, `--out`,
|
|
798
|
-
* `--force` and `--sample`. Same
|
|
796
|
+
* `--force` and `--sample`. Same closed-option-set posture as `parseRenderMemmapArgs()`
|
|
799
797
|
* above: an unimplemented flag is refused as `unknownOption`, and
|
|
800
798
|
* `--store`/`--out`/`--sample` with a missing or flag-shaped value are refused
|
|
801
799
|
* through their own `*MissingValue` fields rather than silently swallowing the
|
|
@@ -986,7 +984,7 @@ function addressList(addresses: readonly number[], cap = 12): string {
|
|
|
986
984
|
/**
|
|
987
985
|
* Renders the report as separately-headed sections.
|
|
988
986
|
*
|
|
989
|
-
* THE ONE RULE THIS FUNCTION EXISTS TO HOLD (
|
|
987
|
+
* THE ONE RULE THIS FUNCTION EXISTS TO HOLD (and the reason the
|
|
990
988
|
* rendering lives here rather than being a generic pretty-printer): print
|
|
991
989
|
* every measure's own numbers under its own heading, and never compute a
|
|
992
990
|
* combined figure at the point of display. `anno-coverage.ts`'s report
|
|
@@ -1106,21 +1104,21 @@ function printCoverageReport(report: CoverageReport): void {
|
|
|
1106
1104
|
|
|
1107
1105
|
/**
|
|
1108
1106
|
* `coverage <image> --store FILE [--out FILE] [--force] [--sample N]` --
|
|
1109
|
-
*
|
|
1107
|
+
* This verb's delivery path: the instrument from `anno-coverage.ts`, run against
|
|
1110
1108
|
* a real program and a real annotation store.
|
|
1111
1109
|
*
|
|
1112
1110
|
* TWO PATHS, NEITHER DERIVED FROM THE OTHER. `<image>` carries the payload
|
|
1113
1111
|
* bytes and the load origin; `--store` names the annotation store holding the
|
|
1114
1112
|
* labels, comments and typed ranges. The store holds annotations and never
|
|
1115
1113
|
* bytes, so a derived measure has to be told which bytes it is measuring, and
|
|
1116
|
-
* guessing one path from the other is exactly the auto-pick
|
|
1114
|
+
* guessing one path from the other is exactly the auto-pick this file forbids.
|
|
1117
1115
|
*
|
|
1118
1116
|
* Two properties this function must keep:
|
|
1119
1117
|
* - NO SECOND PATH VALIDATOR (T-19-22 / T-29-28), over ALL THREE of this
|
|
1120
1118
|
* verb's caller-supplied paths -- the positional, `--store` and `--out`.
|
|
1121
1119
|
* The count is stated because it was WRONG: this doc said "both" and meant
|
|
1122
1120
|
* it, while `--out` reached `refuseOverwrite()` and `writeFileSync()` as
|
|
1123
|
-
* the raw caller string.
|
|
1121
|
+
* the raw caller string. An independent review reproduced the escape --
|
|
1124
1122
|
* `coverage <project> --store <store> --out /tmp/...` wrote the report
|
|
1125
1123
|
* outside the workspace root. All three now go through
|
|
1126
1124
|
* `storePathWithinWorkspace()` against `repoRoot()` -- the one seam, the
|
|
@@ -1133,7 +1131,7 @@ function printCoverageReport(report: CoverageReport): void {
|
|
|
1133
1131
|
* synopsis line -- and fails when that inventory and the surface disagree
|
|
1134
1132
|
* either way, or when this file's confinement call sites number fewer
|
|
1135
1133
|
* than the inventory's entries. It does not associate a given argument
|
|
1136
|
-
* with a given call site
|
|
1134
|
+
* with a given call site, so it cannot tell six arguments
|
|
1137
1135
|
* confined once each from five confined with one confined twice.
|
|
1138
1136
|
* - THE STORE IS OPENED ONCE, read-only, for the whole verb, and closed in a
|
|
1139
1137
|
* `finally`. `mustExist` is what makes "the annotations are gone" and
|
|
@@ -1189,10 +1187,10 @@ async function cmdCoverage(rest: string[]): Promise<number> {
|
|
|
1189
1187
|
return 1;
|
|
1190
1188
|
}
|
|
1191
1189
|
|
|
1192
|
-
//
|
|
1190
|
+
// The ONE confinement seam, for ALL THREE
|
|
1193
1191
|
// caller-supplied paths. Never a second hand-rolled one, and never a
|
|
1194
1192
|
// different rule for the store than for the program it annotates -- or, as
|
|
1195
|
-
//
|
|
1193
|
+
// an earlier review found, no rule at all for the report this verb writes.
|
|
1196
1194
|
const workspaceRoot = repoRoot();
|
|
1197
1195
|
let projectPath: string;
|
|
1198
1196
|
let storePath: string;
|
|
@@ -1284,7 +1282,7 @@ async function cmdCoverage(rest: string[]): Promise<number> {
|
|
|
1284
1282
|
if (!report.project.payloadDecoded) {
|
|
1285
1283
|
// Not a low measurement -- an unreadable payload means every byte-side
|
|
1286
1284
|
// measure above was computed over nothing. Reported as the caller-facing
|
|
1287
|
-
// failure it is, AFTER the report, so the reason is on screen
|
|
1285
|
+
// failure it is, AFTER the report, so the reason is on screen.
|
|
1288
1286
|
console.error(`coverage: the project's payload was UNAVAILABLE -- ${report.project.reason ?? "reason not recorded"}`);
|
|
1289
1287
|
return 1;
|
|
1290
1288
|
}
|
|
@@ -1297,7 +1295,7 @@ interface ExportAsmParsedArgs {
|
|
|
1297
1295
|
storeMissingValue?: boolean;
|
|
1298
1296
|
out?: string;
|
|
1299
1297
|
outMissingValue?: boolean;
|
|
1300
|
-
/**
|
|
1298
|
+
/** The ledger `c64-provenance-diff` generates
|
|
1301
1299
|
* (`recovery/PROVENANCE.md`). OPTIONAL -- see `ExportAsmOptions.ledgerPath`
|
|
1302
1300
|
* in `anno-export-asm.ts` for why. */
|
|
1303
1301
|
ledger?: string;
|
|
@@ -1307,7 +1305,7 @@ interface ExportAsmParsedArgs {
|
|
|
1307
1305
|
}
|
|
1308
1306
|
|
|
1309
1307
|
/** Fixed, closed option set for export-asm -- exactly `--store`, `--out`,
|
|
1310
|
-
* `--ledger` and `--force`. The SAME
|
|
1308
|
+
* `--ledger` and `--force`. The SAME closed-option-set posture, and deliberately the same
|
|
1311
1309
|
* SHAPE, as `parseRenderMemmapArgs()` and `parseCoverageArgs()` above rather
|
|
1312
1310
|
* than a third convention: an unimplemented flag is refused as
|
|
1313
1311
|
* `unknownOption`, and `--store`/`--out`/`--ledger` with a missing or
|
|
@@ -1370,8 +1368,8 @@ function parseExportAsmArgs(rest: string[]): ExportAsmParsedArgs {
|
|
|
1370
1368
|
* is a GENERATED VIEW of the annotations, so it belongs beside the artefact it
|
|
1371
1369
|
* was generated from. The image is an input this verb only reads.
|
|
1372
1370
|
*
|
|
1373
|
-
* NO EXTENSION, on purpose (
|
|
1374
|
-
* FILE to a DIRECTORY
|
|
1371
|
+
* NO EXTENSION, on purpose (`--out` was promoted from a
|
|
1372
|
+
* FILE to a DIRECTORY). This names a directory the tree is written
|
|
1375
1373
|
* INTO, never a file -- a name ending in `.a` would read as a file to every
|
|
1376
1374
|
* human and every tool that inspects it, and the tree this verb writes is
|
|
1377
1375
|
* not one. The stem is derived the same way it always was (whatever
|
|
@@ -1410,15 +1408,15 @@ function pathIsOrContains(containerPath: string, candidate: string): boolean {
|
|
|
1410
1408
|
/**
|
|
1411
1409
|
* `export-asm <image> --store FILE [--out DIR] [--ledger FILE] [--force]` --
|
|
1412
1410
|
* a TREE of ACME source files for a program, emitted from its annotation
|
|
1413
|
-
* store by `anno-export-asm.ts`'s `exportAsmTree()` (
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1411
|
+
* store by `anno-export-asm.ts`'s `exportAsmTree()` (`--out` promoted from a
|
|
1412
|
+
* FILE to a DIRECTORY, a decision made deliberately at a checkpoint rather
|
|
1413
|
+
* than left to fall out of implementation).
|
|
1416
1414
|
*
|
|
1417
1415
|
* EVERY ONE OF THIS VERB'S PATHS IS CONFINED, and the ORDER each step happens
|
|
1418
1416
|
* in is the load-bearing part rather than the mere presence of the calls. It
|
|
1419
1417
|
* follows `cmdRenderMemmap()`'s chain deliberately, because that chain is the
|
|
1420
|
-
* corrected shape of three reproduced escapes (
|
|
1421
|
-
*
|
|
1418
|
+
* corrected shape of three reproduced escapes (an independent verification
|
|
1419
|
+
* pass) on exactly the argument shapes this verb
|
|
1422
1420
|
* has:
|
|
1423
1421
|
*
|
|
1424
1422
|
* - `<image>` and `--store` go through `storePathWithinWorkspace()` BEFORE
|
|
@@ -1426,12 +1424,12 @@ function pathIsOrContains(containerPath: string, candidate: string): boolean {
|
|
|
1426
1424
|
* this file exist" for any path this process can reach -- so probing first
|
|
1427
1425
|
* and confining second would leak that answer for a path the seam is about
|
|
1428
1426
|
* to refuse.
|
|
1429
|
-
* - `--ledger`
|
|
1427
|
+
* - `--ledger` joins that SAME confinement
|
|
1430
1428
|
* block, on the SAME terms, WHEN SUPPLIED -- it is a third input this run
|
|
1431
1429
|
* reads, not a second-class one confined later or not at all.
|
|
1432
1430
|
* - `--out`'s DEFAULT is applied FIRST and the result confined AFTER, so a
|
|
1433
1431
|
* path this verb computed is confined by the same rule as one a caller
|
|
1434
|
-
* supplied, rather than trusted because this verb computed it
|
|
1432
|
+
* supplied, rather than trusted because this verb computed it.
|
|
1435
1433
|
* This is unchanged by the file-to-directory promotion: the confined
|
|
1436
1434
|
* result now NAMES A DIRECTORY rather than a file, but it is confined by
|
|
1437
1435
|
* the exact same call.
|
|
@@ -1441,8 +1439,9 @@ function pathIsOrContains(containerPath: string, candidate: string): boolean {
|
|
|
1441
1439
|
* and `exportAsmTree()` -- so every printed line names the file or
|
|
1442
1440
|
* directory that is actually on disk.
|
|
1443
1441
|
* - The output directory may not BE, and may not CONTAIN, any of the three
|
|
1444
|
-
* inputs
|
|
1445
|
-
* equality check,
|
|
1442
|
+
* inputs -- generalised from the single-file version's plain
|
|
1443
|
+
* equality check, once this verb started writing a directory rather than
|
|
1444
|
+
* a file. `pathIsOrContains()` runs against
|
|
1446
1445
|
* the CONFINED destination and each CONFINED input, so what it protects
|
|
1447
1446
|
* is the input that would actually be read and the directory that would
|
|
1448
1447
|
* actually be written into -- and `--force` does not lift this refusal,
|
|
@@ -1451,7 +1450,7 @@ function pathIsOrContains(containerPath: string, candidate: string): boolean {
|
|
|
1451
1450
|
* writing".
|
|
1452
1451
|
* - The output-directory's own overwrite question -- does it already hold
|
|
1453
1452
|
* something, and may `--force` replace it -- is `exportAsmTree()`'s own
|
|
1454
|
-
* contract
|
|
1453
|
+
* contract, not a second check grown here. This
|
|
1455
1454
|
* verb adds no overwrite rule of its own for the directory as a whole.
|
|
1456
1455
|
*
|
|
1457
1456
|
* WHAT THIS VERB DOES NOT DO, stated here as well as in `USAGE` because a
|
|
@@ -1502,7 +1501,7 @@ async function cmdExportAsm(rest: string[]): Promise<number> {
|
|
|
1502
1501
|
return 1;
|
|
1503
1502
|
}
|
|
1504
1503
|
|
|
1505
|
-
//
|
|
1504
|
+
// The ONE confinement seam, on both input paths, BEFORE any
|
|
1506
1505
|
// filesystem probe. `openStore()` downstream is handed this same workspace
|
|
1507
1506
|
// root, so its own confinement agrees by construction rather than by a
|
|
1508
1507
|
// second rule. `--ledger` joins this SAME block, WHEN SUPPLIED -- confined
|
|
@@ -1541,7 +1540,7 @@ async function cmdExportAsm(rest: string[]): Promise<number> {
|
|
|
1541
1540
|
return 1;
|
|
1542
1541
|
}
|
|
1543
1542
|
|
|
1544
|
-
//
|
|
1543
|
+
// The default is applied FIRST and the RESULT confined,
|
|
1545
1544
|
// so the derived path and a caller-supplied one are confined by the same
|
|
1546
1545
|
// rule.
|
|
1547
1546
|
let outPath: string;
|
|
@@ -1553,8 +1552,8 @@ async function cmdExportAsm(rest: string[]): Promise<number> {
|
|
|
1553
1552
|
}
|
|
1554
1553
|
|
|
1555
1554
|
// THE OUTPUT DIRECTORY MAY NOT BE, AND MAY NOT CONTAIN, AN INPUT, AND
|
|
1556
|
-
// `--force` DOES NOT OVERRIDE THIS (
|
|
1557
|
-
//
|
|
1555
|
+
// `--force` DOES NOT OVERRIDE THIS (generalising an earlier plain-equality
|
|
1556
|
+
// refusal to containment now that `--out` names a directory
|
|
1558
1557
|
// a whole tree is written into). The single-file version of this refusal
|
|
1559
1558
|
// existed because `outPath` was confined and overwrite-checked but never
|
|
1560
1559
|
// COMPARED to the inputs, so `anno export-asm game.raw --store g.annostore
|
|
@@ -1563,7 +1562,7 @@ async function cmdExportAsm(rest: string[]): Promise<number> {
|
|
|
1563
1562
|
// same mistake from one file to everything the directory would hold, so the
|
|
1564
1563
|
// check widens from equality to containment with it: the directory may not
|
|
1565
1564
|
// itself BE an input's own path, and no input may live INSIDE it.
|
|
1566
|
-
// `--ledger`
|
|
1565
|
+
// `--ledger` joins this SAME check: it is a
|
|
1567
1566
|
// THIRD input this run reads, and `--force` must not lift the refusal for
|
|
1568
1567
|
// it any more than it lifts it for the store or the image.
|
|
1569
1568
|
//
|
|
@@ -1594,8 +1593,8 @@ async function cmdExportAsm(rest: string[]): Promise<number> {
|
|
|
1594
1593
|
|
|
1595
1594
|
// The output-directory contract itself -- create when missing, refuse a
|
|
1596
1595
|
// non-empty directory without `--force`, and with `--force` replace only
|
|
1597
|
-
// the names this export produces -- lives entirely in `exportAsmTree()
|
|
1598
|
-
//
|
|
1596
|
+
// the names this export produces -- lives entirely in `exportAsmTree()`.
|
|
1597
|
+
// This verb adds no second overwrite rule of its
|
|
1599
1598
|
// own: it forwards the request and reports the library's own refusal
|
|
1600
1599
|
// through this same single-line error path every other exporter refusal
|
|
1601
1600
|
// already takes.
|
|
@@ -1638,7 +1637,7 @@ interface EvidDisagreementsParsedArgs {
|
|
|
1638
1637
|
}
|
|
1639
1638
|
|
|
1640
1639
|
/** Fixed, closed option set for evid-disagreements -- exactly `--store` and
|
|
1641
|
-
* `--json`. Same
|
|
1640
|
+
* `--json`. Same closed-option-set posture as every other verb's own parser: an
|
|
1642
1641
|
* unimplemented flag is refused as `unknownOption`, and `--store` with a
|
|
1643
1642
|
* missing or flag-shaped value is refused through its own `*MissingValue`
|
|
1644
1643
|
* field rather than silently swallowing the next token. `--json` is a plain
|
|
@@ -1709,8 +1708,8 @@ function printEvidDisagreementsReport(storePath: string, r: EvidReconciliation):
|
|
|
1709
1708
|
}
|
|
1710
1709
|
|
|
1711
1710
|
/**
|
|
1712
|
-
* `evid-disagreements --store FILE [--json]` -- the CLI route for
|
|
1713
|
-
* disagreement query
|
|
1711
|
+
* `evid-disagreements --store FILE [--json]` -- the CLI route for the
|
|
1712
|
+
* disagreement query: the criterion that
|
|
1714
1713
|
* settles the question is that a planted test needs the disagreement,
|
|
1715
1714
|
* agreement and silence states rendered as three DIFFERENT pieces of TEXT
|
|
1716
1715
|
* it can tell apart, which an MCP tool's JSON answer can only be inspected
|
|
@@ -1769,10 +1768,10 @@ async function cmdEvidDisagreements(rest: string[]): Promise<number> {
|
|
|
1769
1768
|
return 1;
|
|
1770
1769
|
}
|
|
1771
1770
|
let reconciliation: EvidReconciliation;
|
|
1772
|
-
// Rule 2 (missing critical functionality)
|
|
1771
|
+
// Rule 2 (missing critical functionality): `runIdentity`
|
|
1773
1772
|
// is NOT an `EvidReconciliation` field -- it rides alongside the spread
|
|
1774
1773
|
// reconciliation in the JSON envelope, exactly like `store` already does.
|
|
1775
|
-
// Added so `decomp-completeness`
|
|
1774
|
+
// Added so `decomp-completeness` has a run identity to
|
|
1776
1775
|
// validate this document against the SAME store's own `anno_evid_runs`
|
|
1777
1776
|
// table, rather than accepting a fabricated or foreign empty document as
|
|
1778
1777
|
// this run's own answer. `null` when the store holds zero or more than one
|
|
@@ -1804,15 +1803,15 @@ async function cmdEvidDisagreements(rest: string[]): Promise<number> {
|
|
|
1804
1803
|
}
|
|
1805
1804
|
|
|
1806
1805
|
// ---------------------------------------------------------------------------
|
|
1807
|
-
// decomp-completeness
|
|
1806
|
+
// decomp-completeness -- the fifth verb.
|
|
1808
1807
|
// ---------------------------------------------------------------------------
|
|
1809
1808
|
|
|
1810
1809
|
/**
|
|
1811
|
-
* The frozen survivor prefix set
|
|
1810
|
+
* The frozen survivor prefix set, measured
|
|
1812
1811
|
* against a real dxa+Ghidra-derived store rather than against roadmap prose
|
|
1813
|
-
* alone --
|
|
1814
|
-
*
|
|
1815
|
-
*
|
|
1812
|
+
* alone -- MEASURED against a zero-label population (derivation writes typed
|
|
1813
|
+
* ranges and xrefs, never names) and the reasoning this set was frozen
|
|
1814
|
+
* against. `AUTO_NAME_PREFIX_RE`
|
|
1816
1815
|
* (imported from anno-coverage.ts, NEVER restated as a second literal here --
|
|
1817
1816
|
* a census over this file for any of its own eleven prefix strings returns
|
|
1818
1817
|
* zero, proving that) covers the eleven upstream-analyser-shaped
|
|
@@ -1837,8 +1836,8 @@ function isSurvivorLabelName(name: string): boolean {
|
|
|
1837
1836
|
return AUTO_NAME_PREFIX_RE.test(name) || SURVIVOR_EXTRA_RE.test(name);
|
|
1838
1837
|
}
|
|
1839
1838
|
|
|
1840
|
-
/** One row of the manifest `anno decomp-completeness --manifest FILE` reads
|
|
1841
|
-
*
|
|
1839
|
+
/** One row of the manifest `anno decomp-completeness --manifest FILE` reads.
|
|
1840
|
+
* `path` is relative to `src/mcp/vice/fixtures`; `reason` is
|
|
1842
1841
|
* required (non-empty) when `execution` is `"not-executed"` and `null`
|
|
1843
1842
|
* otherwise. */
|
|
1844
1843
|
interface DecompExecutionManifestEntry {
|
|
@@ -1863,16 +1862,16 @@ function fixtureStem(path: string): string {
|
|
|
1863
1862
|
return base.replace(/\.[^./]+$/, "");
|
|
1864
1863
|
}
|
|
1865
1864
|
|
|
1866
|
-
/** The subset of `EvidReconciliation` (verbatim field names, never renamed
|
|
1867
|
-
*
|
|
1865
|
+
/** The subset of `EvidReconciliation` (verbatim field names, never renamed)
|
|
1866
|
+
* that a `--disagreements` document must carry for
|
|
1868
1867
|
* `decomp-completeness` to accept it as real, plus the `runIdentity` this
|
|
1869
1868
|
* verb (via `cmdEvidDisagreements`'s own `--json` branch) adds alongside it.
|
|
1870
1869
|
* `disagreementInput` in the `--json` answer below is exactly this shape. */
|
|
1871
1870
|
interface DecompDisagreementInput extends EvidReconciliation {
|
|
1872
|
-
//
|
|
1871
|
+
// (Rule 1 fix, disclosed): `null` is a THIRD, LEGITIMATE
|
|
1873
1872
|
// value here -- `anno evid-disagreements --json`'s own `runIdentity` field
|
|
1874
1873
|
// reads `null` when the store holds zero observed runs (listObservedRuns()),
|
|
1875
|
-
// which is exactly the real, non-fabricated answer a
|
|
1874
|
+
// which is exactly the real, non-fabricated answer a non-executed
|
|
1876
1875
|
// fixture's store produces. Refusing null unconditionally made a real
|
|
1877
1876
|
// `anno evid-disagreements --json` answer for a non-executed fixture
|
|
1878
1877
|
// unusable by this verb, contradicting this phase's own must_haves ("a
|
|
@@ -1899,28 +1898,28 @@ const EVID_RECONCILIATION_FIELDS = [
|
|
|
1899
1898
|
/**
|
|
1900
1899
|
* Validates a parsed `--disagreements` document has every `EvidReconciliation`
|
|
1901
1900
|
* field AND a complete `runIdentity` -- refusing BY NAME, never silently
|
|
1902
|
-
* treating a missing field as an empty answer (
|
|
1901
|
+
* treating a missing field as an empty answer (a required
|
|
1903
1902
|
* output-schema field only the real `--disagreements` input can populate).
|
|
1904
1903
|
* Returns the validated document (typed as `DecompDisagreementInput`) or a
|
|
1905
1904
|
* refusal message string. Never throws.
|
|
1906
1905
|
*/
|
|
1907
1906
|
function validateDisagreementDocumentShape(doc: unknown): DecompDisagreementInput | string {
|
|
1908
1907
|
if (typeof doc !== "object" || doc === null) {
|
|
1909
|
-
return "decomp-completeness: the --disagreements document is not a JSON object -- refusing to render
|
|
1908
|
+
return "decomp-completeness: the --disagreements document is not a JSON object -- refusing to render";
|
|
1910
1909
|
}
|
|
1911
1910
|
const bag = doc as Record<string, unknown>;
|
|
1912
1911
|
for (const field of EVID_RECONCILIATION_FIELDS) {
|
|
1913
1912
|
if (!(field in bag)) {
|
|
1914
1913
|
return (
|
|
1915
1914
|
`decomp-completeness: the --disagreements document is missing the "${field}" field -- ` +
|
|
1916
|
-
"this is not a real anno evid-disagreements --json answer, refusing to render
|
|
1915
|
+
"this is not a real anno evid-disagreements --json answer, refusing to render"
|
|
1917
1916
|
);
|
|
1918
1917
|
}
|
|
1919
1918
|
}
|
|
1920
1919
|
const runIdentity = bag.runIdentity;
|
|
1921
|
-
// Rule 1 fix (disclosed
|
|
1920
|
+
// Rule 1 fix (disclosed): `null` is accepted HERE as a
|
|
1922
1921
|
// well-formed shape -- it is `anno evid-disagreements --json`'s own real
|
|
1923
|
-
// answer for a store with zero observed runs (a
|
|
1922
|
+
// answer for a store with zero observed runs (a non-executed
|
|
1924
1923
|
// fixture). It is NOT yet accepted as a legitimate ANSWER: cmdDecompCompleteness's
|
|
1925
1924
|
// own match-check below still refuses a null identity unless the store's
|
|
1926
1925
|
// evid-runs table is ALSO genuinely empty, so a store that DOES carry real
|
|
@@ -1935,7 +1934,7 @@ function validateDisagreementDocumentShape(doc: unknown): DecompDisagreementInpu
|
|
|
1935
1934
|
return (
|
|
1936
1935
|
"decomp-completeness: the --disagreements document carries no complete runIdentity " +
|
|
1937
1936
|
"(image_sha256/argv_digest/seed) -- an empty or ambiguous-run document is refused rather than " +
|
|
1938
|
-
"rendered as \"no disagreements\"
|
|
1937
|
+
"rendered as \"no disagreements\""
|
|
1939
1938
|
);
|
|
1940
1939
|
}
|
|
1941
1940
|
}
|
|
@@ -1943,12 +1942,12 @@ function validateDisagreementDocumentShape(doc: unknown): DecompDisagreementInpu
|
|
|
1943
1942
|
}
|
|
1944
1943
|
|
|
1945
1944
|
// ---------------------------------------------------------------------------
|
|
1946
|
-
// The full measure set
|
|
1947
|
-
// (
|
|
1948
|
-
// disagreementResolution (
|
|
1945
|
+
// The full measure set -- rangeProvenance
|
|
1946
|
+
// (typed by evidence, never inferred), entryPoints, referencedAddresses and
|
|
1947
|
+
// disagreementResolution (the gate-vs-bulletin distinction).
|
|
1949
1948
|
// ---------------------------------------------------------------------------
|
|
1950
1949
|
|
|
1951
|
-
/** One typed range's provenance classification
|
|
1950
|
+
/** One typed range's provenance classification. Always
|
|
1952
1951
|
* one of the three named values -- never a fourth, never a boolean. */
|
|
1953
1952
|
type RangeTypedBy = "observed-executing" | "byte-derived" | "authored";
|
|
1954
1953
|
|
|
@@ -1957,7 +1956,7 @@ interface RangeProvenanceRow {
|
|
|
1957
1956
|
endInclusive: number;
|
|
1958
1957
|
dataType: DataType;
|
|
1959
1958
|
/** `dataType` unless it is one of the four `SPLIT_DATA_TYPES` members, in
|
|
1960
|
-
* which case it renders as `"table"`
|
|
1959
|
+
* which case it renders as `"table"` -- read from `anno-types.ts`'s
|
|
1961
1960
|
* own `isSplitDataType()`, NEVER a restated literal, so the four split
|
|
1962
1961
|
* spellings never appear in this file's own source as strings. */
|
|
1963
1962
|
renderedType: string;
|
|
@@ -2100,7 +2099,7 @@ function commentReasonAfterPrefix(comments: readonly CommentRow[], address: numb
|
|
|
2100
2099
|
return found ? found.text.slice(prefix.length).trim() : null;
|
|
2101
2100
|
}
|
|
2102
2101
|
|
|
2103
|
-
/**
|
|
2102
|
+
/** How ONE typed range was typed. Evidence beats
|
|
2104
2103
|
* inference, stated as a fixed precedence that must never be reordered:
|
|
2105
2104
|
* `observed-executing` (at least one real execute observation falls inside
|
|
2106
2105
|
* the range) beats `authored` (the range's start address carries an
|
|
@@ -2112,7 +2111,7 @@ function typedByFor(hasObservation: boolean, hasAuthoredComment: boolean): Range
|
|
|
2112
2111
|
return "byte-derived";
|
|
2113
2112
|
}
|
|
2114
2113
|
|
|
2115
|
-
/** Builds `rangeProvenance
|
|
2114
|
+
/** Builds `rangeProvenance`: one row per typed range,
|
|
2116
2115
|
* sorted ascending by `start` then `endInclusive` (ranges never overlap, so
|
|
2117
2116
|
* this is already the input order once `ranges` itself is pre-sorted, but
|
|
2118
2117
|
* the sort is restated here so this function's OWN output contract does not
|
|
@@ -2145,7 +2144,7 @@ function buildRangeProvenance(
|
|
|
2145
2144
|
* address (`image.origin`) -- the fixture's own natural entry point.
|
|
2146
2145
|
* Sorted ascending by address.
|
|
2147
2146
|
*
|
|
2148
|
-
* `image === null` (
|
|
2147
|
+
* `image === null` (fixed 2026-09-11) means the fixture's
|
|
2149
2148
|
* own bytes could not be located: no instructions are decoded and NO
|
|
2150
2149
|
* `image.origin` candidate is added -- a missing image degrades this to
|
|
2151
2150
|
* whatever the store's own stored `xrefs` already establish, never a
|
|
@@ -2204,7 +2203,7 @@ function buildEntryPoints(
|
|
|
2204
2203
|
* decline's own reason), or `unresolved` (neither) -- sorted ascending by
|
|
2205
2204
|
* address within each bucket.
|
|
2206
2205
|
*
|
|
2207
|
-
* `image === null` (
|
|
2206
|
+
* `image === null` (fixed 2026-09-11): no instructions are
|
|
2208
2207
|
* decoded, so this degrades to whatever the store's own stored `xrefs`
|
|
2209
2208
|
* establish -- never fabricated from a placeholder image's bytes. */
|
|
2210
2209
|
function buildReferencedAddresses(
|
|
@@ -2243,7 +2242,7 @@ function buildReferencedAddresses(
|
|
|
2243
2242
|
return { resolved, declined, unresolved, denominator: resolved.length + declined.length + unresolved.length };
|
|
2244
2243
|
}
|
|
2245
2244
|
|
|
2246
|
-
/** Builds `disagreementResolution` (
|
|
2245
|
+
/** Builds `disagreementResolution` (the gate-vs-bulletin distinction,
|
|
2247
2246
|
* criterion 2): one row per disagreement the supplied `--disagreements`
|
|
2248
2247
|
* document carries, `accepted` when the address carries a
|
|
2249
2248
|
* `DISAGREEMENT_ACCEPTED_COMMENT_PREFIX` comment, `resolved` identically (the
|
|
@@ -2331,8 +2330,7 @@ function parseDecompCompletenessArgs(rest: string[]): DecompCompletenessParsedAr
|
|
|
2331
2330
|
* unknown option -> refuse missing value -> refuse missing required argument
|
|
2332
2331
|
* BY NAME -> `storePathWithinWorkspace()` every caller-supplied path -> open
|
|
2333
2332
|
* the store `mustExist: true` -> gather -> `--json` branch or rendered
|
|
2334
|
-
* branch. Three required arguments, none defaulted from another
|
|
2335
|
-
* mechanism 1).
|
|
2333
|
+
* branch. Three required arguments, none defaulted from another.
|
|
2336
2334
|
*/
|
|
2337
2335
|
async function cmdDecompCompleteness(rest: string[]): Promise<number> {
|
|
2338
2336
|
const { store, storeMissingValue, disagreements, disagreementsMissingValue, manifest, manifestMissingValue, json, unknownOption } =
|
|
@@ -2366,7 +2364,7 @@ async function cmdDecompCompleteness(rest: string[]): Promise<number> {
|
|
|
2366
2364
|
if (!disagreements) {
|
|
2367
2365
|
console.error(
|
|
2368
2366
|
"decomp-completeness: --disagreements FILE is required -- there is no default and no empty-array " +
|
|
2369
|
-
"substitute; omitting the disagreement input must never render the same report as a real, empty answer
|
|
2367
|
+
"substitute; omitting the disagreement input must never render the same report as a real, empty answer.\n",
|
|
2370
2368
|
);
|
|
2371
2369
|
console.log(USAGE);
|
|
2372
2370
|
return 1;
|
|
@@ -2374,7 +2372,7 @@ async function cmdDecompCompleteness(rest: string[]): Promise<number> {
|
|
|
2374
2372
|
if (!manifest) {
|
|
2375
2373
|
console.error(
|
|
2376
2374
|
"decomp-completeness: --manifest FILE is required -- a fixture absent from the manifest is refused, " +
|
|
2377
|
-
"never defaulted to \"executed\"
|
|
2375
|
+
"never defaulted to \"executed\".\n",
|
|
2378
2376
|
);
|
|
2379
2377
|
console.log(USAGE);
|
|
2380
2378
|
return 1;
|
|
@@ -2445,7 +2443,7 @@ async function cmdDecompCompleteness(rest: string[]): Promise<number> {
|
|
|
2445
2443
|
if (!manifestEntry) {
|
|
2446
2444
|
console.error(
|
|
2447
2445
|
`decomp-completeness: no fixture matching store ${JSON.stringify(basename(storePath))} (stem ${JSON.stringify(stem)}) ` +
|
|
2448
|
-
`is listed in the manifest ${manifestPath} -- an unlisted fixture is refused, never defaulted to "executed"
|
|
2446
|
+
`is listed in the manifest ${manifestPath} -- an unlisted fixture is refused, never defaulted to "executed".`,
|
|
2449
2447
|
);
|
|
2450
2448
|
return 1;
|
|
2451
2449
|
}
|
|
@@ -2466,7 +2464,7 @@ async function cmdDecompCompleteness(rest: string[]): Promise<number> {
|
|
|
2466
2464
|
byteCensus: { byType: Record<string, number>; undefinedCount: number; denominator: number; undefinedRanges: { start: number; endInclusive: number }[] };
|
|
2467
2465
|
survivors: { address: number; name: string }[];
|
|
2468
2466
|
rangeProvenance: RangeProvenanceRow[];
|
|
2469
|
-
//
|
|
2467
|
+
// Fixed 2026-09-11: true when the fixture's own image
|
|
2470
2468
|
// bytes could not be located -- see the fallback below. `entryPoints`/
|
|
2471
2469
|
// `referencedAddresses` are DEGRADED (never fabricated) when this is
|
|
2472
2470
|
// true: no synthetic `$0000` entry point is manufactured from a
|
|
@@ -2480,9 +2478,9 @@ async function cmdDecompCompleteness(rest: string[]): Promise<number> {
|
|
|
2480
2478
|
try {
|
|
2481
2479
|
const ranges = listRanges(handle);
|
|
2482
2480
|
const { runs } = listObservedRuns(handle);
|
|
2483
|
-
// Rule 1 fix (disclosed
|
|
2481
|
+
// Rule 1 fix (disclosed): a `null` runIdentity is accepted
|
|
2484
2482
|
// ONLY when the store's own evid-runs table is ALSO genuinely empty --
|
|
2485
|
-
// the real, honest answer for a
|
|
2483
|
+
// the real, honest answer for a non-executed fixture. A store that
|
|
2486
2484
|
// DOES carry real runs must still supply a real, matching identity; the
|
|
2487
2485
|
// anti-vacuity property this whole check exists for is unaffected.
|
|
2488
2486
|
if (disagreementInput.runIdentity === null) {
|
|
@@ -2543,14 +2541,14 @@ async function cmdDecompCompleteness(rest: string[]): Promise<number> {
|
|
|
2543
2541
|
.map((l) => ({ address: l.address, name: l.name }))
|
|
2544
2542
|
.sort((a, b) => a.address - b.address);
|
|
2545
2543
|
|
|
2546
|
-
// The full measure set
|
|
2544
|
+
// The full measure set. All four use the SAME
|
|
2547
2545
|
// fixture bytes the derivation route itself read -- the fixtures-relative
|
|
2548
2546
|
// manifest path, resolved beside this module (`fixtures/<manifestEntry.path>`),
|
|
2549
2547
|
// never a second guess at where the image lives. An image that cannot be
|
|
2550
2548
|
// located (never expected for a committed fixture, but never fabricated
|
|
2551
2549
|
// either) degrades entryPoints/referencedAddresses to EMPTY -- never a
|
|
2552
2550
|
// synthetic zero-length placeholder whose own `origin` (0) would read as
|
|
2553
|
-
// a real `$0000` entry point (
|
|
2551
|
+
// a real `$0000` entry point (fixed 2026-09-11: the
|
|
2554
2552
|
// placeholder's origin was previously unioned into the candidate set
|
|
2555
2553
|
// unconditionally, fabricating a plausible-looking but fictitious
|
|
2556
2554
|
// finding). `imageUnavailable` reports the condition BY NAME instead.
|
|
@@ -2598,8 +2596,8 @@ async function cmdDecompCompleteness(rest: string[]): Promise<number> {
|
|
|
2598
2596
|
* Copies `printEvidDisagreementsReport()`'s rendering discipline exactly:
|
|
2599
2597
|
* every measure under its own heading, disagreements first, `denominator`
|
|
2600
2598
|
* beside every count, an explicit sentence stating what absence does NOT
|
|
2601
|
-
* prove, and never a percentage, rate or combined figure
|
|
2602
|
-
*
|
|
2599
|
+
* prove, and never a percentage, rate or combined figure -- the same rule
|
|
2600
|
+
* this CLI applies everywhere else. This is the FALLBACK text renderer for a direct CLI
|
|
2603
2601
|
* invocation without `--json`; `completeness-report.mjs`'s
|
|
2604
2602
|
* `renderCompletenessReport()` is the report the routine-queue-walker skill
|
|
2605
2603
|
* actually reads, built from this same verb's `--json` answer.
|
|
@@ -2623,7 +2621,7 @@ function printDecompCompletenessReport(r: {
|
|
|
2623
2621
|
if (r.executionDisposition === "not-executed") {
|
|
2624
2622
|
console.log(` NOT EXECUTED: ${r.notExecutedReason ?? "(no reason recorded)"}`);
|
|
2625
2623
|
} else {
|
|
2626
|
-
console.log(" EXECUTED: this fixture was run under the reproducible-run protocol
|
|
2624
|
+
console.log(" EXECUTED: this fixture was run under the reproducible-run protocol.");
|
|
2627
2625
|
}
|
|
2628
2626
|
console.log("");
|
|
2629
2627
|
console.log(` BYTE CENSUS (denominator ${r.byteCensus.denominator})`);
|
|
@@ -2674,7 +2672,7 @@ function printDecompCompletenessReport(r: {
|
|
|
2674
2672
|
}
|
|
2675
2673
|
console.log("");
|
|
2676
2674
|
|
|
2677
|
-
//
|
|
2675
|
+
// Fixed 2026-09-11: named BY NAME, not inferred from a
|
|
2678
2676
|
// suspiciously-empty entryPoints/referencedAddresses census.
|
|
2679
2677
|
if (r.imageUnavailable) {
|
|
2680
2678
|
console.log(" IMAGE UNAVAILABLE: the fixture's own image bytes could not be located -- entryPoints and referencedAddresses below are degraded to what the store's own stored xrefs establish, never fabricated from a placeholder image.");
|
|
@@ -2730,7 +2728,7 @@ interface HazardReportParsedArgs {
|
|
|
2730
2728
|
}
|
|
2731
2729
|
|
|
2732
2730
|
/** Fixed, closed option set for hazard-report -- exactly `--store`,
|
|
2733
|
-
* `--image` and `--json`. Same
|
|
2731
|
+
* `--image` and `--json`. Same closed-option-set posture as every other verb's own
|
|
2734
2732
|
* parser: an unimplemented flag is refused as `unknownOption`, and an
|
|
2735
2733
|
* option with a missing or flag-shaped value is refused through its own
|
|
2736
2734
|
* `*MissingValue` field rather than silently swallowing the next token. */
|
|
@@ -2820,7 +2818,7 @@ function printHazardReport(storePath: string, imagePath: string, r: HazardReport
|
|
|
2820
2818
|
else for (const region of unclassified) console.log(` ${hexAddr(region.start)}..${hexAddr(region.endInclusive)} (${region.reason ?? "no reason recorded"})`);
|
|
2821
2819
|
console.log("");
|
|
2822
2820
|
|
|
2823
|
-
//
|
|
2821
|
+
// Rendered here so an operator reading ONLY the human-readable
|
|
2824
2822
|
// report (never --json) still sees the declined dispatch candidates
|
|
2825
2823
|
// HazardReport's own doc comment insists must never be silently dropped --
|
|
2826
2824
|
// "an honest decline indistinguishable from an absence" is exactly the
|
|
@@ -2981,13 +2979,13 @@ export async function runAnnoCli(argv: string[]): Promise<number> {
|
|
|
2981
2979
|
}
|
|
2982
2980
|
|
|
2983
2981
|
try {
|
|
2984
|
-
//
|
|
2982
|
+
// The single call site for the shared verb-options
|
|
2985
2983
|
// check, run BEFORE dispatch so a refused option never reaches any cmd*
|
|
2986
2984
|
// function -- one place enforces the closed option set for every verb,
|
|
2987
2985
|
// rather than seven places each doing (or, as `verify` proved, NOT doing)
|
|
2988
2986
|
// it themselves.
|
|
2989
2987
|
//
|
|
2990
|
-
// INSIDE the try since 2026-08-31
|
|
2988
|
+
// INSIDE the try since 2026-08-31, as defence in depth.
|
|
2991
2989
|
// It used to sit above this block, so a throw from it escaped
|
|
2992
2990
|
// `runAnnoCli()` entirely -- which is exactly what a prototype-key verb
|
|
2993
2991
|
// did. `checkAcceptedOptions()` is now own-property-safe and cannot
|
|
@@ -3015,12 +3013,12 @@ export async function runAnnoCli(argv: string[]): Promise<number> {
|
|
|
3015
3013
|
case "hazard-report":
|
|
3016
3014
|
return await cmdHazardReport(rest);
|
|
3017
3015
|
default:
|
|
3018
|
-
//
|
|
3019
|
-
// `anno:` -- the subcommand renamed to `anno` on 2026-08-29
|
|
3016
|
+
// Corrected 2026-08-30. This prefix read
|
|
3017
|
+
// `anno:` -- the subcommand renamed to `anno` on 2026-08-29
|
|
3020
3018
|
// -- so a user who mistyped a verb was answered by a subcommand that
|
|
3021
3019
|
// no longer dispatches. Only the STRING moved: the enclosing function
|
|
3022
3020
|
// keeps its current name, so no consumer, test or record entry moves
|
|
3023
|
-
// with it
|
|
3021
|
+
// with it.
|
|
3024
3022
|
console.error(
|
|
3025
3023
|
`anno: unknown verb "${verb}" -- this CLI has exactly six: render-memmap, coverage, export-asm, ` +
|
|
3026
3024
|
"evid-disagreements, decomp-completeness and hazard-report\n",
|
|
@@ -3032,8 +3030,8 @@ export async function runAnnoCli(argv: string[]): Promise<number> {
|
|
|
3032
3030
|
// A last-resort net: every expected failure path above already returns its
|
|
3033
3031
|
// own code with its own message, so anything arriving here is unexpected
|
|
3034
3032
|
// and is reported verbatim rather than swallowed. The loud failure is the
|
|
3035
|
-
// point
|
|
3036
|
-
//
|
|
3033
|
+
// point.
|
|
3034
|
+
// Second half of that same fix -- same correction, same reason.
|
|
3037
3035
|
console.error(`anno: ${errMsg(err)}`);
|
|
3038
3036
|
return 1;
|
|
3039
3037
|
}
|