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
Activity
- addedpackage-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindingsFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
on 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:fix verified on main #10668 http client surface b36554a2d7+changelog.d/10668-http-client-response-surface.md#10673 namespace/ export =13615eb471(train 223) + fragment#10674 cjs conditional require changelog.d/10674-cjs-conditional-require-deferred.md#10675 dyn_eval class expressions 02a3b6ab5f,a5583cfb3f+ fragment#10699 import provenance 8e59526ac1,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:
- A real rebase onto main — each is stacked on a branch that squash-merged, so its base commits no longer exist as such and a merge will not do.
- A count recompute per the table above.
- A re-run acceptance check (plain
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.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:ref EXPECTED_ROWS tsv lines main 371 323 #10677 pg 371 323 #10693 nanoid 371 323 #10679 axios, #10701 uuid 371 323 #10680 mysql2 343 309 #10704 decimal 359 311 #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:
- Land native_result_ledger.py is red on pristine main (expected 371, found 376), and its path filter means it can only fail on an unrelated PR #10738's fix before any removal that edits the ledger, so removals rebase onto a ledger that is correct and fully classified rather than each re-deriving a decrement from a base that is wrong by five.
- The "recompute from the resolved tree, never by arithmetic from the old figure" rule applies to
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.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:file after clean auto-merge correct for the resolved tree docs/api/perry.d.ts2067 / 132 2066 / 131 docs/src/api/reference.md3009 / 134 2983 / 132 Caught 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.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).- added 7 commits that reference this issue
on Sep 20, 2026
Every queued native-binding removal PR records the workspace baseline as an absolute count in
workspace-architecture.json(workspace_members,decision_counts.externalize/keep), andworkspace_architecture.py --checkgates 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:origin/mainTwo distinct problems.
1. Six PRs record a member count higher than main's. A removal cannot raise
workspace_membersabove the base it lands on. All six will failworkspace_architecture.py --checkon 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
MERGEABLEdoes 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 outMERGEABLE, 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-summaryindependently 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).