From 57927afa75fec8131283d9ae4a3514d9b9f56a2d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 11:05:05 +0000 Subject: [PATCH 1/2] fix(docs-audit): measure the bridge cause split on the advisory path and render it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bridgeCoverageFrom` derives three causes for an unreachable bridge row (`discovery-gap` / `no-in-repo-registrar` / `undecided`), but only against a ceiling passed as its optional third argument. Only the `--bridge-coverage` CLI arm passed one; the PHASE 2 advisory path did not, so on that path every cause read `unmeasured` and the three counts were `null` — and the docs-drift PR comment, the surface a human actually reads, rendered all 177 unreachable rows as ONE population when the census says there are three. Both hops, because either alone is the half-wired state: a ceiling nobody renders is cost paid for no reader, and a render branch with no ceiling prints `unmeasured` in a nicer shape. - the ceiling is extracted to `ceilingTailsFrom` and built in ONE place, so the two arms that now publish these three buckets cannot compute them over two populations; - the advisory arm passes it; - `docs-drift-check.yml` renders the split, with the parts READ off the same object as the total and the breakdown withheld — as a stated verdict — on numbers that do not partition it; - `--self-test` pins `causes` at BOTH ends (#9433), pins the partition guard, and pins that the renderer derives no cause count of its own. Measured on f5a7f9c88, 7 warm runs per arm: advisory run 0.652s -> 1.998s (+1.35s), 1950 source files walked, 1106 past the `path` prefilter, 82-tail ceiling. Same order as the ~1.4s recorded on 589758d22, so the cost that could have flipped this decision has not moved. Part of #11867 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6 --- .github/workflows/docs-drift-check.yml | 42 ++++++++++- scripts/docs-audit/affected-docs.mjs | 98 +++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docs-drift-check.yml b/.github/workflows/docs-drift-check.yml index 9257bebba4..20afe34418 100644 --- a/.github/workflows/docs-drift-check.yml +++ b/.github/workflows/docs-drift-check.yml @@ -259,7 +259,47 @@ jobs: if (crossCutting.length) limits.push(`**${crossCutting.length}** cross-cutting symbol(s) contributed no route anchor: \`${crossCutting.join('`, `')}\``); if (overbroad.length) limits.push(`**${overbroad.length}** anchor(s) matched too much of the corpus to be a work list: \`${overbroad.join('`, `')}\``); if (weak.length) limits.push(`**${weak.length}** name(s) were too generic to anchor anything (single lowercase words)`); - if (bridge && bridge.measured && bridge.unreachable > 0) limits.push(`the SDK route bridge reached **${bridge.reachable}** of **${bridge.clientRows}** client-bound route-ledger rows — the other **${bridge.unreachable}** have no registrar \`path:\` tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: \`node scripts/docs-audit/affected-docs.mjs --bridge-coverage\``); + // ── WHY THOSE ROWS ARE UNREACHABLE, NOT JUST HOW MANY (#11867) ──────── + // + // The line above used to end at the count, and a count is one population. + // The census says there are three, and the difference is the whole reason + // the split exists: the auth ledger's `56 of 56` and the rest ledger's + // `46 of 87` print identically and are NOT the same finding — the first has + // no in-repo registration site at all, so the discovery widening the second + // one wants moves it by zero rows. That conflation already aimed one card + // (#11178) at widening a recognizer that was never the constraint, and this + // comment is the surface where a human meets the number. + // + // ⛔ SAME NAMES, ONE DERIVATION — the rule stated three lines up for the + // `anchorless` pair, and it binds here for the same reason. `bridge.causes` + // is a PARTITION of `bridge.unreachable`, computed ONCE inside + // `bridgeCoverageFrom` and published whole; the parts are READ off the same + // object as the total and never recomputed here. Re-deriving them from + // `bridge.ledgers` would be a second derivation that agrees today, drifts + // silently tomorrow, and renders a breakdown that sums to something the + // headline beside it denies. + // + // And on numbers that do NOT partition that total, the split is WITHHELD and + // the run says the census is broken, rather than printing three figures + // beside a fourth they contradict. That state cannot arise from + // `bridgeCoverageFrom` — `affected-docs.mjs --self-test` pins the partition + // — which is what makes it a verdict here and not a fallback. + // + // `measured: false` keeps its honest arm: a run that supplied no ceiling + // reports that WHY was not measured. It says so in words rather than + // rendering three nulls or, worse, three zeroes — "nobody looked" is not + // "none found", the same distinction `computedOn.dirty`'s null arm draws. + const causes = (bridge && bridge.causes) || null; + let bridgeCauses = ''; + if (causes && causes.measured === true) { + const parts = causes.remediable + causes.structural + causes.undecided; + bridgeCauses = parts === bridge.unreachable + ? ` Of those **${bridge.unreachable}**: **${causes.remediable}** are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); **${causes.structural}** are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; **${causes.undecided}** are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here).` + : ` ⛔ Its cause census is BROKEN — **${causes.remediable}** + **${causes.structural}** + **${causes.undecided}** is not the **${bridge.unreachable}** it claims to break down, so the split is withheld.`; + } else if (causes && causes.measured === false) { + bridgeCauses = ` WHY those rows are unreachable was NOT measured on this run — ${causes.reason} — so no cause may be read into the count above.`; + } + if (bridge && bridge.measured && bridge.unreachable > 0) limits.push(`the SDK route bridge reached **${bridge.reachable}** of **${bridge.clientRows}** client-bound route-ledger rows — the other **${bridge.unreachable}** have no registrar \`path:\` tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run.${bridgeCauses} The rows themselves: \`node scripts/docs-audit/affected-docs.mjs --bridge-coverage\``); // ── THE RULE-CARRYING PAGE AN EMITTER DIFF CANNOT REACH (#11434) ────── // // Every line above is a REPORT about this run: a count this run produced, a diff --git a/scripts/docs-audit/affected-docs.mjs b/scripts/docs-audit/affected-docs.mjs index 6bf5d26326..0a4249636a 100644 --- a/scripts/docs-audit/affected-docs.mjs +++ b/scripts/docs-audit/affected-docs.mjs @@ -524,13 +524,10 @@ if (args.includes('--self-test')) { if (args.includes('--bridge-coverage')) { const { registrarFiles, sourceFiles, ledgers, registrarByTail } = scanRouteSurface(); // THE CEILING (#11178) — read lazily off the walk this scan already did, so the census - // holds one file's source at a time rather than the tree's. Only this mode pays for it: - // the advisory path calls `bridgeCoverageFrom` with no ceiling and gets `unmeasured`. - const ceiling = maximalTailsFrom((function* () { - for (const rel of sourceFiles) { - try { yield { file: rel, text: readFileSync(join(repoRoot, rel), 'utf8') }; } catch { /* unreadable file contributes no tail */ } - } - })()); + // holds one file's source at a time rather than the tree's. Since #11867 the PHASE 2 + // advisory run pays for it too, through this same `ceilingTailsFrom` — one derivation, + // so the two paths cannot report the same three buckets from two populations. + const ceiling = ceilingTailsFrom(sourceFiles); const coverage = bridgeCoverageFrom(ledgers, registrarByTail.keys(), ceiling.keys()); const selects = selectsFrom(registrarByTail.keys()); const causeOf = new Map(coverage.ledgers.map((l) => [l.file, l.cause])); @@ -1190,6 +1187,26 @@ function parseRegistrarSource(text) { * @returns {Map} tail ⟶ the files that declare it, so a "discovery could * reach this" claim can always NAME the witness rather than asserting one exists. */ +/** + * The CEILING, built off a walk's `sourceFiles` — ONE derivation, both callers (#11867). + * + * ⛔ Never inline this at a call site. `--bridge-coverage` and the PHASE 2 advisory run + * both measure causes now, and a census the two paths build differently would report the + * same three buckets from two populations — the exact class of defect the cause split was + * introduced to end, one level up. Read LAZILY, one file's source at a time, so neither + * caller holds the tree in memory. + * + * @param {Iterable} sourceFiles repo-relative paths, as `walkSourceFiles` yields them + * @returns {Map} exactly what `maximalTailsFrom` returns + */ +function ceilingTailsFrom(sourceFiles) { + return maximalTailsFrom((function* () { + for (const rel of sourceFiles) { + try { yield { file: rel, text: readFileSync(join(repoRoot, rel), 'utf8') }; } catch { /* unreadable file contributes no tail */ } + } + })()); +} + function maximalTailsFrom(sources) { const byTail = new Map(); for (const { file, text } of sources) { @@ -4161,6 +4178,48 @@ function selfTest() { })(); check('emit', 'the drift comment RENDERS `bridgeCoverage` — an unrendered key is half-wired (#9433)', 'docs-drift-check.yml', true, driftWorkflow === null || /data\.bridgeCoverage/.test(driftWorkflow)); + // ── `causes` GETS THE SAME TREATMENT, AT BOTH ENDS (#11867) ───────────────── + // + // #9433's rule is about a KEY, not about this one key, so the sub-object that carries + // the three-way cause split is pinned exactly the way `bridgeCoverage` above is — and + // this time BOTH halves were separately broken. The advisory path published `causes` + // for two cards while omitting the ceiling that populates it, so every ledger read + // `unmeasured` and the three counts were `null`; and the workflow had no render branch + // at all (measured: zero occurrences of `causes` in that file). Either half alone is + // the half-wired state — a ceiling nobody renders is cost paid for no reader, and a + // render branch with no ceiling prints `unmeasured` in a nicer shape. + check('emit', 'the ADVISORY path measures causes — it passes a ceiling, not just tails', 'affected-docs.mjs', + true, /bridgeCoverageFrom\(ledgers, registrarByTail\.keys\(\), ceilingTailsFrom\(sourceFiles\)\.keys\(\)\)/.test(ownSource)); + // ⚠️ READ THE CODE, NOT THE COMMENT THAT FORBIDS IT. These three pins are about what + // the renderer DOES, and the block it lives in names `bridge.ledgers` in prose precisely + // to forbid deriving from it — so a raw-text negative pin fails on its own rationale + // (measured: it did, first run). Full-line `//` comments are dropped first; trailing + // ones are left alone rather than risk eating a `//` inside a string. + const driftWorkflowCode = driftWorkflow === null ? null + : driftWorkflow.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n'); + check('emit', 'the drift comment RENDERS `causes` — an unrendered key is half-wired (#9433)', 'docs-drift-check.yml', + true, driftWorkflowCode === null || /bridge\.causes/.test(driftWorkflowCode)); + // AND THE BREAKDOWN MAY NOT BE ABLE TO DISAGREE WITH THE HEADLINE. The workflow's own + // rule at that spot — "Same names, one derivation" — is what this pins: the renderer + // reads the three counts off `bridge.causes` and the total off `bridge.unreachable`, + // and refuses to print the split unless they partition. A renderer that recomputed the + // parts from `bridge.ledgers` would satisfy the grep above while reintroducing exactly + // the drift the partition exists to make visible. + check('emit', 'the rendered split is GUARDED by the partition it claims to be', 'docs-drift-check.yml', + true, driftWorkflowCode === null || /parts === bridge\.unreachable/.test(driftWorkflowCode)); + check('emit', 'and the renderer derives no cause count of its own from the ledger list', 'docs-drift-check.yml', + true, driftWorkflowCode === null || !/bridge\.ledgers/.test(driftWorkflowCode)); + // WRITTEN ONCE, pinned at the source (#11494's rule, applied to the census). Two paths + // now publish these three buckets; two spellings of the population they are computed + // over is how two surfaces start disagreeing about one repo. `ceilingTailsFrom` is the + // single builder, and an inline second copy is what this catches — every behavioural + // fixture above would stay green while the two arms drifted apart. + check('emit', 'the ceiling is built in exactly ONE place', 'affected-docs.mjs', + 1, (ownSource.match(/return maximalTailsFrom\(\(function\* \(\) \{/g) || []).length); + // CALL SITES, which is why the lookbehind: the declaration shares the spelling, and + // counting it would let one arm drop its call while the total stayed put. + check('emit', 'and both arms that measure causes build it through that one place', 'affected-docs.mjs', + 2, (ownSource.match(/(? Date: Tue, 25 Aug 2026 11:06:14 +0000 Subject: [PATCH 2/2] fix(docs-audit): state the unmeasured-cause arm without restating its own reason Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6 --- .github/workflows/docs-drift-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-drift-check.yml b/.github/workflows/docs-drift-check.yml index 20afe34418..50a318b9b2 100644 --- a/.github/workflows/docs-drift-check.yml +++ b/.github/workflows/docs-drift-check.yml @@ -297,7 +297,7 @@ jobs: ? ` Of those **${bridge.unreachable}**: **${causes.remediable}** are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); **${causes.structural}** are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; **${causes.undecided}** are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here).` : ` ⛔ Its cause census is BROKEN — **${causes.remediable}** + **${causes.structural}** + **${causes.undecided}** is not the **${bridge.unreachable}** it claims to break down, so the split is withheld.`; } else if (causes && causes.measured === false) { - bridgeCauses = ` WHY those rows are unreachable was NOT measured on this run — ${causes.reason} — so no cause may be read into the count above.`; + bridgeCauses = ` ⚠️ Cause NOT measured on this run: ${causes.reason}. No cause may be read into the count above — "nobody looked" is not "none found".`; } if (bridge && bridge.measured && bridge.unreachable > 0) limits.push(`the SDK route bridge reached **${bridge.reachable}** of **${bridge.clientRows}** client-bound route-ledger rows — the other **${bridge.unreachable}** have no registrar \`path:\` tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run.${bridgeCauses} The rows themselves: \`node scripts/docs-audit/affected-docs.mjs --bridge-coverage\``); // ── THE RULE-CARRYING PAGE AN EMITTER DIFF CANNOT REACH (#11434) ──────