Skip to content

fix(pm): derive check:i18n by owning-package walk in dispatch-gates - #8363

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-8352-gates-i18n-derive
Aug 13, 2026
Merged

fix(pm): derive check:i18n by owning-package walk in dispatch-gates#8363
hotlong merged 2 commits into
mainfrom
claude/issue-8352-gates-i18n-derive

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#8352

check:i18n was invisible to dispatch-gates.mjs in both halves of its output. Measured on PR #8348: an object-definition edit under packages/services/service-messaging/src/objects/ regenerates that package's four translation bundles, and the derivation named the gate nowhere at all — costing that PR a CI round.

Why both halves missed it

  • Path half.scripts/check-i18n-bundles.mjs discovers its targets at runtime, walking packages/ for files namedi18n-extract.config.ts (findConfigs, ~line 107 — the card cites it as findExtractConfigs; the mechanism it describes is exact), so its source names no population path to match against.
  • Sharper than the card stated. The checker is not hint-less: it carries eleven path-ish literals. They are its CLI-prerequisite and stale-dist checks (packages/cli/dist/commands/i18n/extract.js, packages/spec/dist), none of which describes the population it guards. Because hints.length is non-zero, the gate is also filtered out of the "repo-wide / undetermined" bucket. Matching nothing and being excluded from the fallback bucket is what made it invisible rather than merely unmatched — a gate the derivation cannot mention at all.
  • Convention half.CHANGE_KIND_GATES carried exactly one kind ("adds or edits a test file") and did not know this one.

The fix

A second CHANGE_KIND_GATES entry — the smaller of the two shapes the card offered — whose matches repeats the gate's own walk: same skip set (node_modules, dist, dotted entries), same filename-plus-/scripts/ test. Any input path inside a package owning a config qualifies.

It lists nothing. What is written down is the KIND; the population is discovered per run, so a tenth package growing a bundle is matched by the next run with nothing to update here — the same runtime-discovery contract the workflow and package.json reads already keep. Verified at parity with the gate's own population: 9 owners derived, 9 found, identical sets.

Whole owning package, config file included — the narrowing to src/objects that the card rejects would under-cover, since extraction reads whatever each package's config enumerates and the config itself is part of the trigger surface. Containment is one-directional on purpose: a parent directory must not drag in the owners below it.

Three supporting pure helpers are factored out so the judgments are pinnable offline: isExtractConfigPath, owningPackageOfExtractConfig (which refuses an owner that would collapse to a bare top-level directory), and isInI18nBundlePackage. The walk is memoised per process; an unreadable packages/ throws rather than degrading to "no owners", per this script's rule that unreadable input must never look like an empty answer.

Acceptance demo (live runs)

The measured incident path now names the gate:

$ node scripts/pm/dispatch-gates.mjs packages/services/service-messaging/src/objects/http-delivery.object.ts
Convention-triggered gates (this change KIND moves them; no path derivation can name them):
edits a file in a package that owns an i18n-extract.config.ts: packages/services/service-messaging/src/objects/http-delivery.object.ts
- pnpm check:i18n — it re-extracts every owning package's translation bundles and fails on drift, ...

A non-owning package still does not:

$ node scripts/pm/dispatch-gates.mjs packages/objectql/src/engine.ts | grep -c check:i18n
0

The config file itself qualifies (packages/plugins/plugin-audit/scripts/i18n-extract.config.tspnpm check:i18n).

Reverse verification — direction predicted first, and one prediction was uncomfortable

Predicted: renaming the entry's gate to a name no workflow runs leaves the KIND matching (the walk is untouched) and rots only name resolution, so the live run must print the STALE line in place of the runnable invocation.

Observed, both halves:

  1. Live run printed ⚠ check:i18n-renamed-probe: STALE — no workflow runs a gate under this name. — as designed.
  2. The self-test stayed green at 47/47.includes('pnpm check:i18n') is satisfied by any name that merely starts with it, so a prefix-preserving rename was invisible to exactly the pins meant to catch it.

That hole was in coverage this PR itself added, so it is fixed here (second commit): both pins now anchor on the rendered delimiters (- pnpm x —, ⚠ x: STALE). Re-running the identical break now fails 2 of 47 cases, and fails the right two — the kind-match cases stay green, confirming the walk still matches and only resolution rotted. Restored byte-identical from the commit each time (git restore; blob hash re-checked against HEAD, git status clean — never git checkout ref -- path).

Self-test

28 → 47 cases, all passing. Both directions of the new kind (a path inside an owning package qualifies; one in a package without a config does not), the config file itself, a sibling sharing a name prefix, a parent directory, the live walk against the real tree, and the new entry's STALE branch. The self-test section comment is updated to say honestly that the i18n cases touch the filesystem on purpose: that entry's whole content is a walk of the real tree, and a fixture-only test passes just as happily when the walk is rooted at the wrong directory.

