fix(scripts): report every dynamic head the i18n dead-key pack sweep applies - #9312
Conversation
…applies `check-i18n-dead-keys.mjs` sweeps two corpora with the same mirror question, and the two halves disagree about how wide a template head may be. The designer half refuses a head with no segment of its own, drops it from the applied set, and prints a loud `Dynamic head(s) with no segment of their own, NOT applied`. The pack half applies every collected head unfiltered — and reported nothing about it, so a key held live merely by starting with some head was indistinguishable from a key nothing reaches. The cost is the one objectui#9126 names: an under-reporting sweep reads as evidence of cleanliness. Today's run prints 358 candidates across 46 namespaces, and a reader has no way to learn that 375 further keys were never offered, that 28 heads account for them, or that three heads naming only a top-level namespace hold 196 of those on their own. So `sweep()` now returns one row per APPLIED head — the head, how many segments it names of its own, every `en` leaf under it, and how many of those no other leg already keeps live — and the CLI prints the table plus a line naming the namespace-wide heads. `--json` carries the same rows as `appliedDynamicHeads`. ⛔ Reporting only. The rows are computed AFTER `candidates`, from the same unfiltered head list, and nothing feeds back: the candidate set is byte-identical before and after (358, same `confirmed`/`needsReview` lists, same designer-table output). No filtering is added at any depth and the designer half's `MIN_HEAD_SEGMENTS` is deliberately NOT adopted for the packs — their namespaces are shaped differently and that number is the maintainer's call. No key, tier or retirement is touched, and no claim is made that any of the three heads is wrong: each comes from a real call site building a key from a runtime value. The row measures the head's reach, not its correctness. Tests pin both directions: the rows themselves on a synthetic pack, and the negative control that the namespace-wide head is still applied — plus repository-scale pins that every applied head gets a row, that every row describes a subtraction that really happened, and that the threshold stays unadopted. Part of #9126 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
ACCEPT — reviewed against the DIFF. Flipped ready; ⛔ not armed. Probe below, posted BEFORE any merge.
⭐ The acceptance criterion fired, and the round answered it the right wayTriage's criterion 1 required the re-derived heads to match
⭐ This is the criterion working as designed. A stale acceptance number is a prediction, and the correct response to a mismatch is to re-derive the prediction's own tree — ⛔ not to move the code until it agrees. ⭐ The head table reproduces exactly
Held still sums to 196. Nothing tuned. The central fence, verified in the file rather than in the report⛔ 「Do not add filtering to the pack half」 is the one way this card could have gone wrong. The new code does contain a ⇒ ⭐ The ablation reproduced the card's headline figure independentlyInjecting the forbidden filter on ⭐ Pre-merge probe — counted on base
|
| leg | base | head | role |
|---|---|---|---|
appliedHeads |
0 | 4 | must ARRIVE |
ownSegments |
0 | 3 | must ARRIVE |
⭐ MIN_HEAD_SEGMENTS |
2 | 2 | must STAY — the designer threshold was ⛔ NOT adopted for the packs |
⭐ NOT applied |
1 | 1 | must STAY — the designer half's loud refusal line is untouched |
⭐ headHeldCounts |
4 | 4 | must STAY — designer half untouched |
⭐ wideHeads |
8 | 8 | must STAY — designer half untouched |
⭐ Four of the six legs are must-STAY, and that is the right shape for this card: the deliverable is visibility only, so almost everything interesting is what must NOT have moved. A probe weighted toward arrivals would have passed happily on a diff that also "tidied" the designer half — which triage explicitly forbade.
State
Changeset: A (new file), genuine empty frontmatter — ⛔ not a patch bump, and the pre-existing .changeset/*.md files are untouched. Gates all exit 0, tests 122 → 133.
Flipped draft → ready. ⛔ Not armed — the flip adds Governed Surface Queue Guard, so pre-flip green is stale; CI is re-read on d1ce8e9f85 after the flip and every check must be green, ⛔ not the required subset. 9ae871d004; ⛔ re-confirm against the ACTUAL merge parent before reading the probe as passed.
Part of #9126 — a non-closing reference, so it cannot close the card, and correcting it would require an amend, which is forbidden on a pushed branch. ⛔ Left as-is deliberately.
Generated by Claude Code
ARMED — in the merge queue at 17:51:30Z. ⛔ Legs frozen as posted before arming.
Green re-read AFTER the flip, waiting for every check rather than the required subset: ALLGREEN at 17:51:24Z, 0 failing, 0 pending. ( Queue entry verified by the EVENT — ⭐ errata 62b — clean, no stacking. Queue ref The probe ( Generated by Claude Code |
Part of #9126
The pack half of
check-i18n-dead-keys.mjsapplies every dynamic template head it collects, unfiltered, and reported nothing about it. Its sibling corpus in the same file refuses a head with no segment of its own, drops it from the applied set, and prints a loudDynamic head(s) with no segment of their own, NOT applied. The pack half neither filtered nor reported, so a key held live merely by starting with some head was indistinguishable from a key nothing reaches — and the candidate count read as evidence of cleanliness.This PR changes no verdict. It makes the pack half say what it is already doing.
⛔ The fences, and what was done instead
appliedHeadsis computed aftercandidates, from the same unfiltered head list. Nothing feeds back.MIN_HEAD_SEGMENTSownSegmentsis a printed column, never a predicate. The threshold stays the maintainer's call.confirmed/needsReviewlists are byte-identical before and after.⭐ The lit control — the candidate set did not move
Both runs are
node scripts/check-i18n-dead-keys.mjs --jsonon this branch's tip, before and after the change:The card's reading is two days old and the file has moved twice. Rather than tune anything, the card's own tree was re-run: a throwaway worktree at
bef003c243(the tip ofmainat the card's filing minute, 2026-09-11T08:01:36Z) reproduces the card exactly —365candidates across47namespaces. Diffing that candidate set against today's:search.badge*/search.type*, subtracted by the one-hop indirect template leg that landed asc4a1d39fe9(fix(scripts): follow a key template assigned one hop before t() in the dead-key sweep (objectui#8754 round 1) #9222, objectui#8754). That family is the wholesearchnamespace bucket, hence 47 → 46 namespaces.kanban.columns, which lost itst()call site in3dd533f1d6(fix(plugin-kanban): announce "No cards" by card count, not by lane count #9169, "announce No cards by card count, not by lane count") and is now a NEEDS-REVIEW candidate.365 − 8 + 1 = 358. ⇒ the lit control for this PR is 358 → 358, and the card's 365 is confirmed as a correct reading of the tree it was taken on.⭐ The head table, re-derived on today's tip
The card's three measured heads match to the key:
under)approvalsInbox.common.managedByBadge.The card's second column ("of those, with no literal call site at all" — 164 / 17 / 15, summing to 196) reproduces exactly as the new
heldcolumn.The new output, verbatim
Ablation — the negative control can actually fail
Turning the new
ownSegmentscolumn into a filter (the exact thing the fences forbid) was injected on disk, measured, and restored:554 − 358 = 196— the mutation's cost is exactly the card's headline figure, arriving independently. The restore leg isgit checkout HEAD -- PATHand is proven by an emptygit diff HEADand a blob-hash match againstHEAD, not by an exit code; the script carries atrap … EXIT INT TERMso a kill mid-mutation still restores.Gates
scripts/__tests__/check-i18n-dead-keys.test.tsTests 133 passed (133)(was 122; +11)Tests 187 passed (187)pnpm check:i18n-dead-keyspnpm check:i18n-designer-paritypnpm check:control-bytes✅ check-control-bytes: OK (scanned 7504 tracked text file(s); skipped 85 binary).pnpm check:changeset-claimspnpm check:new-line-citationsVERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0node scripts/check-changeset-presence.mjs✅ No source or published contract of a released package changed in this range, so no changeset is owed.eslint —
lint:rootscope run at final commitd1ce8e9f85,--no-inline-config --format json: population 341 files (read from eslint's own file resolution, not guessed), of which both changed files are present with 0 errors, 0 warnings. The single error in the population is pre-existingno-consoleate2e/live/global-setup.ts:47, in a file this diff does not touch. Not a narrowed run — this is the whole root scope.A changeset with empty frontmatter is included:
scripts/-only, nothing published moves.维护者速读(草稿)
改了什么 —
check-i18n-dead-keys.mjs的 pack 半边现在会打印它应用了哪些 dynamic template head:每个 head 自身有几段、底下有多少enleaf、其中多少是这个 head 独自按住(没有任何其它 leg 已经保活)的。--json多一个appliedDynamicHeads字段。另加一行,点名那些只命名了一个顶层 namespace 的 head。为什么改 — 同一个文件的 designer 半边遇到这类 head 会拒绝并响亮报错;pack 半边既不过滤也不报告。于是"358 个候选"读起来像一份很干净的清单,而读者无从得知另有 375 个 key 压根没被提出来,其中 196 个由三个 head 独自按住。本 PR 只把沉默变成读数。
风险与代价(含回滚) — 风险接近零:候选集、两个 tier、designer 半边的输出逐字节不变(上面 lit control 有对照)。这是纯增量的打印 + 一个新的返回字段。回滚 = revert 这一个 commit,没有数据迁移、没有 key 变动、没有门禁语义变化。唯一的代价是报告变长了约 35 行。
席位意见 — (留空,待席位定稿)
你要做的 — ⭐ 门槛仍然是你的。本 PR 刻意不为 packs 选定
MIN_HEAD_SEGMENTS那类数字;卡里说 pack namespace 形态与 designer 不同,那个数字归维护者。现在你手上第一次有了做这个决定所需的读数:三个 head(approvalsInbox./common./managedByBadge.)覆盖 232 个 leaf、独自按住 196 个 key。要不要收窄、收窄到几段,请开一张新卡裁决 —— ⛔ 不要在本 PR 里改。Generated by Claude Code