Skip to content

fix(docs-audit): measure the bridge cause split on the advisory path and render it in the drift comment - #12146

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-11867-drift-comment-cause-split
Aug 25, 2026
Merged

fix(docs-audit): measure the bridge cause split on the advisory path and render it in the drift comment#12146
yinlianghui merged 2 commits into
mainfrom
claude/issue-11867-drift-comment-cause-split

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#11867

bridgeCoverageFrom(ledgers, tails, maximalTails) 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 ledger's 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, or neither. 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 cost, re-measured on this tree — the one input that could have reversed this

The card measured ~1093 files masked / ~1.4s on 589758d22. Re-measured here on f5a7f9c88, 7 warm runs of each arm, paired (the before arm is git show origin/main:… run as a probe beside the modified file, so both arms read the same tree):

advisory run, median
before (no ceiling)0.652s
after (ceiling paid)1.998s
delta+1.35s

Ceiling population, instrumented directly: 1950packages/** source files walked, 1106 past maximalTailsFrom's path prefilter, yielding the same 82-tail ceiling --bridge-coverage builds. The isolated census costs ~1.37–1.61s on both arms.

So: 1106 files / ~1.4s today against 1093 / ~1.4s on 589758d22 — the file count drifted by +13 and the time did not move. Not materially worse, so this proceeded rather than coming back as a number. The cost is paid only on a run that already carried a bridgeable symbol and already walked this same file list, against a CI job measured in minutes. The measurement and the two arms that reproduce it are recorded at the call site, with a note to re-take the decision — not absorb the cost — if that number ever grows.

The breakdown, re-derived

14 / 56 / 107 against 177 was measured at 589758d22. On f5a7f9c88 it is unchanged: remediable 14 · structural 56 · undecided 107, partitioning 177. clientRows has moved 221 → 222 and reachable is still 45, so the total moved by one row while the split did not. No drift to report.

The judgment call: three-way split, not a single "N are structural" clause

The card left this open and it is decided here, on the numbers.

A single "and 56 of these are structural" clause is shorter but, at this distribution, actively misleading: it implies the remaining 121 are remediable, and they are not — only 14 are. 107 are undecided, which means "no in-repo declaration, on a ledger that has other in-repo registrars", i.e. absence and an unreadable spelling are not distinguishable there. Collapsing undecided into "not structural" reads as "remediable" and reproduces the exact conflation the split exists to end — the one that aimed #11178 at widening a recognizer that was never the constraint. The dominant bucket is the unknown one, and a two-way rendering has nowhere honest to put it.

So all three render, in the same order and under the same names --bridge-coverage prints, as one bullet in the existing fold rather than three — the total and its parts cannot then drift apart onto separate conditions. Wording was tightened once after reading the rendered bytes; the bullet is ~750 chars, ~300 of which is the pre-existing sentence.

- the SDK route bridge reached **45** of **222** client-bound route-ledger rows — the other
**177** have no registrar `path:` tail to select them, so pages documenting THEIR client
methods cannot appear above, on this or any run. Of those **177**: **14** are remediable by
widening that discovery convention (an in-repo file declares the path; the convention did not
scan it); **56** are structural — on a ledger where NOT ONE row is declared in-repo, so no
discovery change reaches them at any price; **107** are undecided (no in-repo declaration, on
a ledger that has other in-repo registrars — absence and an unreadable spelling are not
distinguishable here). The rows themselves: `node scripts/docs-audit/affected-docs.mjs --bridge-coverage`

The breakdown cannot disagree with the headline

The workflow's own rule at that spot — "Same names, one derivation: the headline and this line must never be able to disagree about which files went unseen" — binds here:

  • the parts are read off the same object as the total (bridge.causes beside bridge.unreachable), computed once inside bridgeCoverageFrom and published whole. Nothing is recomputed in the renderer.
  • on numbers that do not partition that total, the split is withheld and the run states that the census is broken, rather than printing three figures beside a fourth they contradict. Verified behaviourally: a payload with undecided off by one renders ⛔ Its cause census is BROKEN — 14 + 56 + 106 is not the 177 it claims to break down, so the split is withheld.
  • measured: false keeps its honest arm for a genuinely ceiling-less run — it reports that WHY was not measured, in words, rather than three nulls or three zeroes. Also verified behaviourally.

One derivation for the ceiling itself

Two arms now publish these three buckets, so the ceiling construction is extracted to ceilingTailsFrom and built in exactly one place — two spellings of the population they are computed over is how two surfaces start disagreeing about one repo. --bridge-coverage was rewired onto it; it is not a second copy.

Pins, at both ends (#9433)

affected-docs.mjs --self-test already pinned the bridgeCoverage key at both ends; causes now gets the same treatment, and here both halves had been separately broken. Five new pins:

pinend
the ADVISORY path passes a ceiling, not just tailsaffected-docs.mjs
the drift comment RENDERS causesdocs-drift-check.yml
the rendered split is GUARDED by the partition it claims to bedocs-drift-check.yml
the renderer derives no cause count of its own from bridge.ledgersdocs-drift-check.yml
the ceiling is built in exactly ONE place, and both arms use itaffected-docs.mjs

The three workflow pins read the block-scalar with full-line // comments stripped first. That is not incidental: the negative pin failed on its own rationale on the first run, because the block names bridge.ledgers in prose precisely to forbid deriving from it. A raw-text pin there tests the comment, not the code.

Reverse-verified, each mutation confirmed on disk before the reading was taken (marker present/absent grep, not an editor exit code), each restored and re-run green afterwards, under an EXIT INT TERM trap so a mid-mutation kill cannot leave the tree mutated:

mutationself-test
advisory arm loses its ceilingexit 1 (3 cases)
workflow loses the causes render branchexit 1 (2 cases)
workflow loses the partition guardexit 1 (2 cases)
renderer starts deriving from bridge.ledgersexit 1 (2 cases)
a second inline census appearsexit 1 (2 cases)

Baseline and all five restores: exit 0.

Verification

At the final commit 961081924 (the same commit dispatch-gates.mjs derived its list from), exit codes captured before any pipe:

  • affected-docs.mjs --self-test457 cases pass
  • check-drift-comment.mjs56 cases pass across 5 fixture diff(s)
  • check-affected-docs.mjs — exit 0
  • the full derived gate union, node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack25 matched families, plus check:nul-bytesall exit 0, run under scripts/pm/os-verify-lock.sh.

One family, scripts/pm/check-governed-queue-guard.mjs, exits 1 in a local shell for a reason unrelated to this diff: it reads GITHUB_EVENT_PATH"and nothing else" and refuses to exit 0 when it cannot look. Given a real event payload for this branch it exits 0✅ CLEAR — the diff touches no governed surface.

No changeset: root scripts/ + workflow only, no published package source changes, so this carries the skip-changeset label instead.


Generated by Claude Code

…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
… own reason
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 487e3614341379860d66554b8037840c093cb590packageMentionDocs.

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dequeued by a failure that is not this PR's — no fix will be pushed here

github-merge-queue[bot] removed this PR from the queue at 12:13Z with reason: CI_FAILURE. The failing check is red on main, not on this diff. Per the standing rule, a base-branch failure gets diagnosed and stated, never patched around on the affected PR.

What actually failed

Read from the merge-queue branch's job log, not inferred:

Type Check · consumer gates
→ pnpm --filter @objectstack/client run check:exported-any-returns
❌ 1 stale exported-any-returns.json entr(y/ies) — the gap is closed, delete the entry:
• ObjectStackClient.packages.update — no longer resolves to `any`

Verified on origin/main directly:

The ledger is shrink-only and judged exactly, so a stale entry is a failure. main is therefore red on a job every PR runs, and every branch inherits it.

Why this PR is not the cause, stated so it is checkable

This diff is scripts/docs-audit/affected-docs.mjs + .github/workflows/docs-drift-check.yml. It touches nothing under packages/client, cannot alter a TypeScript return type, and cannot add or remove a ledger row. Its own head checks were all 33 green, including Type Check · consumer gates, before it entered the queue — the job only goes red once the branch is rebased onto the poisoned main.

No changes pushed here. Patching a base-branch failure on a bystander PR would widen this diff for a defect it did not cause, and would leave main red for everyone else.

Already filed — twice — and being fixed

⛔ I did not file a third card. The duplicate scan found two independent filings ahead of mine:

⚠️Those two are the same defect, both dispatched, and both assigned to the same identity. Two devs deleting the same line of the same file will collide. Flagging it on both so they converge to one dispatch entry — that is a coordination note from a bystander lane, not a claim on either card, and domain:cli owns the routing.

Sibling PRs from this seat, same cause

#12154, #12164 and #12171 are in or waiting on the queue with the same inherited failure — Lint & Type Check was already observed red on the queue branches for #12154 and #12164. None of them will be patched for it either.

What happens next

This PR waits for main to go green, then re-enters the queue — it needs no change. If it is not re-queued automatically once the ledger fix lands, I will re-queue it. If a rebase is needed at that point I will merge main in rather than rebase, per the branch rule.

⭐ Worth recording for whoever reads this thread later: the only thing separating "my PR is broken" from "main is broken" was opening the job log.#12180 notes the same trap — it surfaced there on a PR whose entire diff was inside comments. A seat that trusted "red ⇒ mine" would have spent a cycle hunting a type error in a docs-drift renderer.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cdsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@yinlianghui@claude