Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): anchor check-i18n-coverage to a module-derived root and refuse an empty population - #11397
Merged
Merged
Conversation
… 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
This was referenced Aug 23, 2026
…-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
marked this pull request as ready for review
August 23, 2026 16:14
This was referenced Aug 23, 2026
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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#10907
scripts/check-i18n-coverage.mjsresolved 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 baselinederrors; anchor the population alone and you get 12 spuriousconfig is goneerrors. Only anchoring both restores a verdict, and only refusing the empty case keepsgreen over nothingunreachable by routes the anchoring does not know about.What changed
Half 1 — every read anchored to a module-derived root.
HERE/REPO_ROOTfromimport.meta.url, the spellingcheck-skills-token-ratchet.mjsandcheck-ratchet-remedy-authority.mjsuse.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 12not baselinedplus 12config is goneerrors. They are also the paths in every message and the commandsrerunFixtells 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. Theos lintspawn getscwd: REPO_ROOT, which is what resolves the repo-relativeCLIand config path it is handed; the prerequisite probe'sexistsSyncis 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
trackedFilesstates inscripts/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
--updatecan write. That ordering is load-bearing:--updateruns 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/mainfile restored into it (git hash-object=8c88cf0e, byte-identical toorigin/main:scripts/check-i18n-coverage.mjs;REPO_ROOTandemptyPopulationVerdictboth grep to 0 occurrences, confirming the revert landed on disk). Restored afterwards to54e40b88, byte-identical to the commit, tree clean.OK (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 0The 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/tmpandpackages/cliboth reportedOK (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:scripts/);[];The self-test's own summary line now carries the count, so a reader sees the population rather than inferring it:
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
0a4df7con a clean treeDerived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackagainst 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-test—OK 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-coverage—OK (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 editsscripts/pm/)pnpm check:entry-guard—✓ 138 scripts/ file(s) — every entry guard goes through invoked-as.mjspnpm check:parse-guard—✓ 137 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.pnpm check:cross-package-test-inputs—OK: 14 package(s) read outside themselves, all declaredpnpm check:pnpm-filter-targets—✓ 120/148 --filter occurrence(s) across 25 file(s) resolvenode scripts/check-ci-filter-parity.mjs—OK: all 88 declared cross-package glob(s) (76 unique) are coveredpnpm check:nul-bytes—OK (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.mjsandscripts/pm/dispatch-gates.mjswere read as models and not edited. Two files changed — the gate itself, and one recorded verdict inscripts/pm/bare-root-worklist.mjs(see the patch round below).Three findings were measured and filed rather than folded in:
scripts/cli-build-prerequisite.mjsexportsCLI/CLI_PKGas CWD-relative constants, so its probe still defers off-root. This is the residualcould not read packages/cli/package.json (ENOENT …)note above the now-correct off-root verdict. Same defect class, but the module is shared withcheck-i18n-bundles.mjs, so fixing it moves a second gate under a card that has not measured it.groupFailuresByCausekeys on[reason, fix], and the fallback branch'sfixembeds the config path, so one shared cause reports as N causes — the check-i18n-bundles 在工作区未构建时把「CLI 没 build」报成 9 个包各自的 bundle 问题 #5217 shape the grouping exists to prevent. Measured as12 of 12 config(s) failed to lint (12 distinct causes)for one environment fact.check-prefix - five required-lane steps, including the one that shipped a red on PR #11397, are absent from the family universe entirely #11404 —dispatch-gatesnever discovers a gate whose script lacks acheck-prefix. The gate that reds below is absent from all 139 families, not merely unmatched; five required-lane steps share the shape. Measured on this PR.Patch round 1 — the verdict
PACKAGES_DIRowesLint & Repo Gatesfailed onf5fe23batnode scripts/pm/bare-root-worklist.mjs --self-test, withFRESH: check:i18n-coverage PACKAGES_DIR packages.Naming the previously anonymous
'packages'default parameterPACKAGES_DIRmade it match that sweep'sPOPULATION_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.discoverPackagesadmits files namedi18n-extract.config.tsbeneath ascriptssegment — 9 of 5035 tracked files under the root (0.18%), the narrowest row on the whole list. That is a FILENAME filter, andcollapseHintcan only ever name a subtree, so the one spellable declaration would name this gate for 5035 files to reach 9 — the costlier errorhintCoversprices, and the same grounds itsEXAMPLES_DIR examplessibling was already refused on at 1.3%.The refusal is also recorded beside the constants, as
check-driver-conformance.mjsandcheck-examples-live-imports.mjsdo, 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.mjsis provably comment-only: with comments masked and blank lines removed its executable content is byte-identical tof5fe23b(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-changesetapplied.Generated by Claude Code
Generated by Claude Code