Skip to content

fix(devx): add a property-chain leg to check-i18n-dead-keys - #6995

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain
Aug 31, 2026
Merged

fix(devx): add a property-chain leg to check-i18n-dead-keys#6995
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6666

A consumer that imports a locale pack object and reads it by property access is invisible to both of the sweep's legs at once. The AST pass classifies t()/tt() call sites and there is no call; the text safety net greps the full dotted key and the namespace segment is bound to a local variable, so the dotted key is never spelled. The key lands in CONFIRMED — the tier documented as the safest thing to delete — while a shipping screen renders it.

textFootprint() now probes each candidate's property chain (the key minus its leading namespace segment, leading dot kept) alongside the full dotted key, in the same single grep -F pass.

The card's tier claim is stale — measured, and stated rather than repeated

The card measured the five keys at CONFIRMED on 813bf8323. On origin/main at 4357ec754 they are NEEDS-REVIEW, demoted by side effect: PR #6662's retirement pin and ar-participle-orthography-6610.test.ts both spell the dotted keys, which gives them a textual footprint. Per the triage ruling that is a mitigation, not a fix — the defect is unchanged, and the before/after evidence below is therefore built on what the hits are rather than on a tier flip.

before (4357ec754)after (fb4ccd348)
CONFIRMED115115
NEEDS-REVIEW247247
candidates362362
hits naming the real reader LoadingScreen.tsx07 keys

Nothing was demoted; no pre-existing hit was lost. What changed is that seven keys now name the file that actually renders them, marked (via property chain):

  • the five from the card, and
  • console.actions.retry / console.actions.retryingtwo more the card did not list, read by the same file the same way (LoadingScreen.tsx:118,123).

Both negative controls stay CONFIRMED with an empty hit list.

Three boundaries, each pinned by an ablation

  1. Three segments or more, never two. A two-segment key's chain is a single generic word (.ok, .no, .empty). propertyChainProbe() returns null below three segments. Two-segment keys stay on the enumerated importer list, read by hand — which is what refactor(i18n): retire 25 confirmed-dead locale keys across all ten packs #6662 did.
  2. The match must end at a property boundary. Otherwise a leaf would demote on a reader of a longer sibling leaf that it merely prefixes.
  3. A chain-only hit is suffixed (via property chain), so the report never sends a reader grepping for a dotted key that file does not contain.

The importer list is written into the file

Per the ruling, the pack-object importer class is enumerated in the script header with the two-stage grep that re-derives it. The outboundAgentText.ts caveat is stated explicitly: it indexes its subtree dynamically (ai?.[key]), so neither leg sees the read — its four keys land in NEEDS-REVIEW only because those property names happen to appear as string literals in the OutboundAgentTextKey union above them. By luck, not by design. Replace that union with anything generated and all four drop to CONFIRMED with a live consumer still reading them.

Self-pollution, and why the tests spell keys in segments

textFootprint() greps scripts/ too, so a dotted key written into the test file would make that file a hit for it — the negative controls would stop being reader-less because the test asserting they are reader-less mentioned them. The real-repo controls are therefore joined from segments, and this was verified empirically: with the new test file on disk, console.error.serverUnreachable and console.error.timeout are still CONFIRMED and CONFIRMED is still 115.

Verification — all on fb4ccd348

Whole scripts/__tests__ tree (not a subject-matter subset): Test Files 89 passed (89) · Tests 2512 passed (2512).

