Repository navigation
[finding] The interpolation-parity gate cannot see an unfilled {{count}} hole: count is reserved in BOTH directions, not just the one that needs it #4206
Description
Activity
Findings triage (first grade; premise check @
origin/maine1ade8f): PROMOTED —findingremoved,pm:queueapplied.This is the one card in this round's set where nothing is left to rule, and its stated precondition has now landed.
Anchors verified live (2-line drift from the body, substance exact):
RESERVED_OPTION_NAMESatscripts/check-i18n-call-site-keys.mjs:325, and the shared-holes filter the card indicts at:1103(body says :1101):[...holesOf(enValue)].filter((hole) => !RESERVED_OPTION_NAMES.has(hole) && !downstream.has(hole)),
The reservation is applied to
holesbefore both directions are derived, sounfilledstructurally cannot containcount— exactly as filed. The set is also consulted at:821and:839on the options side, which is the direction the reservation is genuinely for.Why promote rather than hold. It is a scoped gate fix with a named landing site, a single option, and a measurement that already answers the only question a dev would have to ask: with
countun-reserved on theunfilleddirection, the tree yields 1 report at the old main (#4157 itself) and 0 with the fix applied, across 2277 judged call sites. That is the false-positive cost, measured, and it is zero. The plural family the reservation protects never reaches this comparison (enValue === undefinedupstream), so the narrowing cannot regress it.The staged precondition has fired. PR #4205 merged 2026-08-10T23:45:43Z, and
packages/plugin-gantt/src/GanttView.tsx:5058now readst('gantt.conflict.body', { count: pendingConflict.length })— so the one call site that would have gone red under the narrower reservation is already fixed on main. The gate can be tightened without a red tree. Had #4205 still been open, this would have stayed held.Scope for whoever takes it — deliberately narrow: apply the reservation per direction (keep
countfiltered when computinginert, leave it in when computingunfilled); do not extend the gate to read.replace()chains. That larger question is adjacent to ui#4135'sEXTERNALLY_INTERPOLATED_HOLESregistry and is its own card; the card body says so and this promotion does not widen it.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Claiming — PM dispatch (unblocked: PR #4714 merged, the hot-file serial hold on
check-i18n-call-site-keys.mjsis lifted). Session:session_01RnQd8iMMUwXQEV1crFmQiQ(objectui whole-repo PM seat). Branch:claude/issue-4206-count-reservation-per-direction.Scope per the 2026-08-11 triage promotion, deliberately narrow: apply the
countreservation per direction — keep it filtered when computinginert, leave it in when computingunfilled— inscripts/check-i18n-call-site-keys.mjs(+ self-test). ⛔ Do NOT extend the gate to read.replace()chains (its own card territory). The promotion's measurement stands as acceptance basis: 1 revealed finding at old main (already fixed by PR #4205), 0 on current trees, across 2277 judged call sites. Note the file changed since the card: PR #4714 retired theEXTERNALLY_INTERPOLATED_HOLESentry — re-derive line anchors on currentmain. PR will beFixes #4206.
Generated by Claude Code
<!-- os-dev-report -->
{ "issue": 4206, "status": "done", "branch": "claude/issue-4206-count-reservation-per-direction", "pr": "https://github.com/objectstack-ai/objectui/pull/4719", "premise_still_valid": true, "summary": "Applied the RESERVED_OPTION_NAMES reservation per direction in scripts/check-i18n-call-site-keys.mjs, matching the 2026-08-11 triage promotion. interpolationOptions() now returns a call site's option names raw (reserved names included); the comparison filters count/ns/lng/etc. out of the option-name set only when judging inert, and keeps them when judging unfilled, so a real {{count}} miss (the objectui#4157 shape) is caught while a call site that actually passes count is still correctly recognised as filling it. First-draft attempt (filtering only the shared holes set, per the dispatch's literal mechanism-assumption-1 wording) regressed: it made every count-passing call site read as unfilled=[count], caught by main's own real corpus in the self-test (65 tests, 3 failed) before push — corrected to filter the NAMES side instead, which is the only shape that keeps both directions correct simultaneously.", "tests": "At HEAD 5da80f105: `pnpm exec vitest run scripts/__tests__/check-i18n-call-site-keys.test.ts --maxWorkers=2` -> 65/65 passed (added two direction-specific cases: count passed against a holed key stays silent; count omitted against the same key now reports unfilled=[count]). `node scripts/check-i18n-call-site-keys.mjs` (whole-repo gate) -> 0 interpolation-parity findings across 2383 judged call sites (the card measured 2277 on 2026-08-10/11; corpus grew since). Positive control: uncommitted strip of `{ count: pendingConflict.length }` from packages/plugin-gantt/src/GanttView.tsx:5058 made the gate report `unfilled (hole, no argument): count` on that exact site; reverted via `git checkout --`, gate back to 0 findings. `pnpm exec eslint scripts/__tests__/check-i18n-call-site-keys.test.ts scripts/check-i18n-call-site-keys.mjs` -> clean. `pnpm run type-check:scripts` -> clean. `node scripts/check-control-bytes.mjs` -> OK. `node scripts/check-changeset-presence.mjs` -> no changeset owed (scripts-only, not under a released package's src/); PR labeled skip-changeset, labels read back as [\"tests\",\"skip-changeset\"].", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
{ "issue": 4206, "status": "done", "branch": "claude/issue-4206-count-reservation-per-direction", "pr": "https://github.com/objectstack-ai/objectui/pull/4719", "premise_still_valid": true, "summary": "Applied the RESERVED_OPTION_NAMES reservation per direction in scripts/check-i18n-call-site-keys.mjs, matching the 2026-08-11 triage promotion. interpolationOptions() now returns a call site's option names raw (reserved names included); the comparison filters count/ns/lng/etc. out of the option-name set only when judging inert, and keeps them when judging unfilled, so a real {{count}} miss (the objectui#4157 shape) is caught while a call site that actually passes count is still correctly recognised as filling it. First-draft attempt (filtering only the shared holes set, per the dispatch's literal mechanism-assumption-1 wording) regressed: it made every count-passing call site read as unfilled=[count], caught by main's own real corpus in the self-test (65 tests, 3 failed) before push — corrected to filter the NAMES side instead, which is the only shape that keeps both directions correct simultaneously.", "tests": "At HEAD 5da80f105: `pnpm exec vitest run scripts/__tests__/check-i18n-call-site-keys.test.ts --maxWorkers=2` -> 65/65 passed (added two direction-specific cases: count passed against a holed key stays silent; count omitted against the same key now reports unfilled=[count]). `node scripts/check-i18n-call-site-keys.mjs` (whole-repo gate) -> 0 interpolation-parity findings across 2383 judged call sites (the card measured 2277 on 2026-08-10/11; corpus grew since). Positive control: uncommitted strip of `{ count: pendingConflict.length }` from packages/plugin-gantt/src/GanttView.tsx:5058 made the gate report `unfilled (hole, no argument): count` on that exact site; reverted via `git checkout --`, gate back to 0 findings. `pnpm exec eslint scripts/__tests__/check-i18n-call-site-keys.test.ts scripts/check-i18n-call-site-keys.mjs` -> clean. `pnpm run type-check:scripts` -> clean. `node scripts/check-control-bytes.mjs` -> OK. `node scripts/check-changeset-presence.mjs` -> no changeset owed (scripts-only, not under a released package's src/); PR labeled skip-changeset, labels read back as [\"tests\",\"skip-changeset\"].", "open_questions": [], "out_of_scope_findings": [] }(Supersedes the previous report comment on this issue, which mis-escaped the marker as literal
<!--entities instead of the real characters.)
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
Measured while fixing objectui#4157 (PR #4205) — not in that PR's scope, since
scripts/check-i18n-call-site-keys.mjsbelongs to the objectui#3845 gate family. Observation-class: once #4205 lands, nothing a user hits today depends on this (measured below).The blind spot
check:i18n-call-site-keysruns an interpolation-parity check that is explicitly bidirectional —inert(an option passed that theenvalue has no hole for) andunfilled(a hole the call site passes nothing for). That is exactly the shape of #4157:t('gantt.conflict.body')passed no options while theenvalue read…Auto-reschedule {{count}} affected task(s)?.The gate was green on it. Mechanism,
scripts/check-i18n-call-site-keys.mjs:325:and at the comparison (
:1101):The reservation is applied to
holesbefore both directions are computed, so a{{count}}hole is removed from the comparison entirely andunfilledcan never containcount.The reservation is correct for the direction it was written for.
countis i18next's plural selector, so passing it is legitimate even when the resolvedenform shows no visible{{count}}— suppressing thatinertreport is right. What is over-broad is that the same filter also suppresses the opposite direction, where theenvalue does carry the hole and the call site fills nothing.Measurement
Re-ran the gate's own
analyze(root)withcountdeleted fromRESERVED_OPTION_NAMES, against two trees:countreservedorigin/main@b1e42d09bThe one finding revealed at
mainis #4157 itself, exactly:So the narrower reservation would have caught #4157 at authoring time, and across all 2277 judged call sites it produces zero other reports — no false-positive cost to absorb, and no second live defect of this class hiding behind the reservation today. That is what makes this observation-class rather than a queued defect: the blind spot is real and the class recurs, but after #4205 there is nothing currently unfilled.
Option
Apply the reservation per direction rather than to the shared
holesset: keepcountfiltered out when computinginert, and leave it in when computingunfilled. Plural families are already unjudged upstream (enValue === undefined— "including the plural families, where there is no one form to compare against"), so the case the reservation exists to protect does not reach this comparison in the first place.Worth noting what this would and would not have bought. It catches the general class — an
envalue with a hole and a call site that fills nothing. It does not, on its own, catch the narrower thing #4157 actually was: a call site that fills the hole downstream, with a literal.replace()in a different brace syntax than the pack uses. At(key).replace('{count}', n)chain still reads to this gate as a call site passing no options; it is only visible because the pack spells the hole{{count}}while the replace targets{count}. Whether the gate should also learn to read the.replace()chain is a larger question and adjacent to objectui#4135'sEXTERNALLY_INTERPOLATED_HOLESregistry, which is the same family from the other side (a hole i18next must deliberately NOT fill).No recommendation on timing attached — the change is inside the #3845 gate's own contract, so it belongs to whoever owns that gate's next pass.
Related: objectui#3845 (the gate and its bidirectional parity check), objectui#4135 (same family — downstream-filled holes and their two spellings), objectui#4157 / #4205 (the defect that measured this).
Generated by Claude Code