diff --git a/.claude/rules/gates.md b/.claude/rules/gates.md index 130b5ef..708f722 100644 --- a/.claude/rules/gates.md +++ b/.claude/rules/gates.md @@ -54,3 +54,25 @@ a probe stayed in D state 3m50s after kill -9; a buffered write with no fsync bl needs journal access); and statfs/getdents returned HEALTHY on a namespace that EIOs every byte — fast, and wrong. --> + +## A gate ships with a decoy test that has been seen to fail (R-421) + +**A decoy is the LABEL without the FACT** — a directory with the right name and no bake log, a +handler case that exists only in a comment, a note whose prose mentions the marker it lacks. Write +one for every new gate, run it, and watch it convict. `scripts/decoy_coverage_gate.py` refuses a gate +registered without one, or without a named exemption carrying its row. + +**Earned by five instances, every one found by accident:** R-410, R-400, R-378, R-419, R-94. The +2026-09-01 sweep read all 29 gate scripts and fooled 16 of them. The four shapes to test against: + +1. **name-for-fact** — it matches a path or directory NAME while the fact lives inside the file. +2. **substring-for-field** — it matches a token anywhere in a body instead of in the field carrying it. +3. **declaration-for-reachability** — it checks a thing is declared, not that it RESOLVES. +4. **constant-for-measurement** — it compares a value against itself. + +**Scope is a fact too.** Eight of the sixteen were `os.listdir` (one level) where `os.walk` was meant: +green and correct today, blind the moment anyone adds a subdirectory. Prefer `os.walk`, and prefer a +glob over a hand-maintained list of files. + +**A decoy nobody would write proves nothing** — say the gate is sound and move on. Five of mine were +withdrawn as illegitimate and are named in `documentation/audits/AUDIT-gate-decoys-2026-09-01.md`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6efcac7..d11065e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## the decoy sweep — can this gate be fooled by a label? (2026-09-01, R-421) — NOT A RELEASE + +**No product code, no version bump, no image, no golden.** A scripts change is not a release. + +Four times in one week a gate turned out to match a NAME instead of the thing it named — R-410 (a +`mkdir` turned the release gate green), R-400 (seven debug controls answering nothing), R-378 (a +status word inside a sentence), R-419 (a phrase inside prose, including prose saying the marker was +absent). **All four found by accident.** The gates enforce everything else here and were the one part +nothing had checked. + +**All 29 gate scripts read and decoyed. 16 were fooled.** 10 fixed here, 4 left with rows +(R-422..R-425), 6 could not be given a plausible decoy and are named (R-426 group d). + +**The largest single cause was mundane:** eight gates set their SCOPE with `os.listdir` (one level). +Green and correct today; blind the moment anyone adds `templates/partials/`. `mojibake` and +`docker-v` already used `os.walk`, caught the identical planted file, and are the control that +proves the cause was the listing rather than the decoy. + +Full survey table, and the five decoys withdrawn as illegitimate (mine, named): +`documentation/audits/AUDIT-gate-decoys-2026-09-01.md`. + +**In this repo:** six gates (`emoji`, `native-confirm`, `app-row-dedup`, `template-id`, +`secret-markup`, `retrieval-promise`) now walk instead of listing one directory; `debug-routes` and +`app-row-dedup` strip comments before matching — a dispatcher case left in a commented-out block +counted as a live handler, which is R-400 reached through the one door its own gate could not see. +New: `controller/scripts/test_gate_decoys.py` (10 decoys). R-425 (`offbox-rename`'s fixed FILES list) +is left OPEN with its decoy recorded rather than quietly fixed. + ## scripts — the golden NOTICE, where the debt is created (2026-09-01, R-404) — NOT A RELEASE **No version heading on purpose.** This changes no Go code, builds no image and bumps nothing. Giving diff --git a/controller/scripts/app_row_dedup_gate.py b/controller/scripts/app_row_dedup_gate.py index a6e2e04..c52e9a7 100644 --- a/controller/scripts/app_row_dedup_gate.py +++ b/controller/scripts/app_row_dedup_gate.py @@ -49,8 +49,13 @@ if not os.path.isdir(TPL): print("run from controller/ (internal/web/templates not found)") sys.exit(2) -files = {f: io.open(os.path.join(TPL, f), encoding="utf-8").read() - for f in sorted(os.listdir(TPL)) if f.endswith(".html")} +_paths = [] +for _dp, _dirs, _names in os.walk(TPL): # R-421: any depth, was os.listdir + for _f in sorted(_names): + if _f.endswith('.html'): + _paths.append(os.path.join(_dp, _f)) +files = {os.path.relpath(p, TPL): io.open(p, encoding='utf-8').read() + for p in sorted(_paths)} if "app_row.html" not in files: failures.append("templates/app_row.html is missing — the canonical row partial") @@ -70,8 +75,13 @@ for fname, pat, why in FORBIDDEN: if re.search(pat, src): failures.append("%s: forbidden old structure %r survives (%s)" % (fname, pat, why)) +# R-421 (2026-09-01): a commented-out partial call is not a render. Measured — replacing the real +# call with `` satisfied this check while the surface +# hand-rolled its own row again, which is the exact defect the gate exists to extinguish. +HTML_COMMENT_RE = re.compile(r"", re.S) + for fname in MUST_USE: - if '{{template "app_list_row"' not in files.get(fname, ""): + if '{{template "app_list_row"' not in HTML_COMMENT_RE.sub("", files.get(fname, "")): failures.append("%s: does not render through the app_list_row partial" % fname) if failures: diff --git a/controller/scripts/debug_route_gate.py b/controller/scripts/debug_route_gate.py index 06cdc4b..f5bb3e4 100644 --- a/controller/scripts/debug_route_gate.py +++ b/controller/scripts/debug_route_gate.py @@ -31,6 +31,30 @@ REF_RE = re.compile(r"/api/debug/([A-Za-z0-9/_-]+)") CASE_RE = re.compile(r'subpath\s*==\s*"([A-Za-z0-9/_-]+)"') +# R-421 (2026-09-01) — COMMENTS ARE NOT CODE, AND COMMENTS ARE NOT CONTROLS. +# +# Both sides of this gate were plain regexes over raw file text, so a `case subpath == "x":` left +# behind in a commented-out block counted as a live handler, and a `/api/debug/x` inside an HTML +# comment counted as a live control. Measured 2026-09-01: commenting out one dispatcher case while +# adding the matching button made this gate report OK on a control that does nothing — which is +# R-400's original defect, reachable again through the one door the gate could not see. +# +# Stripping is deliberately crude and that is correct here: this gate's own docstring insists on ten +# lines of logic that cannot rot. A `//` inside a string literal (a URL, say) would truncate that +# line — which can only ever HIDE a reference, never invent one, so it fails in the safe direction. +GO_COMMENT_RE = re.compile(r"//[^\n]*") +GO_BLOCK_RE = re.compile(r"/\*.*?\*/", re.S) +HTML_COMMENT_RE = re.compile(r"", re.S) + + +def strip_go_comments(src): + return GO_COMMENT_RE.sub("", GO_BLOCK_RE.sub("", src)) + + +def strip_html_comments(src): + return HTML_COMMENT_RE.sub("", src) + + def read(path): if not os.path.exists(path): print("DEBUG ROUTE GATE INCONCLUSIVE: %s not found (run from controller/)" % path) @@ -40,8 +64,8 @@ def read(path): def main(): # Sets, not lists: the same address referenced by two controls is satisfied by one case (§8). - refs = set(REF_RE.findall(read(TEMPLATE))) - cases = set(CASE_RE.findall(read(DISPATCH))) + refs = set(REF_RE.findall(strip_html_comments(read(TEMPLATE)))) + cases = set(CASE_RE.findall(strip_go_comments(read(DISPATCH)))) dead = sorted(refs - cases) unreached = sorted(cases - refs) diff --git a/controller/scripts/emoji_gate.py b/controller/scripts/emoji_gate.py index 2a5a937..2a60bed 100644 --- a/controller/scripts/emoji_gate.py +++ b/controller/scripts/emoji_gate.py @@ -49,13 +49,25 @@ def scan(path): return hits +def _html_files(root): + # R-421 (2026-09-01): AT ANY DEPTH. This was `os.listdir`, one level only. There are no + # template subdirectories today, so the gate was green and correct — and would have stayed + # green the moment anyone added `templates/partials/`, which is an ordinary thing to do. + # Measured: a planted template in a new partials/ directory passed every listdir-based gate + # and was caught by the two that already used os.walk. See AUDIT-gate-decoys-2026-09-01.md. + 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 main(): total = 0 for root in ROOTS: - for fn in sorted(os.listdir(root)): - if not fn.endswith(".html"): - continue - path = os.path.join(root, fn) + for path in _html_files(root): + fn = os.path.relpath(path, root) for lineno, ch, name in scan(path): total += 1 print("%s:%d %s %s" % (fn, lineno, ch, name)) diff --git a/controller/scripts/native_confirm_gate.py b/controller/scripts/native_confirm_gate.py index a18ce2e..8e475bd 100644 --- a/controller/scripts/native_confirm_gate.py +++ b/controller/scripts/native_confirm_gate.py @@ -22,13 +22,25 @@ ROOTS = [ NATIVE = re.compile(r"(?Kesz \U0001F600

