Skip to content

feat(scripts): value-compare createSafeTranslation defaults tables against the en pack (objectui#7567) - #7870

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-7567-i18n-factory-defaults-value-compare
Sep 6, 2026
Merged

feat(scripts): value-compare createSafeTranslation defaults tables against the en pack (objectui#7567)#7870
os-sam merged 2 commits into
mainfrom
claude/issue-7567-i18n-factory-defaults-value-compare

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7567

check-i18n-call-site-keys already held one flavour of provider-less English to the en pack and never read the other:

channel value-compared against en before this PR
t(key, { defaultValue }) inline literal yes — 996 literal, 993 matching, per the gate's own summary
createSafeTranslation(DEFAULT_TRANSLATIONS, …) table row no — the hook was classified pack-backed, its keys were checked, its table was never read

They are the same promise one indirection apart. packages/i18n/src/useSafeTranslation.ts's fallbackT resolves defaults[key] || defaultValue || key, and its own comment calls the table "the pack value's stand-in here … it takes the pack's position in i18next's own order". So a row that differs from en is the same control labelled one way on a provider-less host and another in the console — #7454's measured instance.

This adds class 8, factory-default-drift.

Which branch of the ruling this landed on, and on what evidence

Neither of the two the dispatch anticipated. The ruling was: one known instance ⇒ blocking + a named ledger citing #7454; more than one ⇒ report-only. The census reports zero, so it landed blocking with no ledger at all.

The reason is dated: #7454's instance landed on 2026-09-04 as b8fc1e2bf ("fix(plugin-calendar): align the calendar.allDay default to the pack and drop its dead ternary", #7574), two days before this card was dispatched. packages/plugin-calendar/src/CalendarView.tsx:84 now reads 'calendar.allDay': 'All Day', byte-equal to en, and CalendarView.allDaySpelling-7454.test.tsx pins it. A ledger built for that entry would have been empty on the day it shipped, and an empty ledger reads exactly like a populated one — so there is no ledger section. That is also the gate's own precedent for classes 3, 4, 5 and 7, each of which is stated to be hard from day one because its first full run found no debt for a ratchet to hold.

⚠️ This is a deviation from both written branches, driven by a reading rather than a preference. If the intent was that a ledger exists regardless, say so and it is a small follow-up.

The census — the comparison surface, not just the hit count

Measured with a scratch instrument before the rule was written, then reproduced by the shipped rule (identical numbers):

Factory defaults tables: 32 createSafeTranslation site(s) over 32 distinct table(s) (0 unreadable)
  — 841 row(s) compared against their en value, 841 matching,
    5 on a key en does not define, 0 with no single comparable en value, 0 unreadable.
measure count note
createSafeTranslation / createSafeTranslationHook invocations 32 the population, read from source, not a registry
distinct tables behind them 32 one table handed to two factories is scanned once
rows 846
compared against en 841
matching 841 drift: 0
abstained — key en does not define 5 all timeline.relative.*
abstained — no single comparable en value 0 plural families, unreadable leaves
abstained — unreadable row (computed key, non-static value, nested literal) 0
unreadable TABLE (the loud one — all its rows leave the surface) 0

The blind spot, sized rather than implied. The abstentions above total 5 rows out of 846 (0.6%), and all five are one shape: timeline.relative.today/tomorrow/yesterday/inDays/daysAgo in packages/plugin-timeline/src/useTimelineTranslation.ts. No en leaf and — checked repo-wide including tests — no caller anywhere. Class 1 cannot reach them either, because it judges call sites and these have none. Filed separately rather than touched here.

What this class does not reach, measured rather than assumed. Three tables hand-roll fallbackT instead of taking the factory (HAND_ROLLED_TABLES in packages/i18n/src/__tests__/fallback-placeholder-spelling-3512.test.ts). TIMELINE_DEFAULT_TRANSLATIONS also reaches the factory and is covered; the other two are not. They were measured while writing this: GANTT_DEFAULT_TRANSLATIONS 80 rows / 80 compared / 0 drifted, IMPORT_DEFAULT_TRANSLATIONS 135 / 135 / 0. Reaching them needs a declared registry — nothing in the source says which local function is a fallbackT — which is a second thing to keep from rotting, so it is named in the header and left to a follow-up rather than folded in. The residue is real, named, and currently clean.

Controls

Positive control — on disk, #7454's exact drift reintroduced. 'calendar.allDay': 'All Day''all-day' in CalendarView.tsx. Mutation proven landed before running (anchor count flipped 1→0 for the pack spelling and 0→1 for the mutated one, blob hash moved 389b6e11…5d44b909…); restore was git checkout HEAD -- plus the absolute path inside a trap … EXIT INT TERM armed only after the implementation was committed, and proven by state afterwards (worktree hash back to 389b6e11… = the HEAD blob, anchors back to 1/0, git status --porcelain and git diff HEAD --stat both empty).

The gate exited 1 and said:

Factory defaults tables: 32 createSafeTranslation site(s) over 32 distinct table(s) (0 unreadable) — 841 row(s) compared against their en value, 840 matching, …
1 createSafeTranslation defaults row contradicts the en value of a key that EXISTS (1 distinct key) — that table stands in for the pack on a provider-less host, so this control is labelled one way there and another in the console:
  packages/plugin-calendar/src/CalendarView.tsx:84:3  [factory-default-drift]  calendar.allDay  (DEFAULT_TRANSLATIONS)
      en renders:      "All Day"
      defaults table:  "all-day"

Note the shape of the change: compared stayed 841, matching went 841 → 840. The row moved from matching into a finding rather than the surface moving — which is what says the instrument was already looking at that row.

⛔ No ablation rebuild leg, and that is a statement about the resolution path rather than an omission: the subject is a .mjs gate run directly by node, and it reads the mutated file as source (collectSourceFiles walks packages/). Nothing here resolves through a package exports field or a dist/ directory, so there is no artefact between the edit and the reading.

Negative control: the 841 aligned rows are silent, and is silent when every row copies the pack value byte for byte pins it synthetically with factoryMatchingRows asserted, not merely "no findings".

The whole-run control. Diffing the gate's summary before and after the change, the only two lines that move are the new census line and the green verdict sentence. Every pre-existing counter — 3937 call sites, 2728/2728 keys, 996/993 inline defaults, 2702 parity comparisons, 25 families, 2 sibling fallbacks — is byte-identical, which is the reading that says restructuring the file walk's pre-filter did not disturb any existing class.

Design notes worth reviewing

Tests

13 new cases in scripts/__tests__/check-i18n-call-site-keys.test.ts, 122 passed (122) for that file. Every abstention is pinned as a count, not as "no finding" — because the verdict on main is zero and a resolver that stopped resolving reports zero too.

Merge resolution against 84c117f0d (objectui#7592), and the re-measurement it obliged

origin/main gained objectui#7592's key-builder leg (84c117f0d), which touches exactly these two files. Merged with git merge origin/main — no rebase, no amend, no force-push — as merge commit 6ee07ba2a.

It was a real interaction, not a textual collision. Both commits widened the same file-walk pre-filter for the same structural reason: a module the walk was dropping unparsed because it holds no t( spelling. objectui#7592 needed key-builder modules (tool-display.ts); this PR needs factory-and-table modules (useTimelineTranslation.ts and its siblings). Neither side was "taken" — the pre-filter is now a three-way union:

const hasTranslatorCall = /\btt?\s*(?:\?\.)?\s*\(/.test(text);
const hasKeyTemplate    = text.includes('.${');            // objectui#7592
const declaresFactory   = text.includes('createSafeTranslation');  // objectui#7567
if (!hasTranslatorCall && !hasKeyTemplate && !declaresFactory) continue;

The load-bearing half is that each leg stays guarded by its OWN predicate, never by "this file was parsed." A leg gated on the union would silently inherit the other legs' widenings, and each card's measured blast radius would stop being a reading about that leg. Verified rather than assumed: the merged run still reports 1 head(s) read off a key-building helper rather than a call site — exactly the one builder objectui#7592 measured.

Re-measured on the merged tree, not carried forward

measure pre-merge merged (6ee07ba2a)
createSafeTranslation sites 32 32
distinct tables 32 32
unreadable tables (the blind spot the no-ledger ruling rests on) 0 0
rows compared against en 841 841
matching 841 841
abstained, key en does not define (timeline.relative.*, objectui#7874) 5 5
abstained, no single comparable en value 0 0
unreadable rows 0 0

Every figure unchanged, including the two the ruling depends on: 0 unreadable and 0 drifted. The collapse guard's threshold is still live against the new population — 841 compared against a floor of 500.

The whole-run control, re-established against the correct baseline

The pre-merge version of this control compared against a base that is now 19 commits stale, so it was re-run against plain origin/main at 565817a72 in a throwaway detached worktree. Diffing that against the merged branch, exactly two lines move: the new factory census line, and the green verdict sentence. Every pre-existing counter is byte-identical — 3938 call sites, 2728/2728 keys, 996/993 inline defaults, 2702 parity comparisons, 26 families, 1 key-builder head, 2 sibling fallbacks.

The positive control, re-run on the merged tree

A control proved on the pre-merge tree is not a control on this one, so objectui#7454's drift was reintroduced again on the merged tree. Same discipline: mutation proven landed first (anchors flipped 1→0 / 0→1, blob hash 389b6e11…5d44b909…), restore via git checkout HEAD -- plus the absolute path inside a trap … EXIT INT TERM armed only after the merge commit existed, restoration proven by state (hash back to the HEAD blob, anchors back to 1/0, git status --porcelain and git diff HEAD --stat both empty). The gate exited 1 and named itself, with the population moving exactly as before — compared stayed 841, matching went 841 → 840.

Verification run — all re-run at 6ee07ba2a, the merge commit

command result
node scripts/check-i18n-call-site-keys.mjs exit 0, green verdict line quoted above; output byte-identical to the pre-ablation merged run
pnpm exec vitest run — this file plus the three consumer suites (check-i18n-dead-keys.test.ts, scripts-type-check.test.ts, CapabilityMultiSelectField.specParity-6285.test.tsx) Test Files 4 passed (4) · Tests 181 passed (181) — includes objectui#7592's own 8 new cases, which pass unchanged
node scripts/check-i18n-dead-keys.mjs (the only other module importing analyze) exit 0
pnpm type-check:scripts exit 0
pnpm lint:root exit 0 — 33 problems (0 errors, 33 warnings), all pre-existing, none in either touched file
pnpm check:control-bytes · check:entry-guard · check:shell-escape-residue · check:vi-mock-specifiers · check:vi-mock-inherit exit 0 each
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over both touched files no hits
node scripts/check-changeset-presence.mjs exit 0 — verbatim: No source or published contract of a released package changed in this range, so no changeset is owed. (merge-base now 565817a72, still 2 files changed — mine)

Lint scope is complete, not narrowed. pnpm lint is turbo run lint, which runs each package's own lint over its own directory; scripts/** belongs to no package, so lint:root is the only lint configuration that reads either touched file, and it ran in full (14s).

What I did not run: the rest of the repo's per-package test and lint farm, and CI itself. I also did not update the branch against the 4 commits main gained after 565817a72 — none of them touches either file and git merge-tree reports a clean merge, so the behind state is the queue's to resolve, not mine. Neither touched file is imported by any package source — the only two importers of analyze are scripts/check-i18n-dead-keys.mjs and one packages/fields test, both run above.

ci.yml untouched, as instructed. The gate already has its CI step (.github/workflows/ci.yml:487, run: pnpm check:i18n-keys), and scripts/__tests__/check-i18n-call-site-keys.test.ts already pins that it runs after the install it needs. Nothing to add.

Clause-② stayed no: two files, both under scripts/. No runtime change, no locale-pack change, and packages/plugin-calendar is byte-identical to HEAD (proven above).


🤖 Generated with Claude Code

https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3


Generated by Claude Code

…ainst the en pack (objectui#7567)

`check-i18n-call-site-keys` already held one flavour of provider-less English
to the pack — an inline `t(key, { defaultValue })` — and never read the other.
A `createSafeTranslation(DEFAULT_TRANSLATIONS, …)` row carries the identical
promise one indirection away: `fallbackT` resolves `defaults[key] ||
defaultValue || key`, so the table stands in for the pack value on a
provider-less host. A row that says something else is the same control labelled
two ways depending on where it is mounted, which is exactly what objectui#7454
measured.

Class 8, `factory-default-drift`. The population is read from the source —
every `createSafeTranslation` / `createSafeTranslationHook` invocation — rather
than from a registry, so a 33rd factory is covered the day it is written. It
generalises `defaults-maps-mirror-en-pack.test.tsx` (objectui#4401), which
covers the three tables listed in it by hand.

Blocking from day one, with no ledger section, because the census says it can
be: 32 factory sites over 32 distinct tables, 846 rows, 841 comparable, 0
drifted. objectui#7454's instance — the one known member of the class — landed
in objectui#7574 before this rule did, so a ledger would have nothing in it and
would read exactly like a populated one.

The abstention counts are printed on every run and asserted by the collapse
guard, because "0 drifted" and "0 rows compared" read identically otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
…compare

Conflict: `scripts/check-i18n-call-site-keys.mjs`, against objectui#7592's
`84c117f0d` — and it is a real interaction, not a textual collision. Both
commits widened the SAME file-walk pre-filter for the SAME structural reason:
a module the walk was dropping unparsed because it holds no `t(` spelling.
objectui#7592 needed key-builder modules; objectui#7567 needs factory-and-table
modules (`useTimelineTranslation.ts` and its siblings).

Resolved as a three-way union rather than by taking a side: three independent
reasons to parse, and each leg guarded by its OWN predicate, never by "this
file was parsed". That last part is load-bearing. A leg gated on the union
would silently inherit the other legs' widenings, and each card's measured
blast radius would stop being a reading about that leg. Verified: the merged
run still reports `1 head(s) read off a key-building helper`, exactly the one
builder objectui#7592 measured.

Re-measured on the merged tree, not carried over: 32 createSafeTranslation
sites over 32 distinct tables, 0 unreadable, 841 rows compared, 841 matching, 5
on a key `en` does not define. Unchanged in every figure, which is what the
blocking-with-no-ledger ruling rests on.

Control re-run against the correct baseline (plain `origin/main` at 565817a,
not the stale pre-merge base): exactly two lines of the gate's summary differ —
the new factory census line and the green verdict sentence. Every pre-existing
counter is byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants