Skip to content

fix(scripts): anchor check-i18n-coverage to a module-derived root and refuse an empty population - #11397

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-10907-i18n-coverage-root-anchoring
Aug 23, 2026
Merged

fix(scripts): anchor check-i18n-coverage to a module-derived root and refuse an empty population#11397
os-steve merged 2 commits into
mainfrom
claude/issue-10907-i18n-coverage-root-anchoring

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#10907

scripts/check-i18n-coverage.mjs resolved every path CWD-relatively — examples, packages, the baseline, and the CLI stub it spawns. Run from anywhere but the repo root it discovered no configs, compared nothing, and printed the same sentence and the same exit code a real pass uses.

The interlock that made it silent

The card names the symptom; measuring it named the mechanism. This gate is a two-sided ratchet, so a config that vanishes is normally caught by the DOWN direction (baselined config is gone). That interlock is exactly what a wrong root defeats: the population and the baseline are resolved the same way, so they go empty together and the comparison is left with nothing to disagree about.

That is also why anchoring only one side would not have been a fix. Anchor the baseline alone and you get 12 spurious not baselined errors; anchor the population alone and you get 12 spurious config is gone errors. Only anchoring both restores a verdict, and only refusing the empty case keeps green over nothing unreachable by routes the anchoring does not know about.

What changed

Half 1 — every read anchored to a module-derived root.HERE / REPO_ROOT from import.meta.url, the spelling check-skills-token-ratchet.mjs and check-ratchet-remedy-authority.mjs use.

The repo-relative spellings stay, deliberately, and this is the one place the shape departs from a literal reading of the card's join(ROOT, 'examples'). Those strings are the committed baseline's keys — making them absolute would silently re-key all twelve entries and turn the next run into 12 not baselined plus 12 config is gone errors. They are also the paths in every message and the commands rerunFix tells a reader to run. So repo-relative is the file's vocabulary and a single seam, at(rel), is the only place one becomes a path on disk. The os lint spawn gets cwd: REPO_ROOT, which is what resolves the repo-relative CLI and config path it is handed; the prerequisite probe's existsSync is anchored too, since unanchored it could report "the workspace CLI is not built" about a CLI that is built.

Half 2 — an empty population is refused, not returned. Zero is a broken scan, not a repo with nothing to translate — the rule trackedFiles states in scripts/pm/dispatch-gates.mjs, which this gate was the counter-example to.

Judged on the union, not per half. A single vanished config is already this gate's business: the ratchet reports it as a DOWN and prescribes --update. Refusing on an empty half would preempt that legitimate path — retiring the last example is a real event, not a broken scan. Only a total wipe is indistinguishable from a scan that read nothing.

Placed before any CLI is spawned and before --update can write. That ordering is load-bearing: --update runs before any comparison, so over an empty population it would have written {} and ratcheted all twelve baselined configs out of existence — 602 frozen strings of real debt discarded by the command whose purpose is to record it.

Evidence — the pre-fix vacuous green, and the delta

All four runs below are on one fully built worktree, and the two pre-fix runs are the real origin/main file restored into it (git hash-object = 8c88cf0e, byte-identical to origin/main:scripts/check-i18n-coverage.mjs; REPO_ROOT and emptyPopulationVerdict both grep to 0 occurrences, confirming the revert landed on disk). Restored afterwards to 54e40b88, byte-identical to the commit, tree clean.

cwdpre-fixpost-fix
repo rootOK (12 config(s), 602 baselined untranslated string(s), none new) · exit 0OK (12 config(s), 602 baselined untranslated string(s), none new) · exit 0
/tmpOK (0 config(s), 0 baselined untranslated string(s), none new) · exit 0OK (12 config(s), 602 baselined untranslated string(s), none new) · exit 0

The bolded cell is the defect: same tree, same second, a clean pass reported over a scan of nothing — and indistinguishable from the row above it. Post-fix the off-root run returns the identical verdict to the root run, which is what "correctly resolves off-root" means here. The root verdict is unchanged in both directions, so the CI invocation is provably unaffected.

Two further pre-fix readings, on the unbuilt tree, show the same asymmetry from the other side — the root run refused (PREREQUISITE NOT MET, exit 1) while /tmp and packages/cli both reported OK (0 config(s)), exit 0. The off-root run did not merely fail to check; it contradicted the root run's verdict about the same tree.

