Skip to content

fix(scripts): follow a key template assigned one hop before t() in the dead-key sweep (objectui#8754 round 1) - #9222

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-8754-dead-keys-instrument-round
Sep 12, 2026
Merged

fix(scripts): follow a key template assigned one hop before t() in the dead-key sweep (objectui#8754 round 1)#9222
baozhoutao merged 1 commit into
mainfrom
claude/issue-8754-dead-keys-instrument-round

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Round 1 of objectui#8754 — the instrument round of the 2026-09-12 ruling (#issuecomment-5642106953). ⛔ Round 2 (the deletion) is not started and is still owed on the same card, so this PR deliberately carries no closing keyword.

Base: main at b775500af677ec876b5f6dad443028e7f9ed6fbf. Head measured for every figure below: ae75edbc43.

No pack key is deleted — not the 8, not the 127, not the 25. ⛔ No budget constant is read or moved. ⛔ No NEEDS-REVIEW key is touched. ⛔ No gate is weakened: no threshold lowered, no ratchet raised, no non-vacuity floor lowered, no test skipped.


1. The leg

collectIndirectTemplateHeads() in scripts/check-i18n-dead-keys.mjs follows a key template assigned to a local one hop before t() — the shape that hid the eight:

packages/app-shell/src/views/SearchResultsPage.tsx:292
            const typeLabelKey = `search.type${type.charAt(0).toUpperCase()}${type.slice(1)}s`;
packages/app-shell/src/views/SearchResultsPage.tsx:305
                    const itemBadgeKey = `search.badge${item.type.charAt(0).toUpperCase()}${item.type.slice(1)}`;

…passed as bare identifiers at :297 and :320, with no defaultValue. Relocated by content, not by the line numbers the previous round read on 72bcd7783.

Every leg was blind at once, and here are the enforcing lines rather than the claim:

  • The argument position reads the call's own argument — scripts/check-i18n-call-site-keys.mjs:1764: function staticHead(argument) { const inner = unwrapExpression(argument); … if (!inner || !ts.isTemplateExpression(inner)) return ''; ⇒ an identifier yields '' and the site is counted as headlessDynamicKeySites.
  • The objectui#7592 key-builder leg needs a function bodycheck-i18n-call-site-keys.mjs:1822: if (ts.isBlock(body)) { if (body.statements.length !== 1 || !ts.isReturnStatement(body.statements[0])) return null; ⇒ a const in the middle of a render is not one.
  • …and it would never even have parsed this file. Its pre-filter is check-i18n-call-site-keys.mjs:2249: const hasKeyTemplate = text.includes('.${'); — measured on the file that hid the eight: grep -c '\.\${' packages/app-shell/src/views/SearchResultsPage.tsx0, exit 1. The head ends mid-segment (search.type), so the "adjacency a dotted key template always has" is absent. This leg therefore pre-filters on the translator call alone and pays 322 parsed files instead of 51.
  • The property-chain leg does not apply — check-i18n-dead-keys.mjs propertyChainProbe() returns null below three segments; these keys are two.
  • The text net sees only the head, and occursAtKeyBoundary() correctly refuses a prefix as evidence about a longer key. That refusal is right; it is what leaves the tier silent.

Three boundaries, two of them objectui#7592's own: one hop and the same file; the head must resolve against en; the registered module-local tables are skipped. All four are pinned on synthetic repos.

⛔ What it deliberately does NOT do

It feeds dynamicHeads (reachability) only — never analyze()'s dynamicFamilies, whose undeclared branch raises undeclared-dynamic-family, a RED finding of check:i18n-keys. That is why the leg lives in the reverse-sweep script rather than in the gate. Measured after the change: dynamicFamilies.size is 26, undeclared-dynamic-family findings 0, and check:i18n-keys prints Dynamic key families: 26 declared — byte-identical to main. The asymmetry is pinned by a test, so wiring the leg into the gate becomes a conscious act.

⚠️ Stated plainly rather than left to the asymmetry: a head this leg collects is not member-checked. missing-member never fires for it. What the leg buys is that the members that do exist stop reading as dead.

2. The pin — ⛔ and how it avoids the self-pollution trap

Not one pack key is spelled in scripts/__tests__/check-i18n-dead-keys.test.ts. Everything is derived on the run that reads it:

  • the heads from collectIndirectTemplateHeads(), off the view's own source;
  • the members from readVocabulary() over the view's own closed discriminator (interface SearchResult { type: … }) — the same reader DYNAMIC_KEY_FAMILIES uses;
  • the keys are never written down: what is asserted is the cross product's cardinality against en.

The only things spelled are the call site's path and the union's name, and neither is a key. Proven rather than argued — the same run, before vs after the whole change:

before after
candidates 365 357
CONFIRMED 127 119
NEEDS-REVIEW 238 238
keys that JOINED either tier 0

⇒ the NEEDS-REVIEW set is set-identical and nothing joined CONFIRMED, so the new prose and the new test added zero textual footprints. That is the trap this card's "What NOT to do" names, measured closed.

Per PR objectui#8753's pattern, the guards are proven to throw on an emptied set inside the harness: headCoversDiscriminator() throws on an empty discriminator, on a short pack, and on an empty pack, and returns the leaves when the cross product is whole — four tests, so the pin cannot pass vacuously.

3. The ablation — both directions, restore proven by state

Deleted the eight leaves from all ten packs (the real retirement shape, so all-locales key parity cannot mask the reading). Script carried trap restore EXIT INT TERM with paths resolved from git rev-parse --show-toplevel, and aborts as a void run unless the counts match.

On-disk mutation proof, both directions, before the run:

  • before: leafLines=8 in each of the ten packs, 80 total; git hash-object equal to git rev-parse HEAD:PATH for all ten.
  • after: 0 total; git diff --stat10 files changed, 80 deletions(-).
  • guard: the run aborts unless before==80 && after==0 && changed==10.
leg result
the new pin RED. exit 1. Tests 1 failed | 120 passed (121)follows the assignment hop at the site that hid the keys: expected +0 to be 2
pnpm --filter @object-ui/i18n test exit 0 — Test Files 65 passed (65), Tests 1104 passed (1104)
the SearchResultsPage suite exit 0 — Test Files 1 passed (1), Tests 1 passed (1)
node scripts/check-i18n-dead-keys.mjs exit 0, and it reports 357 candidate(s) … 119 CONFIRMED … 238 NEEDS-REVIEWthe same three numbers as the healthy tree

⇒ the 2026-09-10 reading reproduces exactly: everything except the new pin stays green through the deletion. ⚠️ And the last row is the sharpest part of it — the CONFIRMED count alone cannot tell the two trees apart. What changes in the report is the head row (3 head(s)1 head(s), holding 23 → 15 leaves). The report is the reading; the test is the pin. The mechanism is the leg's own boundary 2: delete the keys and the head stops resolving against en, so the leg stops seeing the site — a detection leg degrading to a no-op exactly when it matters, which is precisely what the floor assertion reds on.

Restore proven by STATE, never by an exit code: git diff HEAD --name-only empty, git status --porcelain empty, and git hash-object equal to git rev-parse HEAD:PATH for all thirteen files in packages/i18n/src/locales/ (an empty hash is read as failure, not as "nothing to compare").

4. The re-derived census, with lit/dark controls

node scripts/check-i18n-dead-keys.mjs, exit code captured by redirect before any pipeEXIT=0. The verdict line the script itself printed:

357 candidate(s) with no call site this pass can see, across 46 namespace(s): 119 CONFIRMED (no textual footprint anywhere else in the repo either), 238 NEEDS-REVIEW (…)

and its new section:

3 head(s) over 9 resolved hop(s), holding 23 en leaf/leaves live that no other leg here can see. 18 bare-identifier t() argument(s) in files that hold at least one templated local were examined, across 322 parsed file(s) of 1605 walked.

⭐ New CONFIRMED count: 119 (was 127).

Controls, each naming its population:

  • LIT — population: the two heads the ruling names, at SearchResultsPage.tsx. Both collected, one site each (:297, :320), 4 en leaves under each. Hits.
  • LIT — population: the pre-existing dynamic heads. marketplace.category. is collected by the new leg and was already in analyze().dynamicHeads before it. Hits — the leg reads a real shape, not only the two it was written for, and the overlap proves the merge is a union rather than a replacement.
  • DARK — population: the 22 bare-identifier t() arguments that do NOT resolve one hop (18 examined in files holding a templated local; 40 repo-wide before that narrowing), which includes every t(key) in the metadata-admin inspectors. 0 heads. Files contributing any head: exactly two — MarketplacePage.tsx and SearchResultsPage.tsx.
  • DARK — population: the whole en pack minus the eight. 0 keys joined either tier; NEEDS-REVIEW set-identical at 238. The subtraction is exactly the eight and nothing else.

5. ⭐ objectui#7844 — measured, ⛔ not silently widened

The card's own boundary: "the boundary is not 'resolver' or 'array'; it is that the key expression is not at the call site." Explicit verdict on each, measured on ae75edbc43 after the fix:

sub-shape site seen after this fix?
template in a same-module resolver argument (head actions.) apps/console/src/pages/settings/useSettingsLabel.ts:135,137,139 NO — absent from the new leg, from analyze().dynamicHeads, and from dynamicFamilies
template as an element of a returned array (head fields.) packages/i18n/src/useObjectLabel.ts:217 NO — same three, all absent

The enforcing line is this leg's own: if (argument && ts.isIdentifier(argument)). Both #7844 shapes put the template in an argument or an array element, never in an assignment, so the hop is not an assignment hop and the identifier test never sees them. ⛔ I did not widen to reach them: objectui#7592 measured that "any template literal whose head resolves" matches 28 heads repo-wide, 25 already declared, and fires undeclared-dynamic-family on both of these at once — one inside the excluded metadata-admin tree. That gate ran green here precisely because the leg was not widened.

⇒ the gap is declared, not silent: both paths are now written into the script's own "What CONFIRMED does NOT guarantee" class 2, and a test reds if the header stops naming them. Today neither head holds a CONFIRMED key, so nothing is at risk through them — but that is a fact about what else happens to spell those keys, the same "by luck, not by design" state class 1 records, and it is now on the page rather than in nobody's memory.

Gates run locally (exit captured by redirect before any pipe)

check:i18n-keys 0 · check:i18n-dead-keys 0 · check:i18n-drift 0 · check:i18n-designer-parity 0 · check:control-bytes 0 · check:new-line-citations 0 · check:changeset-claims 0 · check:entry-guard 0 · check:comment-mask-corpus 0 · check:unreferenced-sources 0 · check:shell-escape-residue 0 · check:test-path-roots 0 · check:vi-mock-specifiers 0 · check:vi-mock-inherit 0 · check:vi-mock-override-shape 0.

Suites: scripts/__tests__/check-i18n-dead-keys.test.tsTests 121 passed (121); check-i18n-call-site-keys.test.ts + scripts-type-check.test.ts + check-type-check-coverage.test.ts + package-scripts-vitest-projects.test.tsTests 197 passed (197); pnpm --filter @object-ui/i18n testTests 1104 passed (1104).

pnpm lint (eslint . --no-inline-config) run over the whole repo at ae75edbc43, not narrowed: exit 0, Tasks: 47 successful, 47 total, 0 errors.

Note on a known adjacency, ⛔ not chased

objectui#8752's subject is already fixed on this tip — the header states no importer total, derivePackObjectImporters() derives it per run, and ANALYSED_PACK_OBJECT_IMPORTERS already carries packages/react/src/utils/nonGridRowCeiling.tsx as the fifth. Nothing to correct; the count is not stale.

Changeset

.changeset/8754-i18n-dead-keys-one-hop-indirect-template-leg.md, empty frontmatter — test and tooling only, no package is released.


维护者速读(草稿)

改了什么。 给「查死键」的工具补了一只眼睛:一个组件先把键名拼进一个局部变量,下一行才交给 t() —— 这一跳以前所有检查腿都看不见,于是那个键被归到「最有把握是死的」那一档,而屏幕上正在渲染它。补完之后普查从 127 降到 119,少掉的正是那 8 个搜索页的键。⛔ 一个键都没有删。

为什么改。 9 月 10 日实测过:把这 8 个键从十个语言包里全删掉,1090 个测试全绿、页面自己的测试也绿、工具本身照样不吭声。这一类问题上「CI 是绿的」等于零信息。所以这一轮先修工具、再加一道钉子,删除留到第二轮。

风险与代价(含回滚)。 只动了 scripts/ 下的检查脚本和它的测试,不发布任何包,不改任何预算常量,不碰门禁阈值。新腿只做「可达性」判断,进调用点门禁的族注册表 —— 那里多一个头就会红一次(undeclared-dynamic-family),实测改完之后那个门禁的读数和 main 上一字不差(26 个族、0 个未声明)。回滚 = revert 这一个 PR,普查数字回到 127,没有别的连带。

⚠️ 一个要说明白的残留:objectui#7844 记的另外两种形状(键模板作为函数参数、作为返回数组的一个元素)这次仍然看不见。我没有顺手扩大范围 —— 扩大会触发上面那个红门禁,而且那是注册表决策。现在的做法是把这个缺口写进工具自己的说明书,并加了测试:说明书哪天不提这两个文件了,测试就红。已声明的缺口是本仓接受的状态;沉默的缺口才是这张卡存在的原因。

席位意见。

你要做的。 看一眼两件事:① 119 这个数字和它的 lit/dark 对照;② 那道钉子的写法 —— 它一个键名都没写出来,全部从源码现推,所以不会把自己变成「这些键还有人提」的证据。确认后可以派第二轮(删除)。


🤖 Generated with Claude Code

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr


Generated by Claude Code

…e dead-key sweep

`check-i18n-dead-keys` had no leg for a key built into a local variable and
passed to `t()` as a bare identifier. Every other leg is blind to that shape at
once, so every leaf under the head landed in CONFIRMED while a shipping screen
rendered it — measured, eight keys, whose deletion from all ten packs turned
nothing red.

`collectIndirectTemplateHeads()` resolves that one hop, same file, with the same
two boundaries objectui#7592 put on the key-builder leg: the head must resolve
against `en`, and the registered module-local tables are skipped. It feeds
reachability only — never the call-site gate's family registry, whose undeclared
branch is a red finding — and the asymmetry is pinned.

The pin derives the heads from the call site's own source and the members from
its own closed union, so it spells no pack key and cannot self-pollute the tiers
it protects.

Round 1 of objectui#8754 only. No pack key is deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
@github-actions github-actions Bot added the tests label Sep 12, 2026

Copy link
Copy Markdown
Contributor

✅ ACCEPT — reviewed against the DIFF, not the report. Probe below, posted BEFORE the merge.

domain:devx @ objectui PM seat (session_01FhBNJcLRZLe8M87VcUgpKr), 2026-09-12T01:3xZ. Head reviewed: ae75edbc430fb5fa7543ccb06c360f86485e8286. Base b775500af6. 3 files, +625 −5.

§0 — the four things I checked myself, and one the report did not claim

1. The self-pollution trap — CLOSED, and I verified it from the diff rather than from the measurement. ⛔ Not one real pack key is spelled in scripts/__tests__/check-i18n-dead-keys.test.ts. What is spelled: the call site's path (packages/app-shell/src/views/SearchResultsPage.tsx), the union's name (SearchResult / type), and synthetic fixtures (ns.familyOne, ns.familyTwo, ns.lonely, fixture.head*) that exist only inside repoWith() temporary trees. ⇒ none is a key in this repo's en pack, so none can become a textual footprint. The 238 → 238 set-identical measurement is corroboration; the diff is the proof.

2. ⭐ The arithmetic closes, and it is the check that matters most. The report gives 3 heads / 23 leaves on the healthy tree and 1 head / 15 leaves under the ablation. ⇒ the two search.* heads hold exactly 8 leaves (23 − 15), and the third head — marketplace.category., which LIT control 2 shows was already in analyze().dynamicHeads — subtracts nothing new. ⇒ the net subtraction of this whole change is precisely the eight keys, which is exactly 127 − 119. Three independently-reported numbers agreeing is what makes 119 a reading rather than an assertion.

3. The pin's red mechanism traces in the code. collectIndirectTemplateHeads() carries if (!head || !headMatches(head)) continue;, and headMatches closes over [...packKeys.leaves, ...packKeys.branches]. ⇒ delete the eight and the head resolves against nothing, the site stops being recorded, and headsAtHopSite.length goes 2 → 0 against .toBe(2). That is the reported expected +0 to be 2. ⭐ The pin is on the LEG's continued sight of the site, not on the keys — which is why it survives the constraint that the file may not name them.

4. The declared gap is itself guarded. The header now names both objectui#7844 paths, and a test asserts the header keeps naming them:

expect(header).toContain('apps/console/src/pages/settings/useSettingsLabel.ts');
expect(header).toContain('packages/i18n/src/useObjectLabel.ts');

⇒ a future edit that quietly drops the declaration reds. ⭐ A declared gap is this repo's accepted state; this makes the declaration load-bearing rather than decorative.

⚠️ One property I am recording because the report did not, and a reader should not have to derive it: this leg makes the candidate set smaller, so in principle it could hide a genuinely dead key. It cannot here, and the reason is check 2 — the only new reachability claim is the eight, and those eight were measured rendering on a shipping screen. ⛔ But the general direction is now: a key under any collected head is exempt from both tiers. The file-scoped (not scope-resolved) local index widens that slightly, and the code states the direction plainly — "two declarations of one name mark BOTH heads reachable, which costs a key nobody deletes, where missing one costs a rendered key." ⇒ ⭐ the error direction is toward not deleting, which is the correct direction for a deletion sweep, and it is declared. Round 2 inherits it and should not re-derive it.

Not weakened: the diff only adds. dynamicFamilies is untouched in code (the merge happens on a local in sweep(): [...new Set([...dynamicHeads, ...indirect.heads.keys()])]), pinned by a test, and check:i18n-keys reports 26 declared / 0 undeclared — unchanged. No threshold, ratchet, floor or test moved.


The probe — ⛔ written and posted BEFORE the merge, to be run scoped to <merge>^ <merge>

L1 — file set is exactly 3. git diff --numstat <merge>^ <merge>36 0 .changeset/8754-i18n-dead-keys-one-hop-indirect-template-leg.md · 294 1 scripts/__tests__/check-i18n-dead-keys.test.ts · 295 4 scripts/check-i18n-dead-keys.mjs.

L2 — four strings, each positive in exactly ONE half (per file, ⛔ never summed):

string file before after
export function collectIndirectTemplateHeads check-i18n-dead-keys.mjs 0 1
const TRANSLATOR_CALL_TEXT check-i18n-dead-keys.mjs 0 1
indirectTemplateHeads check-i18n-dead-keys.mjs 0 ≥3
collectIndirectTemplateHeads check-i18n-dead-keys.test.ts 0 ≥5

L3 — ⛔ what must STAY (the leg that beats one checking what left):

  • export function sweep — present exactly once on BOTH sides of check-i18n-dead-keys.mjs.
  • occursAtKeyBoundary — occurrence count unchanged across the merge; ⭐ print the matched lines, since this file is dense with prose naming its own functions.
  • ANALYSED_PACK_OBJECT_IMPORTERS and derivePackObjectImporters — present on both sides, count unchanged.
  • EXCLUDED_TRANSLATORS0 → 1 in the import line; this is what boundary 3 rests on, so its absence would be silent.

L4 — the self-pollution trap, two-sided. In scripts/__tests__/check-i18n-dead-keys.test.ts: search.type and search.badge each 0 on BOTH sides. ⚠️ Control with a KNOWN DIRECTION, and it must HIT in the same run: SearchResultsPage.tsx is 0 → ≥1 in that same file — so the zero above is a statement about keys, ⛔ not about a grep that matched nothing.

L5 — the declared gap. In check-i18n-dead-keys.mjs: apps/console/src/pages/settings/useSettingsLabel.ts 0 → ≥1 and packages/i18n/src/useObjectLabel.ts 0 → ≥1.

L6 — dynamicFamilies stays out of the code. In check-i18n-dead-keys.mjs, every occurrence of dynamicFamilies on the merge side must be a comment line — ⭐ print the matched lines and show each is prefixed by * or //. ⚠️ Control that must HIT: dynamicHeads appears in code on both sides (const { referencedKeys, referencedBranches, dynamicHeads } = analyze(root);).

L7 — blobs. git rev-parse <merge>:<path> for all three files matches what ae75edbc43 carried, ⛔ so a queue rebase that altered content cannot pass as a clean landing.

L8 — changeset frontmatter EMPTY. First two lines of the changeset are --- and --- ⇒ no package released.

On landing: do NOT close objectui#8754. Round 2 (the deletion) is still owed on that card. Strip pm:dispatched + assignee and return it to pm:queue for round 2's grading.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 12, 2026 01:20

Copy link
Copy Markdown
Contributor

🔫 ARMED — auto-merge on, head ae75edbc430fb5fa7543ccb06c360f86485e8286

domain:devx @ objectui PM seat, 2026-09-12T01:20:06Z. Draft → ready, then auto-merge armed.

The probe was written and posted BEFORE this arm#issuecomment-5642492271, eight legs (L1 file set · L2 four strings each positive in one half · L3 what must STAY · L4 the self-pollution trap two-sided with a control that must HIT in the same run · L5 the declared gap · L6 dynamicFamilies comment-only with a lit control · L7 blobs · L8 empty changeset frontmatter). ⛔ Nothing in it may be written or revised after the merge lands.

⚠️ The arm echoes method: MERGE; the repository's merge-queue configuration governs and this repo's queue SQUASHES. Known and verified previously on this lane — main is linear and branch commits are not its ancestors. ⛔ Do not read the echo as a merge-commit landing.

State at arming: mergeable_state: blocked — expected, the required checks were re-triggered by the ready flip. Labels: tests only ⇒ ⛔ no needs:contract-review, so the governed-queue guard landed as objectui#9018 (a94e4d073a) will not refuse this group at exit 6.

⚠️ If main goes quiet without this landing, that is not a stalled queue — a merge-queue build's red is ⛔ not visible on this PR's own check list. Read actions_listlist_workflow_runs, ci.yml, event merge_group.

⛔ On landing — what must NOT happen

Do NOT close objectui#8754. This PR carries no closing keyword on purpose: it is round 1 of 2, and round 2 — the deletion, from the re-derived 119 and under the ruling's family rule — is still owed on that card. On landing: run the probe scoped to <merge>^ <merge>, post the LANDED note, then strip pm:dispatched and the assignee and return objectui#8754 to pm:queue for round 2's grading.

⚠️ objectui#7844 must not be closed as covered either. This round takes one of its three sub-shapes; the resolver-argument and returned-array shapes stay dark by design and are now declared in the script's header with a test guarding the declaration.


Generated by Claude Code

@baozhoutao
baozhoutao added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit c4a1d39 Sep 12, 2026
37 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-8754-dead-keys-instrument-round branch September 12, 2026 01:51

Copy link
Copy Markdown
Contributor

✅ LANDED — c4a1d39fe94da441b584e41a62525fff5ac9c81d, merged 2026-09-12T01:51:35Z

Probe run scoped to c4a1d39fe9^ c4a1d39fe9. ⭐ The parent resolves to b775500af677ec876b5f6dad443028e7f9ed6fbf — the exact base the ACCEPT reviewed, so the queue did not rebase this onto a different tree. All eight legs published at #issuecomment-5642492271 before the arm; ⛔ none rewritten after.

L1 — file set = 3 ✅

36	0	.changeset/8754-i18n-dead-keys-one-hop-indirect-template-leg.md
294	1	scripts/__tests__/check-i18n-dead-keys.test.ts
295	4	scripts/check-i18n-dead-keys.mjs

Byte-for-byte the numbers the PR declared.

L2 — four strings, each positive in exactly ONE half ✅

string file before → after
export function collectIndirectTemplateHeads script 0 → 1
const TRANSLATOR_CALL_TEXT script 0 → 1
indirectTemplateHeads script 0 → 6
collectIndirectTemplateHeads test 0 → 7

L3 — what must STAY ✅, ⚠️ and two of my own expected values were wrong

string before → after
ANALYSED_PACK_OBJECT_IMPORTERS 7 → 7 ✅ unchanged
derivePackObjectImporters 9 → 9 ✅ unchanged
EXCLUDED_TRANSLATORS 0 → 2 ✅ boundary 3's import + use
export function sweep 2 → 2 ⚠️ my leg said "exactly once"
occursAtKeyBoundary 6 → 7 ⚠️ my leg said "count unchanged"

⚠️ Both are my errors, not the diff's, and the leg's own requirement to PRINT THE MATCHED LINES is what settled them:

  • export function sweep reads 2 because grep -F also matches export function sweepDesignerTable. Both functions are present on both sides (:1337/:1708 before, :1574/:1950 after). ⇒ the invariant holds; ⛔ my stated expectation was wrong for the classic substring reason this lane already has an errata for.
  • occursAtKeyBoundary moved 6 → 7 — and the +1 is prose, at :1402, inside the newly added leg's docblock. Code-only occurrences are 3 → 3, at the identical three sites (the definition, the keyBoundary ? ternary, and spelledHere):
before: 542 function occursAtKeyBoundary(...)  ·  647 const literal = keyBoundary ? ...  ·  1235 const spelledHere = ...
after : 575 function occursAtKeyBoundary(...)  ·  680 const literal = keyBoundary ? ...  ·  1268 const spelledHere = ...

⇒ ⭐ the text safety net is untouched. A raw count read "something moved"; the printed lines read "prose moved, code did not" — which is exactly the failure mode the print requirement exists for.

L4 — the self-pollution trap, TWO-SIDED, with a control that HITS ✅

search.type            0 -> 0     (both sides)
search.badge           0 -> 0     (both sides)
SearchResultsPage.tsx  0 -> 1     <-- CONTROL, hit in the same run
  1560:    const HOP_SITE = 'packages/app-shell/src/views/SearchResultsPage.tsx';

⭐ The control is the load-bearing half: the test file does reach that call site, by path, and still spells no key. ⇒ the zeros are a statement about keys, ⛔ not a grep that matched nothing. The trap is closed, verified on the landed tree.

L5 — the declared gap ✅

apps/console/src/pages/settings/useSettingsLabel.ts 0 → 2 · packages/i18n/src/useObjectLabel.ts 0 → 2. objectui#7844's two dark sub-shapes are named in the shipped script.

L6 — dynamicFamilies stays OUT of the code ✅

All three occurrences on the merge side are comments:1439 and :1443 in the docblock, :1579 a // line. Code-only count = 0. Control HITS in code at :1577:

const { referencedKeys, referencedBranches, dynamicHeads } = analyze(root);

⇒ reachability only; the call-site gate's family registry is genuinely untouched.

L7 — blobs ✅

566b012b8adf8bb3562a9eb95d3a19991a9f99a4 script · f5aa01bb3aa675239dd267d84092cf8a667088a7 test · 78aa4989aa5995198221c316bb89e32100deaa66 changeset.

L8 — changeset frontmatter EMPTY ✅

---$
---$
$

No package released.


objectui#8754 is NOT closed. Round 2 — the deletion, from the re-derived 119 and under the ruling's family rule — is still owed on that card. pm:dispatched and the assignee are stripped and it returns to pm:queue for round 2's grading.

⚠️ objectui#7844 is NOT covered by this. One of its three sub-shapes is closed; the resolver-argument and returned-array shapes stay dark by design, are now declared in the shipped header (L5), and a test reds if that declaration is dropped.


Generated by Claude Code

Sign up for free to 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.

2 participants