Repository navigation
Every queued binding-removal PR records an absolute workspace count; six are stale and the two correct ones collide with each other #10739
Description
Every queued native-binding removal PR records the workspace baseline as an absolute count in workspace-architecture.json (workspace_members, decision_counts.externalize/keep), and workspace_architecture.py --check gates on it. The PRs were authored in parallel from a base of 83, so most of them carry a number that main has since moved past.
Read from each PR head via the contents API against main 023dc0b653:
| ref | members | externalize | keep | |
|---|---|---|---|---|
origin/main |
80 | 31 | 44 | |
| #10691 dotenv | 79 | 30 | 44 | correct vs today's main |
| #10701 uuid | 79 | 30 | 44 | correct vs today's main |
| #10677 pg | 82 | 32 | 45 | stale |
| #10680 mysql2 | 82 | 32 | 45 | stale |
| #10693 nanoid | 82 | 32 | 45 | stale |
| #10704 decimal | 82 | 32 | 45 | stale |
| #10708 lru-cache | 82 | 32 | 45 | stale |
| #10712 commander | 82 | 32 | 45 | stale |
Two distinct problems.
1. Six PRs record a member count higher than main's. A removal cannot raise workspace_members above the base it lands on. All six will fail workspace_architecture.py --check on rebase. They are also mutually inconsistent — six different crates cannot all produce the same 83→82 transition.
2. The two correct PRs collide with each other. #10691 and #10701 both compute to 79/30/44, because each was computed against main independently. They cannot both be right at merge time: whichever lands second must read 79→78 / 30→29. This is not a defect in either PR — it is inherent to recording an absolute in a queue of sequential removals.
What makes this easy to miss
MERGEABLE does not catch it, and neither does review. The counts live on different JSON lines from the crate entry a removal deletes, so git has no textual conflict to raise and auto-merges both sides cleanly. A PR can rebase green, review clean, and still carry a number that was true a week ago. #10679 demonstrated this today: it was rebased onto current main, came out MERGEABLE, and still recorded 82/32/45.
Rule
At every rebase, for every removal: recompute from the resolved tree and have workspace_architecture.py --check --print-summary independently reproduce the number. Never derive it by arithmetic from the old figure, never copy a sibling's, and never copy one out of prose — including this issue, which will itself be stale as soon as the next removal lands.
The same absolute-count hazard applies to scripts/native_result_ledger.tsv, scripts/string_payload_access_baseline.txt, and the governance/pins tables. Regenerate them with their own scripts rather than resolving them by hand.
Worth considering
The recurring cost here is structural: an absolute count in a tracked file, in a queue of sequential changes that each shift it by one, guarantees that every PR but the front of the queue is wrong. If the baseline were derived at check time rather than recorded, or recorded as a delta, none of these eight PRs would need touching. That is a larger change than the queue should absorb right now, but it is the actual fix — what is described above is a discipline that has to be applied by hand every single time, which is the kind of rule that holds until it doesn't.
Related: #10738 (the ledger gate's path filter means it fires on an unrelated PR).
Activity
proggeramlug commentedon Sep 19, 2026
Queue status update: every enabling fix is now on main, so the whole removal queue is unblocked.
All five were closed with an empty
mergedAt, which is the merge-train signature (a train lands the code under its own PR and closes the sources), so "closed, not merged" reads as unlanded if you go by the PR state alone. Verified by content onorigin/mainrather than by PR status:b36554a2d7+changelog.d/10668-http-client-response-surface.mdexport =13615eb471(train 223) + fragmentchangelog.d/10674-cjs-conditional-require-deferred.md02a3b6ab5f,a5583cfb3f+ fragment8e59526ac1,0900ce4d03+ fragmentSo the "do not merge before #N" note on each removal is now satisfied for all of them. What remains for each is mechanical but not optional:
import, nocompilePackagesentry, byte-compared against Node 26.5.1).One sequencing point worth stating, because it determines how fast this queue can actually move: rebasing them all now would be wasted work. Every removal edits the same registry surfaces and shifts the same absolute counts by one, so the moment any one lands, the other six are stale again — in their counts if not in their conflicts. The queue can only advance at one rebase per landing. Rebasing ahead of that does not buy parallelism; it buys rework.
Done so far under that protocol: #10691 (dotenv) and #10701 (uuid) are rebased,
MERGEABLE, and carry correct counts against today's main — though they collide with each other, so the second to land needs 79→78 / 30→29. #10679 (axios) is rebased andMERGEABLEbut still carries stale counts; that is being fixed.proggeramlug commentedon Sep 19, 2026
The same mechanism exists in a second file, and there it is already self-inconsistent.
scripts/native_result_ledger.pycarriesEXPECTED_ROWS, another absolute. Read from each head against main023dc0b653:#10680 removes 28 provider rows and #10704 removes 12, each computed from 371. They collide directly: after #10680 lands, #10704's base is 343, not 371, so its recorded 359 is wrong by the size of the other PR's removal.
It is worse than that, because main's 371 is itself wrong — see #10738, where the true count is 376. So both numbers are derived from a base that was already stale, and once #10738's fix lands (correcting the constant and adding four newly-classified rows) both are wrong again by a different amount.
Two consequences for sequencing:
EXPECTED_ROWSexactly as it does toworkspace_members. Any removal deleting native providers must recount, not subtract.Worth recording the more general shape, because it is what made #10738 dangerous rather than merely untidy: the stale constant was load-bearing as a mask. The row-count check ran first and failed, so the classification-coverage check never ran, and four providers with no classification at all sat behind it. Bumping the constant alone would have turned the gate green and shipped the real defect — an unclassified
result_kindmisrepresents to the GC what a native call returns. A gate that stops at its first failure reports one defect and conceals the others, and there is no reason the concealed one should be the less serious.proggeramlug commentedon Sep 19, 2026
Fourth file class hit by this mechanism, and this one nothing checks: the generated docs.
While rebasing #10704 (decimal.js removal),
docs/api/perry.d.tsanddocs/src/api/reference.mdboth auto-merged with zero conflict markers. The merge was partly right — it correctly stripped decimal.js from the body — and silently kept main's stale header counts:docs/api/perry.d.tsdocs/src/api/reference.mdCaught only by regenerating from a fresh build rather than trusting the clean merge.
This is the same shape as the workspace counts, one file class further out, and it is worse in one specific way: a body that is correct plus a header that is stale looks more trustworthy than either error alone. A reviewer scanning the diff sees the decimal.js entries correctly removed and reasonably concludes the file was handled.
Running tally of surfaces this has now hit
workspace-architecture.json—workspace_members/decision_countsscripts/native_result_ledger.py—EXPECTED_ROWS/EXPECTED_PROVIDERSscripts/unrooted_local_shape_baseline.json— per-file countsdocs/api/perry.d.tsanddocs/src/api/reference.md— header counts (new)The first three are gated:
workspace_architecture.py --check, the ledger check, and the unrooted ratchet each fail on a stale value, so the error surfaces eventually even if late and on the wrong PR. The docs are the exception — nothing compares the committed file against a regeneration unless someone regenerates, so a stale header can land and stay.Two method notes from the same rebase, both worth generalising
A count that decrements is not derivable by arithmetic. #10704 does decrement
EXPECTED_ROWS/EXPECTED_PROVIDERS, unlike the axios/dotenv/uuid removals which touch no ledger rows. The correct values came out at 364 / 314, recounted from the resolved tree — not obtainable by subtracting the PR's old delta from main's new base, because the tree shape differs. Subtraction would have produced a plausible, wrong number.Re-derive a stale value even when it passes.
unrooted_local_shape_baseline.json--checkpassed at 577 against a baseline of 578 — an improvement, therefore green. It was re-derived anyway. A technically-passing stale number is precisely how the ledger's371survived long enough to mask four unclassifiedresult_kinds (#10738): the gate was green, so nobody looked.proggeramlug commentedon Sep 19, 2026
Two refinements from #10712's rebase, both sharpening what was written above.
1. The docs staleness is not confined to the header counts. Regenerating
docs/api/perry.d.tsin full — rather than patching the header line — also removed a staledeclare module "commander"body block that the clean auto-merge had retained. A header-only correction would have produced a file with a correct count and a phantom module declaration in it.That matters for how the fix is framed. "Update the header after a removal" is not sufficient; the body carries removed surface too, and the two can go stale independently. The remedy is to regenerate from a fresh binary and diff, never to patch.
2. The three-way consistency check caught a second instance, in a form the first one wouldn't have suggested.
The check is
sum(decision_counts) == workspace_members == len(crates)— three independent quantities that must agree. On #10712's second rebase round,workspace-architecture.jsonauto-merged with zero conflict markers and silently kept the stale77/28/44from round one. Critically:sum(decision_counts)andworkspace_membersagreed with each other — both 77.len(crates)was 76.So two of the three were mutually consistent and both wrong. A check comparing only the counts to each other passes;
workspace_architecture.py --checkalone did not catch it. The disagreement with the crate map is what surfaced it.That is the second defect this check has caught (the first being #10691's file, where the crate map had already shrunk to 77 while the counts still read 78) and it is now standard for every removal in this queue. Worth considering whether
workspace_architecture.py --checkshould assert the three-way identity itself, since the data it needs is already in the file it reads — which would move this from a discipline someone has to remember to a gate that cannot be forgotten.Running tally of stale-merge instances caught on this campaign: five — #10679, #10691, #10704, #10708, #10712 — across
workspace-architecture.json,native_result_ledger.py,unrooted_local_shape_baseline.json, the generated docs (headers and bodies), andCargo.lock(a staleperry-ext-*entry survivinggit checkout --ours, caught only by the next build regenerating it away).fixup(rebase): re-derive every absolute count on the combined tree
chore(pg): remove native pg binding, compile real package from source (…
refactor(stdlib): remove mysql2 native binding (#10680)
chore: re-derive every absolute-count surface for the combined pg + m…
chore(10704): re-derive the ledger absolutes against the new main tip
chore(train266): re-record the tokio inventory from the merged tree
chore(train266): re-record the tokio inventory from the merged tree