test(i18n-gate): pin the placeholder-spelling rule's two implementations to each other - #8028
Conversation
…ons to each other
objectui#3512's rule is implemented twice — `placeholderViolations` for the
copy tables and `check-i18n-call-site-keys.mjs` class 7 for the inline
`defaultValue` call-site arguments. The copies were byte-equivalent and each
named the other, but that is a promise kept by whoever remembers to read both,
not a mechanism, and two implementations of one contract drift the first time
one side learns a spelling or an exemption.
Merging them was measured first, as the card ordered, and both directions are
closed:
- `@object-ui/i18n` does not resolve from `scripts/`: `tsc -p
tsconfig.scripts.json` answers TS2307 and node answers ERR_MODULE_NOT_FOUND,
while the same probe resolves `@object-ui/test-support` clean. Its `exports`
also point at a `dist/` no fresh checkout has built.
- A shared `.mjs` under `scripts/` type-checks there and fails in
`packages/i18n`, whose `tsconfig.test.json` has no `allowJs` (TS7016 plus
cascading TS7006s) and is deliberately fenced.
So the duplication stays and a parity gate makes it honest: one corpus — both
self-tests' cases, a grammar matrix over every dialect and boundary, all 29,638
locale-pack string leaves and all 1,072 discovered defaults rows, 23,420
distinct inputs — through both implementations, failing on the input they
disagree on. The rule moves out of the `.test.ts` into a module beside it
because importing a test file registers its cases into the importer (measured:
a one-case probe reported nineteen).
No rule change: no new spelling, no new exemption, and `check:i18n-keys`'
output is byte-identical to its output on the merge base.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
✅ 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
|
|
Standing-down note — Generated by Claude Code |
…ifier `scripts/__tests__/scripts-type-check.test.ts` pins that no root file of `tsconfig.scripts.json`'s program names an `@object-ui/*` specifier — the premise that lets `ci.yml` run `pnpm type-check:scripts` in the cheap half of the job, above the build. The parity test spelled the defaults-table walk `@object-ui/test-support/defaults-table-scan` and failed it. Fixed at the import, not at the pin: the same module is reached as `../../packages/test-support/src/defaults-table-scan`, exactly as the locale packs already were. Same file (that package's `exports` maps the specifier to `./src/defaults-table-scan.ts`), same corpus, same floors. Worth recording what the pin is, because it is easy to misread as a build-need measurement: with no package `dist` on disk at all, `pnpm type-check:scripts` was green with the workspace specifier too — that package's `exports` point at source. The specifier cost nothing today and would have quietly retired the guarantee the next `@object-ui/*` import does cost. Neither `packages/i18n/src/locales` nor the defaults-table walk imports anything beyond node builtins and `typescript`, so nothing here needs a built declaration. Whole suite, not the two files the first round ran: `pnpm exec vitest run scripts/__tests__/` — 111 files, 3327 tests, green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
✅ 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
|
Fixes #7310
objectui#3512's placeholder-spelling rule is implemented twice. This does not
merge the two — measured first, both merge directions are closed — it makes the
duplication mechanical instead of a promise: one corpus through both
implementations, red on any disagreement.
Step 1, as the card ordered: can
packages/i18nbe imported fromscripts/?No — and both resolvers say so, which is why both were asked. The gate is a
.mjsexecuted by node and its pin tests are type-checked bytsconfig.scripts.json, so a shared module has to satisfy the type program ANDnode's resolver.
A probe file under
scripts/importing@object-ui/i18n, no path mappingadded:
Two controls, so the reading is about the package and not about the method:
tsc -p tsconfig.scripts.jsonnode@object-ui/i18n@object-ui/test-support/defaults-table-scan(same file, same config)So
moduleResolution: bundleris not the blocker.scripts/is not a workspacepackage, and
node_modules/@object-ui/at the root holds exactly one entry —test-support, the one the rootpackage.jsondeclares. Reaching@object-ui/i18ncosts a new root dependency, and itsexportsmap points at./dist/*, which a fresh checkout has not built — putting a build step in frontof a gate that deliberately needs none ("the
enkey set is READ FROM AST, notimported, so the gate needs no build").
This is the same answer PR #7309 recorded for
@object-ui/core, and its commentis the precedent for not answering it with a path mapping: "
tsconfig.scripts.jsonhas no path mapping into the workspace packages, and adding one to type ONE line
would put every
scripts/file on a different module resolution than the one CItype-checks them with."
Step 2: route (b) was tried, and it fails a gate
The card's next route is the shared module under
scripts/, with thepackages/i18ntest importing it. It was implemented, not reasoned about: therule moved to
scripts/placeholder-spelling.mjs, the gate imported it andcheck:i18n-keysproduced byte-identical output, the i18n suite went greenat 1031 tests — and then:
packages/i18n/tsconfig.test.jsonhas noallowJs, and it is deliberatelyfenced:
paths: {}so workspace specifiers resolve through built declarationsrather than pulling source in, an explicit
typeslist,includelimited tothis package's own tests. The two ways out are a hand-written
.d.mtsbesidethe
.mjs— "a second source of truth, free to drift silently", rejected inthis repo on objectui#3494 — or widening that config to admit one CI script.
Widening the consumer to accept what the producer cannot supply is the shape
this repo's rules refuse, so route (b) was reverted rather than shipped.
Step 3: route (c), which the triage rated best value-for-cost
scripts/__tests__/placeholder-spelling-parity.test.tsfeeds ONE corpus throughboth implementations and fails naming the input they disagreed on and each
side's verdict.
The rule had to come out of the
.test.tsfirst, and the reason is measuredrather than stylistic: a probe file declaring one test that imports
fallback-placeholder-spelling-3512.test.tsreported 19 passed — vitestregisters each
describeinto whichever file is collecting when the moduleevaluates, so importing the suite drags its cases into the importer, and under
the
unitproject'sisolate: falseshared module graph which file they land inis worker-order dependent. So
placeholderViolationsmoved verbatim intopackages/i18n/src/__tests__/placeholder-spelling-rule.ts; the suite imports itback, and every one of its cases is unchanged.
The corpus, and its floors
GRAMMAR_MATRIX@object-ui/test-support's walk — the same one objectui#4401 usesEach part carries a floor (
> 25000leaves,> 700rows,>= 37matrixentries,
unreadableempty) so it cannot silently shrink to nothing while theagreement assertion goes on passing — the objectui#3009 shape. A positive
control asserts at least 20 inputs actually reach a verdict and that all seven
of the rule's reasons appear among them, and one case asserts the two imported
functions are not the same object, which is the only way this gate could pass
while checking nothing.
The gate's own population (inline
defaultValuetext) is not walked here — thatwould mean running the node gate's TypeScript pass inside a unit test. It is
reached transitively (
check:i18n-keysreports 993 of 996 literal inlinedefaults byte-equal to their
envalue, and everyenvalue is in the corpus)and the residue is recorded rather than claimed as covered.
Equivalence before, and after
Before either copy was touched, both were run over that corpus and the outputs
compared as JSON. After route (b) was reverted and route (c) landed, the same
corpus was replayed:
23,420 inputs, one md5. 25 of them are flagged, all from the matrix — the real
corpus is clean today, which is what the two gates are for.
Reverse verification: predicted, then run, both directions
Predicted before running: the parity gate reds naming the input, while both
existing self-tests and the full
check:i18n-keysrun stay green — a divergenceinvisible to everything except the new gate. A spelling neither self-test pins
was chosen deliberately (a hyphenated hole name), by widening
CANONICAL_HOLE_NAMEto/^[A-Za-z0-9_-]+$/on ONE side.check-i18n-call-site-keys.test.ts(130 cases)fallback-placeholder-spelling-3512.test.ts(18 cases)pnpm check:i18n-keys(full run)placeholder-spelling-parity.test.tsThe failure, from the first leg:
{{-name}}was not predicted and is worth reading twice: one edit to acharacter class silently retired the unescape-prefix dialect in its UNSPACED
form, which the i18n suite pins only as
{{- name}}. That is the drift classthis card was filed about, reproduced, and caught only here. The mirrored leg
prints the same two inputs with the verdicts on the other side.
Restores are proven by state, not by exit code:
git hash-objectback to theHEADblob,git diff HEADempty,git statusclean, and the parity gate greenagain. The first mutation attempt did not land on disk (a
perl -0777quotingmiss); the harness's own hash comparison refused it as a no-op rather than
reporting a result, and it was redone.
Boundaries held
today.
pnpm check:i18n-keysoutput is byte-identical to its output on4bb33d7, counter for counter (diffclean) — including "Inline defaultspelling: 1031 default(s) … 38 … 25".
i18next-only spellings plus both out-of-range classes in the i18n suite, the
class-7 cases in
scripts/__tests__/.__tests__/, excluded from this package'sbuild by directory —
check:published-tsconfig-excludegreen, nothing reachesdist.Gates
Green at
77de761:check:i18n-keys(byte-identical to base) ·check:i18n-drift·check:i18n-dead-keys(report) ·vitest scripts/__tests__/check-i18n-call-site-keys.test.tsvitest packages/i18n/(62 files, 1031) ·type-check:scripts·--filter @object-ui/i18n type-checkandlint·lint:root(whole scope, 0 errors) ·check:entry-guard·check:esm-specifiers·check:control-bytes+ agrep -naPself-scan ·check:published-tsconfig-exclude·check-changeset-presence·check-governed-queue-guard --teston all five paths(NOT GOVERNED).
The i18n type-check is proven non-vacuous rather than assumed:
tsc -p tsconfig.test.json --listFileslistsplaceholder-spelling-rule.tsamong its653 program inputs.
One declared narrowing.
node scripts/check-node-esm-load.mjs(the full loadleg) exits 1 here for a reason that is not this diff: it builds every published
package, turbo shares one cache across worktrees, and its provenance leg refuses
to grade artifacts another tree produced — it named 8 entries coming from
objectui-issue-5174-b20andobjectui-issue-7834. Zero load-leg findings onthe 29 it did grade.
--force-buildwould rebuild 43 packages and repopulatethat shared cache underneath the agents using it, so it was not run; the cheap
--specifiers-onlyleg is green over the whole repository, and this diff reachesno published artifact (
scripts/ships in no package,packages/i18n's__tests__/is excluded from its build).Patch round —
589fd4bCI
Test (shard 4/4)was red on77de761, in this diff:scripts/__tests__/scripts-type-check.test.tspins that no root file oftsconfig.scripts.json's program names an@object-ui/*specifier — thepremise that lets
ci.ymlrunpnpm type-check:scriptsin the cheap half ofthe job, above the build. The parity test spelled the defaults-table walk
@object-ui/test-support/defaults-table-scan, and the pin's AST walk reportedexactly that one specifier. The relative
packages/i18n/src/localesimport onthe line above was not counted — the matcher filters on
startsWith('@object-ui/')— and correctly so: a relative import compiles thepackage's source directly and needs nothing built.
Fixed at the import, not at the pin: the same module is now reached as
../../packages/test-support/src/defaults-table-scan, which is the very filethat package's
exportsmaps the specifier to. Same corpus, same floors, samecounts. The pin is green (15/15), and so is the whole directory the first round
should have run:
pnpm exec vitest run scripts/__tests__/— 111 files, 3327tests.
One correction to the record, because it is easy to misread this pin as a
build-need measurement: with no package
distdirectory on disk at all,pnpm type-check:scriptswas green with the workspace specifier too, because@object-ui/test-support'sexportspoint at./src/*.tsrather than a buildoutput. The specifier cost nothing today; what it did was quietly retire the
guarantee that the next
@object-ui/*import — one that does need a build —would be caught. That is exactly what a premise pin is for, and it did its job.
Neither
packages/i18n/src/localesnor the defaults-table walk imports anythingbeyond node builtins and
typescript, so no part of this test needs a builtdeclaration.
(A second, self-inflicted lesson landed in the same round: the first draft of
the explanatory comment above contained a glob ending in a star-slash, which
closes a block comment.
tscand the pin both read the file fine — the pinwalks it as text — and only the vitest transform caught it. It is the same trap
tsconfig.scripts.json's own header records, and it is why the whole-directoryrun is the one that counts.)
Generated by Claude Code