Skip to content

CI does not run pnpm lint:i18n-gate — the one gate that catches a missing translation key is the one a human has to remember - #72

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-67-ci-i18n-gate
Sep 10, 2026
Merged

CI does not run pnpm lint:i18n-gate — the one gate that catches a missing translation key is the one a human has to remember#72
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-67-ci-i18n-gate

Conversation

@zhuangjianguo

@zhuangjianguo zhuangjianguo commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #67

CI ran three of this project's four gates. pnpm lint:i18n-gate existed as a script, scripts/check-lint-i18n-gate.mjs implemented it, and every dev ran it by hand — nothing made it unskippable, which is the one thing this workflow exists to do.

The change — 8 lines, all appended, one run step

@@ -65,3 +65,11 @@ jobs:
        - run: pnpm lint

        - run: pnpm typecheck
+
+      # Same silent class as `validate`, one layer up: `fallbackLocale: 'en'`
+      # renders a key missing from `zh-CN` as its English source string — no
+      # error, no warning, nothing on screen that tells a gap from a
+      # translation. `pnpm lint` does not catch it either; a non-default-locale
+      # gap is a WARNING there, and `objectstack lint` exits non-zero only on
+      # ERRORS. This is the step that fails the build on one.
+      - run: pnpm lint:i18n-gate

1 file changed, 8 insertions(+), 0 deletions(-). Every insertion is after the last pre-existing line, so no existing step moved. Parsed rather than eyeballed — yaml.safe_load on the result:

jobs: ['verify']
run steps in order:
    pnpm install --frozen-lockfile
    pnpm validate
    pnpm lint
    pnpm typecheck
    pnpm lint:i18n-gate

One job, still. No caching change, no action-version bump, no reordering. The comment follows the file's own convention — validate and lint each carry a why, typecheck does not because it needs none; this step needs one, and the reason it needs one is measured below.

The reverse check — the deliverable

A gate nobody has watched fail is not known to be a gate. Run with the fix already committed, so the restore leg points at HEAD; the mutation runs under trap 'restore' EXIT INT TERM with an absolute path resolved from git rev-parse --show-toplevel, so an interrupted run still restores.

Mutation: delete the leaf dashboards.executive_overview.widgets.expiring_90_days.description (one line, 123) from src/translations/zh-CN/app.ts.

Proof the mutation reached disk — never an editor's exit code:

HEAD        : fff1a29
HEAD blob   : 7fdc8851e048be0c329354f6fafe4372309c3809
before      : lines=434  marker=1  on-disk=7fdc8851e048be0c329354f6fafe4372309c3809

  lines now: 433   (HEAD version had: 434)
  marker present now: 0   (was: 1)
  on-disk hash d633f4d7e0cbdc59a6d7a418dde88e0288533174  vs  HEAD blob 7fdc8851…

The anchor was asserted to match exactly once before deletion, so an edit that silently matched nothing could not have reported success.

The gate fails, and names the key (exit captured before any pipe):

GATE EXIT (captured before any pipe) = 1

✗ i18n gate FAILED

  COVERAGE: 1 missing translation key(s) — must be zero.
      [zh-CN] widget     dashboards.executive_overview.widgets.expiring_90_days.description
        Widget dashboards.executive_overview.widgets.expiring_90_days.description missing translation for locale "zh-CN"

Restore verified by STATE, not by an exit code:

git hash-object src/translations/zh-CN/app.ts      = 7fdc8851e048be0c329354f6fafe4372309c3809
git rev-parse HEAD:src/translations/zh-CN/app.ts   = 7fdc8851e048be0c329354f6fafe4372309c3809
lines: 434   marker: 1
git diff HEAD --stat  : (empty)
git status --porcelain: (empty)

⛔ And the half that says why this step is not redundant — all four gates against the same mutation

The card asserts a missing key passes validate, lint and typecheck. Measured rather than inherited, same mutation, same proof-on-disk preamble, same trap:

mutation on disk: lines 434 -> 433 · marker 0 · hash d633f4d7… != 7fdc8851…

ABLATION exits (each captured before any pipe): validate=0 lint=0 typecheck=0 i18n-gate=1

validate still prints ✓ Validation passed (991ms); typecheck prints nothing at all.

