i18n(gate): widen the defaults value-compare over the declared hand-rolled tables - #8101
Conversation
…and-rolled tables `scripts/check-i18n-call-site-keys.mjs`'s class 8 compared the factory-resolved population only: every `createSafeTranslation` / `createSafeTranslationHook` invocation, read from source with no registry. Two tables hand-roll `fallbackT` instead of taking the factory (`GANTT_DEFAULT_TRANSLATIONS`, `IMPORT_DEFAULT_TRANSLATIONS`), so nothing in their source says they are defaults tables and that walk cannot reach them. Covering them takes a declaration, which is why objectui#7567 shipped the registry-free half first and split this out. The declaration is the one that already exists — `HAND_ROLLED_TABLES` in `packages/test-support` — and it is now ONE list rather than two pinned copies: the data moves to `hand-rolled-tables.json` behind an `exports` subpath, the shape objectui#6923 ruled for data crossing the `.mjs` / TypeScript boundary (`zod-wrapper-keys` is its first instance). The gate reads those bytes through `createRequire`; `defaults-table-scan.ts` re-exports them typed, and keeps the prose JSON cannot carry. The widened half keeps its own counters, its own collapse floor (`handRolledComparedRows < 150` against a measured 215, chosen to sit above the LARGER of the two tables so losing either one fails rather than shrinking quietly) and its own summary line with its abstention counts. `factoryComparedRows < 500` is untouched. A table that is in the registry AND reachable from the factory — `TIMELINE_DEFAULT_TRANSLATIONS` — is de-duplicated on the literal's own position and reported as such, so neither census double-counts it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
…hat ask about main `analyze(repoRoot)` parses ~1600 files, and seven cases in this file each paid for their own walk. That held until this card added two more and pushed `main carries no factory drift` past vitest's 15s window under a full `pnpm exec vitest run scripts/__tests__/` — measured, not predicted: that case does two full-repo walks of its own. AGENTS.md 测试纪律 names this exact shape (an unbounded workload counted inside a bounded one) and prescribes moving the cost into the import phase rather than raising the timeout, so the run is computed once at module scope. Cases that inject their own registry keep their own runs, because they are asking a different question. Measured either side, same 112 files / 3348 tests: 219.72s -> 113.76s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
…d-rolled-fallback-tables-registry
|
Standing down on Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Generated by Claude Code |
|
Re-run issued: run 34042814334 completed ( Generated by Claude Code |
Fixes #7877
The B half of the ruling on objectui#7567 Q2. PR objectui#7870 landed the
createSafeTranslationvalue-compare over the factory-resolved population —every factory invocation, read from source with no registry. Two tables
hand-roll
fallbackTinstead of taking the factory, so nothing in their sourcesays they are defaults tables and that walk cannot reach them. Covering them
takes a declaration, which is why A went first and B was split out.
Head this was measured on:
924edd915.The declaration: ONE list, not two pinned copies
HAND_ROLLED_TABLESalready existed, inpackages/test-support(it moved therefrom the objectui#3512 test in objectui#7884). The gate is a bare
node scripts/check-*.mjsand cannot import it:exports["./defaults-table-scan"]resolves to TypeScript source with no build artefact.
That wall already has a ruling — objectui#6923, whose first instance is
zod-wrapper-keys.jsonin the same package, read by two other.mjsgates inscripts/. Same shape here: the DATA moves tohand-rolled-tables.jsonbehindits own
exportssubpath,resolveJsonModuletypes it for TypeScript,createRequirereads the same bytes fornode, anddefaults-table-scan.tskeeps the prose JSON cannot carry.
The alternative — a second literal list in the gate, pinned to this one by a test
(the objectui#7310 / PR objectui#8028 shape) — was available and deliberately
not taken: two lists can disagree for the window between one edit and the next
pin run, and the failure mode of a scan population that quietly loses an entry is
"0 drifted" over nothing. A shared declaration was measurable here, so it wins.
⛔ Option C on objectui#7567 (pattern-matching identifier names, or every
Recordof strings that looks like a defaults map) is not built and is named asrejected in both the gate header and the new pin block.
The census, re-measured (not carried forward from the card)
Per table, measured by dropping one registry entry at a time (below):
GANTT_DEFAULT_TRANSLATIONS80,IMPORT_DEFAULT_TRANSLATIONS135. That matchesthe card's 2026-09-06 baseline exactly. Both read 0 drifted, so the widened
half ships blocking in this PR rather than report-only.
(
timeline.relative.*). Measured today that count is 0 — those rows wereretired in objectui#7887. The factory half's abstention counts are all zero on
this tree, and so are the widened half's. See the out-of-scope note at the end.
TIMELINE_DEFAULT_TRANSLATIONSis in the registry (it mirrors the needle-fileset objectui#3512 pins) and reaches the factory, so it is de-duplicated on
the literal's own position and reported as
1 already covered by the factory walkrather than counted twice.The collapse guard is not weakened
factoryComparedRows < 500at its original site is untouched. The widened halfgets its own counter and its own floor,
handRolledComparedRows < 150.How 150 was derived rather than picked: the two reachable-nowhere-else tables
compare 80 and 135 rows. The floor sits above the larger of the two, so
losing either table collapses the scan into a failure — not only losing both.
A floor below 135 would let the bigger table carry the smaller one's
disappearance. Demonstrated, one entry dropped at a time:
Both legs restored under a
trap, verified by blob hash (git diff HEAD= 0bytes both times).
(a) Positive control on the widened half
A real drift planted on disk in
GANTT_DEFAULT_TRANSLATIONS— a table thefactory walk structurally cannot see. On-disk mutation proved by counting the
removed and injected text before and after (1/0, then 0/1) and by the blob hash
moving
33fe2321… -> a6a2fcec…:Restore proved by blob hash back to
33fe2321…andgit diff HEAD= 0 bytes.The finding carries its own reason,
hand-rolled-default-drift, with its ownhint — the two halves are reached differently and the fix for one names a
registry the other does not have.
(b) The staleness reading on the registry — demonstrated, not asserted
A declared list rots when a table is added and not declared. What catches
that is not diligence and does not live in this gate:
packages/i18n/src/__tests__/fallback-placeholder-spelling-3512.test.tsassertsthe set of runtime files carrying the hand-rolled needle equals a pinned list.
Because the gate now reads the same bytes that test reads, that one ratchet
covers both readers.
Shown by planting an undeclared fourth hand-rolled
fallbackT:Removed; restored run is
18 passed (18), exit 0.The other direction — an entry that names a file or a
constno longer there —is covered here: a declaration that does not resolve is counted as an unreadable
TABLE, never skipped, and pinned both synthetically and against
main.(c) The abstention counts are printed
The widened half prints its own summary line in the shape objectui#7870 prints
the factory half's, including all four abstention buckets. objectui#7874 exists
only because the factory half printed its blind spot instead of absorbing it.
Also in this PR: one shared full-repo
analyze()run in the pin fileanalyze(repoRoot)parses about 1600 files. Seven cases inscripts/__tests__/check-i18n-call-site-keys.test.tseach paid for their ownwalk; the two new cases here took that to nine and pushed the slowest case past
vitest's 15s window under a full
scripts/__tests__/run. AGENTS.md 测试纪律names that shape and prescribes moving the cost into the import phase rather than
raising the timeout, so the default-options run is now computed once at module
scope. Cases that inject their own registry keep their own runs.
Measured either side, same 112 files / 3348 tests: 219.72s to 113.76s. This
is the same failure objectui#8074 reports; that card is named here without a
closing keyword because this is a measured mitigation of its instance, not
necessarily the whole class it names — objectui#8074 stays open for a maintainer
to judge.
Gates, all on
924edd915pnpm check:i18n-keyspnpm exec vitest run scripts/__tests__/check-i18n-call-site-keys.test.tspnpm exec vitest run scripts/__tests__/packages/test-support/+ the 3512 test + the 4401 mirror testpnpm --filter @object-ui/test-support type-check/lintpnpm type-check:scriptspnpm lint:rootpnpm check:i18n-drift·check:i18n-dead-keys·check:control-bytesnode scripts/check-changeset-presence.mjsnode scripts/check-governed-queue-guard.mjs --test(5 paths)Every exit code captured by redirect-then-capture, never across a pipe.
Changeset, clause ② confirmed rather than assumed: the checker reports
0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 3 under a package changesets ignores.@object-ui/test-supportisprivate: trueand changesets ignores it;scripts/publishes nothing. No published contract's accept/reject behaviour changes — a
gate's scan population widened.
CI invocation unchanged:
ci.yml:487runspnpm check:i18n-keys, the samescript name, untouched. Every reader of the gate found by
git grep -l check-i18n-call-site-keysoverscripts/__tests__,.github/workflowsandpackages/i18nwas run.mainmoved under this branch while it was in flight (PRs objectui#8081,objectui#8082, objectui#8087 and two others);
origin/mainwas merged in andeverything above was re-run on the merged head.
Out of scope, filed separately
The class-8 header and one pin comment still say
timeline.relative.*is fiveabstaining rows on
main; measured today it is zero. Not touched here — filed asa
findingrather than ridden in on this PR.Back-links, none with a closing keyword: objectui#7567 · objectui#7870 ·
objectui#3512 · objectui#7874 · objectui#7454 · objectui#7574 · objectui#8028 ·
objectui#6923 · objectui#7884 · objectui#7887 · objectui#8074.
Drafted by the
domain:devx @ objectuiseat, sessionsession_01FhBNJcLRZLe8M87VcUgpKr, round R46.🤖 Generated with Claude Code
https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Generated by Claude Code