ci(i18n): scope the protected-terminology gate to what a change introduces - #1958
Merged
zksquirrel merged 2 commits intoAug 14, 2026
Merged
Conversation
The gate walked the whole translations/ tree and hard-failed on any
violation. Because the English pages are edited continuously and the 18
translations follow in a later sync pass, every new protected term in
site/ turned EVERY open PR red — including PRs that touch no translation.
Contributors had no action available inside their own PR, and the check
stopped carrying information, so it was routinely merged through.
With --base the gate now polices what a change did to TRANSLATIONS and
leaves English-source drift to its owner, the staleness detector and its
dashboard issue:
introduced translated file changed in base..HEAD and did not violate
at the merge base (a new translation counts) -> FAIL
stale-source translation untouched, English moved under it -> notice
pre-existing already violating at the merge base -> notice
Pre-existence is judged against the pairing that actually existed at the
base — the translated file's base path and the English source THAT path
derives from. Pairing old content against the current path's source would
invent a comparison that never existed: moving a translation into another
page's slot would read every violation it creates as pre-existing, because
the new slot's English source legitimately carries terms the moved content
never had to. Renames are followed (-M) so a page moved together with its
source is still judged against its own history.
A violation carried forward in a file the change does edit is reported
separately — visible and cheap to fix in passing, but not fatal, since
failing there would put the 18-locale sync burden on whoever next touches
the file. Local runs also fold uncommitted and untracked translations into
the scope, since the audit reads the working tree.
Base handling mirrors the manifest-invariants job in this same workflow,
including its fail-closed rule that a requested-but-unresolvable base is an
error rather than a silent skip; an explicitly empty --base is likewise an
error rather than a quiet downgrade to a whole-tree audit. The push path
falls back to HEAD^ when github.event.before is the all-zero SHA (branch
creation) or has become unreachable (force-push). With no --base at all the
behaviour is unchanged, so a local audit still reports the full backlog.
Verified against the current tree (162 inherited violations): whole-tree
audit exits 1; a no-op PR exits 0; stripping a term from a translation
fails with exactly that violation, committed or not; adding a term to an
English page alone exits 0 and reports staleness; a page moved with its
source while already carrying drift exits 0; moving a translation into a
different page's slot fails with the 4 violations it creates.
The change-tracking failure message offered `edited: true` alongside src/mode/tool as if they were interchangeable ways to record a change. They are not: `edited: true` means "a human authored this translation, never let automation touch it", and sync.mjs holds such pages out of every subsequent re-sync permanently. Taken at face value during the Aug-12 frontmatter cleanup it froze 220 locale-pages against future syncs — including pages the terminology gate was failing on, which no sync run could then cure. The message now leads with `tool` (free-form, and what the provenance model wants for a mechanical pass) and states the flag's consequence instead of presenting it as an equal option.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
protected-termsis red on every open PR in this repo right now — #1930, #1933, #1948, #1957 — and onmainsince Aug 12. None of those PRs did anything wrong, and none of their authors can fix it from inside their own PR.The gate asserts that a protected term present in an English page also appears verbatim in each of the 18 translations. Three English pages picked up new protected terms in the last week, all of them correct edits that should have merged:
Glossary_and_FAQs/Zcash_Library.mdZkoolUsing_Zcash/Shielded_Pools.mdZIP,Halo,NU6Zcash_Tech/Lightwallet_Nodes.mdZebra,zcashd,Zcashd,Zaino,ZODLThat lag is structural: English lands in one PR, the 18 locales follow in a later sync pass. But
check-protected-terms.mjsscanned the wholetranslations/tree with no notion of what the change under test did, so one English edit anywhere reddened everything. The only workable response was to merge through the red — which is how a real regression eventually gets waved past.What this does
--base <ref>classifies each violation instead of treating them all as equally fatal:introducedbase..HEADand did not violate at the merge base (a newly added translation counts)stale-sourcepre-existingEnglish-source drift goes to its actual owner —
detect-staleness.mjsand the staleness dashboard (#1889), which already tracks it and regenerates on everysite/**push.Net effect: editing an English page no longer reds anything. Shipping a bad translation still fails, immediately, with only the real violations in the log.
Details worth reviewing
-M), so a page moved together with its source is still judged against its own history rather than looking newly broken.manifest-invariantsjob in this same workflow — sameDetermine base refstep, same env-passed refs (never interpolated into the shell), same fail-closed rule that a requested-but-unresolvable base is an error, not a silent skip. An explicitly empty--baseis likewise an error rather than a quiet downgrade.HEAD^whengithub.event.beforeis the all-zero SHA (branch creation) or has become unreachable (force-push), so the job never fails for a pure history reason.fetch-depth: 0is now required, matching the invariants job.--basenothing changes, so a localnode scripts/check-protected-terms.mjsstill reports the whole backlog (162 violations today).Second commit
check-invariants.mjsanswered a change-tracking failure with "record the new src/mode/tool, or flip edited:true for a deliberate hand-edit", as if those were interchangeable. They aren't:edited: truemeans a human authored this translation, never let automation touch it, and the sync agent holds those pages out of every future re-sync permanently.Taken at face value during the Aug-12 frontmatter cleanup, it got set on 220 locale-pages — silently freezing them against all future syncs, including pages this gate is failing on today, which no sync run could then have cured. (Those flags are reverted in the companion re-sync PR; they were all one-line frontmatter deletions with no content change.) The message now leads with
tooland states the flag's consequence.Verification
Against the current tree, which carries 162 inherited violations:
--base--base ""or an unresolvable refPlus
node --test translation/lib/*.test.mjs→ 38 pass / 0 fail, andcheck-invariants.mjs --baseclean (203 curated pages, 18 locales).Reviewed by three independent adversarial passes. One found a HIGH-severity false negative in an earlier revision — the base-pairing bug described above, where a pure rename could manufacture real violations that the gate waved through as pre-existing — which is fixed here and re-verified, along with six further attack variants (path swaps, mismatched source/translation moves, move-plus-rewrite that defeats
-Mdetection, moves from outsidetranslations/, rename chains, untracked bad translations), all of which now fail closed.Two known limitations, both fail-closed and neither affecting CI: an uncommitted paired
git mvfalse-positives on a local run (rename data is only available from the committed range; committing makes it pass), andgit showon base-absent paths leaks afatal: path … exists on diskline to stderr. Out of scope and pre-existing: renaming an English page still orphans its 18 translations into the "no English source (skipped)" bucket, and this gate has never consulted the curated page list.