Evidence — non-vacuity, pinned in the self-test

The card's irony is that a test which passes without proving the population is non-empty reproduces the defect. So the proof is in the gate's own --self-test, which CI runs:

  • the root derivation lands on this repo's root (pinned against files only the root has, this gate's own two included — a depth error would still find a scripts/);
  • discovery chdirs out of the repo and asserts it still resolves a non-empty population, that it is the same population the root resolves, and that it is spelled repo-relative. This one assertion is the defect: before the fix it returns [];
  • the empty-population classifier is pinned red on nothing and silent on real work — including silent on the live 12-config population, so it cannot preempt the ratchet.

The self-test's own summary line now carries the count, so a reader sees the population rather than inferring it:

✓ check:i18n-coverage --self-test — … the population resolves to 12 config(s)
from outside the repo root as well as inside it, and an empty one is refused
rather than reported OK.

Independently: the discovered root population is exactly the 12 baseline keys — 0 not-baselined, 0 gone — so the population side of the ratchet has nothing to report and only the per-config counts (produced by os lint, untouched here) can move it.

Gates — union re-run at 0a4df7c on a clean tree

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack against the actual changed path, not recalled. Each line below is the gate's own verdict, not a shell $?.

  • node scripts/pm/bare-root-worklist.mjs --self-testOK self-test: 34 live row(s), 31 unreachable as spelled, 31 recorded verdict(s) — none stale, none missing. (red before the verdict row, green after — see the patch round below)
  • pnpm check:i18n-coverageOK (12 config(s), 602 baselined untranslated string(s), none new). (self-test half green first)
  • pnpm check:pm-dispatch-gates✓ dispatch-gates self-test: 557 cases pass. (run because the patch round edits scripts/pm/)
  • pnpm check:entry-guard✓ 138 scripts/ file(s) — every entry guard goes through invoked-as.mjs
  • pnpm check:parse-guard✓ 137 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
  • pnpm check:cross-package-test-inputsOK: 14 package(s) read outside themselves, all declared
  • pnpm check:pnpm-filter-targets✓ 120/148 --filter occurrence(s) across 25 file(s) resolve
  • node scripts/check-ci-filter-parity.mjsOK: all 88 declared cross-package glob(s) (76 unique) are covered
  • pnpm check:nul-bytesOK (scanned 6381 text file(s) … no raw ASCII control bytes), plus an independent control-character grep over the changed file (no match)
  • pnpm lint — full repo, eslint . --no-inline-config, exit 0 in 90s. Not narrowed.

Scope held

check-skills-token-ratchet.mjs, check-ratchet-remedy-authority.mjs and scripts/pm/dispatch-gates.mjs were read as models and not edited. Two files changed — the gate itself, and one recorded verdict in scripts/pm/bare-root-worklist.mjs (see the patch round below).

Three findings were measured and filed rather than folded in:

Patch round 1 — the verdict PACKAGES_DIR owes

Lint & Repo Gates failed on f5fe23b at node scripts/pm/bare-root-worklist.mjs --self-test, with FRESH: check:i18n-coverage PACKAGES_DIR packages.

Naming the previously anonymous 'packages' default parameter PACKAGES_DIR made it match that sweep's POPULATION_CONSTANT, so the row appeared for the first time and demanded a verdict. The invisibility is not new — the literal was always a bare single-segment word the derivation cannot build a hint from. It was simply unnameable by the sweep too, so nothing recorded it. The row now records a population that was already there; the scope expansion is what made it recordable.

Verdict: REFUSE-UNSPELLABLE, measured.discoverPackages admits files named i18n-extract.config.ts beneath a scripts segment — 9 of 5035 tracked files under the root (0.18%), the narrowest row on the whole list. That is a FILENAME filter, and collapseHint can only ever name a subtree, so the one spellable declaration would name this gate for 5035 files to reach 9 — the costlier error hintCovers prices, and the same grounds its EXAMPLES_DIR examples sibling was already refused on at 1.3%.

The refusal is also recorded beside the constants, as check-driver-conformance.mjs and check-examples-live-imports.mjs do, so the next reader meets the reasoning at the population rather than only in the triage table.

