612c417024
gates / gates (push) Successful in 19s
Message bundles (internal/i18n) expanded into templates before parsing, one template set per language. Launcher, /backups, /apps/<slug> and the layout converted; household language setting, POST /settings/language, ?lang= override, report field. Parity test against fixtures captured from unconverted templates; copy gates read templates expanded; new i18n_missing_gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
177 lines
11 KiB
Python
177 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""retrieval_promise_gate — pin the CLAIM, not the word (R-302).
|
|
|
|
WHY THIS IS NOT A STRING BAN. Five instances of "you can get your old backups back with your recovery
|
|
code" have surfaced ONE AT A TIME (R-294, R-299, and the two R-302 fixed this session), each found only
|
|
after the previous one was fixed. The obvious guard — forbid the sentence — was tried twice and failed
|
|
twice:
|
|
|
|
* v0.211.0 asserted the SINGULAR „visszaállítható lehet"; the card carried the PLURAL
|
|
„visszaállíthatók lehetnek" one paragraph above it and walked straight past (R-299).
|
|
* Broadening to the stem `visszaállíthat` then missed the banner entirely, because the banner says
|
|
„visszaszerezheted" — a different verb for the same claim.
|
|
|
|
AND THE STEM CANNOT BE BANNED. The honest replacement copy this session ships *contains the stem*:
|
|
„Hogy ezek még visszaszerezhetők-e … azt innen nem tudjuk megállapítani" is a QUESTION about
|
|
retrievability, and it is the correct sentence. A guard that forbade the stem would force the product
|
|
to avoid a normal Hungarian verb — a guard shaping the product around itself.
|
|
|
|
SO: every occurrence of a retrieval stem in a customer-facing template must be REGISTERED here with a
|
|
reason. Unregistered occurrences fail. The failure mode this actually catches is the real one — a new
|
|
claim appearing somewhere nobody was looking — without pretending a word is a claim.
|
|
|
|
Go template comments ({{/* … */}}) are stripped before scanning: html/template never renders them, so
|
|
prose explaining a fix is not a claim. HTML <!-- --> comments DO ship and are deliberately scanned.
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import i18n_bundle # noqa: E402
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
TEMPLATES = os.path.join(_HERE, "..", "internal", "web", "templates")
|
|
|
|
# R-311 — THE GATE HAD A BLIND SPOT THE SIZE OF THE RECOVERY SCREEN.
|
|
#
|
|
# It scanned `internal/web/templates` only. But every one of the recovery screen's messages is a Go
|
|
# STRING in a handler, not template text — including the four R-224 messages and the R-222/R-226 one,
|
|
# i.e. the highest-stakes customer copy in the product, on the one screen whose whole purpose is to be
|
|
# believed about someone's backups. None of it had ever been scanned.
|
|
#
|
|
# Go `//` and `/* */` comments are stripped for the same reason template comments are: they never
|
|
# reach a customer. (A `//` inside a Hungarian string literal would be stripped too — there are none,
|
|
# and a false NEGATIVE there is the safe direction for a guard that convicts on presence.)
|
|
GO_SOURCES = [
|
|
os.path.join(_HERE, "..", "internal", "web", "recovery_handlers.py".replace(".py", ".go")),
|
|
]
|
|
|
|
# The verbs that carry the claim "your old backups can be got back".
|
|
# R-311 adds `visszanyit`: the honest new message says a customer needs support's help „a régebbi
|
|
# előzményed visszanyitásához". That is the SAME claim in a fourth verb, and the docstring above
|
|
# records what happens when the guard chases words instead of claims — it misses the next one.
|
|
STEMS = ["visszaállíthat", "visszaszerezhet", "visszahozhat", "visszanyit"]
|
|
|
|
# (template, substring that identifies the occurrence) -> why it is allowed.
|
|
# The substring must be specific enough that a DIFFERENT claim in the same file does not match it.
|
|
ALLOWLIST = {
|
|
("backups.html", "amelyből az egész készülék visszaállítható"):
|
|
"the LOCAL whole-device backup, made and held by the host agent. Nothing to do with the "
|
|
"off-site escrow claim — no recovery code is involved.",
|
|
("backups_apps.html", "Eltávolítva — visszaállítható"):
|
|
"R-487: a REMOVED app's kept recovery unit. The row renders only when the unit's manifest "
|
|
"is readable on a CONNECTED registered drive (backup.ListRemovedAppUnits), and the claim is "
|
|
"the very action beside it — POST /backup/restore from that unit. No recovery code, no "
|
|
"off-site store: what the box can see on its own disk.",
|
|
("backups_apps.html", "alkalmazásonként visszaállítható"):
|
|
"per-app restore from the local app-data backup. Same: local, no recovery code.",
|
|
("backups_remote.html", "mentéseid visszaszerezhetők.</strong>"):
|
|
"the RecoveryOffer entry point. TRUE where it renders: it is gated on the hub telling this box "
|
|
"it holds a sealed package for it, which is the claim being made. Deliberately left alone.",
|
|
("backups_remote.html", "a mentéseid visszaszerezhetők, és a törlés elmarad."):
|
|
"the abandon block's promise — R-302 made it conditional on AbandonRetrievalOffered; this is "
|
|
"the TRUE branch.",
|
|
("backups_remote.html", "Hogy ezek még visszaszerezhetők-e"):
|
|
"R-302's cautious branch. Contains the stem inside a QUESTION about knowability — the sentence "
|
|
"the gate exists to protect, not to forbid.",
|
|
("layout.html", "Addig még visszaszerezheted őket a helyreállítási kóddal."):
|
|
"the banner's promise — R-302 made it conditional on RecoveryAbandonRetrievalOffered; TRUE branch.",
|
|
("layout.html", "Hogy ezek még visszaszerezhetők-e"):
|
|
"R-302's cautious branch on the banner. As above.",
|
|
("recovery_handlers.go", "A régi előzmény visszanyitása felülírná azt"):
|
|
"PRE-EXISTING and never scanned until R-311 extended this gate to Go handlers — which is the "
|
|
"point of extending it. It is NOT a promise: it is the reason for a REFUSAL (RecoverRefused, "
|
|
"a different repository password is already present), i.e. the sentence says the reopening "
|
|
"would overwrite and was therefore not done. Registered as an explanation, not a claim.",
|
|
("recovery_handlers.go", "A régebbi előzményed visszanyitásához a Felhom ügyfélszolgálatának segítsége kell"):
|
|
"R-311's truthful message for a code that opens a RETAINED package. It is a claim, and it is "
|
|
"TRUE: the drill of 2026-08-12 recovered exactly this by hand (unsealed the retained package, "
|
|
"opened the set-aside store, restored planted files byte-identical). It routes to SUPPORT "
|
|
"rather than to a button precisely because there is no in-product route yet — the restore "
|
|
"machinery resolves its repository from settings and its password from one file. If that route "
|
|
"is ever built, this entry changes; if support ever cannot do it, this sentence must go.",
|
|
("recovery.html", "a mentéseid visszaszerezhetők, és a törlés elmarad;"):
|
|
"the abandon CONFIRMATION screen, shown at the moment of the decision. True by construction "
|
|
"there: the package the hub holds right now is the one about to be pinned. Left alone.",
|
|
}
|
|
|
|
TEMPLATE_COMMENT = re.compile(r"\{\{/\*.*?\*/\}\}", re.S)
|
|
GO_COMMENT = re.compile(r"//[^\n]*|/\*.*?\*/", re.S)
|
|
|
|
|
|
# R-421 (2026-09-01): any depth, was os.listdir (one level). No template subdirectory exists
|
|
# today, so this was green and correct — and would have stayed green the moment anyone added
|
|
# templates/partials/. Measured: a planted file there passed every listdir-based gate.
|
|
def _html_at_any_depth(root):
|
|
out = []
|
|
for dirpath, _dirs, names in os.walk(root):
|
|
for fn in sorted(names):
|
|
if fn.endswith('.html'):
|
|
out.append(os.path.join(dirpath, fn))
|
|
return sorted(out)
|
|
|
|
|
|
def scan():
|
|
convictions, seen_keys = [], set()
|
|
files = [os.path.relpath(x, TEMPLATES) for x in _html_at_any_depth(TEMPLATES)]
|
|
sources = [(f, os.path.join(TEMPLATES, f), TEMPLATE_COMMENT) for f in files]
|
|
for gp in GO_SOURCES:
|
|
if not os.path.exists(gp):
|
|
raise SystemExit(f"retrieval-promise gate: declared Go source is missing: {gp}")
|
|
sources.append((os.path.basename(gp), gp, GO_COMMENT))
|
|
files = files + [os.path.basename(gp)]
|
|
for name, path, stripper in sources:
|
|
# v0.247.0: a template's copy lives in the i18n bundle; judge the page as it renders in
|
|
# Hungarian (the stems are Hungarian). English retrieval claims are not scanned yet -- R-row
|
|
# in 10-localisation.md; the English bundle covers three pages today.
|
|
raw = open(path, encoding="utf-8").read()
|
|
text = stripper.sub("", i18n_bundle.expand(raw, "hu") if path.endswith(".html") else raw)
|
|
for stem in STEMS:
|
|
for m in re.finditer(re.escape(stem) + r"[a-záéíóöőúüű]*", text):
|
|
line = text[: m.start()].count("\n") + 1
|
|
# SPAN-based, not window-based. The promise and the cautious disclaimer sit within a
|
|
# hundred characters of each other in the same paragraph, so a proximity window matches
|
|
# whichever key it tries first and reports the other as stale — which is exactly what a
|
|
# first draft of this gate did. An occurrence belongs to an entry only if it falls
|
|
# INSIDE that entry's own text.
|
|
hit = None
|
|
for k in (k for k in ALLOWLIST if k[0] == name):
|
|
for om in re.finditer(re.escape(k[1]), text):
|
|
if om.start() <= m.start() and m.end() <= om.end():
|
|
hit = k
|
|
break
|
|
if hit:
|
|
break
|
|
if hit:
|
|
seen_keys.add(hit)
|
|
else:
|
|
ctx = text[max(0, m.start() - 100): m.end() + 100]
|
|
convictions.append((name, line, m.group(0), " ".join(ctx.split())[:160]))
|
|
return files, convictions, seen_keys
|
|
|
|
|
|
def main():
|
|
files, convictions, seen = scan()
|
|
stale = [k for k in ALLOWLIST if k not in seen]
|
|
for name, line, word, ctx in convictions:
|
|
print(f" {name}:{line} unregistered retrieval claim ({word}):\n …{ctx}…")
|
|
for k in stale:
|
|
print(f" STALE ALLOWLIST ENTRY (no longer present): {k[0]} :: {k[1]!r}")
|
|
if convictions or stale:
|
|
print(f"\nRETRIEVAL-PROMISE GATE FAILED: {len(convictions)} unregistered, {len(stale)} stale, "
|
|
f"across {len(files)} template(s).")
|
|
print("Five instances of this claim have surfaced one at a time. If the new text is a genuine")
|
|
print("claim, make it CONDITIONAL on what the box can see; if it is a question about")
|
|
print("knowability, or an unrelated local-backup sentence, add it to ALLOWLIST with the reason.")
|
|
return 1
|
|
print(f"retrieval-promise gate OK — {len(files)} surface(s) incl. {len(GO_SOURCES)} Go handler file(s), "
|
|
f"{len(ALLOWLIST)} registered claim(s), none unregistered")
|
|
print(" (BLIND SPOT: it registers WHERE the claim is made, not whether each conditional is wired")
|
|
print(" to a true predicate — that is what the R-302 render tests are for.)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|