Gates

GateResult
node scripts/pm/dispatch-gates.mjs --self-test47/47 pass
npx eslint scripts/pm/dispatch-gates.mjsexit 0
pnpm check:nul-bytesOK (7577 files, no control bytes)
Self-re-derivation against the changed pathno additional families surfaced

Labels

skip-changeset: internal PM dispatch tooling under scripts/pm/, nothing user-visible ships and no package changes. No changeset file.

Draft on purpose — awaiting a human merge, per the claim comment. No auto-merge, no merge queue, no ready-flip.


Generated by Claude Code

`check:i18n` was invisible to the derivation in BOTH halves of the output.
The path half cannot reach it: check-i18n-bundles.mjs discovers its targets
at runtime by walking packages/ for files named i18n-extract.config.ts, so
its source names no population path to match. Worse than silent — it does
carry eleven path-ish literals (CLI prerequisite, stale-dist checks), so it
also never lands in the "repo-wide / undetermined" bucket. The convention
half did not know the kind. Measured on PR #8348: an object-definition edit
under packages/services/service-messaging/src/objects/ regenerates that
package's four bundles, and the derivation named the gate nowhere at all.
Adds a second CHANGE_KIND_GATES entry that repeats the gate's own walk
(same skip set, same filename-plus-/scripts/ test) and matches any input
path inside an owning package. The whole package counts, config file
included — narrowing to src/objects/** would under-cover, since extraction
reads whatever each package's config enumerates.
Lists nothing: the KIND is written down, never its population, so a tenth
package growing a bundle is matched by the next run. Owner population
verified at parity with the gate's own nine. Self-test 28 -> 47 cases,
pinning both directions and the new entry's STALE branch.
Fixes#8352
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01139NJ9Wg5pFeZi1Zh8WLg6
Found by reverse-verifying the entry above: renaming its gate to
`check:i18n-renamed-probe` made the live run print the STALE line exactly as
designed, and the self-test stayed green at 47/47 — `includes('pnpm
check:i18n')` is satisfied by every name that merely STARTS with it, so a
prefix-preserving rename is invisible to a substring pin. That is precisely
the rot the STALE branch exists to report.
Anchors both pins on the rendered delimiters (`- pnpm x —`, `⚠ x: STALE`)
so the name must match exactly. Re-verified: the same rename now fails 2
cases instead of 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01139NJ9Wg5pFeZi1Zh8WLg6
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 13, 2026 9:06am

Request Review

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM review: ACCEPT — skills seat (#7623), session session_01139NJ9Wg5pFeZi1Zh8WLg6. Independently re-verified in a clean worktree at 341407c (not trusting the dev report):

  • node scripts/pm/dispatch-gates.mjs --self-test → 47/47 pass (re-run, not quoted).
  • Acceptance demo re-run: the measured incident path (packages/services/service-messaging/src/objects/http-delivery.object.ts) now derives check:i18n under the convention section; packages/objectql/src/engine.ts → 0 hits; the config file itself (packages/plugins/plugin-audit/scripts/i18n-extract.config.ts) → 1 hit. All three match the card's acceptance criteria, including the whole-owning-package (not src/objects/**) scope the card demanded.
  • Diff review: the three pure helpers mirror findConfigs line-for-line (same skip set, same filename-plus-/scripts/ test); owningPackageOfExtractConfig refuses bare top-level owners; containment is one-directional (a parent directory cannot drag in owners below it — pinned); walk memoised, unreadable packages/ throws per the script's no-empty-answer contract; the table's deletability criteria are stated per entry.
  • The two sharper-than-card findings are verified from the diff and comment text: check:i18n was excluded from the undetermined bucket too (11 non-population hints), and the delimiter-anchored pins close the prefix-rename hole the dev measured live. The same pre-existing hole in the test-file entry is correctly left out of scope and filed as [finding] dispatch-gates self-test pins its convention gate names with substring includes, so a prefix-preserving rename passes the pin that exists to catch it #8364.

Merge path unchanged: stays draft, awaiting a human merge (conservative pending the maintainer's ruling on scripts/pm/**, asked in seat chat 2026-08-13). CI on the PR should be allowed to finish before merging.


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review August 13, 2026 12:47
@hotlong
hotlong added this pull request to the merge queueAug 13, 2026
Merged via the queue into main with commit f46cda9Aug 13, 2026
24 checks passed
@hotlong
hotlong deleted the claude/issue-8352-gates-i18n-derive branch August 13, 2026 13:05
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

2 participants

@hotlong@claude