#!/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 TEMPLATES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "internal", "web", "templates") # The verbs that carry the claim "your old backups can be got back". STEMS = ["visszaállíthat", "visszaszerezhet", "visszahozhat"] # (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", "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."): "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.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) def scan(): convictions, seen_keys = [], set() files = sorted(f for f in os.listdir(TEMPLATES) if f.endswith(".html")) for name in files: path = os.path.join(TEMPLATES, name) text = TEMPLATE_COMMENT.sub("", open(path, encoding="utf-8").read()) 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)} templates, {len(ALLOWLIST)} registered claim(s), " f"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())