Uh oh!
There was an error while loading. Please reload this page.
feat(scripts): check template-literal t() key families member by member, not just by prefix - #6036
Conversation
…er, not just by prefix `missing-prefix` is the only claim about a dynamic key that is true without knowing the substitution, and its own header says so. The cost is the complement: a head that resolves and a MEMBER that does not is invisible to every gate in the repo. Pack-vs-pack parity reads ten packs missing it identically as full parity; en-drift fires on a value CHANGING, which a key that was never added never does. `filterBuilder.operators` was the measured instance — six of twenty-two operators missing from all ten packs while the head resolved sixteen members deep, and users read the raw key. A member set is a decision, not a measurement: this file parses source with no type checker, so it cannot follow `job.status` to its declaration. It CAN read a declaration a human names. So every pack-backed template family is declared in DYNAMIC_KEY_FAMILIES as one of two things — a `vocabulary` naming a readable declaration (union, const array, object table, Set, interface field), or `enumerable: false` with one of four reasons it has no static member set. Measured on this checkout: 25 families, not the card's estimated ~20; 18 with a static vocabulary (112 member keys now checked exactly), 7 without. Plus 35 dynamic call sites with no static head at all, which neither rule can reach. Coverage grows in one direction only. `missing-prefix` still runs for every dynamic head, declared or not, and a test pins that declaring a family does not buy it out of the prefix rule. Three ratchet rules keep the registry honest — an undeclared family fails, a stale entry fails, and a vocabulary resolving to zero members fails, because vacuous and passing read identically. The first run found two genuinely missing members, `home.recentApps.itemType`'s `report` and `metadata`, both written at runtime by useTrackRouteAsRecent. Baselined against objectui#6023 rather than invented into ten packs.
yinlianghui-tw
commented
Aug 24, 2026
PM: PATCH ROUND — CI red on this PR's own change
Root causeThis PR rewrote the ratchet baseline's The fix — restore the sentinel, keep your new paragraphDo not edit Rework the note so it carries both: the literal phrase Also check line 793's companion assertion ( Gates required before you report back
Push to the same branch. Report the before/after of gate 2 explicitly. Standing note on the card's scopeSeparately — and this is my error, not yours — the ACCEPT comment I posted on this PR records that the dispatch order I wrote inverted the triage seat's scope for #4964. That record stands; this patch round is only about the red check and does not re-open the scope question. Generated by Claude Code |
…ped to its pair The third-list rewrite dropped the literal string `BOTH LISTS ARE NOW EMPTY`, which `residue-namespaces-3546.test.tsx:792` reads. That assertion is #3546's guard: a reader who finds two empty objects must not conclude the ratchet is obsolete, so the note has to keep saying so in those words. Restored inside a sentence that names WHICH two lists, because with a third list present "both" alone would be actively misleading — the pair missingKeys + missingPrefixes is the one #3546 ratcheted to zero, and the missingMembers paragraph now says explicitly that it is outside that pair and not empty. The guard's own line is the only thing this touches; the test file is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
yinlianghui-tw
commented
Aug 24, 2026
{
"issue": 4964,
"status": "done",
"branch": "claude/issue-4964-i18n-template-literal-key-families",
"pr": "https://github.com/objectstack-ai/objectui/pull/6036",
"premise_still_valid": true,
"summary": "Patch round on PR #6036: restored the literal sentinel `BOTH LISTS ARE NOW EMPTY` in scripts/i18n-call-site-key-baseline.json's note, scoped by name to the missingKeys + missingPrefixes pair, and kept the missingMembers paragraph - now saying explicitly that the third list is NOT part of that pair and NOT empty, so 'both' can never be read as covering all three. residue-namespaces-3546.test.tsx was NOT touched: the pushed diff is one file, 8 insertions / 6 deletions, all inside the note array. New head 35c1adabd1eebebcb32b1379284015aa2529eb86.",
"tests": "GATE 1 (reproduce, BEFORE fix, at 1f0686b5e) - predicted red at :792, observed exactly that: `npx vitest run packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` -> `AssertionError: expected 'Keys a t() call site references that ...' to contain 'BOTH LISTS ARE NOW EMPTY'` at residue-namespaces-3546.test.tsx:792:37; `Test Files 1 failed (1)` / `Tests 1 failed | 42 passed (43)`; verify-lock VERDICT command-exit 1. GATE 2 (same command, AFTER fix, at 35c1adabd) - `Test Files 1 passed (1)` / `Tests 43 passed (43)`; VERDICT command-exit 0. Same command, same file, only the note edit between them. GATE 3 `npx vitest run packages/i18n/` -> `Test Files 53 passed (53)` / `Tests 893 passed (893)`, VERDICT command-exit 0 - no other reader of the note. GATE 4 `node scripts/check-i18n-call-site-keys.mjs` -> exit 0, verdict line UNCHANGED from the PR body: 'Every in-scope call-site key resolves against the en pack (2936 keys), every literal inline defaultValue matches the value the pack serves, every call site passes exactly the arguments that value has holes for, no call site carries a literal fallback beside itself, and every dynamic key family either checks its members against a declared vocabulary or says in writing why it has none.' - counters identical too (25 families / 18 with vocabulary / 112 members checked / 7 non-enumerable / 35 headless). GATE 5 (the PR body's other claims, re-run at the new head, all still true): check-changeset-presence.mjs -> 'No source of a released package changed in this range, so no changeset is owed.' (exit 0); `pnpm type-check:scripts` -> exit 0; `pnpm lint:root` -> '26 problems (0 errors, 26 warnings)' (same count as the PR body, none in the changed file); `npx vitest run scripts/__tests__/check-i18n-call-site-keys.test.ts packages/i18n/src/__tests__/all-locales-key-parity.test.ts` -> `Test Files 2 passed (2)` / `Tests 127 passed (127)` (matches the PR body). Control-byte scan over the changed file: clean (no match). Every heavy run went through scripts/pm/os-verify-lock.sh and verdicts are read off its VERDICT line, never a bare $?. `git diff --stat 1f0686b5e..35c1adabd` = `scripts/i18n-call-site-key-baseline.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-)`; `git diff 1f0686b5e..35c1adabd -- packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` is EMPTY (0 lines). FINAL NOTE TEXT (verbatim, the reworked paragraphs; the earlier ones are unchanged): 'Of the three lists in this file, missingKeys and missingPrefixes are the PAIR that' / 'objectui#3546 ratcheted to zero -- BOTH LISTS ARE NOW EMPTY. The 258-key stock it opened' / 'with was paid off across seven slices, the last of them two template families. Keep both' / 'of that pair: empty is their terminal, load-bearing state. Any NEW unresolved call-site' / 'key is `unexpected` against an empty baseline and fails the build, which is the point.' / 'missingMembers is a THIRD list (objectui#4964), NOT part of that pair and NOT empty -- so' / 'the sentence above counts missingKeys and missingPrefixes only, never all three. It holds' / 'debt the exact-member check MEASURED on its first run, not debt this repo took on.' - the sentinel sits intact inside ONE array element, so note.join(' ') carries it unbroken, and 'fails the build' (line 793's companion assertion) survives in two places.",
"open_questions": [],
"out_of_scope_findings": []
}PR left in draft; auto-merge not enabled; no labels touched. The PR is Generated by Claude Code Generated by Claude Code |
yinlianghui-tw
commented
Aug 24, 2026
os-dev-report (Re-posting: the previous comment #issuecomment-5396577695 opened with the HTML-comment marker, and GitHub's sanitizer ate it on the way into storage — the stored body starts at the fenced block, so a scan for that marker cannot see it. Same report, marker as literal text. This comment is the authoritative one.) {
"issue": 4964,
"status": "done",
"branch": "claude/issue-4964-i18n-template-literal-key-families",
"pr": "https://github.com/objectstack-ai/objectui/pull/6036",
"premise_still_valid": true,
"summary": "Patch round on PR #6036: restored the literal sentinel `BOTH LISTS ARE NOW EMPTY` in scripts/i18n-call-site-key-baseline.json's note, scoped by name to the missingKeys + missingPrefixes pair, and kept the missingMembers paragraph - now saying explicitly that the third list is NOT part of that pair and NOT empty, so 'both' can never be read as covering all three. residue-namespaces-3546.test.tsx was NOT touched: the pushed diff is one file, 8 insertions / 6 deletions, all inside the note array. New head 35c1adabd1eebebcb32b1379284015aa2529eb86.",
"tests": "GATE 1 (reproduce, BEFORE fix, at 1f0686b5e) - predicted red at :792, observed exactly that: `npx vitest run packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` -> `AssertionError: expected 'Keys a t() call site references that ...' to contain 'BOTH LISTS ARE NOW EMPTY'` at residue-namespaces-3546.test.tsx:792:37; `Test Files 1 failed (1)` / `Tests 1 failed | 42 passed (43)`; verify-lock VERDICT command-exit 1. GATE 2 (same command, AFTER fix, at 35c1adabd) - `Test Files 1 passed (1)` / `Tests 43 passed (43)`; VERDICT command-exit 0. Same command, same file, only the note edit between them. GATE 3 `npx vitest run packages/i18n/` -> `Test Files 53 passed (53)` / `Tests 893 passed (893)`, VERDICT command-exit 0 - no other reader of the note. GATE 4 `node scripts/check-i18n-call-site-keys.mjs` -> exit 0, verdict line UNCHANGED from the PR body: 'Every in-scope call-site key resolves against the en pack (2936 keys), every literal inline defaultValue matches the value the pack serves, every call site passes exactly the arguments that value has holes for, no call site carries a literal fallback beside itself, and every dynamic key family either checks its members against a declared vocabulary or says in writing why it has none.' - counters identical too (25 families / 18 with vocabulary / 112 members checked / 7 non-enumerable / 35 headless). GATE 5 (the PR body's other claims, re-run at the new head, all still true): check-changeset-presence.mjs -> 'No source of a released package changed in this range, so no changeset is owed.' (exit 0); `pnpm type-check:scripts` -> exit 0; `pnpm lint:root` -> '26 problems (0 errors, 26 warnings)' (same count as the PR body, none in the changed file); `npx vitest run scripts/__tests__/check-i18n-call-site-keys.test.ts packages/i18n/src/__tests__/all-locales-key-parity.test.ts` -> `Test Files 2 passed (2)` / `Tests 127 passed (127)` (matches the PR body). Control-byte scan over the changed file: clean (no match). Every heavy run went through scripts/pm/os-verify-lock.sh and verdicts are read off its VERDICT line, never a bare $?. `git diff --stat 1f0686b5e..35c1adabd` = `scripts/i18n-call-site-key-baseline.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-)`; `git diff 1f0686b5e..35c1adabd -- packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx` is EMPTY (0 lines). FINAL NOTE TEXT (verbatim, the reworked paragraphs; the earlier ones are unchanged): 'Of the three lists in this file, missingKeys and missingPrefixes are the PAIR that' / 'objectui#3546 ratcheted to zero -- BOTH LISTS ARE NOW EMPTY. The 258-key stock it opened' / 'with was paid off across seven slices, the last of them two template families. Keep both' / 'of that pair: empty is their terminal, load-bearing state. Any NEW unresolved call-site' / 'key is `unexpected` against an empty baseline and fails the build, which is the point.' / 'missingMembers is a THIRD list (objectui#4964), NOT part of that pair and NOT empty -- so' / 'the sentence above counts missingKeys and missingPrefixes only, never all three. It holds' / 'debt the exact-member check MEASURED on its first run, not debt this repo took on.' - the sentinel sits intact inside ONE array element, so note.join(' ') carries it unbroken, and 'fails the build' (line 793's companion assertion) survives in two places.",
"open_questions": [],
"out_of_scope_findings": []
}PR left in draft; auto-merge not enabled; no labels touched. The PR is Generated by Claude Code Generated by Claude Code |
yinlianghui-tw
commented
Aug 24, 2026
PM: patch round verified against GitHub — awaiting CIReport received and checked against the repository rather than taken on trust. The one claim that mattered holds. The changed-file list on this PR is exactly three files:
The rework is the right shape. The sentinel now sits inside a single array element (so Gate 1/2 pairing accepted. Same command, same file, only the note edit between the two runs: ⏳ Not landing yet. CI is re-running on the new head and seven checks — including all four test shards, the class the failure came from — are still in progress. Verifying a fix locally is necessary but not sufficient; this PR does not get turned ready until every check on Also noted for the landing step: Standing record — unchanged by this roundThe scope-divergence record posted with the ACCEPT stands: the dispatch order I wrote inverted the triage seat's scope for #4964 (triage ruled options 2 and 3 were follow-ups to file, not fold in; my order led to them being built). That error is mine, not the dev's. This patch round addressed only the red check and does not re-open or re-settle that question — the triage seat may still reopen or re-scope this card. Generated by Claude Code |
yinlianghui-tw
commented
Aug 24, 2026
PM: CI green on the patch head — turned ready, auto-merge armedRe-read the check runs on The one that matters: Turned ready and auto-merge armed (squash), per the maintainer's standing authorisation. Round summary for this cardRed → patched → green in one round, with the fix landing where it belonged: the sentinel was restored in the note and #3546's guard was left untouched (verified independently — the PR's changed-file list is three files, none of them that test). The before/after pairing was properly evidenced on the same command, and the gate this card is actually about reported an identical verdict line and identical counters afterwards, so the patch did not perturb the deliverable.
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#4964
missing-prefixis, in its own header's words, "the only claim about a dynamic key that is true without knowing the value". The complement is the hole: a head that resolves and a member that does not is invisible to the whole gate farm — pack-vs-pack parity reads ten packs missing it identically as full parity, andcheck:i18n-driftfires when anenvalue changes, which a key that was never added never does.Measured, not estimated
The card estimated "~20 families". The real numbers, from the gate's own run over
main:t(key)on a variable)The 35 headless sites are a class the card did not name and this PR does not close: with no static head, neither the prefix rule nor the member rule can say anything, and nothing short of a type checker could. They are counted and printed.
The design question the card names: what ARE the members?
A prefix check cannot become an exact check without deciding the member set, and a member set is a decision, not a measurement. This file parses source with no type checker and no build step (
collectEnKeys's header explains why), so it cannot followjob.statusto its declaration. What it can do is read a declaration a human names.So
DYNAMIC_KEY_FAMILIESdeclares every pack-backed family as one of two things, and the split is the deliverable:Exactly checkable (18) —
vocabularynames a declaration read from source in one of seven shapes (union,array,arrayField,objectKeys,objectField,set,interfaceField):appDesigner.fieldDesigner.typeCategory.·capability.group.·capability.label.·console.ai.group.·console.identityImport.policy.·console.identityImport.policyHint.·dashboard.trend.·filterBuilder.operators.·gantt.link.rejected.·gantt.linkType.·gantt.viewMode.·grid.import.confidence.·grid.import.jobStatus.·grid.import.type.·home.recentApps.itemType.·managedByBadge.·marketplace.disclosure.runtime.·organization.invitations.status.Not enumerable (7), each with a
why— because the four reasons are not the same finding and only one of them is permanent:whyruntime-dataconsole.settingsHub.categories.,marketplace.category.external-vocabularydashboard.filters.range.,report.aggregate.Record<Union, …>; the vocabulary lives in@objectstack/specunnamed-unioncommon.,gantt.linkEnd.open-forwarderapprovalsInbox.tr(key: string, …)forwards arbitrary keys, so the "family" is a 169-key namespace and the template is a namespace prefix, not a member positionenumerable: falseis a real answer, deliberately preferred over a guessed vocabulary.dashboard.filters.range.is the worked example:packages/types/src/data-protocol.tshas aFilterBuilderDateRangePresetunion that looks like it would fit, but it is the filter builder's vocabulary, not the dashboard bar's — using it would be a guess, and a guess produces false reds or a check that quietly skips the family.Coverage grows in one direction only
The failure mode this lane keeps hitting is a stricter-looking gate that silently covers less. Three things guard against it:
missing-prefixis untouched. It still runs for every dynamic head, declared or not. A test pins exactly this — a declared family whose head matches nothing producesmissing-prefixand the member findings, not one instead of the other.undeclared-dynamic-family(a new family cannot land unguarded — which is how these 25 accumulated unmeasured); an entry whose head is gone isstale-dynamic-family.empty-vocabularyand one that cannot be read as its declared shape isunreadable-vocabulary— never silently "no members to check", because a vacuous exact check and a passing one produce identical output. Every green synthetic test assertscounters.checkedMembers, not just an empty finding list. And a repo-level test asserts every checked-in vocabulary resolves non-empty on this checkout, plus a positive control that the checker finds a known-present key (gantt.viewMode.day) before any "not found" is trusted.Reverse verification
Deliberate ablation, with the mutation proven on disk and the restore proven after (
git checkout HEAD -- <path>, thengit diff HEAD):The load-bearing half is what did not fire:
missing-prefixproduced no finding, becausegantt.viewMode.still resolved through its four surviving members. That is the gap, reproduced on demand.A first attempt at this ablation was a no-op — the
perl -0pianchor matched zero lines (wrong indentation),perlexited 0, and the gate run read as a clean "no finding". It is recorded here because the disk-proof step is the only thing that caught it.What the first run found — reported, not papered over
home.recentApps.itemType.reportandhome.recentApps.itemType.metadataare missing from all ten packs.RecentItem['type']is a six-member union andendefines four of them;useTrackRouteAsRecent.tswrites both missing members at runtime (:105metadata,:148report).Per this card's ruling they are not invented into ten packs here. They are baselined in
missingMembersagainst #6023, which is filed with the measurement and the fix order. The baseline note says in as many words that this third list holds debt the check measured, not debt this repo took on.Gates
Run on the final commit
1f0686b5e(this branch's head):check:i18n-keysEvery in-scope call-site key resolves against the en pack (2936 keys) … and every dynamic key family either checks its members against a declared vocabulary or says in writing why it has none.(exit 0)check:i18n-dead-keys(consumer ofanalyze)check-changeset-presence.mjs✅ No source of a released package changed in this range, so no changeset is owed.type-check:scriptslint:root✖ 26 problems (0 errors, 26 warnings)— all pre-existing, none in the changed filesall-locales-key-parity)Test Files 2 passed (2)/Tests 127 passed (127)No changeset is added and no
skip-changesetlabel is applied (#4912 / #3724) — the presence check's own verdict is that none is owed,scripts/being outside every released package'ssrc/. Control-byte scan over the three changed files is clean.Adjacent-but-different axis #4905 is not addressed here and remains open — it covers the placeholder-spelling gate on inline
t(key, { defaultValue })strings; nothing in this diff touches that rule.Follow-ups this measurement priced (filed, not folded in)
home.recentApps.itemType.reportand.metadataare missing from all ten packs — the first members the #4964 exact check measured #6023 — the two genuinely missinghome.recentApps.itemTypemembers.enumerable: falsewithwhy: 'unnamed-union'/'external-vocabulary'prices — four families, two of them bridgeable by naming a union, two by mirroring a spec vocabulary in a repo-local exhaustiveRecord.Generated by Claude Code
Generated by Claude Code