R-302: the abandon banner promises only what the box can still see is true
gates / gates (push) Successful in 10s
gates / gates (push) Successful in 10s
The retrieval clause rendered unconditionally on every page and is false on a reachable state - the same screen where the orphan card says we cannot tell. The condition is a fingerprint PINNED at the decision, not a comparison against the current key. The obvious proxy asks about the wrong key: the set-aside copies were written under an older key the box no longer has, so on a twice-rebuilt box the proxy promises about copies nothing can open. Demonstrated - under the proxy, the replaced-package and legacy cases both flip back to promising. The pin is a recorded assumption and says so: nothing on the box records which key wrote those copies. Empty is not a match. A countdown started before this carries no pin and takes the cautious branch, not a backfill. A sweep of all 36 templates found a fourth instance (backups page, same condition applied) and a fifth (the confirmation screen, correctly left alone - true at the moment of the decision). New retrieval_promise_gate registers each claim with a reason rather than banning a verb: a string ban failed twice, and the honest replacement copy contains the stem.
This commit is contained in:
@@ -58,6 +58,7 @@ GATES = [
|
||||
("mojibake", os.path.join(SCRIPTS, "mojibake_gate.py"), [], True),
|
||||
("docker-v", os.path.join(SCRIPTS, "docker_run_volume_path_gate.py"), [], True),
|
||||
("secret-markup", os.path.join(SCRIPTS, "secret_in_markup_gate.py"), [], True),
|
||||
("retrieval-promise", os.path.join(SCRIPTS, "retrieval_promise_gate.py"), [], True),
|
||||
("reuse-refs", SHARED_REUSE, [REPO], True),
|
||||
("instructions", SHARED_INSTRUCTIONS, [REPO], True),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/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.</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.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())
|
||||
Reference in New Issue
Block a user