Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): anchor check-i18n-bundles' population at the repo root - #11675
Conversation
`findExtractConfigs`'s first parameter is its ABSOLUTE walk root — its own docstring says so, and the module's other consumer (scripts/pm/dispatch-gates.mjs) has always passed one. This gate passed the repo-relative vocabulary word for BOTH parameters, so the walk landed on `<cwd>/packages`: right by coincidence at the repo root, and from anywhere else an uncaught `ENOENT ... scandir 'packages'` with a `node:fs` stack. The anchor goes at the call site, not in the shared module: that module's contract is already correct and already honoured by its other caller, so moving the anchor into it would silently re-root a second consumer's population. The direction is LOUD, not silently green — the cost was a wrong first diagnosis, not a false pass. What makes it worth fixing is that the throw bypassed every worded channel this gate owns (#5217, #7681, reportPrerequisiteNotMet), which exist so an environment fact never reaches the reader as a content verdict. Anchoring a scan without refusing an empty result would trade the loud crash for a silent green, so the population verdict is a pure classifier splitting the two causes: an empty population is a prerequisite failure (#4690), while a --filter that matched nothing is a typo and must not describe a healthy repo as broken. Also anchors the reads the population feeds (the config docstring, the documented --out=) and the child extractor's cwd — all three resolve repo-relative argv, and without them the anchored walk would only move the failure one line down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 24, 2026
ACCEPT — PM review. Verified by content, including the claim that killed one of my two suggested routes. ⭐ Route B would have broken |
Anchoring the population gave the bare literal a population-constant name (`PACKAGES_DIR`), which is what made it visible to the bare-root sweep. A bare single-segment word yields no watch hint, so the row is real and needed a verdict. REFUSE-UNSPELLABLE, not REFUSE-WIDE: the population is a filename-and-segment filter, 9 of 5093 tracked files under the root (0.18%), so a subtree declaration would be false rather than merely wide. That is the table's own definition of the two refusals, and it ties the check:i18n-coverage sibling row for the narrowest on the list — the two gates select the same nine configs by the same test, so they are refused alike. No narrower declaration is spellable, measured rather than assumed: collapseHint deletes glob segments, so every glob spelling of the real population reduces to a malformed double-separator prefix that hintCovers matches against nothing. Such a hint would be live and cover zero files, which is worse than the refusal. The miss is also smaller than the row: check:i18n already reaches the cards that can actually move a bundle through the convention triggers -- a package owning an extract config, and a metadata form module -- both verified live against the derivation. Data ledger only: 16 insertions, 0 deletions, no change to the recogniser, the sweep or the self-test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 24, 2026
Re-ACCEPT at My prior was directionally right and labelled wrong — the correction mattersI said REFUSE-WIDE. The ledger defines that as "the population really IS the whole top-level root. A declaration would be TRUE, and is refused anyway." Nine files out of 5093 is 0.18% — a declaration here would be false, not merely expensive. That is REFUSE-UNSPELLABLE, and the distinction is not pedantry: WIDE records "we could say this truthfully and choose not to," UNSPELLABLE records "there is no true thing to say." A future reader triaging the ledger acts differently on each. And you found the twin already in the file — ⭐ My "third way" was worse than both, and you measured it rather than declining it politelyI suggested a narrow declaration (
A hint that is live and covers zero files is the fabricated-lead defect's mirror image — this whole card family exists because a hint can name files it should not; my suggestion would have produced one that names files it cannot, while reading as real to anyone auditing the hint set. Worse than a refusal, and invisible in exactly the way refusals are not. ⭐ The positive assertion, inverted — this is the part I would not have specifiedI asked for "prove the declaration changes the derivation." There is no declaration here, so you proved the dual: refusing loses nothing. That inversion is the right shape for a refusal and I did not think to ask for it. Adjudication 1 — the ready flip was me. Leave it.
Adjudication 2 — the |
Fixes#11647
scripts/check-i18n-bundles.mjsenumerated its population with two bare relativeliterals, so from any cwd but the repo root it died with an uncaught
ENOENT ... scandir 'packages'and anode:fsstack. This anchors the walk,routes its failure through the gate's existing worded channel, and refuses an
empty population.
Re-measured at today's
mainbefore startingThe card's own
Unlock-action:. Re-measured atc251ef421(the report waswritten at
3637731e2), with an on-root positive control so the off-rootreading is not a zero-hit that proves nothing:
The premise holds. One correction to how it reproduces: running the gate from
/tmpon a fresh worktree stops earlier, at the unbuilt-CLI prerequisite, so thepopulation stack is only reachable on a built tree. It was measured both ways.
Where the anchor goes, and why not in the shared module
findExtractConfigs(absDir, rel)'s first parameter is its absolute walk root— its own docstring says so ("Every extract config under
absDir"), and themodule's other consumer already passes one:
scripts/pm/dispatch-gates.mjs:2097findExtractConfigs(join(ROOT, 'packages'), 'packages')scripts/check-i18n-bundles.mjs:748findExtractConfigs('packages', 'packages')So the shared module's contract was never wrong; only this call site was. Route B
from the card (anchor inside
i18n-bundle-surface.mjs) is therefore rejected:it would silently re-root
dispatch-gates.mjs's population, which is themistake this gate family keeps paying for, in reverse.
The direction is LOUD, not silently green
Stated because the opposite claim would be wrong: an uncaught throw naming
node:fscosts a wrong first diagnosis, not a false pass. This PR did notprevent a false green. What makes it worth fixing is that the throw bypassed
every worded channel this gate owns — #5217, #7681 and
reportPrerequisiteNotMetexist precisely so an environment fact never reaches the reader as a content
verdict, and an uncaught stack reaches them as neither.
An empty population is REFUSED, not reported OK
Anchoring the scan without this would trade the loud crash for a silent green,
which is strictly worse than the bug.
populationVerdict()is a pure classifier(the shape every other classifier in this file has, so
--self-testdrives it)splitting two causes that previously shared one sentence:
reportPrerequisiteNotMetwith its "Nothing was checked" statement, and afix:that is deliberately not the CLI build.--filtermatched nothing — a typo in an argument the developer justtyped. Not a prerequisite: it must not borrow "nothing was checked", must not
prescribe a rebuild, and must not describe a healthy repository as broken.
On the question of whether
=== 0is the right condition: no legitimate tree ofthis repo has zero —
lint.ymlruns this gate because packages here shiptranslation bundles. The narrower condition needed was not on the count, it was
on the cause, which is the split above.
The failure now arrives worded
Same synthetic root that previously produced the bare stack:
Scope beyond the walk, and why it is not creep
Anchoring only the walk moves the crash one line down:
flagsFromDocstring,the documented
--out=existence check and the child extractor's argv are allrepo-relative too. Left unanchored, an off-root run would have reported an
environment fact as
N bundle problem(s)— the exact shape ruling #7681 removed.So the seam is applied to all four, and the spawn takes
cwd: REPO_ROOT, thesame line
check-i18n-coverage.mjsalready carries for the same reason (#10907).REPO_ROOTis taken from the sharedatRepoRootseam rather than derivedagain from
import.meta.url— a second derivation two lines from the first isthe duplication #11394 removed when it exported that one.
What #11650 had already done here
Verified rather than assumed: it anchored line 736's existence check
(
existsSync(atRepoRoot(resolved.file))) and added the#11394self-test block— 61 insertions, 1 deletion, population untouched. Nothing in this PR redoes it.
Verification — all at
9d41b11e0Gate union derived, not recalled:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(provenance line asserts the repo matches this checkout's
origin).pnpm check:i18ncheck-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).pnpm check:entry-guard✓ check:entry-guard: 142 scripts/ file(s) …pnpm check:parse-guard✓ check:parse-guard: 141 scripts/ file(s) …pnpm check:pnpm-filter-targets✓ check:pnpm-filter-targets: 120/148 \--filter` occurrence(s) …`pnpm check:cross-package-test-inputsOK: 16 package(s) read outside themselves …pnpm check:nul-bytescheck-nul-bytes: OK (scanned 6519 text file(s) …)pnpm lint(whole repo, not narrowed)Exit codes captured before any pipe.
End-to-end, on a built tree
The claim this card is about, measured rather than argued — the gate's full
output from
/tmpand from the repo root:Both exit 0, both
OK (9 package(s) …). That single comparison is what provesthe whole chain moved, not just the walk: had any one of the four sites stayed
cwd-relative, the off-root run would have diverged.
Non-vacuity, both directions
Each mutation was proven on disk by marker counts and the file's sha, and each
restore proven byte-identical; both ran under
trap … EXIT INT TERM.A — revert the anchor to the pre-fix spelling. Predicted before running: an
uncaught ENOENT, not a worded assertion failure, because the walk throws before
the assertion evaluates. Observed exactly that:
B — delete the empty-population refusal. Predicted a worded
✗. Observed:Both restored to
5e3f435f, byte-identical.The new self-test also carries its own positive control: it records that the
pre-fix spelling throws
ENOENTfromtmpdir(), so "the anchored walk worksoff-root" cannot pass on a tree where the bare spelling would have resolved too.
check:i18nruns--self-testand then the real run, so CI runs all of this on every PR.No changeset
pnpm changesetis not required: AGENTS.md §942 — "Pure bug fixes do notrequire a changeset" — and nothing here is released. The change is confined to
scripts/, the root package isprivate: true, and the file appears in nopublished package's
files; its only referent is the rootcheck:i18nscript.Per
pr-automation.yml, theskip-changesetlabel is the author's explicitopt-out and is applied to this PR.
Follow-up: the bare-root verdict this fix owed (
d741c991d)Lint & Repo Gateswent red onscripts/pm/bare-root-worklist.mjs --self-test.In scope and correct: anchoring the population gave the bare literal a
population-constant name (
PACKAGES_DIR), and a bare single-segment wordbuilds no watch hint, so
check:i18nnewly joined the invisible bare-rootspecies and owed a recorded verdict.
Verdict: REFUSE-UNSPELLABLE — not REFUSE-WIDE
The refusal direction was right, the specific verdict was not, and the table's
own definitions make the difference load-bearing:
declaration would be TRUE, and is refused anyway."
inside the root."
This gate opens nine named config files. A
packages/**declaration would befalse, not merely wide, which is the second row, not the first.
Measured on the tree at
d741c991d, numerator from the gate's own walk filter,denominator from
trackedFiles():The narrow-declaration hypothesis: falsified, measured
Checked what the vocabulary can actually express before concluding the choice
was binary.
collapseHintdeletes glob segments (hint.replace(/\*\*?/g, '')),so every narrower spelling collapses to a malformed double-separator prefix:
packages/**packagespackages/*/scripts/i18n-extract.config.tspackages//scripts/…packages/**/scripts/i18n-extract.config.tspackages//scripts/…packages/*/scripts/**packages//scriptsWorse than useless: each narrow spelling does survive
extractWatchHintsinto a live hint, and then covers nothing at all — a declaration that names
the gate for zero files. So the choice really is binary, and the honest half of
it is refusal.
Precedent — the identical row already exists
check:i18n-coverage PACKAGES_DIR packagesis already recordedREFUSE-UNSPELLABLE at "9 of 5035 (0.18%)". That is not an analogy: the
sibling selects the population with the same filename-and-segment test
(
e.name === 'i18n-extract.config.ts' && p.includes('/scripts/')), so bothgates walk the same nine files under the same constant name at the
same root. Refusing them alike is consistency, not novelty.
The positive assertion, adapted to a refusal
The declaration route asks you to prove the derivation actually changes. A
refusal owes the converse, proven the same way — that refusing loses nothing,
because the gate already reaches the right cards by a better route. Verified
live against the derivation, both directions:
check:i18nis therefore not unreachable for package cards. The conventiontriggers already name it for exactly the cards that can move a bundle, with a
precision a
packages/**subtree hint would destroy rather than add to.What was edited, and what was not
scripts/pm/bare-root-worklist.mjs: 16 insertions, 0 deletions, entirelyinside the
TRIAGEdata ledger. The recogniser (POPULATION_CONSTANT), thesweep and the self-test are untouched — verified by filtering the diff for those
anchors. Recording a verdict is the remedy the gate's own failure text
prescribes, and
REFUSE-*verdicts are recordable only there.scripts/check-agent-test-spelling.mjswas not touched.Non-vacuity
Ablation C — delete the row just recorded. Mutation proven on disk (row key
1 → 0, sha
09b8d6a3→02576657), restore proven byte-identical back to09b8d6a3, undertrap … EXIT INT TERM.Gates at
d741c991dUnion re-derived (
--repo objectstack-ai/objectstack); the change set is now 2paths and the derivation added exactly one family, the one that fired.
bare-root-worklist --self-testOK self-test: 38 live row(s), 35 unreachable as spelled, 35 recorded verdict(s) — none stale, none missing.check:i18n --self-test✓ check:i18n --self-test — … all go red, and stay distinctcheck:entry-guard✓ 142 scripts/ file(s)check:parse-guard✓ 141 scripts/ file(s)check:pnpm-filter-targets✓ 120/148 --filter occurrence(s)check:cross-package-test-inputsOK: 16 package(s) read outside themselvescheck:nul-bytescheck-nul-bytes: OK (scanned 6519 text file(s))pnpm lint(whole repo)One declared narrowing: the full
check:i18nrun (the nine-package extract,which needs a built CLI) was not re-run at
d741c991d. It ran green at9d41b11e0, andscripts/check-i18n-bundles.mjsis byte-identical acrossthe two commits — blob
5e3f435fat both — withbare-root-worklist.mjstheonly file that changed between them. So that green still describes this tree;
re-running would have cost an 11.5-minute shared CLI rebuild for no new
information. Stated rather than silently skipped.
Generated by Claude Code
Generated by Claude Code