One correction to the card's wording, and it sharpens the case rather than weakening it. pnpm lint is not blind to the gap — it reports it:

  ⚠ Widget dashboards.executive_overview.widgets.expiring_90_days.description missing translation for locale "zh-CN"
    i18n/missing-widget  at translations.zh-CN.dashboards.executive_overview.widgets.expiring_90_days.description

  22 warning(s), 5 suggestion(s)     ← baseline on this branch: 21 warning(s), 5 suggestion(s)

It sees it, prints it, and exits 0 anyway — a non-default-locale gap is a WARNING and objectstack lint exits non-zero only on ERRORS. So the fallback hides the gap on screen and the exit code hides it in CI, and a reviewer's only remaining defence is noticing that one number went 21 → 22 in a wall of pre-existing warnings. That is the failure this step removes. It also means the step is not a duplicate lint run: the two share the finding and differ on the only thing CI reads.

Gates — all four 0, exit codes captured before any pipe

Each redirected to its own file, $? read on the next statement:

on 30fd863 + the ci.yml edit — validate=0 lint=0 typecheck=0 lint:i18n-gate=0
✓ Validation passed (932ms)
21 warning(s), 5 suggestion(s) (955ms)
tsc --noEmit → (no output)
✓ i18n gate
  LOCALES  : "en", "zh-CN" checked (required: en, zh-CN)
  REACH    : 12 surface classes walked — object=34 · field=273 · option=188 · section=35 ·
             view=37 · action=55 · app=2 · navigation=28 · dashboard=6 · widget=38 ·
             dataset=70 · page=2
  COVERAGE : 0 missing keys across 2 locale(s)
             (1541 keys expected in total, …)

validate's 27 and lint's 21 warnings are all pre-existing and none is reachable from this diff, which changes no metadata — the gates' walk root is objectstack.config.ts + src/** (per tsconfig.json include), and .github/workflows/ci.yml is in neither.

The step is green in CI on this PR's own head — self-demonstrating

The workflow that this PR edits is the workflow that ran it. Run #145, fff1a29https://github.com/objectstack-ai/hotclm/actions/runs/34471319104 — job Validate, conclusion success, step 9 of 9 Run pnpm lint:i18n-gate, 11:27:07 → 11:27:13. Its verdict, from the runner's own log:

2026-09-10T11:27:13.3048052Z ✓ i18n gate
2026-09-10T11:27:13.3049194Z   LOCALES  : "en", "zh-CN" checked (required: en, zh-CN)
2026-09-10T11:27:13.3051078Z   REACH    : 12 surface classes walked — object=34 · field=273 · option=188 · …
2026-09-10T11:27:13.3052444Z   COVERAGE : 0 missing keys across 2 locale(s)

Identical numbers to the local run — 12 surfaces, 1541 keys expected, 0 missing.

The check attached to this PR is run #146https://github.com/objectstack-ai/hotclm/actions/runs/34471782986 — the pull_request-event run on the same fff1a29: job Validate conclusion success, step 9 Run pnpm lint:i18n-gate 11:32:36 → 11:32:41, success. Two runs, two events, same commit, both green with the new step executed.

The contrast is on this same branch: run #143, the empty-branch push at 30fd863, ran four steps and stopped at pnpm typecheck. Runs #145 and #146 run five.

Three readings — reported, not changed

1. package.json's verify already composes all four. Line 21, verbatim:

"verify": "pnpm validate && pnpm lint && pnpm typecheck && pnpm lint:i18n-gate"

So the divergence was CI's alone. Anyone who ran pnpm verify was running the i18n gate; only the machine that cannot be forgotten was not. Left untouched, per the card.

2. CLAUDE.md names three gates, and after this merges it names one fewer than CI enforces. Line 8: pnpm validate && pnpm lint && pnpm typecheck must all pass. ⚠️ It should say four. CLAUDE.md is a governed surface, so this PR does not touch it — flagged here for a maintainer-merged PR.

3. AGENTS.md states the gate set in two places, both three. Also governed, also untouched:

  • Lines 30–37, ## Verify your work — after every metadata change: a three-command block, then「Never report a change as done, and never open a PR, until all three pass. Paste the three green tails into the PR body.」 The count is welded into the prose twice — all three, three green tails — so this is a rewrite, not a line insert.
  • Line 129, the Delivery process table: | Gates | pnpm validate && pnpm lint && pnpm typecheck. |

Acceptance notes

