Skip to content

Commit 57927af

Browse files
committed
fix(docs-audit): measure the bridge cause split on the advisory path and render it
`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 f5a7f9c, 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 589758d, so the cost that could have flipped this decision has not moved. Part of #11867 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
1 parent f5a7f9c commit 57927af

2 files changed

Lines changed: 130 additions & 10 deletions

File tree

‎.github/workflows/docs-drift-check.yml‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,47 @@ jobs:
259259
if (crossCutting.length) limits.push(`**${crossCutting.length}** cross-cutting symbol(s) contributed no route anchor: \`${crossCutting.join('`, `')}\``);
260260
if (overbroad.length) limits.push(`**${overbroad.length}** anchor(s) matched too much of the corpus to be a work list: \`${overbroad.join('`, `')}\``);
261261
if (weak.length) limits.push(`**${weak.length}** name(s) were too generic to anchor anything (single lowercase words)`);
262-
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\``);
262+
// ── WHY THOSE ROWS ARE UNREACHABLE, NOT JUST HOW MANY (#11867) ────────
263+
//
264+
// The line above used to end at the count, and a count is one population.
265+
// The census says there are three, and the difference is the whole reason
266+
// the split exists: the auth ledger's `56 of 56` and the rest ledger's
267+
// `46 of 87` print identically and are NOT the same finding — the first has
268+
// no in-repo registration site at all, so the discovery widening the second
269+
// one wants moves it by zero rows. That conflation already aimed one card
270+
// (#11178) at widening a recognizer that was never the constraint, and this
271+
// comment is the surface where a human meets the number.
272+
//
273+
// ⛔ SAME NAMES, ONE DERIVATION — the rule stated three lines up for the
274+
// `anchorless` pair, and it binds here for the same reason. `bridge.causes`
275+
// is a PARTITION of `bridge.unreachable`, computed ONCE inside
276+
// `bridgeCoverageFrom` and published whole; the parts are READ off the same
277+
// object as the total and never recomputed here. Re-deriving them from
278+
// `bridge.ledgers` would be a second derivation that agrees today, drifts
279+
// silently tomorrow, and renders a breakdown that sums to something the
280+
// headline beside it denies.
281+
//
282+
// And on numbers that do NOT partition that total, the split is WITHHELD and
283+
// the run says the census is broken, rather than printing three figures
284+
// beside a fourth they contradict. That state cannot arise from
285+
// `bridgeCoverageFrom` — `affected-docs.mjs --self-test` pins the partition
286+
// — which is what makes it a verdict here and not a fallback.
287+
//
288+
// `measured: false` keeps its honest arm: a run that supplied no ceiling
289+
// reports that WHY was not measured. It says so in words rather than
290+
// rendering three nulls or, worse, three zeroes — "nobody looked" is not
291+
// "none found", the same distinction `computedOn.dirty`'s null arm draws.
292+
const causes = (bridge && bridge.causes) || null;
293+
let bridgeCauses = '';
294+
if (causes && causes.measured === true) {
295+
const parts = causes.remediable + causes.structural + causes.undecided;
296+
bridgeCauses = parts === bridge.unreachable
297+
? ` 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).`
298+
: ` ⛔ 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.`;
299+
} else if (causes && causes.measured === false) {
300+
bridgeCauses = ` WHY those rows are unreachable was NOT measured on this run — ${causes.reason} — so no cause may be read into the count above.`;
301+
}
302+
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\``);
263303
// ── THE RULE-CARRYING PAGE AN EMITTER DIFF CANNOT REACH (#11434) ──────
264304
//
265305
// Every line above is a REPORT about this run: a count this run produced, a