The gate is not weakened, narrowed or skipped, and the scope expansion that surfaced the row stands. This round's change to check-i18n-coverage.mjs is provably comment-only: with comments masked and blank lines removed its executable content is byte-identical to f5fe23b (23418 == 23418 bytes), the only non-comment change being one added blank line — so the full green run recorded above still describes the code that ships.

No changeset: this is a repo-internal gate script and publishes nothing. skip-changeset applied.


Generated by Claude Code


Generated by Claude Code

… refuse an empty population
`scripts/check-i18n-coverage.mjs` resolved every path CWD-relatively — `examples`,
`packages`, the baseline, and the CLI stub it spawns. Run from anywhere but the repo
root it discovered no configs, compared nothing, and printed
`OK (0 config(s), 0 baselined untranslated string(s), none new)` with exit 0 — the
same sentence and the same exit code a real pass uses.
What made that silent rather than merely wrong is an interlock: this is a two-sided
ratchet, so a config that vanishes is normally caught by the DOWN direction. But the
population and the baseline were resolved the same way, so a wrong root emptied both
together and left the comparison with nothing to disagree about.
Two halves:
1. Every read is anchored to a root derived from `import.meta.url`, as
`check-skills-token-ratchet.mjs` and `check-ratchet-remedy-authority.mjs` do. The
repo-relative spellings stay — they are the committed baseline's KEYS and the text
a reader acts on — and `at()` is the one seam between the two. The `os lint` spawn
gets `cwd: REPO_ROOT`, which is what resolves the repo-relative `CLI` and config
paths it is handed.
2. An empty population is refused rather than returned: zero is a broken scan, not a
repo with nothing to translate, the rule `trackedFiles` states in
`scripts/pm/dispatch-gates.mjs`. Judged on the union, not per half, so it cannot
preempt the legitimate ratchet-DOWN path when a single config is retired. It is
placed before any CLI is spawned and before `--update` can write, which over an
empty population would have discarded all twelve baselined entries.
The self-test gains the non-vacuity proof this gate could not previously give: it
chdirs out of the repo and asserts discovery still resolves the same 12 configs,
spelled repo-relative, and pins the empty-population classifier red on nothing and
silent on real work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
…-species worklist
`node scripts/pm/bare-root-worklist.mjs --self-test` went red on f5fe23b:
FRESH: check:i18n-coverage PACKAGES_DIR packages.
Naming the previously anonymous `'packages'` default parameter `PACKAGES_DIR`
made it match `POPULATION_CONSTANT`, so the sweep saw it for the first time and
demanded a verdict. The invisibility is not new — the literal was always a bare
single-segment word the dispatch derivation cannot build a hint from — but it
was previously unnameable by the sweep too, so nothing recorded it. The row now
records a population that was already there.
Verdict: REFUSE-UNSPELLABLE, measured. `discoverPackages` admits files named
`i18n-extract.config.ts` beneath a `scripts` segment — 9 of 5035 tracked files
under the root (0.18%), the narrowest row on the list. That is a FILENAME
filter, and `collapseHint` can only ever name a whole subtree, so the sole
spellable declaration would name this gate for 5035 files to reach 9 — the
costlier error `hintCovers` prices, and the same shape its `EXAMPLES_DIR examples`
sibling was already refused for at 1.3%.
Also records the refusal beside the constants themselves, as
`check-driver-conformance.mjs` and `check-examples-live-imports.mjs` do, so the
next reader meets the reasoning at the population rather than only in the triage.
The gate is not weakened, narrowed or skipped, and the scope expansion that
surfaced the row stands. Executable content of `check-i18n-coverage.mjs` is
byte-identical to f5fe23b with comments masked and blank lines removed
(23418 == 23418); the only non-comment change is one added blank line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@os-steve
os-steve marked this pull request as ready for review August 23, 2026 16:14
@os-steve
os-steve added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 35c1ca3Aug 23, 2026
32 checks passed
@os-steve
os-steve deleted the claude/issue-10907-i18n-coverage-root-anchoring branch August 23, 2026 16:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] check:i18n-coverage resolves its population CWD-relatively, so an off-root run reports OK over a scan of nothing

2 participants

@os-steve@claude