Skip to content

refactor(scripts): the population-floor mechanism is spelled once, in one module - #19174

Merged
os-try-charles merged 1 commit into
mainfrom
claude/issue-17118-population-floor-one-spelling
Sep 19, 2026
Merged

os-try-charles merged 1 commit into
mainfrom
claude/issue-17118-population-floor-one-spelling

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

Fixes #17118

Clause-②: no

scripts/** only — four files, no packages/** source, no published surface, no workflow file.
Re-derived rather than recalled: node -e "import('./scripts/pm/check-governed-merges.mjs')…" prints
today's register as docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md · docs/NORTH-STAR.md,
and check-governed-merges.mjs --test answers 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 completed on 2026-09-14, so the order was settled by events: this is the
second 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:

⛔ NOT a second copy of the floor: populationFloorProblem is still the only thing that decides, and
this is the only thing that exits.

Measured on c229223e9, the three declared gates carry 3 definitions of the row-walk and 4 places a
real run turns it into a verdict
:

gate definition production call sites
check-engine-double-contract.mjs populationFloorProblem (4 rows) 2 — report() and --write, both via refusePopulationFloor
check-type-check-coverage.mjs populationFloorProblem (3 rows) 1
check-dual-build-cjs-loads.mjs floorProblem (5 rows) 1

⇒ 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 c229223e9 by which spellings live in each file, as the
deferral note asked:

file populationFloorProblem floorProblem same mechanism?
scripts/check-engine-double-contract.mjs 25 0 yes — in this PR
scripts/check-type-check-coverage.mjs 19 4 yes — in this PR
scripts/check-dual-build-cjs-loads.mjs 0 15 yes — in this PR
scripts/check-manifest-repository-directory.mjs 0 17 yes — ⛔ NOT in this PR
scripts/check-keyed-text-bounds.mjs 0 8 yes — ⛔ NOT in this PR
scripts/check-undeclared-dep-imports.mjs 0 7 yes — ⛔ NOT in this PR
packages/spec/scripts/*.ts (4 files) · check-comment-mask-corpus · check-osv-exemptions · typecheck-configs 0 3–7 each nofloorProblems there is the local of batteryFloorFailures(), the self-test battery floor: a different family, same word

⭐ The deferral note's reading is confirmed and refined: check-type-check-coverage.mjs carries both
spellings (19 + 4), but its 4 floorProblem hits are the battery family at :7539, not a second
population 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
:

wording gates
measured only … + the two-sentence ⛔ tail naming row independence engine, type-check
measured only … + a one-sentence ⛔ tail dual-build
measured only … + a different two-sentence ⛔ tail ("Find what stopped being read") manifest-repository-directory
discovered only … (measured N on ref)no ⛔ line at all keyed-text-bounds
discovered only … / read only … / extracted only …, one hand-written per row undeclared-dep-imports

That 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 provenance
line. definePopulationFloor({ ref, rows, provenance?, reproduce? }) returns the two functions each gate
already 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?"):

the honest extraction is the row-walk plus the provenance formatting, parameterised by the row table;
the row tables themselves must stay in their own gates, because each why is a claim about that gate's
internals.

Row shape is { key, min, measured, what, why, at? }. The three tables keep their own why prose verbatim
— no row was reworded, merged or moved. Two parameters exist only because a real gate needs them, not on
principle: provenance (dual-build floors typedJudged but cannot give it a column, because its number
comes from a second census on a different commit and the line carries one ref) and at (that row names its
own ref, which is where the refusal quotes it from).

check-dual-build-cjs-loads's older spelling is retired: floorProblempopulationFloorProblem,
provenanceLinepopulationProvenanceLine, including the byte-reading self-test pin that quotes the
identifier as a string. No importer existed to break — git grep finds only package.json running the
script. ⛔ 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 the
engine/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-test of its own, and why

The 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 are
spelling gates — they assert routing and never behaviour, so nothing downstream checks that ts-parse
still 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_test line would put a workflow file
in 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/objectstack32 commands
(4 paths, 531 changed lines vs merge base c229223e9).

⚠️ "all derived families green" is not "CI green." The same derivation names, outside those 32: 52
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:

gate --self-test bare
check:engine-double-contract exit 0 exit 0 — 885 pinned, 133 DEBT, 3 exempt; provenance 3771/2786/940/808 · floors 2800/2000/700/600 · +268/+190/+29/+29
check:type-check-coverage exit 0 exit 0; provenance 81/3771/2710 · floors 64/2800/2000 · +1/+268/+189
check:dual-build-cjs-loads exit 0 — 93 cases, real emitted bytes, real spawns, every vacuity floor driven to zero with its green control, the record's ref printed on the pass path see below

Both provenance lines are byte-identical in form to the pre-change runs, on the real tree.

⚠️ pnpm check:dual-build-cjs-loads bare answers exit 3 — PREREQUISITE NOT MET, not a red. It needs
a real dist/ and says so in its own header ("it is a step in Build Core (ci.yml)"); on an unbuilt
worktree it lists 96 missing dist/ paths and prints "⛔ This is NOT a pass: nothing was measured."
pnpm check:type-check-debt answers 3 for the same class of reason (it is the --re-measure path and
needs 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.

⚠️ No new TypeScript program sees anything new. The new import relationship is ESM-only: the repo root
declares no typecheck script, and the root tsconfig.json sets no allowJs, so no scripts/**.mjs file
is 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-gates has no bucket for them.

Changeset

None, and skip-changeset is measured rather than asserted: across 70 published manifests, 0
files[] entries name scripts/; positive control, 23 name dist/. The repo root package is
private: 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:

  • The same mechanism lives in three more gatescheck-manifest-repository-directory.mjs:773,
    check-keyed-text-bounds.mjs:964, check-undeclared-dep-imports.mjs:592 — each with its own
    provenanceLine, 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 (three
    hand-unrolled ifs with three different verbs), so absorbing it is a behaviour change to its text, not
    a move. Successor: whoever takes the next population-floor card touches one of these files.
  • floorProblems means two different things in scripts/ — the population floor, and the local of
    batteryFloorFailures() (self-test battery floors, ~31 files). Each is internally consistent; the
    collision 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

… 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>
@os-try-charles os-try-charles added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 19, 2026 — with Claude
@os-try-charles
os-try-charles marked this pull request as ready for review September 19, 2026 06:12
@os-try-charles
os-try-charles added this pull request to the merge queue Sep 19, 2026
Merged via the queue into main with commit 83649b8 Sep 19, 2026
37 checks passed
@os-try-charles
os-try-charles deleted the claude/issue-17118-population-floor-one-spelling branch September 19, 2026 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants