refactor(scripts): the population-floor mechanism is spelled once, in one module - #19174
Merged
os-try-charles merged 1 commit intoSep 19, 2026
Merged
Conversation
… one module The row-walk, the refusal wording and the provenance line lived hand-typed in three gates under two names. The refusal text is what an operator acts on, so three copies are three places for it to drift -- invisibly, because each gate's --self-test asserts only its own text. scripts/population-floor.mjs now owns the walk and both formats; each gate keeps its own row table, because every `why` is a claim about that gate's internals. check-dual-build-cjs-loads' older `floorProblem` / `provenanceLine` spelling is retired, so the mechanism is spelled one way in all three. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 19, 2026
os-try-charles
marked this pull request as ready for review
September 19, 2026 06:12
os-try-charles
deleted the
claude/issue-17118-population-floor-one-spelling
branch
September 19, 2026 06:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #17118
Clause-②: no
scripts/**only — four files, nopackages/**source, no published surface, no workflow file.Re-derived rather than recalled:
node -e "import('./scripts/pm/check-governed-merges.mjs')…"printstoday's register as
docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md · docs/NORTH-STAR.md,and
check-governed-merges.mjs --testanswers exit 0 (NOT governed) for each of the four paths.⭐ Two corrections to the card's framing, both measured
① The extraction surface is THREE implementations and FOUR production call sites — not "four copies"
Triage asked which order this landed in and warned that landing second means "extracting over four copies
rather than three". #17117 closed
completedon 2026-09-14, so the order was settled by events: this is thesecond landing. But #17117 did not add a fourth copy of the mechanism. It added
refusePopulationFloor()— one exit funnel in the engine gate, whose own doc comment says so:Measured on
c229223e9, the three declared gates carry 3 definitions of the row-walk and 4 places areal run turns it into a verdict:
check-engine-double-contract.mjspopulationFloorProblem(4 rows)report()and--write, both viarefusePopulationFloorcheck-type-check-coverage.mjspopulationFloorProblem(3 rows)check-dual-build-cjs-loads.mjsfloorProblem(5 rows)⇒ All three definitions and all four call sites are covered. Nothing is omitted, so triage's
"a helper extracted over two of four copies is worse than none" is satisfied by construction rather
than by judgement.
② The population is SIX gates, not three — and the drift triage predicted has already happened
Triage recorded what it had not swept: "I did not sweep for other gates carrying an unnamed floor of the
same shape." Swept here. Re-counted on
c229223e9by which spellings live in each file, as thedeferral note asked:
populationFloorProblemfloorProblemscripts/check-engine-double-contract.mjsscripts/check-type-check-coverage.mjsscripts/check-dual-build-cjs-loads.mjsscripts/check-manifest-repository-directory.mjsscripts/check-keyed-text-bounds.mjsscripts/check-undeclared-dep-imports.mjspackages/spec/scripts/*.ts(4 files) ·check-comment-mask-corpus·check-osv-exemptions·typecheck-configsfloorProblemsthere is the local ofbatteryFloorFailures(), the self-test battery floor: a different family, same word⭐ The deferral note's reading is confirmed and refined:
check-type-check-coverage.mjscarries bothspellings (19 + 4), but its 4
floorProblemhits are the battery family at:7539, not a secondpopulation floor. So the honest statement of the defect is not "three implementations, two spellings" and
not "one file holds both spellings of the population floor" — it is six implementations across two
spellings, and the refusal wording has already drifted into five distinct forms:
measured only …+ the two-sentence ⛔ tail naming row independencemeasured only …+ a one-sentence ⛔ tailmeasured only …+ a different two-sentence ⛔ tail ("Find what stopped being read")discovered only … (measured N on ref)— no ⛔ line at alldiscovered only …/read only …/extracted only …, one hand-written per rowThat is exactly the failure the card predicted, already materialised: five ways of explaining one refusal
to one reader. This PR reduces six copies to four and five wordings to three. The remaining three gates
are reported for the seat rather than swept into this diff — see Acceptance notes.
The change
scripts/population-floor.mjs— a pure module owning the row-walk, the refusal format and the provenanceline.
definePopulationFloor({ ref, rows, provenance?, reproduce? })returns the two functions each gatealready calls, under one spelling, so every existing call site and every self-test case reads unchanged.
⛔ The row tables stay in their gates, per #17099's dev's note, which triage adopted as the ruling and
which I read before writing anything (PR #17099 body, "Do the two gates want the same floor shape?"):
Row shape is
{ key, min, measured, what, why, at? }. The three tables keep their ownwhyprose verbatim— no row was reworded, merged or moved. Two parameters exist only because a real gate needs them, not on
principle:
provenance(dual-build floorstypedJudgedbut cannot give it a column, because its numbercomes from a second census on a different commit and the line carries one ref) and
at(that row names itsown ref, which is where the refusal quotes it from).
check-dual-build-cjs-loads's older spelling is retired:floorProblem→populationFloorProblem,provenanceLine→populationProvenanceLine, including the byte-reading self-test pin that quotes theidentifier as a string. No importer existed to break —
git grepfinds onlypackage.jsonrunning thescript. ⛔ No fifth spelling was introduced: the module's exports ARE the one spelling.
The one deliberate text change
dual-build's refusal tail was
⛔ NOT a pass: nothing, or nearly nothing, was read.It now carries theengine/type-check tail, which adds "This says WHICH population fell and nothing about why the others
stand — they are reported by their own rows." That sentence is true of dual-build too (5 rows,
first-row-wins), so converging loses nothing and is the point of the card: the wording an operator acts on
becomes one wording. Nothing asserts the old tail; its self-test pins the first sentence by regex, which is
unchanged.
⛔ No
--self-testof its own, and whyThe four shared
scripts/modules lint.yml runs a self-test for (invoked-as,ts-parse,js-comment-mask,import-prerequisite) are pinned at the module because the gates that route to them arespelling gates — they assert routing and never behaviour, so nothing downstream checks that
ts-parsestill refuses. That is not the shape here: all three importers drive these two functions as pure functions
over their own row tables and assert the output — the refusal text, the ref it cites, which row wins,
that a missing count is zero, and the provenance deltas in both directions. After this extraction every one
of those assertions exercises the one implementation. A fifth
run_self_testline would put a workflow filein this diff and falsify lint.yml's own "all four ran and passed" line, to buy coverage that already exists
three times over. Recorded in the module header as a considered omission. The precedent for the shape is
scripts/glob-match.mjs: "This module is deliberately not a gate, so it has no CI invocation of its own:its coverage is that its importers run
--self-test."Verification
Gate families re-derived in this worktree against the real changed set:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack→ 32 commands(4 paths, 531 changed lines vs merge base
c229223e9).artifact-roster families whose silence is a fact about a list, 11 declared-wide-population families, 14
that apply once a changeset exists, 2 that take a value from the workflow, and one path-scheduled CI job.
CI is the authority on those.
All three edited files were run both ways, bare and
--self-test, because each is itself a gate:--self-testcheck:engine-double-contract885 pinned, 133 DEBT, 3 exempt; provenance3771/2786/940/808 · floors 2800/2000/700/600 · +268/+190/+29/+29check:type-check-coverage81/3771/2710 · floors 64/2800/2000 · +1/+268/+189check:dual-build-cjs-loadsBoth provenance lines are byte-identical in form to the pre-change runs, on the real tree.
pnpm check:dual-build-cjs-loadsbare answers exit 3 —PREREQUISITE NOT MET, not a red. It needsa real
dist/and says so in its own header ("it is a step in Build Core (ci.yml)"); on an unbuiltworktree it lists 96 missing
dist/paths and prints "⛔ This is NOT a pass: nothing was measured."pnpm check:type-check-debtanswers 3 for the same class of reason (it is the--re-measurepath andneeds a built workspace closure). Both are declared to CI. The 93-case self-test is the half that reaches
this diff's subject, and it spawns the real gate over a real fixture tree.
declares no
typecheckscript, and the roottsconfig.jsonsets noallowJs, so noscripts/**.mjsfileis in any tsc program. The four type-check lanes are therefore not owed per-package runs for this diff —
stated rather than assumed, because
dispatch-gateshas no bucket for them.Changeset
None, and
skip-changesetis measured rather than asserted: across 70 published manifests, 0files[]entries namescripts/; positive control, 23 namedist/. The repo root package isprivate: true. ⇒ nothing published moves. ⛔ No label applied from here — reporting it for the PM.Acceptance notes
⛔ Not filed, not fixed here — noted for the seat:
check-manifest-repository-directory.mjs:773,check-keyed-text-bounds.mjs:964,check-undeclared-dep-imports.mjs:592— each with its ownprovenanceLine, and the refusal wording has already drifted into three further forms (table above).This is the population triage named as un-swept. ⛔ Deliberately out of this diff: each is a gate this
card does not otherwise touch, each brings its own self-test battery and pinned case floor into the
verification surface, and the card warns in its own words that overselling it "will produce a refactor
with more churn than value".
check-undeclared-dep-imports's copy is not even table-driven (threehand-unrolled
ifs with three different verbs), so absorbing it is a behaviour change to its text, nota move. Successor: whoever takes the next population-floor card touches one of these files.
floorProblemsmeans two different things inscripts/— the population floor, and the local ofbatteryFloorFailures()(self-test battery floors, ~31 files). Each is internally consistent; thecollision is in the word, and it is what made the original count read as three. Naming only.
🤖 Generated with Claude Code
https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Generated by Claude Code