Skip to content

Commit c76fa97

Browse files
committed
test(i18n,app-shell): discover the createSafeTranslation defaults tables instead of hand-listing three (objectui#7884)
The rule "every row names a key the en pack actually defines" is the one rule in this repo that rejects a defaults row whose key no locale pack defines. It did not miss objectui#7874's five dead `timeline.relative.*` rows because it was wrong -- it never saw them: its `MAPS` was a hand-written list of three IMPORTED maps, and `TIMELINE_DEFAULT_TRANSLATIONS` was not on it. Measured: the three-map list judged 400 of 1056 rows (37.9%). It was also structurally incompletable by its own mechanism -- 11 of the 32 factory call sites pass an anonymous inline object literal exported under no name at all, so no list of imports could ever reach them. The AST walk that discovers these tables already existed, module-private inside `fallback-placeholder-spelling-3512.test.ts`. It is MOVED, not copied, into `@object-ui/test-support`, so both gates ask their different questions of one definition of the population instead of two that drift apart. Behaviour of the objectui#3512 gate is preserved, verified on one tree either side of the move: 35 discovery entries, 1072 rows, 0 unreadable, the same 4 needle files, and an identical sha256 over every row (f7910279e114cd6e1bf1d2b97bf7164661283cd9fbb5eef88286f2f75bea8065). The scanner is reached as a DECLARED subpath, `@object-ui/test-support/ defaults-table-scan`, and NOT from the package index. It reads the workspace from disk, so it only type-checks where Node's ambient types are present, and a barrel re-export puts a module into the program of every consumer that imports the index for anything at all. Measured with it on the index: `data-objectstack` imports `{ enumOptions }` from that index in one test, so `tsc -p packages/data-objectstack/tsconfig.json --listFiles` pulled this file into that package's program and the repo-wide type-check failed with three TS2591s in a file that package never asked for. `./zod-wrapper-keys` is the same escape hatch for the mirror case, and objectui#4325's lesson is untouched: the hazard there was an UNDECLARED deep path, TS2882 for `tsc`; this one is in `exports`. One spelling also had to change. The walk skips `__tests__` directories, which is why the needle could be a single literal while it lived in one; in a normal `src/` module a verbatim spelling makes the scanner match its own source and report itself as a fifth interpolator. It is assembled from fragments now, with the runtime value unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
1 parent d9580f4 commit c76fa97

9 files changed

Lines changed: 630 additions & 288 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
---
3+
4+
Test-only change (objectui#7884): the `defaults-maps-mirror-en-pack` gate's rule "every
5+
row names a key the en pack actually defines" now runs over the `createSafeTranslation`
6+
defaults tables discovered from source, instead of a hand-written list of three imported
7+
maps that judged 400 of 1056 rows. The AST walk objectui#3512 already had moved into
8+
`@object-ui/test-support` (private, never published) so both gates share one definition of
9+
the population rather than growing a second traversal. No published behaviour changes.

‎packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx‎

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,48 @@
77
*/
88

99
/**
10-
* The three ungated `createSafeTranslation` defaults maps mirror the `en` pack —
11-
* objectui#4401, generalizing objectui#3440's collaboration-only precedent.
10+
* The `createSafeTranslation` defaults maps mirror the `en` pack —
11+
* objectui#4401, generalizing objectui#3440's collaboration-only precedent,
12+
* widened from three hand-listed maps to the discovered population by
13+
* objectui#7884.
14+
*
15+
* ## What objectui#7884 changed, and why it was not a new rule
16+
*
17+
* The case "every row names a key the en pack actually defines" below is the
18+
* ONE rule in this repo that rejects a defaults row whose key no pack defines.
19+
* It did not fail to catch objectui#7874's five dead `timeline.relative.*` rows
20+
* because it was wrong — it never saw them. Its `MAPS` was a hand-written list
21+
* of three IMPORTED maps, and `TIMELINE_DEFAULT_TRANSLATIONS` was not on it: a
22+
* correct rule held outside the door by a hand-written list, the same family as
23+
* objectui#7448 / #7528 / #7548 / #7825 / #7853.
24+
*
25+
* Measured before the widening: the three-map list judged **400 of 1056 rows,
26+
* 37.9%** — the rule was held outside 62% of its own population. And the list
27+
* was not merely incomplete, it was **structurally incompletable by its own
28+
* mechanism**: 11 of today's 32 factory tables are anonymous inline object
29+
* literals passed straight to `createSafeTranslation(…)` and exported under no
30+
* name at all, so no amount of diligence in maintaining a list of IMPORTS could
31+
* ever reach them. With objectui#7874's five rows retired this class is empty
32+
* repo-wide: 0 offending rows across 34 unique tables and 1056 rows, and a
33+
* blind spot of 0 — nothing was unreadable, so the zero is a real zero.
34+
*
35+
* So the rule now runs over the population `@object-ui/test-support`'s
36+
* `scanDefaultsTables` discovers from source — the same walk objectui#3512's
37+
* `fallback-placeholder-spelling-3512.test.ts` already used, MOVED rather than
38+
* copied, because two traversals are two definitions of the population that
39+
* drift apart.
40+
*
41+
* ## The two reading paths are kept, deliberately
42+
*
43+
* The three hand-listed maps stay, and are still read as RUNTIME OBJECTS
44+
* through their package imports, while the discovered population is read as
45+
* SOURCE TEXT through the AST. That is not duplication: on the three maps they
46+
* overlap, each is a check on the other, and a case below pins that every row
47+
* of every imported map is present in the discovered set. Only the
48+
* key-existence rule is widened — the byte-identity comparison still needs the
49+
* runtime object, and judging factory-table VALUES against the pack is
50+
* objectui#7567's `factory-default-drift`, a different question with its own
51+
* deliberate abstention.
1252
*
1353
* ## The defect this pins
1454
*
@@ -75,6 +115,9 @@
75115
*/
76116

77117
import { describe, it, expect } from 'vitest';
118+
import path from 'node:path';
119+
import { fileURLToPath } from 'node:url';
120+
import { scanDefaultsTables } from '@object-ui/test-support/defaults-table-scan';
78121
import { en } from '@object-ui/i18n';
79122
import { DETAIL_DEFAULT_TRANSLATIONS } from '@object-ui/plugin-detail';
80123
import { LIST_DEFAULT_TRANSLATIONS } from '@object-ui/plugin-list';
@@ -89,6 +132,33 @@ const packValueAt = (key: string): unknown =>
89132
/** Codepoints, so a failure distinguishes `…` from `...` and NBSP from a space. */
90133
const codepoints = (s: string) => [...s].map((c) => c.codePointAt(0)).join(',');
91134

135+
const here = path.dirname(fileURLToPath(import.meta.url));
136+
// packages/app-shell/src/__tests__ -> repo root
137+
const REPO_ROOT = path.resolve(here, '../../../..');
138+
139+
/**
140+
* The discovered population — every `createSafeTranslation(…)` first argument
141+
* resolved from source, plus the three hand-rolled sibling tables. One walk,
142+
* shared with objectui#3512's gate; see `@object-ui/test-support`'s
143+
* `defaults-table-scan.ts` for why it is not a second traversal.
144+
*/
145+
const DISCOVERED = scanDefaultsTables(REPO_ROOT);
146+
147+
/**
148+
* Rows de-duplicated on `file:line` + key.
149+
*
150+
* `TIMELINE_DEFAULT_TRANSLATIONS` is discovered TWICE on purpose — once as the
151+
* factory call in `useTimelineTranslation.ts` and once through the hand-rolled
152+
* registry, which objectui#3512 keeps so the registry mirrors its needle-file
153+
* set. Its rows are therefore scanned twice, and a gate that reported them raw
154+
* would inflate every count it prints (objectui#7874's five rows read as ten).
155+
* The identity is the ROW's own location plus its key, not the table label,
156+
* because the two discoveries of that table carry different labels.
157+
*/
158+
const DISCOVERED_ROWS = [
159+
...new Map(DISCOVERED.rows.map((row) => [`${row.where}::${row.key}`, row])).values(),
160+
];
161+
92162
interface MapUnderGate {
93163
/** Name used in test titles and failure messages. */
94164
readonly name: string;
@@ -237,3 +307,61 @@ describe('the plugin defaults maps mirror the en pack (objectui#4401)', () => {
237307
}
238308
});
239309
});
310+
311+
describe('the discovered defaults tables name keys the en pack defines (objectui#7884)', () => {
312+
it('discovers the population, and states its blind-spot size', () => {
313+
// Non-vacuity, #4118 family standard: the rule below is "no row offends",
314+
// which an empty or broken walk satisfies trivially. Floors, not pins — a
315+
// new table raises them for free, only a table DISAPPEARING has to be
316+
// explained.
317+
expect(DISCOVERED.tables.length).toBeGreaterThanOrEqual(34);
318+
expect(DISCOVERED_ROWS.length).toBeGreaterThanOrEqual(1_000);
319+
expect(DISCOVERED.sourceFiles.length).toBeGreaterThan(1_000);
320+
321+
// THE blind spot, asserted rather than counted in silence. A table that
322+
// does not resolve, a computed key, a non-static value: each is a row this
323+
// instrument cannot judge, and an instrument that hides how much it cannot
324+
// see reads as 100% coverage forever. Measured 0 when this landed —
325+
// objectui#7567's census surfaced objectui#7874 precisely because it
326+
// printed its abstention count.
327+
expect(DISCOVERED.unreadable).toEqual([]);
328+
});
329+
330+
it('every row of every hand-listed map is present in the discovered set', () => {
331+
// Ties the two reading paths together. The maps above are IMPORTED runtime
332+
// objects; the population here is read from SOURCE by the AST. If the walk
333+
// ever stops reaching one of these three tables, this fails loudly instead
334+
// of quietly shrinking the population the rule below judges.
335+
const discoveredByFile = new Map<string, Set<string>>();
336+
for (const row of DISCOVERED_ROWS) {
337+
const file = row.where.slice(0, row.where.lastIndexOf(':'));
338+
let keys = discoveredByFile.get(file);
339+
if (!keys) discoveredByFile.set(file, (keys = new Set<string>()));
340+
keys.add(row.key);
341+
}
342+
343+
const missed = MAPS.flatMap(({ map, source }) =>
344+
Object.keys(map)
345+
.filter((key) => !(discoveredByFile.get(source)?.has(key) ?? false))
346+
.map((key) => `${source} row ${key} is not in the discovered population`),
347+
);
348+
expect(missed).toEqual([]);
349+
});
350+
351+
it('every row names a key the en pack actually defines', () => {
352+
// THE widened rule. Same verdict objectui#4401 wrote for three maps, now
353+
// asked of every discovered table: a row whose key the pack lacks means the
354+
// provider path cannot serve this string at all (i18next answers with the
355+
// call site's `defaultValue`, or the raw key), so the two paths disagree by
356+
// construction. If this goes red, FILE the row — never add the key to an
357+
// allow-list, and never re-narrow the population to make it green.
358+
const missing = DISCOVERED_ROWS.filter(
359+
({ key }) => typeof packValueAt(key) !== 'string',
360+
).map(
361+
({ where, key, table }) =>
362+
`${where} ${key} — row of ${table}, absent from the en pack — FINDING, file it`,
363+
);
364+
365+
expect(missing).toEqual([]);
366+
});
367+
});

‎packages/i18n/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"react": "^18.0.0 || ^19.0.0"
4545
},
4646
"devDependencies": {
47+
"@object-ui/test-support": "workspace:*",
4748
"@types/react": "19.2.18",
4849
"react": "19.2.8",
4950
"typescript": "^6.0.3",

0 commit comments

Comments
 (0)