#!/usr/bin/env python3 """guide_quote_gate -- the English guide may only quote what the screen actually says (R-596/R-598). Run from anywhere: python3 scripts/guide_quote_gate.py Exit 0 clean * 1 convicted * 2 inconclusive (a file it needs is missing). WHY THIS EXISTS. `documentation/runbooks/VOLUNTEER-first-hour.en.md` is what a volunteer tester follows instead of the Hungarian guide. Three of its lines QUOTE messages the dashboard prints: the claim page's wrong-code and lockout answers, and the Backup page's system-disk warning. Those three sentences live in the CONTROLLER's English bundle, in a different repo, and nothing bound them together -- so the guide would have gone on quoting Hungarian for as long as nobody re-walked it. That is exactly how it read for a day: the 2026-09-20 drill's guide quoted the Hungarian because the Hungarian was what shipped, and when v0.259.0 changed the screen the guide became wrong in the other direction. WHAT IT CHECKS. Each entry below names a bundle KEY and asserts its English value appears verbatim in the guide. A reworded key, or a reworded guide, convicts and prints both sides. WHAT IT DOES NOT CHECK, deliberately: that the guide is complete, or that any OTHER sentence in it is accurate. It binds the three quotes that are quotes. Everything else in that document is prose a person has to re-walk, and pretending otherwise would be the label without the fact. SCOPE IS A FACT. The controller clone may not sit beside this one (CI checks out one repo). An absent sibling is INCONCLUSIVE (exit 2), never a silent pass -- the failure this project keeps closing is a check that reports green because it could not look. THE DECOY (R-421). scripts/test_guide_quote_gate.py builds a guide that MENTIONS every key by name in prose -- the label without the fact -- and asserts this gate still convicts, because it compares the VALUE, not the key. """ from __future__ import annotations import io import json import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.dirname(HERE) WORKSPACE = os.path.dirname(REPO) GUIDE = os.path.join(REPO, "documentation", "runbooks", "VOLUNTEER-first-hour.en.md") EN_BUNDLE = os.path.join( WORKSPACE, "felhom-controller", "controller", "internal", "i18n", "locales", "en.json" ) # key -> why the guide quotes it. The reason is part of the record: a future session deciding to # drop a quote should have to argue with the reason, not just with the list. QUOTED = { "claim.msg.bad_code": "section 13 -- the answer to a mistyped setup code. This is the sentence the 2026-09-20 " "English drill stopped on; a tester who reads a different one cannot tell a typo from a " "dead code, which is the whole point of the section.", "claim.msg.too_many": "section 13 -- the lockout answer after five wrong codes. The guide promises 15 minutes; " "if the message ever says something else, the guide is telling a tester to wait wrongly.", "claim.msg.password_too_short": "section 13 -- the minimum-password answer. Quoted with its number filled in, so the guide " "and claimMinPassword cannot disagree silently.", "backup.target.degraded": "section 9 -- the warning that says the backup does NOT survive a disk failure. It is a " "promise about whether the tester's files are safe.", } # Keys whose English carries a printf verb: the guide quotes the rendered form. value -> rendered. RENDER = { "claim.msg.password_too_short": lambda v: v % 12, } def main() -> int: if not os.path.exists(GUIDE): print("guide-quote: INCONCLUSIVE -- no %s" % os.path.relpath(GUIDE, REPO), file=sys.stderr) return 2 if not os.path.exists(EN_BUNDLE): print( "guide-quote: INCONCLUSIVE -- the felhom-controller clone is not beside this one " "(looked for %s). Not a pass: this gate cannot see what the screen says." % EN_BUNDLE, file=sys.stderr, ) return 2 guide = io.open(GUIDE, encoding="utf-8").read() bundle = json.load(io.open(EN_BUNDLE, encoding="utf-8")) problems = [] for key, why in sorted(QUOTED.items()): if key not in bundle: problems.append("MISSING KEY %s\n en.json does not know it.\n %s" % (key, why)) continue want = bundle[key] if key in RENDER: want = RENDER[key](want) if want not in guide: problems.append( "NOT QUOTED %s\n" " the screen says: %r\n" " the guide does not contain that sentence.\n" " %s" % (key, want, why) ) print("guide-quote: %d quoted messages checked against the controller's English bundle" % len(QUOTED)) if problems: print("\nguide-quote gate CONVICTS (%d):" % len(problems)) for p in problems: print(" " + p) print( "\nThe English guide and the English dashboard disagree. Fix whichever is wrong --\n" "but a tester follows the guide, so a guide that quotes a sentence nobody sees is the\n" "more expensive of the two." ) return 1 print("guide-quote gate OK: every quoted message is what the screen says.") return 0 if __name__ == "__main__": sys.exit(main())