b03a105375
gates / gates (push) Successful in 17s
Hub only. No controller change, no agent change, no wire change — nothing to bake. demo-hp untouched: the operator is re-deploying it this evening. R-323 — the five-word phrase is „Tulajdonosi jelmondat". It was „Visszaállító jelszó": one word from the name retired last week, and false besides — it restores nothing, it proves the account owns the box being bound. Five sites, all in the hub; felhom-controller and felhom-agent carry the name nowhere, so no halt and no bake. Both suggested names were rejected with reasons: „Fiókjelszó" would collide with the dashboard login (a DIFFERENT real secret), and „Összekötési jelszó" would leave the two factors on this page separated only by kód-versus-jelszó — the exact shape being removed, since the other factor is the „Párosító kód". The chosen name differs on both axes, stem and noun. Naming only; the acceptance pin drives the real handler. R-324 — the hub's customer copy is under a guard for the first time. Retired names banned across all 95 hub files; retrieval stems registered in four declared customer surfaces. The selftest found a defect in its own instrument on the first run. One shared vocabulary in scripts/, drift-checked into the controller gate rather than copied (R-325 removes the scaffold). R-321 — a machine we told to be quiet is no longer reported as dead, and it was two doors, not one: because the state is RECORDED rather than deleted, the morning deadline check can skip it too. A deleted state returns "", which is not "down" — R-195's shape returning through a second door. The clock runs from the report the hub can see, so re-enabling starts it there and emits no recovery for an outage that never happened. Three red-proofs; the one that matters showed a genuinely dead machine sitting at "disabled" when the suppression was made unconditional. R-326 — "which claims are unproven" is answerable by a command now. The nine I have been repeating was the count of claims the 9 August pass DOWNGRADED, not the count of unproven ones. The real figures: 55 claims, 23 walked, 32 not — and only 6 of those 32 cite evidence. Its first run found a stale claim (R-327).
245 lines
12 KiB
Python
245 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""hub_copy_gate — the hub's customer-facing copy comes under a guard (R-324).
|
|
|
|
THE SCOPE GAP THIS CLOSES. `retrieval_promise_gate.py` lives in `felhom-controller` and scans that
|
|
repo only. It was extended to Go string literals on 2026-08-12 (R-311) on the express ground that the
|
|
recovery screen's messages are Go strings in a handler and "none of it had ever been scanned" — and
|
|
the identical sentence was true one repo over the whole time. **The hub composes every customer
|
|
e-mail and renders the binding pages: the first sentences a customer ever reads, before they have
|
|
seen any box screen.** Nothing looked at them.
|
|
|
|
A hand scan on 2026-08-13 returned zero retrieval claims, so this was a SCOPE gap rather than a live
|
|
defect — which is the moment to close it, before someone writes the first hub-side promise assuming
|
|
the guard has them covered.
|
|
|
|
TWO CHECKS, and the difference is deliberate:
|
|
|
|
1. RETIRED NAMES — banned outright, scanned across the WHOLE hub, not just declared surfaces. A
|
|
name for a secret that a different secret now owns is never correct anywhere, so there is no
|
|
allowlist and no "unless". Comments are stripped: prose explaining a rename is not the rename
|
|
coming back, and the register rows that record these decisions quote the retired names by
|
|
necessity.
|
|
|
|
2. RETRIEVAL STEMS — registered, not banned, in the DECLARED customer-facing surfaces. The claim
|
|
they carry is sometimes true and must stay sayable; what must not happen is a new one appearing
|
|
where nobody was looking. Same contract as the controller gate: an occurrence is allowlisted
|
|
WITH A REASON, and a stale allowlist entry is also a failure.
|
|
|
|
BOTH lists come from `customer_copy_vocab.py` — see that file for why they are not literals here.
|
|
|
|
DRIFT CHECK, and it is load-bearing. The controller gate still owns its own `STEMS` literal (the
|
|
session that wrote this could not touch felhom-controller). This gate reads that literal and FAILS if
|
|
it disagrees with the shared list, so the two cannot silently diverge in the meantime. An ABSENT
|
|
sibling clone is INCONCLUSIVE (exit 2), never a pass — the G-1 lesson: a gate that skips when its
|
|
sibling is missing runs in neither home.
|
|
|
|
Run: python3 scripts/hub_copy_gate.py (from the felhom.eu repo root)
|
|
python3 scripts/hub_copy_gate.py --selftest (plant → convict → remove → pass)
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from customer_copy_vocab import RETIRED_NAMES, RETRIEVAL_STEMS # noqa: E402
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
_REPO = os.path.dirname(_HERE)
|
|
_HUB = os.path.join(_REPO, "hub")
|
|
|
|
# The hub files that carry HUNGARIAN CUSTOMER-FACING text — the surfaces where a retrieval claim
|
|
# could be made to a customer. Enumerated by scanning every non-test .go/.html under hub/internal for
|
|
# accented Hungarian outside comments, then keeping the ones a CUSTOMER (not the operator) reads.
|
|
#
|
|
# A declared file that is missing is a FAILURE, never a skip: the controller gate learned that when a
|
|
# renamed handler would have silently emptied its own scope.
|
|
CUSTOMER_SURFACES = [
|
|
os.path.join("hub", "internal", "notify", "templates.go"), # every customer e-mail + event copy
|
|
os.path.join("hub", "internal", "web", "selfbind.go"), # the binding pages
|
|
os.path.join("hub", "internal", "api", "handler.go"), # customer-visible event messages
|
|
os.path.join("hub", "internal", "notify", "dispatcher.go"), # the customer channel's own wording
|
|
]
|
|
|
|
# (file basename, substring that identifies the occurrence) -> why it is allowed.
|
|
# Empty today, and that is a measurement rather than an oversight: the hub makes no retrieval promise.
|
|
ALLOWLIST = {}
|
|
|
|
GO_COMMENT = re.compile(r"//[^\n]*|/\*.*?\*/", re.S)
|
|
TPL_COMMENT = re.compile(r"\{\{/\*.*?\*/\}\}", re.S)
|
|
HTML_COMMENT = re.compile(r"<!--.*?-->", re.S)
|
|
|
|
|
|
def strip_comments(text, path):
|
|
"""Remove what never reaches a customer.
|
|
|
|
Go `//` and `/* */`, Go-template `{{/* */}}`, and — unlike the controller gate — HTML `<!-- -->`
|
|
too. The controller gate deliberately SCANS HTML comments because its templates ship to a browser
|
|
where View Source is one keystroke. The hub's Hungarian lives in Go string literals that happen
|
|
to contain HTML, and its operator templates are not customer copy, so an HTML comment here is a
|
|
note to the next maintainer. Stated because it is a real difference between the two gates.
|
|
"""
|
|
text = TPL_COMMENT.sub("", text)
|
|
text = HTML_COMMENT.sub("", text)
|
|
if path.endswith(".go"):
|
|
text = GO_COMMENT.sub("", text)
|
|
return text
|
|
|
|
|
|
def _iter_hub_sources():
|
|
for root, _dirs, files in os.walk(os.path.join(_REPO, "hub", "internal")):
|
|
for f in sorted(files):
|
|
if f.endswith("_test.go") or not (f.endswith(".go") or f.endswith(".html")):
|
|
continue
|
|
yield os.path.join(root, f)
|
|
|
|
|
|
def scan_retired(extra_text=None):
|
|
"""Every retired name, everywhere in the hub. Returns a list of convictions."""
|
|
convictions = []
|
|
sources = [(p, open(p, encoding="utf-8").read()) for p in _iter_hub_sources()]
|
|
if extra_text is not None:
|
|
# The synthetic path MUST end in .go: strip_comments keys comment-stripping off the
|
|
# extension, so a selftest source named anything else would be scanned WITH its comments —
|
|
# which is how the first run of this selftest convicted its own step-3 control. The bug was
|
|
# in the instrument, and the control is what found it.
|
|
sources.append((os.path.join(_REPO, "hub", "internal", "selftest_planted.go"), extra_text))
|
|
for path, raw in sources:
|
|
text = strip_comments(raw, path)
|
|
for name in RETIRED_NAMES:
|
|
for m in re.finditer(re.escape(name), text):
|
|
line = text[: m.start()].count("\n") + 1
|
|
ctx = " ".join(text[max(0, m.start() - 70): m.end() + 70].split())[:150]
|
|
convictions.append((os.path.relpath(path, _REPO), line, name, ctx))
|
|
return convictions
|
|
|
|
|
|
def scan_stems():
|
|
"""Retrieval stems in the declared customer surfaces. Returns (convictions, seen_keys)."""
|
|
convictions, seen = [], set()
|
|
for rel in CUSTOMER_SURFACES:
|
|
path = os.path.join(_REPO, rel)
|
|
if not os.path.exists(path):
|
|
raise SystemExit("hub-copy gate: declared customer surface is MISSING: %s" % rel)
|
|
name = os.path.basename(path)
|
|
text = strip_comments(open(path, encoding="utf-8").read(), path)
|
|
for stem in RETRIEVAL_STEMS:
|
|
for m in re.finditer(re.escape(stem) + r"[a-záéíóöőúüű]*", 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.add(hit)
|
|
else:
|
|
line = text[: m.start()].count("\n") + 1
|
|
ctx = " ".join(text[max(0, m.start() - 90): m.end() + 90].split())[:170]
|
|
convictions.append((name, line, m.group(0), ctx))
|
|
return convictions, seen
|
|
|
|
|
|
def check_drift():
|
|
"""The controller gate's STEMS must equal the shared list. Returns (status, message).
|
|
|
|
status: "ok" | "drift" | "inconclusive"
|
|
"""
|
|
sibling = os.path.join(os.path.dirname(_REPO), "felhom-controller",
|
|
"controller", "scripts", "retrieval_promise_gate.py")
|
|
if not os.path.exists(sibling):
|
|
return "inconclusive", ("the felhom-controller clone is absent, so the shared stem list "
|
|
"could not be compared against the gate that also uses it (%s)" % sibling)
|
|
text = open(sibling, encoding="utf-8").read()
|
|
m = re.search(r"^STEMS\s*=\s*\[(.*?)\]", text, re.M | re.S)
|
|
if not m:
|
|
return "drift", "could not find a STEMS list in the controller gate — its shape changed"
|
|
theirs = re.findall(r"[\"']([^\"']+)[\"']", m.group(1))
|
|
if theirs != RETRIEVAL_STEMS:
|
|
return "drift", ("the controller gate's STEMS have diverged from customer_copy_vocab.py\n"
|
|
" controller : %r\n shared : %r" % (theirs, RETRIEVAL_STEMS))
|
|
return "ok", "controller gate's STEMS match the shared list (%d stem(s))" % len(theirs)
|
|
|
|
|
|
def selftest():
|
|
"""Plant → convict → remove → pass. A guard never seen catching anything proves nothing."""
|
|
print("hub-copy gate SELFTEST")
|
|
baseline = scan_retired()
|
|
if baseline:
|
|
print(" FAIL: the tree is not clean before planting — %d conviction(s)" % len(baseline))
|
|
for c in baseline:
|
|
print(" %s:%d [%s]" % (c[0], c[1], c[2]))
|
|
return 1
|
|
print(" 1. clean tree : 0 conviction(s) OK")
|
|
|
|
planted = 'body := "Add meg a visszaállító jelszavadat a folytatáshoz."\n'
|
|
convicted = scan_retired(extra_text=planted)
|
|
if len(convicted) != 1 or convicted[0][2] != "isszaállító jelsz":
|
|
print(" 2. planted retired name : NOT CONVICTED — the guard is inert")
|
|
print(" got: %r" % (convicted,))
|
|
return 1
|
|
print(" 2. planted „visszaállító jelszavadat”: CONVICTED (%s) OK" % convicted[0][2])
|
|
|
|
commented = '// the old name was „visszaállító jelszó" and is retired\n'
|
|
if scan_retired(extra_text=commented):
|
|
print(" 3. same phrase inside a COMMENT : convicted — comments must not be scanned")
|
|
return 1
|
|
print(" 3. same phrase inside a comment : not convicted OK")
|
|
|
|
if scan_retired():
|
|
print(" 4. planting removed : still convicting — the scan is not deterministic")
|
|
return 1
|
|
print(" 4. planting removed : 0 conviction(s) OK")
|
|
print("hub-copy gate selftest OK — the guard has been watched catching, ignoring and releasing")
|
|
return 0
|
|
|
|
|
|
def main():
|
|
if "--selftest" in sys.argv:
|
|
return selftest()
|
|
|
|
retired = scan_retired()
|
|
stems, seen = scan_stems()
|
|
stale = [k for k in ALLOWLIST if k not in seen]
|
|
drift_status, drift_msg = check_drift()
|
|
|
|
for path, line, name, ctx in retired:
|
|
print(" %s:%d RETIRED NAME in customer copy (%s):\n …%s…" % (path, line, name, ctx))
|
|
print(" reason it is retired: %s" % RETIRED_NAMES[name])
|
|
for name, line, word, ctx in stems:
|
|
print(" %s:%d unregistered retrieval claim (%s):\n …%s…" % (name, line, word, ctx))
|
|
for k in stale:
|
|
print(" STALE ALLOWLIST ENTRY (no longer present): %s :: %r" % (k[0], k[1]))
|
|
|
|
n_files = len(list(_iter_hub_sources()))
|
|
if retired or stems or stale:
|
|
print("\nHUB-COPY GATE FAILED: %d retired name(s), %d unregistered claim(s), %d stale."
|
|
% (len(retired), len(stems), len(stale)))
|
|
print("A retired name is never correct in customer copy — use the current name. If a new")
|
|
print("retrieval claim is genuinely TRUE where it renders, make it conditional on what the")
|
|
print("hub can actually see, then register it in ALLOWLIST with the reason.")
|
|
return 1
|
|
|
|
if drift_status == "drift":
|
|
print(" DRIFT: %s" % drift_msg)
|
|
print("\nHUB-COPY GATE FAILED: the shared vocabulary is no longer shared.")
|
|
return 1
|
|
|
|
print("hub-copy gate OK — %d hub file(s) scanned for %d retired name(s); %d customer surface(s) "
|
|
"scanned for %d retrieval stem(s), %d registered claim(s), none unregistered"
|
|
% (n_files, len(RETIRED_NAMES), len(CUSTOMER_SURFACES), len(RETRIEVAL_STEMS), len(ALLOWLIST)))
|
|
if drift_status == "inconclusive":
|
|
print(" ⚠ INCONCLUSIVE (exit 2): %s" % drift_msg)
|
|
return 2
|
|
print(" drift: %s" % drift_msg)
|
|
print(" (BLIND SPOT: this checks the WORDS in the four declared customer surfaces. It cannot")
|
|
print(" tell whether a true-looking sentence is wired to a predicate that is actually true —")
|
|
print(" that is what render tests are for. And it does not read the operator's screens.)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|