The gap
.github/workflows/ci.yml runs three gates:
- run: pnpm validate
- run: pnpm lint
- run: pnpm typecheck
pnpm lint:i18n-gate is not among them. It exists as a script, scripts/check-lint-i18n-gate.mjs implements it, and every dev this session ran it by hand — but nothing makes it unskippable.
Why that is the wrong one to leave out
The workflow's own header says what it is for:
ObjectStack metadata fails SILENTLY at runtime, not at edit time … objectstack validate is where that class surfaces — and running it is something a human, or an AI agent authoring metadata in this project, has to remember. This workflow is what makes it unskippable.
A missing translation key is precisely that class. It raises nothing, passes validate, passes lint, passes typecheck, and surfaces as a raw key or an English string in front of a zh-CN user — the exact failure mode this repository has already had to route upstream twice this week (objectstack-ai/objectstack#17344, #13469). The gate that catches it is the one left to memory.
It is also a two-locale repo by rule (DESIGN.md §11: en default · zh-CN), and the i18n gate is the only check that both bundles stayed in step. src/translations/en/app.ts and src/translations/zh-CN/app.ts are edited together on nearly every card that touches a user-visible string — three times this session alone (#48/PR #57, #59/PR #62, and the widget description in each). Nothing in CI notices if one of them is forgotten.
What it is not
⛔ Not a CLAUDE.md violation. CLAUDE.md's ⛔ rule names exactly three gates — pnpm validate && pnpm lint && pnpm typecheck — and CI matches that contract precisely. This card proposes widening the contract, not enforcing an existing one.
⛔ Not currently broken. Measured on 296fab8: the gate passes — 0 missing keys across 2 locale(s), 1541 keys expected, 12 surface classes walked. This is about the next PR, not this tree.
The change
Add one step to the verify job, after pnpm typecheck:
- run: pnpm lint:i18n-gate
Consider whether CLAUDE.md's inlined gate sentence should name four gates rather than three so the two stay in step — ⚠️ but CLAUDE.md is a governed surface, so if you conclude it should, say so and ⛔ leave it to a separate maintainer-merged PR rather than folding it in here. AGENTS.md is governed too; check whether it states the gate set anywhere and report, do not edit.
Also check package.json's verify script: if it composes the gate list, it should agree with CI, and if it already includes the i18n gate then the divergence is CI's alone and worth saying so in the PR body.
Acceptance
- The new step runs in CI and is green on the PR's own head — which is self-demonstrating: this PR's CI run is the evidence.
- All four gates 0 locally, exit codes captured before any pipe.
- Verify the step actually fails when it should: make a missing key locally (delete one leaf from
src/translations/zh-CN/app.ts), show the gate exits non-zero and names the key, then restore and show the tree is byte-identical (git diff empty). A gate nobody has watched fail is not known to be a gate — that reverse check is the deliverable here, more than the one-line diff.
- ⛔ Do not touch any governed surface. ⛔ Do not reorganise the workflow, add jobs, add caching, or "modernise" action versions — one step, in the existing job.
- No browser run owed.
Provenance
Found by the PM seat while answering a maintainer question about why this project's PRs merge quickly. The honest answer is that CI here is three metadata gates at ~30 seconds and there is no test suite at all in the repository (.test. / .spec. / __tests__ matches: 0) — which makes the gates that do exist carry more weight than usual, and makes an omitted one cost more than it would elsewhere.
Related
.github/workflows/ci.yml · scripts/check-lint-i18n-gate.mjs · CLAUDE.md's three-gate rule (governed) · objectstack-ai/objectstack#17344 · #13469 (the i18n failure class, upstream)
The gap
.github/workflows/ci.ymlruns three gates:pnpm lint:i18n-gateis not among them. It exists as a script,scripts/check-lint-i18n-gate.mjsimplements it, and every dev this session ran it by hand — but nothing makes it unskippable.Why that is the wrong one to leave out
The workflow's own header says what it is for:
A missing translation key is precisely that class. It raises nothing, passes
validate, passeslint, passestypecheck, and surfaces as a raw key or an English string in front of azh-CNuser — the exact failure mode this repository has already had to route upstream twice this week (objectstack-ai/objectstack#17344,#13469). The gate that catches it is the one left to memory.It is also a two-locale repo by rule (DESIGN.md §11:
endefault ·zh-CN), and the i18n gate is the only check that both bundles stayed in step.src/translations/en/app.tsandsrc/translations/zh-CN/app.tsare edited together on nearly every card that touches a user-visible string — three times this session alone (#48/PR #57, #59/PR #62, and the widget description in each). Nothing in CI notices if one of them is forgotten.What it is not
⛔ Not a
CLAUDE.mdviolation.CLAUDE.md's ⛔ rule names exactly three gates —pnpm validate && pnpm lint && pnpm typecheck— and CI matches that contract precisely. This card proposes widening the contract, not enforcing an existing one.⛔ Not currently broken. Measured on
296fab8: the gate passes —0 missing keys across 2 locale(s), 1541 keys expected, 12 surface classes walked. This is about the next PR, not this tree.The change
Add one step to the
verifyjob, afterpnpm typecheck:Consider whether⚠️ but
CLAUDE.md's inlined gate sentence should name four gates rather than three so the two stay in step —CLAUDE.mdis a governed surface, so if you conclude it should, say so and ⛔ leave it to a separate maintainer-merged PR rather than folding it in here.AGENTS.mdis governed too; check whether it states the gate set anywhere and report, do not edit.Also check
package.json'sverifyscript: if it composes the gate list, it should agree with CI, and if it already includes the i18n gate then the divergence is CI's alone and worth saying so in the PR body.Acceptance
src/translations/zh-CN/app.ts), show the gate exits non-zero and names the key, then restore and show the tree is byte-identical (git diffempty). A gate nobody has watched fail is not known to be a gate — that reverse check is the deliverable here, more than the one-line diff.Provenance
Found by the PM seat while answering a maintainer question about why this project's PRs merge quickly. The honest answer is that CI here is three metadata gates at ~30 seconds and there is no test suite at all in the repository (
.test./.spec./__tests__matches: 0) — which makes the gates that do exist carry more weight than usual, and makes an omitted one cost more than it would elsewhere.Related
.github/workflows/ci.yml·scripts/check-lint-i18n-gate.mjs·CLAUDE.md's three-gate rule (governed) · objectstack-ai/objectstack#17344 · #13469 (the i18n failure class, upstream)