`scripts/check-copy-i18n.py`, fifth row of `catalog_gates.py`, static and in the
pre-push hook. Five checks:
1. FREEZE — every Hungarian copy string equals `copy_freeze/hu.json`. Runs on all
53 apps whatever scope is named: a scoped push that quietly edits a neighbour is
what a freeze is for. A NEW app must be admitted with `--add-app NAME --reason`.
2. STRUCTURE — the `i18n.en` block may carry copy fields and nothing else; every
key-matched entry (`env_var`, option `value`, `match_group`, `target`, `path`)
must have a Hungarian twin, or it would be INERT on the box and the translator
would never know. Lists must have the Hungarian's length — they are replaced
whole, never merged by index.
3. LANGUAGE — no accented Hungarian letter, no ASCII-ONLY Hungarian, no
"please"/"kindly", no English retrieval promise the Hungarian does not make, the
app name and „Felhom" preserved.
4. CREDENTIALS — the login tokens inside `default_creds` and the initial-credentials
note survive translation verbatim.
5. RATCHET — `EN_MISSING_CEILING` (1032 today) convicts above AND below.
MEASURED, against the numbers the task carried: 1 032 copy strings, 832 of them with
a Hungarian letter (that half matches). The ASCII-only Hungarian is NOT three strings
(„Igen"/„Nem"/„Nincs" do not occur in this catalog at all) but roughly 120 — „Aldomain"
and „A szerver domain neve" alone are 53 each. An accent-only gate would have passed
every one of them inside an English block, which is why check 3 folds and stems.
18 decoy cases in `test_gate_decoys.py`, each seen to convict or to pass as intended
(R-421). One of them found a real hole while being written: the credential check
searched for the token as a substring, so „admin" matched "administrator" and a
rewritten login passed. It now requires word boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
The rule (CLAUDE.md, operator ruling 2026-09-13): until the Update button takes a verified backup
as its precondition, no template may move a mariadb:/postgres: image across a major version. Four
MariaDB and eleven PostgreSQL services; the gate finds them by image name, not by a list.
scripts/check-engine-major.py — fast (git reads only), diffs each changed template's per-service
image: line between the two ends of the push range, refuses a major move naming the rule and its
expiry (R-448). Fourth row of catalog_gates.py; .githooks/pre-push now hands the push range
through as --range=<remote sha>..<local sha>.
HONEST LIMIT: it needs a parent commit and CI fetches at --depth 1 (the R-452 gap, not re-filed),
so on a shallow clone the runner SKIPS it out loud instead of reddening every CI push. The hook,
which has the full clone, is where it bites.
Red-proof (scripts/test_gate_decoys.py, 7 cases, all seen to judge correctly): mariadb 11.6->12.3
REFUSED, postgres 16->17 REFUSED, mariadb:lts INCONCLUSIVE; 11.6->11.8 PASSES; the major moving
only in a comment / kimai's serverVersion env / README / the app's own image PASSES. COVERS literal
registered for felhom.eu's decoy_coverage_gate (which now reads 1 covered, 3 exempt, 0 unaccounted).
test_catalog_gates.py pins the four-gate table and the announced shallow-clone skip.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS