Skip to content

ci(i18n): scope the protected-terminology gate to what a change introduces - #1958

Merged
zksquirrel merged 2 commits into
ZecHub:mainfrom
bloxster:i18n/scope-protected-terms-gate
Aug 14, 2026
Merged

ci(i18n): scope the protected-terminology gate to what a change introduces#1958
zksquirrel merged 2 commits into
ZecHub:mainfrom
bloxster:i18n/scope-protected-terms-gate

Conversation

@bloxster

Copy link
Copy Markdown
Contributor

Why

protected-terms is red on every open PR in this repo right now — #1930, #1933, #1948, #1957 — and on main since 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:

English page edited terms the translations don't carry yet
Glossary_and_FAQs/Zcash_Library.md Aug 7 Zkool
Using_Zcash/Shielded_Pools.md Aug 9 (Ironwood) ZIP, Halo, NU6
Zcash_Tech/Lightwallet_Nodes.md Aug 10 (Tor/Zaino) Zebra, zcashd, Zcashd, Zaino, ZODL

That lag is structural: English lands in one PR, the 18 locales follow in a later sync pass. But check-protected-terms.mjs scanned the whole translations/ 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:

class meaning result
introduced the translated file changed in base..HEAD and did not violate at the merge base (a newly added translation counts) FAIL
stale-source translation untouched; the English source moved under it notice
pre-existing already violating at the merge base notice

English-source drift goes to its actual owner — detect-staleness.mjs and the staleness dashboard (#1889), which already tracks it and regenerates on every site/** 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

  • 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 then 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 rather than looking newly broken.
  • Base handling mirrors the manifest-invariants job in this same workflow — same Determine base ref step, 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 --base is likewise an error rather than a quiet downgrade.
  • The push path falls back to HEAD^ when github.event.before is the all-zero SHA (branch creation) or has become unreachable (force-push), so the job never fails for a pure history reason.
  • A violation carried in a file the change does edit is listed separately — visible and cheap to fix in passing, but not fatal. Making it fatal would put the 18-locale sync burden on whoever next touches the file, which is the blocking this PR exists to remove.
  • fetch-depth: 0 is now required, matching the invariants job.
  • Without --base nothing changes, so a local node scripts/check-protected-terms.mjs still reports the whole backlog (162 violations today).

Second commit

check-invariants.mjs answered 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: true means 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 tool and states the flag's consequence.

Verification

Against the current tree, which carries 162 inherited violations:

case expected result
whole-tree audit, no --base exit 1, all 162 reported
PR touching nothing relevant exit 0, 162 as notice
protected term stripped from a translation (committed or uncommitted) exit 1, exactly those violations
protected term added to an English page only exit 0, reported as staleness
page moved together with its source, already carrying drift exit 0
translation moved into a different page's slot exit 1, the 4 violations it creates
--base "" or an unresolvable ref exit 1 with an explicit error

Plus node --test translation/lib/*.test.mjs → 38 pass / 0 fail, and check-invariants.mjs --base clean (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 -M detection, moves from outside translations/, 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 mv false-positives on a local run (rename data is only available from the committed range; committing makes it pass), and git show on base-absent paths leaks a fatal: path … exists on disk line 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.

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.
@zksquirrel
zksquirrel merged commit aea84a0 into ZecHub:main Aug 14, 2026
4 checks passed
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.

2 participants