README.md lines 47–49 carry the same three-command block and is not on the governed list, so it is editable — but it is out of this card's scope, and it is the same sentence as readings 2 and 3. Noted rather than filed, because the natural owner is exactly the maintainer-merged PR that renumbers CLAUDE.md and AGENTS.md: doing it there keeps one edit instead of three, and filing a separate card invites the README half to land while the governed half waits. Four surfaces state the gate setREADME.md 47–49, CLAUDE.md 8, AGENTS.md 30–37 and AGENTS.md 129 — and one PR should move all four.

Not filed as a defect, deliberately: a dev who copies those three commands is not led into an error, only into an omission — the three still pass and still mean what they say.

The card's provenance claim re-measured: *.test.* / *.spec.* / __tests__ across the tree, excluding node_modules0 files. (Seven grep hits for .test./.spec. are prose inside .ts and .md files, not test files.) The gates really are the whole of this repository's automated verification, which is the argument for not leaving one of them to memory.

Scope

  • Touched: .github/workflows/ci.yml only. No src/ file is in the diff — the translation bundle was mutated transiently for the ablation and restored to its HEAD blob byte-for-byte, in this task's own worktree, and Two app-source comments quote a DESIGN.md §09 that no longer says any of it — the stale-pointer pattern, pointing the other way #71's two files were never opened.
  • ⛔ No governed surface touched. Measured, not asserted — blob hashes at HEAD vs origin/main: DESIGN.md 4bf1529b, AGENTS.md 1ff636e3, CLAUDE.md 16a72f5c, LICENSE 16bc23f4, CHANGELOG.md 43b63172, README.md 5bfff001, package.json dd06b914 — all identical on both sides, and git diff --name-status origin/main..HEAD is the single line M .github/workflows/ci.yml.
  • No changeset — this repo has no changeset gate.
  • No browser run owed. The diff changes no runnable surface; the running app cannot observe .github/** at all.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR


Generated by Claude Code

CI ran three of this project's four gates. `pnpm lint:i18n-gate` existed as a
script, `scripts/check-lint-i18n-gate.mjs` implemented it, and every dev ran it
by hand — nothing made it unskippable, which is the one thing this workflow
exists to do.

A missing translation key is exactly the silent class the file's header names.
`fallbackLocale: 'en'` renders a key missing from `zh-CN` as its English source
string: no error, no warning, nothing on screen that tells a gap from a
translation. `pnpm lint` does not catch it either — `objectstack lint` exits
non-zero only on rule-level ERRORS and a non-default-locale gap is a WARNING,
so the fallback hides the gap on screen and the exit code hides it in CI.

This is a two-locale repo by rule (DESIGN.md 11: en default, zh-CN full second
bundle) and the two bundles are edited together on nearly every card that
touches a user-visible string. This gate is the only check that they stayed in
step, and it was the only one left to memory.

One step, appended after `pnpm typecheck` in the existing `verify` job. No new
job, no caching change, no action-version bump, no existing step reordered.

`package.json`'s `verify` script already composes all four; the divergence was
CI's alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 10, 2026 11:33
@zhuangjianguo
zhuangjianguo merged commit 19a8a4c into main Sep 10, 2026
2 checks passed
zhuangjianguo pushed a commit that referenced this pull request Sep 10, 2026
…ated

PR #72 (#67) added `pnpm lint:i18n-gate` to `.github/workflows/ci.yml`, so CI
enforces four gates. `package.json`'s `verify` has composed four for some time.
The documents were the last thing in the repository saying three.

Five surfaces, not the four the card listed — a repository-wide grep for the
gate set also turned up `docs/backlog/README.md`, the acceptance floor every
work card inherits. All five now state the same list in the same order:
validate -> lint -> typecheck -> lint:i18n-gate.

`AGENTS.md` "Verify your work" is rewritten rather than appended to: the count
was welded into the prose twice ("all three exit non-zero", "the three green
tails"), so a bare fourth command would have left the section contradicting
itself within three lines. The replacement prose is deliberately count-free, so
a fifth gate cannot reopen this defect there.

The enumerations are kept; collapsing any of them to `pnpm verify` is a
judgement the PR body raises and does not take.

Documentation only. No gate reads any of these files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI does not run pnpm lint:i18n-gate — the one gate that catches a missing translation key is the one a human has to remember

2 participants