4141// (see proof-registry.mts), a `live` classification MUST carry a valid proof —
4242// the file must exist and declare the `@proof: <id>` tag. CI fails otherwise.
4343//
44+ // EVIDENCE POINTERS (ADR-0087): a `live` verdict IS its evidence pointer — "this
45+ // property has a runtime consumer, here it is". A cited path that is repo-rooted
46+ // and attributed to THIS repo must resolve against this checkout, or CI fails:
47+ // an unresolvable pointer makes the claim unfalsifiable, and a directory move or
48+ // a rename is all it takes (see evidence.mts). Cross-repo attribution
49+ // (`objectui: …`, `cloud: …`, `packages/services/service-ai/…`) is counted, not
50+ // resolved — those files are legitimately absent here. This was a ⚠ until #5623,
51+ // for one historical reason: the pre-#3857 `evidence.split(':')[0]` parser
52+ // flagged 48 of 227 entries with a ~100% false-positive rate, so failing on it
53+ // would have failed every build. The parse fix is what turned a hit into signal.
54+ //
4455// RE-VERIFICATION CLOCK (`verifiedAt`): a ledger entry is a claim with a
4556// timestamp, and code moves under it in BOTH directions — `flow.status` (#3711)
4657// and `action.undoable` (#3714) were both understated by entries that were
5667// tsx check-liveness.mts --stale-verification # print the re-verification worklist
5768// tsx check-liveness.mts --stale-verification=90 # ...with a custom staleness threshold
5869// tsx check-liveness.mts --undrilled # print the undrilled-container worklist
70+ // tsx check-liveness.mts --ledger-root=<dir> # read the ledgers from <dir> instead of
71+ // # packages/spec/liveness — how the self-test
72+ // # runs the REAL gate against a mutated copy
73+ // # of the real ledgers without touching them
5974
6075process . env . OS_EAGER_SCHEMAS = '1' ;
6176
@@ -93,7 +108,22 @@ import {
93108const here = dirname ( fileURLToPath ( import . meta. url ) ) ;
94109const specRoot = resolve ( here , '../..' ) ; // packages/spec
95110const repoRoot = resolve ( specRoot , '../..' ) ;
96- const ledgerRoot = join ( specRoot , 'liveness' ) ;
111+
112+ const args = process . argv . slice ( 2 ) ;
113+
114+ // `--ledger-root=<dir>` points the walk at a COPY of packages/spec/liveness.
115+ // It exists because the evidence gate below now fails the build, and a gate that
116+ // fails is only worth as much as the proof that it fails — which needs a rotted
117+ // pointer to fail ON. Committing one to a shipped ledger is not an option (it
118+ // would fail every other run), and mutating a tracked file mid-test leaves the
119+ // worktree dirty when the test crashes. So the self-test copies the real ledgers
120+ // to a temp dir, breaks exactly one pointer there, and runs THIS script — same
121+ // code path CI runs, no repo state touched. Everything else still resolves
122+ // against the real repoRoot, so the resulting exit 1 has exactly one cause.
123+ const ledgerRootArg = args . find ( ( a ) => a . startsWith ( '--ledger-root=' ) ) ;
124+ const ledgerRoot = ledgerRootArg
125+ ? resolve ( ledgerRootArg . slice ( '--ledger-root=' . length ) )
126+ : join ( specRoot , 'liveness' ) ;
97127
98128// Governed metadata types, rolled out highest-frequency / highest-risk first.
99129// (`query` is not a metadata type — see SPEC_ONLY_SCHEMAS below.)
@@ -262,7 +292,6 @@ function loadLedger(type: string): any {
262292}
263293
264294// ---- dump mode ----
265- const args = process . argv . slice ( 2 ) ;
266295const dumpIdx = args . indexOf ( '--dump' ) ;
267296if ( dumpIdx !== - 1 ) {
268297 const type = args [ dumpIdx + 1 ] ;
@@ -300,8 +329,15 @@ const report: any = {
300329 deferredContainers : [ ] as string [ ] , // containers whose subtree IS classified elsewhere — resolved, not believed
301330 deferredChildKeys : 0 , // how many child keys those resolved deferrals actually cover
302331 verification : null as VerificationReport | null , // `verifiedAt` ages — the re-verification worklist
303- evidenceLocal : 0 , // repo-rooted evidence paths actually resolved against this checkout
304- evidenceForeign : 0 , // evidence paths attributed to objectui / cloud — not resolvable here
332+ // The three evidence counters, and the distinction between the first two is
333+ // the whole point: `evidenceLocal` is how many repo-rooted paths `live` entries
334+ // DECLARE, `evidenceMissing` is how many of those do not exist here. The
335+ // summary line used to print `evidenceLocal` under the word "resolved", so
336+ // breaking five pointers left the count at 330 and the run still said
337+ // "330 resolved" (#5623). Count and word now agree.
338+ evidenceLocal : 0 , // repo-rooted evidence paths attributed to THIS repo — DECLARED, not yet proven
339+ evidenceMissing : 0 , // ...of which this many do not exist here (=== staleEvidence.length) — FAILS the gate
340+ evidenceForeign : 0 , // evidence paths attributed to objectui / cloud — not resolvable here, never failed
305341} ;
306342
307343// Every classified entry, for the `verifiedAt` fold below. Collected during the
@@ -324,6 +360,7 @@ function classify(type: string, path: string, status: string, led: any, cat: any
324360 const ev = checkEvidence ( led . evidence , ( p ) => existsSync ( join ( repoRoot , p ) ) ) ;
325361 report . evidenceForeign += ev . foreign . length ;
326362 report . evidenceLocal += ev . local . length ;
363+ report . evidenceMissing += ev . missing . length ;
327364 for ( const miss of ev . missing ) report . staleEvidence . push ( `${ type } /${ path } → ${ miss } ` ) ;
328365 }
329366 // ── ADR-0054 prove-it-runs ──
@@ -507,6 +544,11 @@ const totalProofFailures = report.proofErrors.length + report.proofMissing.lengt
507544const failed =
508545 totalUnclassified > 0 ||
509546 totalProofFailures > 0 ||
547+ // A `live` entry whose repo-local evidence path is gone. Red since #5623 — the
548+ // ⚠ it replaces was calibrated for the false-positive era, not for the parser
549+ // that now resolves 330 paths and reports zero. Cross-repo attribution never
550+ // reaches this list: checkEvidence only resolves the LOCAL bucket.
551+ report . staleEvidence . length > 0 ||
510552 report . orphanEntries . length > 0 ||
511553 report . verification . errors . length > 0 ||
512554 report . ungoverned . length > 0 ||
@@ -524,13 +566,39 @@ if (asJson) {
524566 }
525567 const boundClasses = HIGH_RISK_CLASSES . filter ( ( c ) => c . bound ) . map ( ( c ) => c . label ) ;
526568 console . log ( `\nprove-it-runs (ADR-0054): proof REQUIRED for bound high-risk classes — ${ boundClasses . join ( ', ' ) || 'none' } ` ) ;
569+ // Two numbers, because one cannot carry both facts. "Declared" is the
570+ // extraction-health signal (#3857's unit test guards it against degrading to
571+ // "extracts nothing"); "resolved" is the verdict. They are equal on a green
572+ // run, which is exactly why printing only the first read as a pass.
527573 console . log (
528- `\nevidence paths: ${ report . evidenceLocal } resolved against this checkout, ` +
529- `${ report . evidenceForeign } attributed to another repo (objectui / cloud — not resolvable here).` ,
574+ `\nevidence paths: ${ report . evidenceLocal } repo-local path(s) declared by 'live' entries, ` +
575+ `${ report . evidenceLocal - report . evidenceMissing } resolved against this checkout` +
576+ ( report . evidenceMissing ? `, ${ report . evidenceMissing } MISSING` : '' ) +
577+ `; ${ report . evidenceForeign } attributed to another repo (objectui / cloud — not resolvable here).` ,
530578 ) ;
531579 if ( report . staleEvidence . length ) {
532- console . log ( `\n⚠ ${ report . staleEvidence . length } 'live' entr(ies) cite a missing file :` ) ;
580+ console . log ( `\n✗ ${ report . staleEvidence . length } 'live' entr(ies) cite a file that is missing from THIS repo :` ) ;
533581 report . staleEvidence . forEach ( ( s : string ) => console . log ( ` ${ s } ` ) ) ;
582+ console . log (
583+ '\n A `live` verdict IS its evidence pointer — "this property has a runtime consumer,\n' +
584+ ' here it is". When the cited file is gone from this checkout the claim is no longer\n' +
585+ ' falsifiable: declared, but nothing enforces that anything still reads the property,\n' +
586+ ' and a directory move or a rename is the whole cost of getting there.\n\n' +
587+ ' Three repairs, and picking the right one is the work:\n' +
588+ ' • the consumer MOVED inside this repo → repoint the path, and stamp `verifiedAt`\n' +
589+ ' while you have the call graph open;\n' +
590+ ' • the consumer moved to ANOTHER repo → say so with a realm marker\n' +
591+ ' (`objectui: packages/app-shell/…`, `cloud: …`). Attributed paths are counted,\n' +
592+ ' never resolved, and never fail here — that boundary is deliberate;\n' +
593+ ' • the consumer is GONE → the verdict is not `live` any more. Re-classify under\n' +
594+ ' ADR-0049 enforce-or-remove instead of repointing at a plausible survivor.\n\n' +
595+ ' This was a ⚠ until #5623 for one reason: the pre-#3857 parser took\n' +
596+ ' `evidence.split(":")[0]` as the filename, flagged 48 of 227 entries and every one\n' +
597+ ' was a false positive — so it could not fail the build, and the one real rot it was\n' +
598+ ' burying (`object.enable.clone`, whose consumer had moved packages) sat unread.\n' +
599+ ' evidence.mts fixed the parse; the list has been empty since, which is what makes\n' +
600+ ' a hit worth stopping for.' ,
601+ ) ;
534602 }
535603 if ( report . orphanProofs . length ) {
536604 console . log ( `\n⚠ ${ report . orphanProofs . length } unregistered dogfood proof tag(s) — add to proof-registry.mts:` ) ;
@@ -659,7 +727,8 @@ if (asJson) {
659727 console . log (
660728 '\n✓ every governed-type property at the walk\'s one-level granularity is classified, every ' +
661729 'registered type is governed or explicitly pending, no ledger row outlives its property, ' +
662- 'every container inheritance is declared, and all bound high-risk proofs resolve.' ,
730+ 'every container inheritance is declared, every `live` entry\'s repo-local evidence path ' +
731+ 'resolves, and all bound high-risk proofs resolve.' ,
663732 ) ;
664733 if ( report . undrilledChildKeys ) {
665734 console . log (
0 commit comments