feat(ci): differential gate for newly added cross-file line citations, and retire two stored line-number ledgers (objectui#8875) - #8974
Conversation
…, and retire two stored line-number ledgers (objectui#8875) Ruling C on objectui#8875, clauses 2, 3 and 5. Clause 1 (the convention text in AGENTS.md) is a governed surface and lands as a separate, human-merged PR. Clause 2 — a DIFFERENTIAL gate. `scripts/check-new-cross-file-line-citations.mjs` reports the cross-file line-address citations a pull request ADDS, against its merge base. The 540 existing citations are not its denominator and it sweeps none of them. An absolute count was refused on a measurement: PR #8887's line shifts flipped one citation from `drifted` to `resolves` by accident, moving the tree-wide false count 540 -> 539, an unearned green that belonged to nobody. It ships report-only (`ENFORCEMENT`), wired into its own workflow so it can see the markdown-, docs- and changeset-only pull requests `lint.yml` short-circuits. Its four synthetic controls are fatal in both modes — a differential gate that reports zero through a broken differ is indistinguishable from a clean branch. `unresolvable` (chiefly regenerated `dist/*.d.ts`) is a third verdict and is never counted false. Clause 3 — the two stored ledgers stop storing line numbers. `UNGATED_EXAMPLES` is re-keyed from `path:line symbol` to `path symbol #ordinal`, and the zh-only family citations in the designer-table parity gate become `{ file, anchor }` with the anchor asserted present in the file exactly once. Behaviour-preserving on today's tree, proven by before/after runs: the doc-example gate's whole report is byte-identical, and 89 of 89 ledger rows map onto the new keys with unchanged values. Clause 5 — the firing control the ruling asked to keep is intact, but not at the address the ruling gives. Reported on the card. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
One conflict, one line, in `scripts/check-doc-example-types.mjs`. `fb828773d` (objectui#8106, PR #8965) had to RE-DERIVE a line-keyed `UNGATED_EXAMPLES` row — `ListView.tsx:808` to `:830` — because its own diff moved lines above an `@example` block it never touched. That is the third instance of this defect in a day, and it landed while clause 3 was abolishing the key shape that causes it. Resolved onto the new key scheme, ⛔ not by picking a side: the row is now `packages/plugin-list/src/ListView.tsx ListViewHandle #1`, and the line number that had to be re-derived is simply gone. Re-derived against the MERGED tree rather than carried: that symbol has exactly one `@example` block in that file, at line 830, so its ordinal is 1. Verified on the merged tree: 124 blocks, 124 unique generated keys, 89 ledger rows, 0 rows naming no block; and against `origin/main` `fb828773d` itself, 89 rows before, 89 after, 89 of 89 carrying an unchanged card / codes / reason under the derived key map, 0 unmapped. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Armed — SQUASH, in the queue at 2026-09-10T12:43:49Z, and the landing probe is written BEFORE the merge
The probe, stated now so it cannot be fitted to the outcomeBaselines read on
⭐ C1 and C2 are the same 89 rows read from both sides, and both transitions are clean — 89→0 and 0→89. A leg that only counted the new shape would pass while old rows survived alongside; this pair cannot. ⇒ it is the leg that actually proves clause 3, because the clause is 「stop storing line numbers」, ⛔ not 「also store symbols」. Controls: leg A's firing control is the sibling What lands, in one lineA differential gate, report-only, that reds only on cross-file line-address citations a pull request adds against its base — plus the two stored ledgers ceasing to store line numbers at all. ⛔ The 540 existing citations are untouched and are not its denominator. ⛔ The convention text in
Next: Generated by Claude Code |
Refs #8875
⛔
Refs, not a closing keyword, and that is deliberate. This is PR 1 of two for ruling C. Clause 1 — the convention text inAGENTS.md— is a governed surface and lands as PR 2, which is third in this lane'sAGENTS.mdserial queue. A closing keyword here would take objectui#8875 out of every open-issue sweep the moment this merges, and the convention half would be lost rather than visibly pending. The card stays open for it.Clause-②: noThe ruling, and its provenance
Maintainer, 2026-09-10, verbatim and untranslated (objectui#8875 comment 5617612419, on the decision analysis at comment 5617468956):
Direction C. Five clauses. This pull request implements 2, 3 and 5.
AGENTS.mdBase
origin/mainfb828773d(tip committed 2026-09-10T11:53:26Z); head45f6523a4(committed 2026-09-10T12:20:50Z), a merge commit — see the section below. The branch point was2c208d5bb(10:57:11Z) andorigin/mainmoved once underneath it.Clause 2 — the gate is DIFFERENTIAL, and that word is the whole design
scripts/check-new-cross-file-line-citations.mjsreports the cross-file line-address citations a pull request adds, against its merge base. The 540 existing citations are ⛔ not its denominator and it sweeps ⛔ none of them.⛔ An absolute count was refused on a measurement, not on taste. PR #8887's line shifts flipped one citation from
driftedtoresolvesby accident, moving the tree-wide false count 540 → 539, and the executing seat had to name that in its own body as an unearned green belonging to nobody. A total-reading gate scores that as progress.Identity is content-shaped, not positional. A citation is keyed, within one citing file, by
syntax | written path | cited line, compared as a multiset against the base blob. The citing file's own line number is deliberately not in the identity — if it were, re-indenting a file or adding an import above would report every citation below it as newly added, which is the positional fragility this card is about rebuilt inside the instrument meant to measure it. Renames are followed (git diff -M).All five spellings the card measured, inherited from the census by import rather than re-implemented (two readers over one population is how two answers start disagreeing): the dominant
NAME.ext:NNN, theNAME.ext#LNNNpermalink,line NNN of NAME.ext,NAME.ext line NNN, and the continuation address — a bare:NNNcarrying no filename at all, which no basename-anchored probe can see. Each has its own test.unresolvableis a third answer and never collapses intofalse. A citation into a regenerateddist/*.d.tsis untracked and rebuilt, so nothing can ever decide it; calling it false would be an assertion. It is still reported as added — the convention is that the address is not written, not that it is written accurately — but its verdict column saysunresolvable. A synthetic control pins the split so it cannot regress silently.The state this lands in, and what flips it
Report-only.
ENFORCEMENTin the script is the whole switch; its test reads the landed value, so the flip cannot arrive as a one-word edit. It ships report-only so pull requests already in flight are not failed by a rule whose convention text has not landed yet — an author failed by a rule is owed a document to be failed against.What flips it: the gate reading zero new across the in-flight population, plus clause 1 landing in
AGENTS.md. When it flips, the workflow owes amerge_groupleg before the context may be required (objectui#3523: a required check that never reports on a queue build stalls the queue until the ruleset's 60-minute timeout).⛔ Report-only does not mean "cannot fail". A control failure exits 1 in both modes: a differential gate reporting zero through a broken differ is indistinguishable from a clean branch, which is this card's own subject one level up.
Wiring, and the obligations that came with it
Its own workflow (⚠️ Not a step in
line-citation-gate.yml), no path filter,pull_requestonly.lint.yml: that job's in-job short-circuit skips every expensive step on a markdown-only, docs-only or changeset-only change — and the ruling names 注释、消息串、文档、changeset、脚本 in that order, so those are exactly the pull requests that carry the population. A gate that cannot see the shape most likely to trip it rebuilds the hole it exists to close.The page objectui#3653 pins is satisfied in both directions: a
##heading naming the workflow file, a Workflow Inventory row, the command written out, and the paragraph declaring it report-only and that it exits 0 regardless of what it finds — the pincheck-doc-snippet-emitted-census.test.tsalready reads for the sibling census, asserted here too.scripts/dependabot-merge-gate.mjsclassifies the new context inNOT_A_GATEwith its reason, which its partition test requires.Clause 3 — the two stored ledgers, and the proof they still behave identically
main, so it is proven by running, ⛔ never by reasoning about the key shape.UNGATED_EXAMPLES(check-doc-example-types.mjs) was keyedpath:line symbol, and the line was a stored literal compared for equality. PR #8895 added three imports topackages/types/src/objectql.ts, every collected block moved down by three, and this gate reddened on a branch that had not touched an example — objectui#8614 is the same failure one card earlier. It is now keyedpath symbol #ordinal, where the ordinal is the block's position among the examples of that symbol in that file. 114 of the 124 blocks are the only example on their symbol and carry#1; the ordinal exists for the five symbols that document more than one, and it moves only when a sibling example on the same symbol is added or removed.The zh-only family citations (
check-i18n-designer-table-parity.mjs) were three storedpath:lineaddresses pointing into another file, printed in the report and followed by nothing. They are now{ file, anchor }, and the pin was strengthened rather than relaxed: the old test asserted the citation matched apath:lineshape and that the file existed; the new one asserts the anchor text is actually present in the file, exactly once, with an anti-vacuity control. A line number could never have been checked that way.Before / after, exit codes captured before any pipe
node scripts/check-doc-example-types.mjsdiffemptynode scripts/check-i18n-designer-table-parity.mjspnpm exec vitest run scripts/__tests__/check-doc-example-types.test.tspnpm exec vitest run scripts/__tests__/check-i18n-designer-table-parity.test.ts⭐ The doc-example gate's before/after
diffis empty. Not "the same verdict" — the same bytes, funnel counts, controls, injection census and closing sentence included. Separately, a row-by-row comparison of the two ledgers under the derived key map: 89 rows before, 89 after, 89 of 89 carrying an unchangedcard/codes/reason, 0 unmapped.turbo run buildover the 35-package filter it names, so neither is acouldNotRunexit 2 read as a verdict.Clause 5 — what the firing control actually is
The ruling asks that a known-wrong citation be kept as the firing control and names it as a
path:linereference tocheck-doc-component-types.mjs.'Not a node type.',— an unrelated string. Control:check-doc-component-typesappears in 45 files, so the zero is a reading and not a failed grep.⭐ Found by content, and it is intact. The known-wrong citation is the
delete:entry of that file's key table, now at line 615, citing the action vocabulary inpackages/core/src/actions/ActionRunner.tsat line 112. Verified by content: line 112 of that file is the closing*/of a docblock andexport interface ActionDef {opens on the next line. Still wrong, still unrepaired, and the census still reports it[non-substantive]as its firing control.⇒ ⭐ The ruling's own citation of a line-address reference could not be resolved as written — which is, without irony, the defect this card exists to stop. ⛔ No synthetic citation was planted in the tree and ⛔ the real one was not touched.
Proving the gate can fail — a gate that cannot fail is worse than no gate
Two ablations on the real tree, each with on-disk mutation proof and a trapped restore verified by blob hash.
① A new citation is injected → it reds. Appended one cross-file address to
scripts/check-lint-coverage.mjs.② An existing citation is merely SHIFTED → it stays silent. Pushed 40 lines above the clause-5 firing control in
check-doc-component-types.mjs, moving that citation from line 615 to 655 without touching its text.⭐ ② is the anti-absolute-count proof on the live differ, and it is the exact shape the ruling refused a total for. The 32 tests in the gate's own file pair every non-firing assertion with a firing one on the same code path.
Every gate run
node scripts/check-new-cross-file-line-citations.mjs(this branch)node scripts/cross-file-line-citation-census.mjsnode scripts/check-doc-example-types.mjsnode scripts/check-i18n-designer-table-parity.mjspnpm exec vitest run scripts/pnpm lint:rootnode scripts/check-entry-guard.mjsnode scripts/check-control-bytes.mjsnode scripts/check-changeset-presence.mjsnode scripts/check-pre-install-import-graph.mjsnode scripts/check-action-ref-convention.mjsnode scripts/check-doc-links.mjsnode scripts/check-doc-fence-languages.mjsnode scripts/check-doc-component-types.mjsnode scripts/check-shell-escape-residue.mjsnode scripts/check-governed-queue-guard.mjs --test(11 paths)turbo ls --affectedagainst the base reports 0 packages: this diff is root tooling, one workflow and one guide page, so the affected suite isscripts/and it was run whole rather than filtered.path:lineliterals from the tree, which is why the census population reads 1267 → 1174 (the four reader files' own fixtures account for the rest). ⛔ That is a consequence of clause 3, ⛔ not a sweep of clause 4's population — no prose citation anywhere was re-addressed.⭐ The merge, and the argument it handed this change
origin/mainmoved tofb828773d(committed 2026-09-10T11:53:26Z) while this branch was open, and the two diffs collided in exactly one file —scripts/check-doc-example-types.mjs— for exactly one reason.fb828773d(objectui#8106, PR #8965) had to RE-DERIVE a line-keyedUNGATED_EXAMPLESrow. Its own diff moved lines above an@exampleblock it never touched, so the row's key had to be edited fromListView.tsx:808to:830to keepmaingreen. That is the third instance of this defect in a day — afteruseExpression.ts:163on objectui#8867 andobjectql.ts:1614on PR #8895 — and it happened while clause 3 was abolishing the key shape that causes it. ⭐ It is the strongest argument this change has, and it is measured rather than asserted: an unrelated pull request paid a round trip to a number that no longer exists after this one.How the conflict was resolved: onto the new key scheme, ⛔ never by picking a side. The row is now
packages/plugin-list/src/ListView.tsx ListViewHandle #1, and the line numberfb828773dhad to re-derive is simply gone. The ordinal was re-derived against the merged tree rather than carried across: that symbol has exactly one@exampleblock in that file, at line 830, so its ordinal is 1. ⛔ Merge, ⛔ not rebase — the branch was already pushed, so history is not rewritten.Everything clause 3 rests on, re-measured against the NEW base
fb828773dorigin/mainfb828773d's own ledgercard/codes/reason, 0 unmappednode scripts/check-doc-example-types.mjsbefore vs afterdiffis EMPTY again — byte-identical, not merely the same verdictnode scripts/check-i18n-designer-table-parity.mjsbefore vs after⭐ The row count did not change: 89 before, 89 after. The before/after pair was taken by swapping only the two gate scripts to
fb828773din place — the corpus underpackages/**is byte-identical between this branch and that commit (git diff --name-only origin/main HEAD -- packages apps examplesreturns 0 files), so the swap isolates the gate change and nothing else. Both scripts were restored by blob hash (4230f632…,2f64df94…) withgit diff HEADempty.Re-run on the merged tree
pnpm exec vitest run scripts/pnpm lint:rootnode scripts/check-new-cross-file-line-citations.mjsfb828773d, 8 files compared, 0 new, 4/4 controls PASSnode scripts/check-changeset-presence.mjsnode scripts/check-control-bytes.mjsnode scripts/cross-file-line-citation-census.mjsturbo ls --affected(base = merge base)a10d237a5had zero workflow runs. On45f6523a4it does: 22 runs created,Line Citation Gateamong them. ⛔ Queued is not green; the PM reads the conclusions.What this PR does not do
AGENTS.mdand every other governed surface. Clause 1, PR 2.it.eachtable, and the table was rewritten so the address stays data instead of reaching a test name.Session reference, written as prose in a code span because a footer link does not survive an edit:
session_01FhBNJcLRZLe8M87VcUgpKr.Generated by Claude Code