‎scripts/docs-audit/affected-docs.mjs‎

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -524,13 +524,10 @@ if (args.includes('--self-test')) {
524524
if (args.includes('--bridge-coverage')) {
525525
const { registrarFiles, sourceFiles, ledgers, registrarByTail } = scanRouteSurface();
526526
// THE CEILING (#11178) — read lazily off the walk this scan already did, so the census
527-
// holds one file's source at a time rather than the tree's. Only this mode pays for it:
528-
// the advisory path calls `bridgeCoverageFrom` with no ceiling and gets `unmeasured`.
529-
const ceiling = maximalTailsFrom((function* () {
530-
for (const rel of sourceFiles) {
531-
try { yield { file: rel, text: readFileSync(join(repoRoot, rel), 'utf8') }; } catch { /* unreadable file contributes no tail */ }
532-
}
533-
})());
527+
// holds one file's source at a time rather than the tree's. Since #11867 the PHASE 2
528+
// advisory run pays for it too, through this same `ceilingTailsFrom` — one derivation,
529+
// so the two paths cannot report the same three buckets from two populations.
530+
const ceiling = ceilingTailsFrom(sourceFiles);
534531
const coverage = bridgeCoverageFrom(ledgers, registrarByTail.keys(), ceiling.keys());
535532
const selects = selectsFrom(registrarByTail.keys());
536533
const causeOf = new Map(coverage.ledgers.map((l) => [l.file, l.cause]));
@@ -1190,6 +1187,26 @@ function parseRegistrarSource(text) {
11901187
* @returns {Map<string, string[]>} tail ⟶ the files that declare it, so a "discovery could
11911188
* reach this" claim can always NAME the witness rather than asserting one exists.
11921189
*/
1190+
/**
1191+
* The CEILING, built off a walk's `sourceFiles` — ONE derivation, both callers (#11867).
1192+
*
1193+
* ⛔ Never inline this at a call site. `--bridge-coverage` and the PHASE 2 advisory run
1194+
* both measure causes now, and a census the two paths build differently would report the
1195+
* same three buckets from two populations — the exact class of defect the cause split was
1196+
* introduced to end, one level up. Read LAZILY, one file's source at a time, so neither
1197+
* caller holds the tree in memory.
1198+
*
1199+
* @param {Iterable<string>} sourceFiles repo-relative paths, as `walkSourceFiles` yields them
1200+
* @returns {Map<string, string[]>} exactly what `maximalTailsFrom` returns
1201+
*/
1202+
function ceilingTailsFrom(sourceFiles) {
1203+
return maximalTailsFrom((function* () {
1204+
for (const rel of sourceFiles) {
1205+
try { yield { file: rel, text: readFileSync(join(repoRoot, rel), 'utf8') }; } catch { /* unreadable file contributes no tail */ }
1206+
}
1207+
})());
1208+
}
1209+
11931210
function maximalTailsFrom(sources) {
11941211
const byTail = new Map();
11951212
for (const { file, text } of sources) {
@@ -4161,6 +4178,48 @@ function selfTest() {
41614178
})();
41624179
check('emit', 'the drift comment RENDERS `bridgeCoverage` — an unrendered key is half-wired (#9433)', 'docs-drift-check.yml',
41634180
true, driftWorkflow === null || /data\.bridgeCoverage/.test(driftWorkflow));
4181+
// ── `causes` GETS THE SAME TREATMENT, AT BOTH ENDS (#11867) ─────────────────
4182+
//
4183+
// #9433's rule is about a KEY, not about this one key, so the sub-object that carries
4184+
// the three-way cause split is pinned exactly the way `bridgeCoverage` above is — and
4185+
// this time BOTH halves were separately broken. The advisory path published `causes`
4186+
// for two cards while omitting the ceiling that populates it, so every ledger read
4187+
// `unmeasured` and the three counts were `null`; and the workflow had no render branch
4188+
// at all (measured: zero occurrences of `causes` in that file). Either half alone is
4189+
// the half-wired state — a ceiling nobody renders is cost paid for no reader, and a
4190+
// render branch with no ceiling prints `unmeasured` in a nicer shape.
4191+
check('emit', 'the ADVISORY path measures causes — it passes a ceiling, not just tails', 'affected-docs.mjs',
4192+
true, /bridgeCoverageFrom\(ledgers, registrarByTail\.keys\(\), ceilingTailsFrom\(sourceFiles\)\.keys\(\)\)/.test(ownSource));
4193+
// ⚠️ READ THE CODE, NOT THE COMMENT THAT FORBIDS IT. These three pins are about what
4194+
// the renderer DOES, and the block it lives in names `bridge.ledgers` in prose precisely
4195+
// to forbid deriving from it — so a raw-text negative pin fails on its own rationale
4196+
// (measured: it did, first run). Full-line `//` comments are dropped first; trailing
4197+
// ones are left alone rather than risk eating a `//` inside a string.
4198+
const driftWorkflowCode = driftWorkflow === null ? null
4199+
: driftWorkflow.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
4200+
check('emit', 'the drift comment RENDERS `causes` — an unrendered key is half-wired (#9433)', 'docs-drift-check.yml',
4201+
true, driftWorkflowCode === null || /bridge\.causes/.test(driftWorkflowCode));
4202+
// AND THE BREAKDOWN MAY NOT BE ABLE TO DISAGREE WITH THE HEADLINE. The workflow's own
4203+
// rule at that spot — "Same names, one derivation" — is what this pins: the renderer
4204+
// reads the three counts off `bridge.causes` and the total off `bridge.unreachable`,
4205+
// and refuses to print the split unless they partition. A renderer that recomputed the
4206+
// parts from `bridge.ledgers` would satisfy the grep above while reintroducing exactly
4207+
// the drift the partition exists to make visible.
4208+
check('emit', 'the rendered split is GUARDED by the partition it claims to be', 'docs-drift-check.yml',
4209+
true, driftWorkflowCode === null || /parts === bridge\.unreachable/.test(driftWorkflowCode));
4210+
check('emit', 'and the renderer derives no cause count of its own from the ledger list', 'docs-drift-check.yml',
4211+
true, driftWorkflowCode === null || !/bridge\.ledgers/.test(driftWorkflowCode));
4212+
// WRITTEN ONCE, pinned at the source (#11494's rule, applied to the census). Two paths
4213+
// now publish these three buckets; two spellings of the population they are computed
4214+
// over is how two surfaces start disagreeing about one repo. `ceilingTailsFrom` is the
4215+
// single builder, and an inline second copy is what this catches — every behavioural
4216+
// fixture above would stay green while the two arms drifted apart.
4217+
check('emit', 'the ceiling is built in exactly ONE place', 'affected-docs.mjs',
4218+
1, (ownSource.match(/return maximalTailsFrom\(\(function\* \(\) \{/g) || []).length);
4219+
// CALL SITES, which is why the lookbehind: the declaration shares the spelling, and
4220+
// counting it would let one arm drop its call while the total stayed put.
4221+
check('emit', 'and both arms that measure causes build it through that one place', 'affected-docs.mjs',
4222+
2, (ownSource.match(/(?<!function )ceilingTailsFrom\(/g) || []).length);
41644223
// `unreachableRows` is the detail of `unreachable`, and the only way it can contradict
41654224
// that count is by deriving selection a second time. Pinned at the source, because the
41664225
// two agreeing today is what a re-statement costs nothing to break tomorrow.
@@ -4471,8 +4530,29 @@ const crossCuttingSymbols = [];
44714530
// same rule), and a fabricated `0 of 0` here would read as "nothing to reach".
44724531
let bridgeCoverage = { measured: false, reason: 'no bridgeable symbol in this change — the sdk route bridge did not run' };
44734532
if (bridgeSymbols.length) {
4474-
const { ledgers, ledgerRows, registrarByTail } = scanRouteSurface();
4475-
bridgeCoverage = bridgeCoverageFrom(ledgers, registrarByTail.keys());
4533+
const { ledgers, ledgerRows, registrarByTail, sourceFiles } = scanRouteSurface();
4534+
// WITH THE CEILING (#11867). Until now this call omitted the third argument, so every
4535+
// ledger's cause came back `unmeasured` and the three counts `null` — honest, but it
4536+
// meant the PR comment, which is the surface a human actually reads, rendered all 177
4537+
// unreachable rows as ONE population when the census says there are three. The ceiling
4538+
// is the ONLY input that turns "unreachable" into WHY, and the reader who needs that
4539+
// distinction most is the one looking at a PR, not the one running a diff-free gate.
4540+
//
4541+
// WHAT IT COSTS, measured on this tree rather than assumed (`f5a7f9c88`, 7 warm runs of
4542+
// each arm): the ceiling reads the 1950 `packages/**` source files this walk already
4543+
// enumerated, of which 1106 pass `maximalTailsFrom`'s `path` prefilter, and yields the
4544+
// same 82 tails `--bridge-coverage` builds. Median advisory run 0.652s → 1.998s, so
4545+
// +1.35s — the same order as the ~1.4s recorded on `589758d22`, against a CI job
4546+
// measured in minutes. It is paid ONLY on a run that already carried a bridgeable symbol.
4547+
//
4548+
// ⚠️ AND IT IS THE INPUT THAT WOULD REVERSE THIS. The cause split is worth ~1.35s on a
4549+
// per-PR advisory run and would not be worth ~15s; whoever finds this number has grown
4550+
// should re-take the decision, not absorb the cost. Re-measure with the two arms above.
4551+
//
4552+
// ⛔ Through `ceilingTailsFrom`, never a second inline census: `--bridge-coverage` and
4553+
// this path now publish the same three buckets, and two spellings of the population they
4554+
// are computed over is how the two surfaces start disagreeing about one repo.
4555+
bridgeCoverage = bridgeCoverageFrom(ledgers, registrarByTail.keys(), ceilingTailsFrom(sourceFiles).keys());
44764556

44774557
// symbol → route, capped: the bridge answers "which routes mention this name", and for
44784558
// a CROSS-CUTTING helper that is every route it is wired into. Measured on the REST

0 commit comments

Comments
 (0)