hub v0.119.0 — English households get English words for their codes (R-597); R-596/R-598 closed
gates / gates (push) Successful in 24s
gates / gates (push) Successful in 24s
The setup code and the owner passphrase now follow the household's language,
one word longer in English so the entropy never drops (setup 3 hu / 4 en,
passphrase 5 hu / 6 en). List and count are chosen together so a caller cannot
pair an English list with a Hungarian count. Hungarian is byte-unchanged.
Three claims in the row were wrong and are recorded as such:
- the RECOVERY CODE is minted by felhom-agent from the EFF list and has
always been English; the hub does not own it and no row was added.
- no claim mail states a word count; the only count wording was the bind
page's passphrase hint, whose English half is now count-free.
- the proposed phone-safe filter removes 68% of the list (5270 of 7772
words) and was measured, then declined, with the reason in source.
Also: guide_quote_gate binds the English volunteer guide's three quoted
messages to the controller's English bundle — nothing did, so the guide would
have gone on quoting Hungarian after the fix. Seven decoys, all convicting,
including the name-for-fact one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user