Skip to content

fix(scripts): key an i18n-coverage cause on its conclusion, not the per-config rerun command - #11547

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11395-i18n-cause-grouping
Aug 24, 2026
Merged

fix(scripts): key an i18n-coverage cause on its conclusion, not the per-config rerun command#11547
os-steve merged 1 commit into
mainfrom
claude/issue-11395-i18n-cause-grouping

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11395

groupFailuresByCause() keys a cause on its conclusion — [reason, fix] — which
holds only while fix is a shared remedy. It was not. Three of the five failure
branches set fix to rerunFix(configPath), a string embedding the config path, so
every config reaching one of them got a unique key and one shared cause was split
into one group per config: exactly the "one cause must not be reported as N results"
shape (#5217) that this grouping was written to prevent.

Measured, not reasoned

The defect is only visible from a specific environment state, so that state was
reproduced rather than argued from the regex: an unbuilt worktree (no
node_modules, no dist/) with the gate invoked from a cwd where its CLI probe
defers, so all twelve configs land in the fallback branch instead of a classified
one. Both runs are over the same twelve failures — the same tree, the same cwd,
nothing built in between — and the numbers are parsed out of the report, not read
off the prose.

before (cd932772)after (f28c758f)
headline12 of 12 config(s) failed to lint (12 distinct causes)12 of 12 config(s) failed to lint (1 distinct cause)
Cause N of M blocks121
distinct why: lines11
distinct fix: lines12 (one per config path)1
configs listed1212

The report's own closing paragraph — "a cause shared by several configs is stated
once, not once per config (#5217)" — was the thing contradicted, and it is left
exactly as written. The prose was right; the code was wrong.

What was fixed, and where

Fixed at the identity, not by normalising the config path back out of the key:
that would make the key a function of a string the fix text happens to embed, and
the next rerunFix-shaped helper would re-open the same hole silently.

  • No classifier receives a config path any more.explainConfigFailure() took
    configPath for exactly one purpose — the rerun command — so the parameter is
    gone. A per-config string is no longer in reach of a cause.
  • The rerun command is rendered per config inside the block, by
    reportUnmeasuredConfigs(), as the remedy detail it is. All twelve commands are
    still there for the reader; they are simply no longer part of an identity.
  • fix is drawn from a closed set of three constants (SHARED_REMEDIES).
  • The classified branches (pnpm build / pnpm install && pnpm build) group
    byte-for-byte as before, pinned by a new assertion on the literal remedy text.

The other half of the key — named because the card's claim needed checking

The card says the same bug applies to the two other rerunFix call sites. Verified:
there are exactly three call sites (fallback, empty output, non-JSON) — no fourth
has appeared. But fixing fix alone would not have closed the non-JSON one: its
reason interpolated raw.length and the parser's message, so two configs whose
non-JSON output differed by a byte were still one cause reported as two, through the
other half of the key. Those are readings, so they moved to evidence, which is
not keyed. This is the same defect class, in a branch the card names, in the same
file and the same gate family — fixed here rather than filed, and called out here so
it is reviewed rather than absorbed.

The two in-loop verdicts are now pure constructors (emptyOutputFailure,
nonJsonOutputFailure) so --self-test can drive them at all: CI builds the
workspace before this gate runs and never reaches these branches, which is why the
split survived.

Non-vacuity

New --self-test assertions are written as a property over every failure branch
(five of them), not one example — the defect arrived through two different halves of
the key, and a test against either half alone passes while the other stays broken.
Each branch is driven with three configs failing the same way; the converse is
pinned too (six genuinely distinct causes must stay six, so "collapse everything"
cannot satisfy the suite).

Ablation, two legs, each mutation confirmed on disk by git hash-object and an
anchored marker count, each restored byte-identical, all wrapped in
trap restore EXIT INT TERM:

  • Leg 1 — a per-config string back into fix on all three branches
    (0c157bd87fa77558): self-test red, 11 failures.
  • Leg 2 — the readings back into reason, non-JSON branch only
    (0c157bd8e888b6b5): self-test red, 2 failures — proving the second half
    of the key is independently guarded.
  • Both legs restored to 0c157bd8, byte-identical to the committed blob.

Leg 1 produced 11 red, not the 12 predicted: on the non-JSON branch that leg injected
${raw.length} rather than a path, so …with no config path in the cause's identity
correctly stayed green while the other three assertions on that branch went red. The
prediction was wrong about that one assertion and the deviation is recorded rather
than smoothed over — it shows the four assertions are independent, not redundant.

Verification

All at f28c758f, exit codes captured before any pipe:

  • pnpm buildVERDICT command-exit 0 (71/71 tasks)
  • pnpm check:i18n-coverage over the built tree — OK (12 config(s), 602 baselined untranslated string(s), none new); the green path and the ratchet are
    untouched
  • Derived gate union (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, 7 families) — all exit 0:
    check:cross-package-test-inputs, check:entry-guard, check:parse-guard,
    check:pnpm-filter-targets, check-ci-filter-parity.mjs,
    check-cross-package-test-inputs.mjs, check:i18n-coverage
  • pnpm check:nul-bytesOK (scanned 6460 text file(s) … no raw ASCII control bytes)
  • pnpm lint — the whole repo, eslint . --no-inline-config, exit 0, no output.
    No narrowing was needed, so none is claimed.

No changeset: the root package is private and scripts/ is in no workspace package,
so this PR publishes nothing — skip-changeset.

Related

#5217 (the rule this branch escaped) · #10907 (where it was measured) · #11394
already covers the deferring CLI probe that makes this state reachable — the
instrument here, filed separately, not touched by this PR.

Generated by Claude Code


Generated by Claude Code

…er-config rerun command
`groupFailuresByCause` keys a cause on `[reason, fix]`, which holds only while
`fix` is a shared remedy. Three of the five failure branches set it to
`rerunFix(configPath)` — a string embedding the config path — so every config
reaching one of them got a unique key and one shared cause was split into one
group per config: the "one cause reported as N results" shape the grouping was
written to prevent.
Measured on an unbuilt worktree with the CLI probe deferring: all twelve configs
fail for one environment fact and the report read `12 of 12 config(s) failed to
lint (12 distinct causes)` — twelve blocks, twelve identical `why:` lines —
while its own closing paragraph asserted a cause shared by several configs is
stated once. Now `1 distinct cause`, over the same twelve failures.
Fixed at the identity: no classifier receives a config path any more, so a
per-config string is not in reach of a cause, and the rerun command is rendered
per config inside the block as the remedy detail it is. The non-JSON branch had
the same defect through the other half of the key (byte count and parser offset
interpolated into `reason`); those are readings and moved to `evidence`.
The classified branches (`pnpm build` / `pnpm install && pnpm build`) group
byte-for-byte as before, pinned. The two in-loop verdicts are now pure
constructors so `--self-test` can drive them — CI builds the workspace before
this gate runs and never reaches these branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@os-steve
os-steve marked this pull request as ready for review August 24, 2026 03:21
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit a150876Aug 24, 2026
32 checks passed
@os-steve
os-steve deleted the claude/issue-11395-i18n-cause-grouping branch August 24, 2026 03:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-i18n-coverage groups failure causes on a key containing the per-config rerun command, so one shared cause reports as N causes

1 participant

@os-steve