From 348b3d1f1d6e184fd1975630d1053f1dfa5bfd17 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 03:46:44 +0000 Subject: [PATCH] fix(spec): scan `planned` and `experimental` evidence, not `live` alone The liveness gate's four evidence checks (existence, line bound, symbol anchor, key mention) ran under `status === 'live'` while `producer` was scanned at any status. An entry whose whole content is a REFUSAL therefore carried evidence the census COUNTED and no check READ. Measured on api.json: `inputMapping.transform` and `outputMapping.transform` are `planned` and were migrated to `path#symbol` anchors precisely because the refusal disappearing is what should go red -- and renaming `mappingDeclarationRejection` moved no verdict. `dead` stays out, now as a declared exclusion rather than an omission: all 80 dead rows carry a `note` and only 6 carry `evidence`, so a dead row's pointer lives in prose no check scans by design. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 --- .../spec/scripts/liveness/check-liveness.mts | 115 +++++++++++--- .../scripts/liveness/check-liveness.test.ts | 148 +++++++++++++++++- .../spec/scripts/liveness/evidence.test.ts | 11 +- .../spec/scripts/liveness/key-mention.mts | 15 +- 4 files changed, 256 insertions(+), 33 deletions(-) diff --git a/packages/spec/scripts/liveness/check-liveness.mts b/packages/spec/scripts/liveness/check-liveness.mts index 8409ccd4c1..28bacc5bb9 100644 --- a/packages/spec/scripts/liveness/check-liveness.mts +++ b/packages/spec/scripts/liveness/check-liveness.mts @@ -41,11 +41,15 @@ // (see proof-registry.mts), a `live` classification MUST carry a valid proof — // the file must exist and declare the `@proof: ` tag. CI fails otherwise. // -// EVIDENCE POINTERS (ADR-0087): a `live` verdict IS its evidence pointer — "this -// property has a runtime consumer, here it is". A cited path that is repo-rooted +// EVIDENCE POINTERS (ADR-0087): a verdict IS its evidence pointer — "this +// property has a runtime consumer, here it is", and for a refusal, "here is the +// code that rejects it". A cited path that is repo-rooted // and attributed to THIS repo must resolve against this checkout, or CI fails: // an unresolvable pointer makes the claim unfalsifiable, and a directory move or -// a rename is all it takes (see evidence.mts). Cross-repo attribution +// a rename is all it takes (see evidence.mts). Which STATUSES get scanned is a +// population decision with its own history — see EVIDENCE_SCANNED_STATUSES +// below, where the `planned`/`experimental` widening and the `dead` exclusion +// are argued from measurements. Cross-repo attribution // (`objectui: …`, `cloud: …`, `packages/services/service-ai/…`) is counted, not // resolved — those files are legitimately absent here. This was a ⚠ until #5623, // for one historical reason: the pre-#3857 `evidence.split(':')[0]` parser @@ -182,6 +186,7 @@ import { STATE_COUNTS_FILE, STATE_COUNTS_GUIDANCE, STATE_COUNTS_PATH, + STATUS_COLUMNS, foldStateCounts, parseStateTable, reconcileReadmeTable, @@ -332,6 +337,69 @@ function markerStatus(d: string): string | null { return null; } +// ── WHICH STATUSES' `evidence` IS SCANNED (#13041) ── +// +// The four evidence checks — existence, line bound, symbol anchor, key mention — +// ran only when `status === 'live'`, on the reading that a `live` verdict IS its +// evidence pointer. `producer` was already scanned at ANY status, and that +// asymmetry is what made this visible: an entry whose whole content is a REFUSAL +// carried evidence the census COUNTED and no check READ. +// +// The measured instance. `api.json`'s `inputMapping.transform` and +// `outputMapping.transform` are `planned` — parsed, then loudly rejected — and +// #13039 migrated both citations to `path#symbol` anchors precisely because the +// refusal disappearing is what should go red. Under the `live`-only population +// those anchors were verified by NOTHING: renaming or deleting +// `mappingDeclarationRejection` changed no check's verdict, while the summary +// line still counted the pointers. "Counted" and "verified" had come apart, +// which is the whole failure this ledger exists to remove. +// +// So the scan reads every status whose evidence is a POINTER AT CODE, whatever +// verdict that pointer supports: `live` (a consumer reads the key), `planned` +// (a refuser rejects it), `experimental` (declared, not enforced). +const EVIDENCE_SCANNED_STATUSES = new Set(['live', 'planned', 'experimental']); + +// `dead` is OUT, and this is that boundary written into the code rather than +// left in a PR residual — the half of #13041 that is worth doing whichever way +// the population question went. +// +// A dead row's pointer does not live in `evidence`. Measured across every ledger +// on the commit that widened this scan: all 80 `dead` entries carry a `note`, +// and only 6 carry an `evidence` string at all — the retirement story (which +// sweep, which ADR, which tombstone rejects the key now) is prose in `note`, +// which no check scans, by design. Scanning `dead.evidence` would therefore hold +// 6 rows to a standard while reading nothing of the other 74 and publishing the +// result as coverage of the class: the same shape as the defect above, one +// status over. If dead rows ever cite code as a convention rather than as a +// remnant, move `dead` across this line — and give `note` a scan of its own, +// because that is the check this class actually needs. +const EVIDENCE_UNSCANNED_STATUSES = new Set(['dead']); + +// A status in NEITHER set is #13041 re-armed: its evidence would be counted by +// the census and read by nothing, silently, exactly as `planned`'s was. So the +// partition is held to the published vocabulary rather than to a reader's +// memory — a fifth status forces the decision instead of defaulting it to +// unread. (`STATUS_COLUMNS` is that vocabulary: the columns the generated count +// artifact publishes.) +for (const s of STATUS_COLUMNS) { + const scanned = EVIDENCE_SCANNED_STATUSES.has(s); + const unscanned = EVIDENCE_UNSCANNED_STATUSES.has(s); + if (scanned === unscanned) { + throw new Error( + `liveness status '${s}' is in ${scanned ? 'BOTH' : 'NEITHER'} evidence-scan set — every status in ` + + 'STATUS_COLUMNS must be declared scanned or explicitly unscanned (#13041: a status in neither ' + + 'has its evidence counted by the census and verified by nothing).', + ); + } +} + +/** + * The scanned population, rendered for the gate's own output. Derived from the + * set rather than written out again, so the numbers and the population they + * describe cannot drift apart — which is the class of bug this whole file is. + */ +const EVIDENCE_SCANNED_LABEL = [...EVIDENCE_SCANNED_STATUSES].map((s) => `'${s}'`).join(' / '); + // ---- Zod schema walking (version-tolerant: prefer _zod.def, fall back to _def) ---- function defOf(s: any): any { return s && (s._zod?.def ?? s._def); @@ -459,8 +527,10 @@ const report: any = { producers: null as ProducerReport | null, // `producer` / `evidenceScope` — the #4837 / #4895 worklists producerMissing: [] as string[], // a `producer` pointer into thin air — FAILS, like a rotted `evidence` // The three evidence counters, and the distinction between the first two is - // the whole point: `evidenceLocal` is how many repo-rooted paths `live` entries - // DECLARE, `evidenceMissing` is how many of those do not exist here. The + // the whole point: `evidenceLocal` is how many repo-rooted paths the SCANNED + // statuses DECLARE (EVIDENCE_SCANNED_STATUSES — see there for why `planned` + // and `experimental` joined `live` and why `dead` did not), `evidenceMissing` + // is how many of those do not exist here. The // summary line used to print `evidenceLocal` under the word "resolved", so // breaking five pointers left the count at 330 and the run still said // "330 resolved" (#5623). Count and word now agree. @@ -478,7 +548,7 @@ const report: any = { // moves WITHIN the cited file satisfies both while pointing at nothing. This // asks the complementary question: does the cited file name the property at // all, in any of its camelCase/snake_case spellings? - keyMentionsChecked: 0, // resolvable (live entry, cited local file) pairs asked + keyMentionsChecked: 0, // resolvable (scanned-status entry, cited local file) pairs asked keyMentionExempt: 0, // ...of which this many are recorded in the shrink-only baseline keyMentionUnanchored: [] as string[], // ...and this many are NOT — FAILS the gate keyMentionStale: [] as string[], // a baseline row whose pair now anchors — also FAILS @@ -585,7 +655,7 @@ function classify(type: string, path: string, status: string, led: any, cat: any collectOutOfRange(pv, `${type}/${path} [producer]`); collectAnchorFindings(pv, `${type}/${path} [producer]`); } - if (status === 'live' && led?.evidence) { + if (EVIDENCE_SCANNED_STATUSES.has(status) && led?.evidence) { // Extract every repo-rooted path the evidence claims and resolve the ones // attributed to THIS repo. Cross-repo pointers (objectui / cloud) are // counted, not resolved — see evidence.mts for why the old @@ -865,7 +935,7 @@ const totalProofFailures = report.proofErrors.length + report.proofMissing.lengt const failed = totalUnclassified > 0 || totalProofFailures > 0 || - // A `live` entry whose repo-local evidence path is gone. Red since #5623 — the + // A scanned-status entry whose repo-local evidence path is gone. Red since #5623 — the // ⚠ it replaces was calibrated for the false-positive era, not for the parser // that now resolves 330 paths and reports zero. Cross-repo attribution never // reaches this list: checkEvidence only resolves the LOCAL bucket. @@ -927,7 +997,7 @@ if (asJson) { // "extracts nothing"); "resolved" is the verdict. They are equal on a green // run, which is exactly why printing only the first read as a pass. console.log( - `\nevidence paths: ${report.evidenceLocal} repo-local path(s) declared by 'live' entries, ` + + `\nevidence paths: ${report.evidenceLocal} repo-local path(s) declared by ${EVIDENCE_SCANNED_LABEL} entries, ` + `${report.evidenceLocal - report.evidenceMissing} resolved against this checkout` + (report.evidenceMissing ? `, ${report.evidenceMissing} MISSING` : '') + `; ${report.evidenceForeign} attributed to another repo (objectui / cloud — not resolvable here).`, @@ -1019,7 +1089,7 @@ if (asJson) { (report.keyMentionUnanchored.length ? `, ${report.keyMentionUnanchored.length} UNANCHORED` : '') + '.', ); if (report.keyMentionUnanchored.length) { - console.log(`\n✗ ${report.keyMentionUnanchored.length} 'live' citation(s) name a file that never names the property:`); + console.log(`\n✗ ${report.keyMentionUnanchored.length} ${EVIDENCE_SCANNED_LABEL} citation(s) name a file that never names the property:`); report.keyMentionUnanchored.forEach((s: string) => console.log(` ${s}`)); console.log('\n' + KEY_MENTION_GUIDANCE.split('\n').map((l) => (l ? ` ${l}` : '')).join('\n')); } @@ -1034,21 +1104,26 @@ if (asJson) { ); } if (report.staleEvidence.length) { - console.log(`\n✗ ${report.staleEvidence.length} 'live' entr(ies) cite a file that is missing from THIS repo:`); + console.log(`\n✗ ${report.staleEvidence.length} ${EVIDENCE_SCANNED_LABEL} entr(ies) cite a file that is missing from THIS repo:`); report.staleEvidence.forEach((s: string) => console.log(` ${s}`)); console.log( - '\n A `live` verdict IS its evidence pointer — "this property has a runtime consumer,\n' + - ' here it is". When the cited file is gone from this checkout the claim is no longer\n' + - ' falsifiable: declared, but nothing enforces that anything still reads the property,\n' + + '\n A verdict IS its evidence pointer — "this property has a runtime consumer,\n' + + ' here it is", or for a refusal ("planned" / "experimental") "here is the code that\n' + + ' rejects it". When the cited file is gone from this checkout the claim is no longer\n' + + ' falsifiable: declared, but nothing enforces that anything still reads the property\n' + + ' — or still refuses it —\n' + ' and a directory move or a rename is the whole cost of getting there.\n\n' + ' Three repairs, and picking the right one is the work:\n' + - ' • the consumer MOVED inside this repo → repoint the path, and stamp `verifiedAt`\n' + - ' while you have the call graph open;\n' + - ' • the consumer moved to ANOTHER repo → say so with a realm marker\n' + + ' • the consumer (or the refuser) MOVED inside this repo → repoint the path, and\n' + + ' stamp `verifiedAt` while you have the call graph open;\n' + + ' • it moved to ANOTHER repo → say so with a realm marker\n' + ' (`objectui: packages/app-shell/…`, `cloud: …`). Attributed paths are counted,\n' + ' never resolved, and never fail here — that boundary is deliberate;\n' + - ' • the consumer is GONE → the verdict is not `live` any more. Re-classify under\n' + - ' ADR-0049 enforce-or-remove instead of repointing at a plausible survivor.\n\n' + + ' • it is GONE → the verdict no longer holds. A `live` entry whose consumer left\n' + + ' re-classifies under ADR-0049 enforce-or-remove; a `planned` / `experimental`\n' + + ' entry whose REFUSER left is the louder case — the key is now accepted in\n' + + ' silence, and the entry is either wrong or the refusal needs restoring. Either\n' + + ' way, do not repoint at a plausible survivor.\n\n' + ' This was a ⚠ until #5623 for one reason: the pre-#3857 parser took\n' + ' `evidence.split(":")[0]` as the filename, flagged 48 of 227 entries and every one\n' + ' was a false positive — so it could not fail the build, and the one real rot it was\n' + @@ -1307,7 +1382,7 @@ if (asJson) { console.log( '\n✓ every governed-type property at the walk\'s one-level granularity is classified, every ' + 'registered type is governed or explicitly pending, no ledger row outlives its property, ' + - 'every container inheritance is declared, every `live` entry\'s repo-local evidence path ' + + `every container inheritance is declared, every ${EVIDENCE_SCANNED_LABEL} entry's repo-local evidence path ` + 'resolves, every `path:NNN` citation names a line that file actually has, every ' + '`path#symbol` anchor names a symbol its file contains, and every cited ' + 'file names the property it is evidence for (or is a recorded exemption), all bound ' + diff --git a/packages/spec/scripts/liveness/check-liveness.test.ts b/packages/spec/scripts/liveness/check-liveness.test.ts index 92d3daecd7..55d0a660e7 100644 --- a/packages/spec/scripts/liveness/check-liveness.test.ts +++ b/packages/spec/scripts/liveness/check-liveness.test.ts @@ -52,6 +52,22 @@ function setEvidence(root: string, type: string, prop: string, evidence: string) writeFileSync(file, `${JSON.stringify(ledger, null, 2)}\n`); } +/** The same, one level down — a drilled child entry (`type/prop.child`). */ +function setChildEvidence(root: string, type: string, prop: string, child: string, evidence: string): void { + const file = path.join(root, `${type}.json`); + const ledger = JSON.parse(readFileSync(file, 'utf8')); + ledger.props[prop].children[child].evidence = evidence; + writeFileSync(file, `${JSON.stringify(ledger, null, 2)}\n`); +} + +/** + * The population label the gate renders from `EVIDENCE_SCANNED_STATUSES` + * (#13041). Mirrored here once rather than inlined at each assertion, and the + * set itself is pinned against the gate's source in the population block below — + * so widening or narrowing the scan has to move both, deliberately. + */ +const SCANNED_LABEL = "'live' / 'planned' / 'experimental'"; + function summaryLine(output: string): string { return output.split('\n').find((l) => l.startsWith('evidence paths:')) ?? ''; } @@ -83,11 +99,11 @@ describe('check:liveness — evidence pointers (#5623)', () => { // exited 0, so a directory move could rot an ADR-0087 evidence chain with // nothing in CI to notice. expect(status, output).toBe(1); - expect(output).toContain("'live' entr(ies) cite a file that is missing from THIS repo"); + expect(output).toContain(`${SCANNED_LABEL} entr(ies) cite a file that is missing from THIS repo`); expect(output).toContain(`query/limit → ${ROTTED}`); // ✗, not ⚠ — the grading is the fix, and the two are one character apart. - expect(output).toMatch(/✗ 1 'live' entr\(ies\) cite a file/); - expect(output).not.toMatch(/⚠ \d+ 'live' entr\(ies\)/); + expect(output).toContain(`✗ 1 ${SCANNED_LABEL} entr(ies) cite a file`); + expect(output).not.toMatch(/⚠ \d+ .* entr\(ies\) cite a file/); }); it('names EVERY rotted pointer, not just the first', () => { @@ -98,7 +114,7 @@ describe('check:liveness — evidence pointers (#5623)', () => { } const { status, output } = runGate(root); expect(status, output).toBe(1); - expect(output).toMatch(/✗ 5 'live' entr\(ies\) cite a file/); + expect(output).toContain(`✗ 5 ${SCANNED_LABEL} entr(ies) cite a file`); for (const prop of ['fields', 'where', 'orderBy', 'limit', 'offset']) { expect(output).toContain(`query/${prop} → ${ROTTED}`); } @@ -310,6 +326,126 @@ describe('check:liveness — symbol anchors (#12516)', () => { }); }); +// #13041 — the scanned POPULATION. Everything above pins how the gate judges an +// entry's evidence; these pin WHICH entries it judges at all, which is the one +// question none of those cases can ask. +// +// THE DEFECT. The four evidence checks ran under `status === 'live'` while +// `producer` was scanned at any status, so an entry whose whole content is a +// REFUSAL carried evidence the census COUNTED and no check READ. Measured on +// `api.json`: `inputMapping.transform` and `outputMapping.transform` are +// `planned`, #13039 migrated both to `path#symbol` anchors precisely because the +// refusal disappearing is what should go red — and renaming +// `mappingDeclarationRejection` moved no verdict. Counted and verified had come +// apart, which is the failure the ledger exists to remove. +// +// Every case runs the REAL gate via `--ledger-root`, for the #5623 reason the +// blocks above state: the population lives in check-liveness.mts and a helper +// test would pin a copy of the decision rather than the decision. +describe('check:liveness — the evidence-scan population (#13041)', () => { + let tmp: string; + + beforeAll(() => { + tmp = mkdtempSync(path.join(tmpdir(), 'os-liveness-pop-')); + }); + afterAll(() => rmSync(tmp, { recursive: true, force: true })); + + // THE CARD'S OWN INSTANCE, replayed. Exactly one cause: `api-mapping.ts` + // resolves, the citation names no line, and the file genuinely names + // `transform` — so neither the existence check, the line bound, nor the + // key-mention check can account for the exit code. Under the old `live`-only + // population this same mutation exited 0. + it("FAILS when a `planned` entry's anchored REFUSER is gone from the file", () => { + const root = path.join(tmp, 'planned-anchor'); + cpSync(LEDGERS, root, { recursive: true }); + setChildEvidence( + root, + 'api', + 'inputMapping', + 'transform', + 'packages/runtime/src/api-mapping.ts#mappingDeclarationRejectionRenamedAway (the refusal, renamed out from under the pointer)', + ); + + const { status, output } = runGate(root); + expect(status, output).toBe(1); + expect(output).toContain('anchored citation(s) name a symbol the cited file does not contain'); + expect(output).toContain( + 'api/inputMapping.transform → packages/runtime/src/api-mapping.ts#mappingDeclarationRejectionRenamedAway', + ); + }); + + it('FAILS when a `planned` entry cites a repo-local file that is gone', () => { + // `field.useGrouping` is `planned` and carries no evidence today, so the + // pointer this writes is the only thing that can fail — and the rot is the + // plainest kind, the one the existence check has caught for `live` entries + // since #5623. + const root = path.join(tmp, 'planned-missing-file'); + cpSync(LEDGERS, root, { recursive: true }); + setEvidence(root, 'field', 'useGrouping', `${ROTTED} (rotted by the self-test)`); + + const { status, output } = runGate(root); + expect(status, output).toBe(1); + expect(output).toContain(`${SCANNED_LABEL} entr(ies) cite a file that is missing from THIS repo`); + expect(output).toContain(`field/useGrouping → ${ROTTED}`); + }); + + it('FAILS when an `experimental` entry cites a repo-local file that is gone', () => { + // The other half of the widening. `agent.lifecycle` is `experimental` and + // its shipped evidence is a prose absence claim ("no runtime reader"), which + // extracts no path at all — so before this change nothing about it could + // ever fail, and after it, a pointer written there is held to the same + // standard as a `live` one. + const root = path.join(tmp, 'experimental-missing-file'); + cpSync(LEDGERS, root, { recursive: true }); + setEvidence(root, 'agent', 'lifecycle', `${ROTTED} (rotted by the self-test)`); + + const { status, output } = runGate(root); + expect(status, output).toBe(1); + expect(output).toContain(`agent/lifecycle → ${ROTTED}`); + }); + + // THE BOUNDARY, and it is a real one rather than an oversight — which is the + // half of this card worth doing whichever way the population question went. + // The SAME rotted string that reds the two cases above stays green on a `dead` + // entry, so the exclusion is attributable to the status and not to anything + // else about the fixture. + // + // Why `dead` is out, measured across every ledger when this landed: all 80 + // `dead` rows carry a `note` and only 6 carry an `evidence` string. A dead + // row's pointer is the retirement story in `note` — which no check scans — so + // scanning `dead.evidence` would hold 6 rows to a standard, read nothing of + // the other 74, and publish that as coverage of the class. + it('stays GREEN when a `dead` entry carries the SAME rotted pointer', () => { + const root = path.join(tmp, 'dead-excluded'); + cpSync(LEDGERS, root, { recursive: true }); + setEvidence(root, 'flow', 'description', `${ROTTED} (rotted by the self-test)`); + + const { status, output } = runGate(root); + expect(status, output).toBe(0); + expect(output).not.toContain(`flow/description → ${ROTTED}`); + }); + + // The partition itself, pinned at the source — the precedent is the + // `manifest` membership case at the end of this file, and the reason is the + // same: a status that falls out of BOTH sets has its evidence counted by the + // census and verified by nothing, silently, which is #13041 re-armed. The gate + // throws on that at startup; this keeps the two sets legible to a reader who + // reaches for the test file first. + it('declares every status either scanned or explicitly unscanned, and prints the population', () => { + const src = readFileSync(GATE, 'utf8'); + expect(src).toContain( + "const EVIDENCE_SCANNED_STATUSES = new Set(['live', 'planned', 'experimental']);", + ); + expect(src).toContain("const EVIDENCE_UNSCANNED_STATUSES = new Set(['dead']);"); + + const { status, output } = runGate(); + expect(status, output).toBe(0); + // The gate's own output is where the population is published — a reader of a + // green run should not have to open the source to learn what was scanned. + expect(summaryLine(output)).toContain(`declared by ${SCANNED_LABEL} entries`); + }); +}); + // The README state table is COMPLETE on a green tree (#7257 back-filled the two // rows that were missing), so `pnpm check:liveness` passing says nothing about // whether this direction can fire. Same argument as the evidence guard above, @@ -489,7 +625,9 @@ describe('check:liveness — the evidence summary line (#5623)', () => { const { status, output } = runGate(); expect(status, output).toBe(0); const line = summaryLine(output); - const m = line.match(/^evidence paths: (\d+) repo-local path\(s\) declared by 'live' entries, (\d+) resolved/); + const m = line.match( + new RegExp(`^evidence paths: (\\d+) repo-local path\\(s\\) declared by ${SCANNED_LABEL} entries, (\\d+) resolved`), + ); expect(m, line).not.toBeNull(); expect(m![1]).toBe(m![2]); // Guards the same degradation evidence.test.ts guards: a parser that extracts diff --git a/packages/spec/scripts/liveness/evidence.test.ts b/packages/spec/scripts/liveness/evidence.test.ts index ef2daaa44b..b0bd308be2 100644 --- a/packages/spec/scripts/liveness/evidence.test.ts +++ b/packages/spec/scripts/liveness/evidence.test.ts @@ -324,13 +324,22 @@ describe('checkCitationLines', () => { // Contract test against the REAL ledgers: the gate reports these, so a rotted // pointer committed to a ledger fails here too. describe('shipped ledgers', () => { + // The statuses whose `evidence` the gate scans (#13041), mirrored from + // `EVIDENCE_SCANNED_STATUSES` in check-liveness.mts. This filter used to read + // `live` alone, which is what the gate itself read — and when the gate widened + // to the refusal statuses this contract test would have stayed on the narrower + // population, re-creating one layer down the exact split the card is about: + // pointers the census counts and nothing verifies. `dead` stays out here for + // the reason the gate states there: a dead row's pointer lives in `note`. + const SCANNED_STATUSES = new Set(['live', 'planned', 'experimental']); + it('every local evidence path resolves', () => { const missing: string[] = []; let local = 0; for (const f of readdirSync(ledgerRoot).filter((x) => x.endsWith('.json'))) { const ledger = JSON.parse(readFileSync(join(ledgerRoot, f), 'utf8')); const visit = (key: string, entry: any) => { - if (entry?.status !== 'live' || typeof entry?.evidence !== 'string') return; + if (!SCANNED_STATUSES.has(entry?.status) || typeof entry?.evidence !== 'string') return; const r = checkEvidence(entry.evidence, (p) => existsSync(join(repoRoot, p))); local += r.local.length; r.missing.forEach((m) => missing.push(`${ledger.type}/${key} → ${m}`)); diff --git a/packages/spec/scripts/liveness/key-mention.mts b/packages/spec/scripts/liveness/key-mention.mts index 74098f7284..6c2521e967 100644 --- a/packages/spec/scripts/liveness/key-mention.mts +++ b/packages/spec/scripts/liveness/key-mention.mts @@ -187,9 +187,9 @@ export function reconcileKeyMentions(input: { /** Prescription printed under newly-unanchored citations. */ export const KEY_MENTION_GUIDANCE = [ - 'A `live` entry cites a file that never names the property — in any of its', - 'camelCase/snake_case spellings. The file exists and every cited line is in', - 'range, so neither the existence check nor the line bound can see this; the', + 'An evidence-scanned entry cites a file that never names the property — in any', + 'of its camelCase/snake_case spellings. The file exists and every cited line is', + 'in range, so neither the existence check nor the line bound can see this; the', 'citation is nonetheless unfalsifiable as written.', '', 'Measured, 7 of the first 11 of these were real rot, so start by assuming it is:', @@ -199,10 +199,11 @@ export const KEY_MENTION_GUIDANCE = [ ' promoted into another package (#10101), or a citation that named a', ' plausible SIBLING file all leave the old path resolving perfectly.', ' Find the file that names the key and cite that one, with a line.', - ' 2. RE-CLASSIFY it — if no file in this repo names the key, the consumer may', - ' be in another realm (prefix it `objectui:` and pin the commit, per the', - ' ledger README) or may not exist at all, in which case the honest verdict', - ' is `dead` under ADR-0049 rather than a repoint to a plausible survivor.', + ' 2. RE-CLASSIFY it — if no file in this repo names the key, the consumer (or,', + ' for a `planned` / `experimental` entry, the refuser) may be in another', + ' realm (prefix it `objectui:` and pin the commit, per the ledger README) or', + ' may not exist at all, in which case the honest verdict is `dead` under', + ' ADR-0049 rather than a repoint to a plausible survivor.', ' 3. EXEMPT it — only when the consumer genuinely reads the key under a name', ' no naming-convention fold reaches (a compound child-key remap such as', ' `fromOverride.address` → `from_address`). Add a row to',