\n") +in_subdir("native-confirm", "native_confirm_gate.py", + u"\n") +in_subdir("app-row-dedup", "app_row_dedup_gate.py", u'
hand-rolled
\n') +in_subdir("template-id", "template_id_gate.py", + u'
\n\n') +in_subdir("secret-markup", "secret_in_markup_gate.py", + u'\n') +in_subdir("retrieval-promise", "retrieval_promise_gate.py", + u"

A jelszavat barmikor visszaallithatja innen.

\n" + u"

Bovebben: visszaállítható a kóddal.

\n") + +# --- CONTROL: two gates already walked. If these ever fail, the decoy is wrong, not the gate ---- +in_subdir("mojibake (CONTROL)", "mojibake_gate.py", u"

árvíztuquotrő

\n") + +# --- COMMENTS ARE NOT CODE (R-421) ------------------------------------------------------------- +DISPATCH = os.path.join(CTRL, "internal", "web", "handler_debug.go") +DEBUG_TPL = os.path.join(CTRL, "internal", "web", "templates", "debug.html") + + +def _comment_out_a_real_case(src): + """Take a LIVE dispatcher case and comment it out. The button stays; the handler dies.""" + m = re.search(r'^(\s*)(case subpath == "[A-Za-z0-9/_-]+".*:)$', src, re.M) + assert m, "no dispatcher case found — the decoy cannot be built" + return src[:m.start()] + m.group(1) + "// " + m.group(2) + src[m.end():] + + +swapped("debug-routes/comment", "debug_route_gate.py", DISPATCH, _comment_out_a_real_case) + + +def _comment_out_the_partial(src): + return re.sub(r'(\{\{template "app_list_row".*?\}\})', r'', src) + + +swapped("app-row-dedup/comment", "app_row_dedup_gate.py", + os.path.join(TPL, "dashboard.html"), _comment_out_the_partial) + +# --- golden-notice: R-410's decoy, in the other direction. It is ADVISORY, so rc is never the --- +# --- question — what it COUNTED is. --- +ran += 1 +EV = os.path.join(os.path.dirname(os.path.dirname(CTRL)), "felhom.eu", "documentation", "tests", + "golden-9.9.9-2026-01-01") +if os.path.isdir(os.path.dirname(EV)): + os.makedirs(EV) + try: + p = subprocess.run([sys.executable, os.path.join("scripts", "golden_notice.py"), + os.path.dirname(CTRL)], cwd=CTRL, capture_output=True, text=True) + out = p.stdout + p.stderr + finally: + os.rmdir(EV) + if "9.9.9" in out and "NOT counted" not in out: + fails.append("golden-notice: an EMPTY directory was counted as a bake (R-410 regressed)") + else: + print(" ok %-20s empty dir not counted as a bake" % "golden-notice") +else: + print(" -- %-20s SKIPPED: no felhom.eu sibling clone" % "golden-notice") + +print() +if fails: + for f in fails: + print("FAIL: %s" % f) + sys.exit(1) +print("all %d controller decoys behaved — labels do not satisfy these gates" % ran)