Ablations. No build step is involved — vitest loads scripts/*.mjs from source, there is no dist/ for a stale artifact to hide in. Each leg mutated with perl -0pi, the mutation proved on disk by counting the injected and removed text, restored with git checkout HEAD -- against an absolute path, and the restore proved byte-identical (git hash-object equal to the HEAD blob 851663da3 and git diff HEAD empty) before the next reading:

ablationresult
probe always returns null (leg removed)Tests 4 failed, 23 passed — all four detection assertions red, every negative control still green
three-segment guard relaxed to twoTests 2 failed, 25 passed
property-boundary check replaced by plain includesTests 1 failed, 26 passed

The first row is the discrimination proof: removing the leg loses detection and touches nothing on the negative side.

Gates, each exit code captured before any pipe:

  • pnpm type-check:scripts — exit 0
  • pnpm check:control-bytescheck-control-bytes: OK (scanned 5804 tracked text file(s); skipped 85 binary)
  • pnpm check:shell-escape-residueOK (4/4 root(s) resolved ...)
  • pnpm check:vi-mock-specifiersOK (4032 tracked source file(s) ...)
  • pnpm check:esm-specifiers — exit 0
  • node scripts/check-changeset-presence.mjsNo source of a released package changed in this range, so no changeset is owed.No changeset and no label are owed: the guarded surface is derived from published packages and scripts/ is not one.
  • node scripts/check-lint-coverage.mjslint coverage: 46/46 packages linted, 0 with outstanding errors (0 total)
  • eslint, run over the whole population rather than narrowed: eslint --no-inline-config --format json . resolved 4030 files from its own config; both changed files report 0 errors, 0 warnings. The run's own non-zero exit reflects 90 pre-existing errors in files outside the per-package lint scope, unchanged by this diff and confirmed green by the coverage gate above.

Generated by Claude Code


Generated by Claude Code

A consumer that imports a locale PACK OBJECT and reads it by property
access is invisible to both of the sweep's legs at once: there is no
t()/tt() call for the AST pass to classify, and the namespace segment is
bound to a local variable so the full dotted key is never spelled for the
text safety net to grep. Such a key lands in CONFIRMED — the tier
documented as the safest thing to delete — while a shipping screen
renders it.
textFootprint() now probes each candidate's property chain (the key minus
its leading namespace segment, leading dot kept) alongside the full
dotted key, bounded three ways: three segments or more, never two; the
match must end at a property boundary so a leaf does not demote on a
longer sibling; and a chain-only hit is reported with a
"(via property chain)" suffix so the report cannot send a reader grepping
for a dotted key the file does not contain.
Both control groups are pinned as self-tests, on synthetic fixtures and
against this repository, because detection alone does not prove
discrimination.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(i18n): check-i18n-dead-keys reports LIVE keys as CONFIRMED when a consumer reads the pack by property access

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(devx): add a property-chain leg to check-i18n-dead-keys by claude[bot] · Pull Request #6995 · objectstack-ai/objectui · GitHub
Skip to content

fix(devx): add a property-chain leg to check-i18n-dead-keys - #6995

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain
Aug 31, 2026
Merged

fix(devx): add a property-chain leg to check-i18n-dead-keys#6995
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6666

A consumer that imports a locale pack object and reads it by property access is invisible to both of the sweep's legs at once. The AST pass classifies t()/tt() call sites and there is no call; the text safety net greps the full dotted key and the namespace segment is bound to a local variable, so the dotted key is never spelled. The key lands in CONFIRMED — the tier documented as the safest thing to delete — while a shipping screen renders it.

textFootprint() now probes each candidate's property chain (the key minus its leading namespace segment, leading dot kept) alongside the full dotted key, in the same single grep -F pass.

The card's tier claim is stale — measured, and stated rather than repeated

The card measured the five keys at CONFIRMED on 813bf8323. On origin/main at 4357ec754 they are NEEDS-REVIEW, demoted by side effect: PR #6662's retirement pin and ar-participle-orthography-6610.test.ts both spell the dotted keys, which gives them a textual footprint. Per the triage ruling that is a mitigation, not a fix — the defect is unchanged, and the before/after evidence below is therefore built on what the hits are rather than on a tier flip.

before (4357ec754)after (fb4ccd348)
CONFIRMED115115
NEEDS-REVIEW247247
candidates362362
hits naming the real reader LoadingScreen.tsx07 keys

Nothing was demoted; no pre-existing hit was lost. What changed is that seven keys now name the file that actually renders them, marked (via property chain):

  • the five from the card, and
  • console.actions.retry / console.actions.retryingtwo more the card did not list, read by the same file the same way (LoadingScreen.tsx:118,123).

Both negative controls stay CONFIRMED with an empty hit list.

Three boundaries, each pinned by an ablation

  1. Three segments or more, never two. A two-segment key's chain is a single generic word (.ok, .no, .empty). propertyChainProbe() returns null below three segments. Two-segment keys stay on the enumerated importer list, read by hand — which is what refactor(i18n): retire 25 confirmed-dead locale keys across all ten packs #6662 did.
  2. The match must end at a property boundary. Otherwise a leaf would demote on a reader of a longer sibling leaf that it merely prefixes.
  3. A chain-only hit is suffixed (via property chain), so the report never sends a reader grepping for a dotted key that file does not contain.

The importer list is written into the file

Per the ruling, the pack-object importer class is enumerated in the script header with the two-stage grep that re-derives it. The outboundAgentText.ts caveat is stated explicitly: it indexes its subtree dynamically (ai?.[key]), so neither leg sees the read — its four keys land in NEEDS-REVIEW only because those property names happen to appear as string literals in the OutboundAgentTextKey union above them. By luck, not by design. Replace that union with anything generated and all four drop to CONFIRMED with a live consumer still reading them.

Self-pollution, and why the tests spell keys in segments

textFootprint() greps scripts/ too, so a dotted key written into the test file would make that file a hit for it — the negative controls would stop being reader-less because the test asserting they are reader-less mentioned them. The real-repo controls are therefore joined from segments, and this was verified empirically: with the new test file on disk, console.error.serverUnreachable and console.error.timeout are still CONFIRMED and CONFIRMED is still 115.

Verification — all on fb4ccd348

Whole scripts/__tests__ tree (not a subject-matter subset): Test Files 89 passed (89) · Tests 2512 passed (2512).

Ablations. No build step is involved — vitest loads scripts/*.mjs from source, there is no dist/ for a stale artifact to hide in. Each leg mutated with perl -0pi, the mutation proved on disk by counting the injected and removed text, restored with git checkout HEAD -- against an absolute path, and the restore proved byte-identical (git hash-object equal to the HEAD blob 851663da3 and git diff HEAD empty) before the next reading:

ablationresult
probe always returns null (leg removed)Tests 4 failed, 23 passed — all four detection assertions red, every negative control still green
three-segment guard relaxed to twoTests 2 failed, 25 passed
property-boundary check replaced by plain includesTests 1 failed, 26 passed

The first row is the discrimination proof: removing the leg loses detection and touches nothing on the negative side.

Gates, each exit code captured before any pipe:

  • pnpm type-check:scripts — exit 0
  • pnpm check:control-bytescheck-control-bytes: OK (scanned 5804 tracked text file(s); skipped 85 binary)
  • pnpm check:shell-escape-residueOK (4/4 root(s) resolved ...)
  • pnpm check:vi-mock-specifiersOK (4032 tracked source file(s) ...)
  • pnpm check:esm-specifiers — exit 0
  • node scripts/check-changeset-presence.mjsNo source of a released package changed in this range, so no changeset is owed.No changeset and no label are owed: the guarded surface is derived from published packages and scripts/ is not one.
  • node scripts/check-lint-coverage.mjslint coverage: 46/46 packages linted, 0 with outstanding errors (0 total)
  • eslint, run over the whole population rather than narrowed: eslint --no-inline-config --format json . resolved 4030 files from its own config; both changed files report 0 errors, 0 warnings. The run's own non-zero exit reflects 90 pre-existing errors in files outside the per-package lint scope, unchanged by this diff and confirmed green by the coverage gate above.

Generated by Claude Code


Generated by Claude Code

A consumer that imports a locale PACK OBJECT and reads it by property
access is invisible to both of the sweep's legs at once: there is no
t()/tt() call for the AST pass to classify, and the namespace segment is
bound to a local variable so the full dotted key is never spelled for the
text safety net to grep. Such a key lands in CONFIRMED — the tier
documented as the safest thing to delete — while a shipping screen
renders it.
textFootprint() now probes each candidate's property chain (the key minus
its leading namespace segment, leading dot kept) alongside the full
dotted key, bounded three ways: three segments or more, never two; the
match must end at a property boundary so a leaf does not demote on a
longer sibling; and a chain-only hit is reported with a
"(via property chain)" suffix so the report cannot send a reader grepping
for a dotted key the file does not contain.
Both control groups are pinned as self-tests, on synthetic fixtures and
against this repository, because detection alone does not prove
discrimination.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(i18n): check-i18n-dead-keys reports LIVE keys as CONFIRMED when a consumer reads the pack by property access

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(devx): add a property-chain leg to check-i18n-dead-keys by claude[bot] · Pull Request #6995 · objectstack-ai/objectui · GitHub
Skip to content

fix(devx): add a property-chain leg to check-i18n-dead-keys - #6995

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain
Aug 31, 2026
Merged

fix(devx): add a property-chain leg to check-i18n-dead-keys#6995
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6666

A consumer that imports a locale pack object and reads it by property access is invisible to both of the sweep's legs at once. The AST pass classifies t()/tt() call sites and there is no call; the text safety net greps the full dotted key and the namespace segment is bound to a local variable, so the dotted key is never spelled. The key lands in CONFIRMED — the tier documented as the safest thing to delete — while a shipping screen renders it.

textFootprint() now probes each candidate's property chain (the key minus its leading namespace segment, leading dot kept) alongside the full dotted key, in the same single grep -F pass.

The card's tier claim is stale — measured, and stated rather than repeated

The card measured the five keys at CONFIRMED on 813bf8323. On origin/main at 4357ec754 they are NEEDS-REVIEW, demoted by side effect: PR #6662's retirement pin and ar-participle-orthography-6610.test.ts both spell the dotted keys, which gives them a textual footprint. Per the triage ruling that is a mitigation, not a fix — the defect is unchanged, and the before/after evidence below is therefore built on what the hits are rather than on a tier flip.

before (4357ec754)after (fb4ccd348)
CONFIRMED115115
NEEDS-REVIEW247247
candidates362362
hits naming the real reader LoadingScreen.tsx07 keys

Nothing was demoted; no pre-existing hit was lost. What changed is that seven keys now name the file that actually renders them, marked (via property chain):

  • the five from the card, and
  • console.actions.retry / console.actions.retryingtwo more the card did not list, read by the same file the same way (LoadingScreen.tsx:118,123).

Both negative controls stay CONFIRMED with an empty hit list.

Three boundaries, each pinned by an ablation

  1. Three segments or more, never two. A two-segment key's chain is a single generic word (.ok, .no, .empty). propertyChainProbe() returns null below three segments. Two-segment keys stay on the enumerated importer list, read by hand — which is what refactor(i18n): retire 25 confirmed-dead locale keys across all ten packs #6662 did.
  2. The match must end at a property boundary. Otherwise a leaf would demote on a reader of a longer sibling leaf that it merely prefixes.
  3. A chain-only hit is suffixed (via property chain), so the report never sends a reader grepping for a dotted key that file does not contain.

The importer list is written into the file

Per the ruling, the pack-object importer class is enumerated in the script header with the two-stage grep that re-derives it. The outboundAgentText.ts caveat is stated explicitly: it indexes its subtree dynamically (ai?.[key]), so neither leg sees the read — its four keys land in NEEDS-REVIEW only because those property names happen to appear as string literals in the OutboundAgentTextKey union above them. By luck, not by design. Replace that union with anything generated and all four drop to CONFIRMED with a live consumer still reading them.

Self-pollution, and why the tests spell keys in segments

textFootprint() greps scripts/ too, so a dotted key written into the test file would make that file a hit for it — the negative controls would stop being reader-less because the test asserting they are reader-less mentioned them. The real-repo controls are therefore joined from segments, and this was verified empirically: with the new test file on disk, console.error.serverUnreachable and console.error.timeout are still CONFIRMED and CONFIRMED is still 115.

Verification — all on fb4ccd348

Whole scripts/__tests__ tree (not a subject-matter subset): Test Files 89 passed (89) · Tests 2512 passed (2512).

Ablations. No build step is involved — vitest loads scripts/*.mjs from source, there is no dist/ for a stale artifact to hide in. Each leg mutated with perl -0pi, the mutation proved on disk by counting the injected and removed text, restored with git checkout HEAD -- against an absolute path, and the restore proved byte-identical (git hash-object equal to the HEAD blob 851663da3 and git diff HEAD empty) before the next reading:

ablationresult
probe always returns null (leg removed)Tests 4 failed, 23 passed — all four detection assertions red, every negative control still green
three-segment guard relaxed to twoTests 2 failed, 25 passed
property-boundary check replaced by plain includesTests 1 failed, 26 passed

The first row is the discrimination proof: removing the leg loses detection and touches nothing on the negative side.

Gates, each exit code captured before any pipe:

  • pnpm type-check:scripts — exit 0
  • pnpm check:control-bytescheck-control-bytes: OK (scanned 5804 tracked text file(s); skipped 85 binary)
  • pnpm check:shell-escape-residueOK (4/4 root(s) resolved ...)
  • pnpm check:vi-mock-specifiersOK (4032 tracked source file(s) ...)
  • pnpm check:esm-specifiers — exit 0
  • node scripts/check-changeset-presence.mjsNo source of a released package changed in this range, so no changeset is owed.No changeset and no label are owed: the guarded surface is derived from published packages and scripts/ is not one.
  • node scripts/check-lint-coverage.mjslint coverage: 46/46 packages linted, 0 with outstanding errors (0 total)
  • eslint, run over the whole population rather than narrowed: eslint --no-inline-config --format json . resolved 4030 files from its own config; both changed files report 0 errors, 0 warnings. The run's own non-zero exit reflects 90 pre-existing errors in files outside the per-package lint scope, unchanged by this diff and confirmed green by the coverage gate above.

Generated by Claude Code


Generated by Claude Code

A consumer that imports a locale PACK OBJECT and reads it by property
access is invisible to both of the sweep's legs at once: there is no
t()/tt() call for the AST pass to classify, and the namespace segment is
bound to a local variable so the full dotted key is never spelled for the
text safety net to grep. Such a key lands in CONFIRMED — the tier
documented as the safest thing to delete — while a shipping screen
renders it.
textFootprint() now probes each candidate's property chain (the key minus
its leading namespace segment, leading dot kept) alongside the full
dotted key, bounded three ways: three segments or more, never two; the
match must end at a property boundary so a leaf does not demote on a
longer sibling; and a chain-only hit is reported with a
"(via property chain)" suffix so the report cannot send a reader grepping
for a dotted key the file does not contain.
Both control groups are pinned as self-tests, on synthetic fixtures and
against this repository, because detection alone does not prove
discrimination.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(i18n): check-i18n-dead-keys reports LIVE keys as CONFIRMED when a consumer reads the pack by property access

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(devx): add a property-chain leg to check-i18n-dead-keys by claude[bot] · Pull Request #6995 · objectstack-ai/objectui · GitHub
Skip to content

fix(devx): add a property-chain leg to check-i18n-dead-keys - #6995

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain
Aug 31, 2026
Merged

fix(devx): add a property-chain leg to check-i18n-dead-keys#6995
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6666

A consumer that imports a locale pack object and reads it by property access is invisible to both of the sweep's legs at once. The AST pass classifies t()/tt() call sites and there is no call; the text safety net greps the full dotted key and the namespace segment is bound to a local variable, so the dotted key is never spelled. The key lands in CONFIRMED — the tier documented as the safest thing to delete — while a shipping screen renders it.

textFootprint() now probes each candidate's property chain (the key minus its leading namespace segment, leading dot kept) alongside the full dotted key, in the same single grep -F pass.

The card's tier claim is stale — measured, and stated rather than repeated

The card measured the five keys at CONFIRMED on 813bf8323. On origin/main at 4357ec754 they are NEEDS-REVIEW, demoted by side effect: PR #6662's retirement pin and ar-participle-orthography-6610.test.ts both spell the dotted keys, which gives them a textual footprint. Per the triage ruling that is a mitigation, not a fix — the defect is unchanged, and the before/after evidence below is therefore built on what the hits are rather than on a tier flip.

before (4357ec754)after (fb4ccd348)
CONFIRMED115115
NEEDS-REVIEW247247
candidates362362
hits naming the real reader LoadingScreen.tsx07 keys

Nothing was demoted; no pre-existing hit was lost. What changed is that seven keys now name the file that actually renders them, marked (via property chain):

  • the five from the card, and
  • console.actions.retry / console.actions.retryingtwo more the card did not list, read by the same file the same way (LoadingScreen.tsx:118,123).

Both negative controls stay CONFIRMED with an empty hit list.

Three boundaries, each pinned by an ablation

  1. Three segments or more, never two. A two-segment key's chain is a single generic word (.ok, .no, .empty). propertyChainProbe() returns null below three segments. Two-segment keys stay on the enumerated importer list, read by hand — which is what refactor(i18n): retire 25 confirmed-dead locale keys across all ten packs #6662 did.
  2. The match must end at a property boundary. Otherwise a leaf would demote on a reader of a longer sibling leaf that it merely prefixes.
  3. A chain-only hit is suffixed (via property chain), so the report never sends a reader grepping for a dotted key that file does not contain.

The importer list is written into the file

Per the ruling, the pack-object importer class is enumerated in the script header with the two-stage grep that re-derives it. The outboundAgentText.ts caveat is stated explicitly: it indexes its subtree dynamically (ai?.[key]), so neither leg sees the read — its four keys land in NEEDS-REVIEW only because those property names happen to appear as string literals in the OutboundAgentTextKey union above them. By luck, not by design. Replace that union with anything generated and all four drop to CONFIRMED with a live consumer still reading them.

Self-pollution, and why the tests spell keys in segments

textFootprint() greps scripts/ too, so a dotted key written into the test file would make that file a hit for it — the negative controls would stop being reader-less because the test asserting they are reader-less mentioned them. The real-repo controls are therefore joined from segments, and this was verified empirically: with the new test file on disk, console.error.serverUnreachable and console.error.timeout are still CONFIRMED and CONFIRMED is still 115.

Verification — all on fb4ccd348

Whole scripts/__tests__ tree (not a subject-matter subset): Test Files 89 passed (89) · Tests 2512 passed (2512).

Ablations. No build step is involved — vitest loads scripts/*.mjs from source, there is no dist/ for a stale artifact to hide in. Each leg mutated with perl -0pi, the mutation proved on disk by counting the injected and removed text, restored with git checkout HEAD -- against an absolute path, and the restore proved byte-identical (git hash-object equal to the HEAD blob 851663da3 and git diff HEAD empty) before the next reading:

ablationresult
probe always returns null (leg removed)Tests 4 failed, 23 passed — all four detection assertions red, every negative control still green
three-segment guard relaxed to twoTests 2 failed, 25 passed
property-boundary check replaced by plain includesTests 1 failed, 26 passed

The first row is the discrimination proof: removing the leg loses detection and touches nothing on the negative side.

Gates, each exit code captured before any pipe:

  • pnpm type-check:scripts — exit 0
  • pnpm check:control-bytescheck-control-bytes: OK (scanned 5804 tracked text file(s); skipped 85 binary)
  • pnpm check:shell-escape-residueOK (4/4 root(s) resolved ...)
  • pnpm check:vi-mock-specifiersOK (4032 tracked source file(s) ...)
  • pnpm check:esm-specifiers — exit 0
  • node scripts/check-changeset-presence.mjsNo source of a released package changed in this range, so no changeset is owed.No changeset and no label are owed: the guarded surface is derived from published packages and scripts/ is not one.
  • node scripts/check-lint-coverage.mjslint coverage: 46/46 packages linted, 0 with outstanding errors (0 total)
  • eslint, run over the whole population rather than narrowed: eslint --no-inline-config --format json . resolved 4030 files from its own config; both changed files report 0 errors, 0 warnings. The run's own non-zero exit reflects 90 pre-existing errors in files outside the per-package lint scope, unchanged by this diff and confirmed green by the coverage gate above.

Generated by Claude Code


Generated by Claude Code

A consumer that imports a locale PACK OBJECT and reads it by property
access is invisible to both of the sweep's legs at once: there is no
t()/tt() call for the AST pass to classify, and the namespace segment is
bound to a local variable so the full dotted key is never spelled for the
text safety net to grep. Such a key lands in CONFIRMED — the tier
documented as the safest thing to delete — while a shipping screen
renders it.
textFootprint() now probes each candidate's property chain (the key minus
its leading namespace segment, leading dot kept) alongside the full
dotted key, bounded three ways: three segments or more, never two; the
match must end at a property boundary so a leaf does not demote on a
longer sibling; and a chain-only hit is reported with a
"(via property chain)" suffix so the report cannot send a reader grepping
for a dotted key the file does not contain.
Both control groups are pinned as self-tests, on synthetic fixtures and
against this repository, because detection alone does not prove
discrimination.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(i18n): check-i18n-dead-keys reports LIVE keys as CONFIRMED when a consumer reads the pack by property access

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(devx): add a property-chain leg to check-i18n-dead-keys by claude[bot] · Pull Request #6995 · objectstack-ai/objectui · GitHub
Skip to content

fix(devx): add a property-chain leg to check-i18n-dead-keys - #6995

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain
Aug 31, 2026
Merged

fix(devx): add a property-chain leg to check-i18n-dead-keys#6995
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6666

A consumer that imports a locale pack object and reads it by property access is invisible to both of the sweep's legs at once. The AST pass classifies t()/tt() call sites and there is no call; the text safety net greps the full dotted key and the namespace segment is bound to a local variable, so the dotted key is never spelled. The key lands in CONFIRMED — the tier documented as the safest thing to delete — while a shipping screen renders it.

textFootprint() now probes each candidate's property chain (the key minus its leading namespace segment, leading dot kept) alongside the full dotted key, in the same single grep -F pass.

The card's tier claim is stale — measured, and stated rather than repeated

The card measured the five keys at CONFIRMED on 813bf8323. On origin/main at 4357ec754 they are NEEDS-REVIEW, demoted by side effect: PR #6662's retirement pin and ar-participle-orthography-6610.test.ts both spell the dotted keys, which gives them a textual footprint. Per the triage ruling that is a mitigation, not a fix — the defect is unchanged, and the before/after evidence below is therefore built on what the hits are rather than on a tier flip.

before (4357ec754)after (fb4ccd348)
CONFIRMED115115
NEEDS-REVIEW247247
candidates362362
hits naming the real reader LoadingScreen.tsx07 keys

Nothing was demoted; no pre-existing hit was lost. What changed is that seven keys now name the file that actually renders them, marked (via property chain):

  • the five from the card, and
  • console.actions.retry / console.actions.retryingtwo more the card did not list, read by the same file the same way (LoadingScreen.tsx:118,123).

Both negative controls stay CONFIRMED with an empty hit list.

Three boundaries, each pinned by an ablation

  1. Three segments or more, never two. A two-segment key's chain is a single generic word (.ok, .no, .empty). propertyChainProbe() returns null below three segments. Two-segment keys stay on the enumerated importer list, read by hand — which is what refactor(i18n): retire 25 confirmed-dead locale keys across all ten packs #6662 did.
  2. The match must end at a property boundary. Otherwise a leaf would demote on a reader of a longer sibling leaf that it merely prefixes.
  3. A chain-only hit is suffixed (via property chain), so the report never sends a reader grepping for a dotted key that file does not contain.

The importer list is written into the file

Per the ruling, the pack-object importer class is enumerated in the script header with the two-stage grep that re-derives it. The outboundAgentText.ts caveat is stated explicitly: it indexes its subtree dynamically (ai?.[key]), so neither leg sees the read — its four keys land in NEEDS-REVIEW only because those property names happen to appear as string literals in the OutboundAgentTextKey union above them. By luck, not by design. Replace that union with anything generated and all four drop to CONFIRMED with a live consumer still reading them.

Self-pollution, and why the tests spell keys in segments

textFootprint() greps scripts/ too, so a dotted key written into the test file would make that file a hit for it — the negative controls would stop being reader-less because the test asserting they are reader-less mentioned them. The real-repo controls are therefore joined from segments, and this was verified empirically: with the new test file on disk, console.error.serverUnreachable and console.error.timeout are still CONFIRMED and CONFIRMED is still 115.

Verification — all on fb4ccd348

Whole scripts/__tests__ tree (not a subject-matter subset): Test Files 89 passed (89) · Tests 2512 passed (2512).

Ablations. No build step is involved — vitest loads scripts/*.mjs from source, there is no dist/ for a stale artifact to hide in. Each leg mutated with perl -0pi, the mutation proved on disk by counting the injected and removed text, restored with git checkout HEAD -- against an absolute path, and the restore proved byte-identical (git hash-object equal to the HEAD blob 851663da3 and git diff HEAD empty) before the next reading:

ablationresult
probe always returns null (leg removed)Tests 4 failed, 23 passed — all four detection assertions red, every negative control still green
three-segment guard relaxed to twoTests 2 failed, 25 passed
property-boundary check replaced by plain includesTests 1 failed, 26 passed

The first row is the discrimination proof: removing the leg loses detection and touches nothing on the negative side.

Gates, each exit code captured before any pipe:

  • pnpm type-check:scripts — exit 0
  • pnpm check:control-bytescheck-control-bytes: OK (scanned 5804 tracked text file(s); skipped 85 binary)
  • pnpm check:shell-escape-residueOK (4/4 root(s) resolved ...)
  • pnpm check:vi-mock-specifiersOK (4032 tracked source file(s) ...)
  • pnpm check:esm-specifiers — exit 0
  • node scripts/check-changeset-presence.mjsNo source of a released package changed in this range, so no changeset is owed.No changeset and no label are owed: the guarded surface is derived from published packages and scripts/ is not one.
  • node scripts/check-lint-coverage.mjslint coverage: 46/46 packages linted, 0 with outstanding errors (0 total)
  • eslint, run over the whole population rather than narrowed: eslint --no-inline-config --format json . resolved 4030 files from its own config; both changed files report 0 errors, 0 warnings. The run's own non-zero exit reflects 90 pre-existing errors in files outside the per-package lint scope, unchanged by this diff and confirmed green by the coverage gate above.

Generated by Claude Code


Generated by Claude Code

A consumer that imports a locale PACK OBJECT and reads it by property
access is invisible to both of the sweep's legs at once: there is no
t()/tt() call for the AST pass to classify, and the namespace segment is
bound to a local variable so the full dotted key is never spelled for the
text safety net to grep. Such a key lands in CONFIRMED — the tier
documented as the safest thing to delete — while a shipping screen
renders it.
textFootprint() now probes each candidate's property chain (the key minus
its leading namespace segment, leading dot kept) alongside the full
dotted key, bounded three ways: three segments or more, never two; the
match must end at a property boundary so a leaf does not demote on a
longer sibling; and a chain-only hit is reported with a
"(via property chain)" suffix so the report cannot send a reader grepping
for a dotted key the file does not contain.
Both control groups are pinned as self-tests, on synthetic fixtures and
against this repository, because detection alone does not prove
discrimination.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(i18n): check-i18n-dead-keys reports LIVE keys as CONFIRMED when a consumer reads the pack by property access

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(devx): add a property-chain leg to check-i18n-dead-keys by claude[bot] · Pull Request #6995 · objectstack-ai/objectui · GitHub
Skip to content

fix(devx): add a property-chain leg to check-i18n-dead-keys - #6995

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain
Aug 31, 2026
Merged

fix(devx): add a property-chain leg to check-i18n-dead-keys#6995
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6666

A consumer that imports a locale pack object and reads it by property access is invisible to both of the sweep's legs at once. The AST pass classifies t()/tt() call sites and there is no call; the text safety net greps the full dotted key and the namespace segment is bound to a local variable, so the dotted key is never spelled. The key lands in CONFIRMED — the tier documented as the safest thing to delete — while a shipping screen renders it.

textFootprint() now probes each candidate's property chain (the key minus its leading namespace segment, leading dot kept) alongside the full dotted key, in the same single grep -F pass.

The card's tier claim is stale — measured, and stated rather than repeated

The card measured the five keys at CONFIRMED on 813bf8323. On origin/main at 4357ec754 they are NEEDS-REVIEW, demoted by side effect: PR #6662's retirement pin and ar-participle-orthography-6610.test.ts both spell the dotted keys, which gives them a textual footprint. Per the triage ruling that is a mitigation, not a fix — the defect is unchanged, and the before/after evidence below is therefore built on what the hits are rather than on a tier flip.

before (4357ec754)after (fb4ccd348)
CONFIRMED115115
NEEDS-REVIEW247247
candidates362362
hits naming the real reader LoadingScreen.tsx07 keys

Nothing was demoted; no pre-existing hit was lost. What changed is that seven keys now name the file that actually renders them, marked (via property chain):

  • the five from the card, and
  • console.actions.retry / console.actions.retryingtwo more the card did not list, read by the same file the same way (LoadingScreen.tsx:118,123).

Both negative controls stay CONFIRMED with an empty hit list.

Three boundaries, each pinned by an ablation

  1. Three segments or more, never two. A two-segment key's chain is a single generic word (.ok, .no, .empty). propertyChainProbe() returns null below three segments. Two-segment keys stay on the enumerated importer list, read by hand — which is what refactor(i18n): retire 25 confirmed-dead locale keys across all ten packs #6662 did.
  2. The match must end at a property boundary. Otherwise a leaf would demote on a reader of a longer sibling leaf that it merely prefixes.
  3. A chain-only hit is suffixed (via property chain), so the report never sends a reader grepping for a dotted key that file does not contain.

The importer list is written into the file

Per the ruling, the pack-object importer class is enumerated in the script header with the two-stage grep that re-derives it. The outboundAgentText.ts caveat is stated explicitly: it indexes its subtree dynamically (ai?.[key]), so neither leg sees the read — its four keys land in NEEDS-REVIEW only because those property names happen to appear as string literals in the OutboundAgentTextKey union above them. By luck, not by design. Replace that union with anything generated and all four drop to CONFIRMED with a live consumer still reading them.

Self-pollution, and why the tests spell keys in segments

textFootprint() greps scripts/ too, so a dotted key written into the test file would make that file a hit for it — the negative controls would stop being reader-less because the test asserting they are reader-less mentioned them. The real-repo controls are therefore joined from segments, and this was verified empirically: with the new test file on disk, console.error.serverUnreachable and console.error.timeout are still CONFIRMED and CONFIRMED is still 115.

Verification — all on fb4ccd348

Whole scripts/__tests__ tree (not a subject-matter subset): Test Files 89 passed (89) · Tests 2512 passed (2512).

Ablations. No build step is involved — vitest loads scripts/*.mjs from source, there is no dist/ for a stale artifact to hide in. Each leg mutated with perl -0pi, the mutation proved on disk by counting the injected and removed text, restored with git checkout HEAD -- against an absolute path, and the restore proved byte-identical (git hash-object equal to the HEAD blob 851663da3 and git diff HEAD empty) before the next reading:

ablationresult
probe always returns null (leg removed)Tests 4 failed, 23 passed — all four detection assertions red, every negative control still green
three-segment guard relaxed to twoTests 2 failed, 25 passed
property-boundary check replaced by plain includesTests 1 failed, 26 passed

The first row is the discrimination proof: removing the leg loses detection and touches nothing on the negative side.

Gates, each exit code captured before any pipe:

  • pnpm type-check:scripts — exit 0
  • pnpm check:control-bytescheck-control-bytes: OK (scanned 5804 tracked text file(s); skipped 85 binary)
  • pnpm check:shell-escape-residueOK (4/4 root(s) resolved ...)
  • pnpm check:vi-mock-specifiersOK (4032 tracked source file(s) ...)
  • pnpm check:esm-specifiers — exit 0
  • node scripts/check-changeset-presence.mjsNo source of a released package changed in this range, so no changeset is owed.No changeset and no label are owed: the guarded surface is derived from published packages and scripts/ is not one.
  • node scripts/check-lint-coverage.mjslint coverage: 46/46 packages linted, 0 with outstanding errors (0 total)
  • eslint, run over the whole population rather than narrowed: eslint --no-inline-config --format json . resolved 4030 files from its own config; both changed files report 0 errors, 0 warnings. The run's own non-zero exit reflects 90 pre-existing errors in files outside the per-package lint scope, unchanged by this diff and confirmed green by the coverage gate above.

Generated by Claude Code


Generated by Claude Code

A consumer that imports a locale PACK OBJECT and reads it by property
access is invisible to both of the sweep's legs at once: there is no
t()/tt() call for the AST pass to classify, and the namespace segment is
bound to a local variable so the full dotted key is never spelled for the
text safety net to grep. Such a key lands in CONFIRMED — the tier
documented as the safest thing to delete — while a shipping screen
renders it.
textFootprint() now probes each candidate's property chain (the key minus
its leading namespace segment, leading dot kept) alongside the full
dotted key, bounded three ways: three segments or more, never two; the
match must end at a property boundary so a leaf does not demote on a
longer sibling; and a chain-only hit is reported with a
"(via property chain)" suffix so the report cannot send a reader grepping
for a dotted key the file does not contain.
Both control groups are pinned as self-tests, on synthetic fixtures and
against this repository, because detection alone does not prove
discrimination.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(i18n): check-i18n-dead-keys reports LIVE keys as CONFIRMED when a consumer reads the pack by property access

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(devx): add a property-chain leg to check-i18n-dead-keys by claude[bot] · Pull Request #6995 · objectstack-ai/objectui · GitHub
Skip to content

fix(devx): add a property-chain leg to check-i18n-dead-keys - #6995

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain
Aug 31, 2026
Merged

fix(devx): add a property-chain leg to check-i18n-dead-keys#6995
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6666

A consumer that imports a locale pack object and reads it by property access is invisible to both of the sweep's legs at once. The AST pass classifies t()/tt() call sites and there is no call; the text safety net greps the full dotted key and the namespace segment is bound to a local variable, so the dotted key is never spelled. The key lands in CONFIRMED — the tier documented as the safest thing to delete — while a shipping screen renders it.

textFootprint() now probes each candidate's property chain (the key minus its leading namespace segment, leading dot kept) alongside the full dotted key, in the same single grep -F pass.

The card's tier claim is stale — measured, and stated rather than repeated

The card measured the five keys at CONFIRMED on 813bf8323. On origin/main at 4357ec754 they are NEEDS-REVIEW, demoted by side effect: PR #6662's retirement pin and ar-participle-orthography-6610.test.ts both spell the dotted keys, which gives them a textual footprint. Per the triage ruling that is a mitigation, not a fix — the defect is unchanged, and the before/after evidence below is therefore built on what the hits are rather than on a tier flip.

before (4357ec754)after (fb4ccd348)
CONFIRMED115115
NEEDS-REVIEW247247
candidates362362
hits naming the real reader LoadingScreen.tsx07 keys

Nothing was demoted; no pre-existing hit was lost. What changed is that seven keys now name the file that actually renders them, marked (via property chain):

  • the five from the card, and
  • console.actions.retry / console.actions.retryingtwo more the card did not list, read by the same file the same way (LoadingScreen.tsx:118,123).

Both negative controls stay CONFIRMED with an empty hit list.

Three boundaries, each pinned by an ablation

  1. Three segments or more, never two. A two-segment key's chain is a single generic word (.ok, .no, .empty). propertyChainProbe() returns null below three segments. Two-segment keys stay on the enumerated importer list, read by hand — which is what refactor(i18n): retire 25 confirmed-dead locale keys across all ten packs #6662 did.
  2. The match must end at a property boundary. Otherwise a leaf would demote on a reader of a longer sibling leaf that it merely prefixes.
  3. A chain-only hit is suffixed (via property chain), so the report never sends a reader grepping for a dotted key that file does not contain.

The importer list is written into the file

Per the ruling, the pack-object importer class is enumerated in the script header with the two-stage grep that re-derives it. The outboundAgentText.ts caveat is stated explicitly: it indexes its subtree dynamically (ai?.[key]), so neither leg sees the read — its four keys land in NEEDS-REVIEW only because those property names happen to appear as string literals in the OutboundAgentTextKey union above them. By luck, not by design. Replace that union with anything generated and all four drop to CONFIRMED with a live consumer still reading them.

Self-pollution, and why the tests spell keys in segments

textFootprint() greps scripts/ too, so a dotted key written into the test file would make that file a hit for it — the negative controls would stop being reader-less because the test asserting they are reader-less mentioned them. The real-repo controls are therefore joined from segments, and this was verified empirically: with the new test file on disk, console.error.serverUnreachable and console.error.timeout are still CONFIRMED and CONFIRMED is still 115.

Verification — all on fb4ccd348

Whole scripts/__tests__ tree (not a subject-matter subset): Test Files 89 passed (89) · Tests 2512 passed (2512).

Ablations. No build step is involved — vitest loads scripts/*.mjs from source, there is no dist/ for a stale artifact to hide in. Each leg mutated with perl -0pi, the mutation proved on disk by counting the injected and removed text, restored with git checkout HEAD -- against an absolute path, and the restore proved byte-identical (git hash-object equal to the HEAD blob 851663da3 and git diff HEAD empty) before the next reading:

ablationresult
probe always returns null (leg removed)Tests 4 failed, 23 passed — all four detection assertions red, every negative control still green
three-segment guard relaxed to twoTests 2 failed, 25 passed
property-boundary check replaced by plain includesTests 1 failed, 26 passed

The first row is the discrimination proof: removing the leg loses detection and touches nothing on the negative side.

Gates, each exit code captured before any pipe:

  • pnpm type-check:scripts — exit 0
  • pnpm check:control-bytescheck-control-bytes: OK (scanned 5804 tracked text file(s); skipped 85 binary)
  • pnpm check:shell-escape-residueOK (4/4 root(s) resolved ...)
  • pnpm check:vi-mock-specifiersOK (4032 tracked source file(s) ...)
  • pnpm check:esm-specifiers — exit 0
  • node scripts/check-changeset-presence.mjsNo source of a released package changed in this range, so no changeset is owed.No changeset and no label are owed: the guarded surface is derived from published packages and scripts/ is not one.
  • node scripts/check-lint-coverage.mjslint coverage: 46/46 packages linted, 0 with outstanding errors (0 total)
  • eslint, run over the whole population rather than narrowed: eslint --no-inline-config --format json . resolved 4030 files from its own config; both changed files report 0 errors, 0 warnings. The run's own non-zero exit reflects 90 pre-existing errors in files outside the per-package lint scope, unchanged by this diff and confirmed green by the coverage gate above.

Generated by Claude Code


Generated by Claude Code

A consumer that imports a locale PACK OBJECT and reads it by property
access is invisible to both of the sweep's legs at once: there is no
t()/tt() call for the AST pass to classify, and the namespace segment is
bound to a local variable so the full dotted key is never spelled for the
text safety net to grep. Such a key lands in CONFIRMED — the tier
documented as the safest thing to delete — while a shipping screen
renders it.
textFootprint() now probes each candidate's property chain (the key minus
its leading namespace segment, leading dot kept) alongside the full
dotted key, bounded three ways: three segments or more, never two; the
match must end at a property boundary so a leaf does not demote on a
longer sibling; and a chain-only hit is reported with a
"(via property chain)" suffix so the report cannot send a reader grepping
for a dotted key the file does not contain.
Both control groups are pinned as self-tests, on synthetic fixtures and
against this repository, because detection alone does not prove
discrimination.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(i18n): check-i18n-dead-keys reports LIVE keys as CONFIRMED when a consumer reads the pack by property access

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(devx): add a property-chain leg to check-i18n-dead-keys by claude[bot] · Pull Request #6995 · objectstack-ai/objectui · GitHub
Skip to content

fix(devx): add a property-chain leg to check-i18n-dead-keys - #6995

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain
Aug 31, 2026
Merged

fix(devx): add a property-chain leg to check-i18n-dead-keys#6995
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6666-i18n-dead-keys-property-chain

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6666

A consumer that imports a locale pack object and reads it by property access is invisible to both of the sweep's legs at once. The AST pass classifies t()/tt() call sites and there is no call; the text safety net greps the full dotted key and the namespace segment is bound to a local variable, so the dotted key is never spelled. The key lands in CONFIRMED — the tier documented as the safest thing to delete — while a shipping screen renders it.

textFootprint() now probes each candidate's property chain (the key minus its leading namespace segment, leading dot kept) alongside the full dotted key, in the same single grep -F pass.

The card's tier claim is stale — measured, and stated rather than repeated

The card measured the five keys at CONFIRMED on 813bf8323. On origin/main at 4357ec754 they are NEEDS-REVIEW, demoted by side effect: PR #6662's retirement pin and ar-participle-orthography-6610.test.ts both spell the dotted keys, which gives them a textual footprint. Per the triage ruling that is a mitigation, not a fix — the defect is unchanged, and the before/after evidence below is therefore built on what the hits are rather than on a tier flip.

before (4357ec754)after (fb4ccd348)
CONFIRMED115115
NEEDS-REVIEW247247
candidates362362
hits naming the real reader LoadingScreen.tsx07 keys

Nothing was demoted; no pre-existing hit was lost. What changed is that seven keys now name the file that actually renders them, marked (via property chain):

  • the five from the card, and
  • console.actions.retry / console.actions.retryingtwo more the card did not list, read by the same file the same way (LoadingScreen.tsx:118,123).

Both negative controls stay CONFIRMED with an empty hit list.

Three boundaries, each pinned by an ablation

  1. Three segments or more, never two. A two-segment key's chain is a single generic word (.ok, .no, .empty). propertyChainProbe() returns null below three segments. Two-segment keys stay on the enumerated importer list, read by hand — which is what refactor(i18n): retire 25 confirmed-dead locale keys across all ten packs #6662 did.
  2. The match must end at a property boundary. Otherwise a leaf would demote on a reader of a longer sibling leaf that it merely prefixes.
  3. A chain-only hit is suffixed (via property chain), so the report never sends a reader grepping for a dotted key that file does not contain.

The importer list is written into the file

Per the ruling, the pack-object importer class is enumerated in the script header with the two-stage grep that re-derives it. The outboundAgentText.ts caveat is stated explicitly: it indexes its subtree dynamically (ai?.[key]), so neither leg sees the read — its four keys land in NEEDS-REVIEW only because those property names happen to appear as string literals in the OutboundAgentTextKey union above them. By luck, not by design. Replace that union with anything generated and all four drop to CONFIRMED with a live consumer still reading them.

Self-pollution, and why the tests spell keys in segments

textFootprint() greps scripts/ too, so a dotted key written into the test file would make that file a hit for it — the negative controls would stop being reader-less because the test asserting they are reader-less mentioned them. The real-repo controls are therefore joined from segments, and this was verified empirically: with the new test file on disk, console.error.serverUnreachable and console.error.timeout are still CONFIRMED and CONFIRMED is still 115.

Verification — all on fb4ccd348

Whole scripts/__tests__ tree (not a subject-matter subset): Test Files 89 passed (89) · Tests 2512 passed (2512).

Ablations. No build step is involved — vitest loads scripts/*.mjs from source, there is no dist/ for a stale artifact to hide in. Each leg mutated with perl -0pi, the mutation proved on disk by counting the injected and removed text, restored with git checkout HEAD -- against an absolute path, and the restore proved byte-identical (git hash-object equal to the HEAD blob 851663da3 and git diff HEAD empty) before the next reading:

ablationresult
probe always returns null (leg removed)Tests 4 failed, 23 passed — all four detection assertions red, every negative control still green
three-segment guard relaxed to twoTests 2 failed, 25 passed
property-boundary check replaced by plain includesTests 1 failed, 26 passed

The first row is the discrimination proof: removing the leg loses detection and touches nothing on the negative side.

Gates, each exit code captured before any pipe:

  • pnpm type-check:scripts — exit 0
  • pnpm check:control-bytescheck-control-bytes: OK (scanned 5804 tracked text file(s); skipped 85 binary)
  • pnpm check:shell-escape-residueOK (4/4 root(s) resolved ...)
  • pnpm check:vi-mock-specifiersOK (4032 tracked source file(s) ...)
  • pnpm check:esm-specifiers — exit 0
  • node scripts/check-changeset-presence.mjsNo source of a released package changed in this range, so no changeset is owed.No changeset and no label are owed: the guarded surface is derived from published packages and scripts/ is not one.
  • node scripts/check-lint-coverage.mjslint coverage: 46/46 packages linted, 0 with outstanding errors (0 total)
  • eslint, run over the whole population rather than narrowed: eslint --no-inline-config --format json . resolved 4030 files from its own config; both changed files report 0 errors, 0 warnings. The run's own non-zero exit reflects 90 pre-existing errors in files outside the per-package lint scope, unchanged by this diff and confirmed green by the coverage gate above.

Generated by Claude Code


Generated by Claude Code

A consumer that imports a locale PACK OBJECT and reads it by property
access is invisible to both of the sweep's legs at once: there is no
t()/tt() call for the AST pass to classify, and the namespace segment is
bound to a local variable so the full dotted key is never spelled for the
text safety net to grep. Such a key lands in CONFIRMED — the tier
documented as the safest thing to delete — while a shipping screen
renders it.
textFootprint() now probes each candidate's property chain (the key minus
its leading namespace segment, leading dot kept) alongside the full
dotted key, bounded three ways: three segments or more, never two; the
match must end at a property boundary so a leaf does not demote on a
longer sibling; and a chain-only hit is reported with a
"(via property chain)" suffix so the report cannot send a reader grepping
for a dotted key the file does not contain.
Both control groups are pinned as self-tests, on synthetic fixtures and
against this repository, because detection alone does not prove
discrimination.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(i18n): check-i18n-dead-keys reports LIVE keys as CONFIRMED when a consumer reads the pack by property access

2 participants

@zhuangjianguo@claude