Files
felhom-controller/controller/scripts/app_row_dedup_gate.py
T
admin 681cc663ef
gates / gates (push) Failing after 13s
decoy sweep: eight holes in this repo's gates, all measured, all fixed (R-421)
Every gate was DECOYED - the label constructed without the fact, the gate run, the verdict recorded.
No verdict here was reached by reading, because reading is exactly how the five prior instances hid.

SCOPE IS A FACT TOO, and it was the big one. Six gates decided what to look at with os.listdir - one
directory level. Every one was green AND CORRECT, because no template subdirectory exists today; every
one would have gone blind the moment anyone added templates/partials/, which is an ordinary act. A
single planted file carrying an emoji, a native confirm(), hand-rolled row markup, a dangling JS id
reference, a templated secret and an unregistered retrieval promise passed all six.

THE CONTROL IS WHAT MAKES THAT A MEASUREMENT: mojibake and docker-v already used os.walk, saw the
identical planted file, and convicted. So the cause was the listing, not the decoy.

COMMENTS ARE NOT CODE, AND COMMENTS ARE NOT CONTROLS. debug-routes matched `case subpath == "x"` in
raw text, so a case left in a commented-out block counted as a live handler - which is R-400's
original defect (seven dead controls on the page an operator opens when something is already wrong)
reached through the one door its own gate could not see. app-row-dedup's MUST_USE check had the same
shape: a commented-out {{template "app_list_row"}} satisfied it.

Stripping is deliberately crude in debug_route_gate, and that is correct there: its own docstring
insists on ten lines that cannot rot. A // inside a string literal truncates that line, which can
only ever HIDE a reference, never invent one - it fails in the safe direction.

NOT FIXED, and left open with its decoy rather than quietly patched: R-425, offbox-rename scans a
fixed three-entry FILES list, so banned NAS branding in a NEW offbox template passes. The scope was
correct when written and silently narrows every time the feature grows a file.

test_gate_decoys.py holds 10 decoys and declares COVERS, which felhom.eu's new decoy-coverage gate
AST-parses - a substring search for coverage would be the very shape this sweep exists to find.

No Go code. No version bump. No image. No golden owed.
Survey: felhom.eu/documentation/audits/AUDIT-gate-decoys-2026-09-01.md
2026-09-01 12:39:17 +02:00

93 lines
4.3 KiB
Python

# -*- coding: utf-8 -*-
"""v0.126.0 shared app-row dedup gate — the app-list row markup exists ONCE.
The canonical row (icon + name left, action block right) is defined in
templates/app_row.html (app_list_row / app_list_row_end). Every list surface renders
THROUGH it; hand-rolled copies are the defect this gate extinguishes (the pre-0.126.0
state: three visually diverging row structures across four surfaces).
Asserts:
1. The row-opening markup (`class="app-row"...`) appears in app_row.html ONLY.
2. The row-icon markup (`app-row-icon`) appears only in app_row.html plus the ONE
allowlisted aligned copy: the backups_apps.html expander header (it owns the
expand/collapse toggle, so it is aligned to the grammar, not rendered through
the partial).
3. The old duplicated structures are gone from the converted surfaces:
storage-path-item rows on the backups pages, stack-card rows on the dashboard.
4. Each converted surface actually references the partial.
Run from controller/: python scripts/app_row_dedup_gate.py
Exit 1 on any violation.
"""
import io, os, re, sys
TPL = os.path.join("internal", "web", "templates")
# (file, forbidden-pattern, why)
FORBIDDEN = [
("backups_remote.html", r"storage-path-item", "toggle list must render through app_list_row"),
("backups_remote.html", r"storage-path-header", "old row structure"),
("backups_restore.html", r"storage-path-item", ".fab + restore-to-verify lists must render through app_list_row"),
("backups_restore.html", r"storage-path-header", "old row structure"),
("dashboard.html", r"stack-card", "dashboard rows must render through app_list_row"),
("dashboard.html", r"stack-info", "old row structure"),
("dashboard.html", r'stack-logo"', "old row logo (stack-logo-lg on stacks.html is the card, not a list row)"),
]
# files that MUST reference the shared partial
MUST_USE = ["backups_remote.html", "backups_restore.html", "dashboard.html"]
# `class="app-row"` / `class="app-row {{...}}"` opening markup — [^-] excludes the
# derived class names (app-row-list, app-row-icon, ...).
ROW_OPEN_RE = re.compile(r'class="app-row[^-]')
ICON_RE = re.compile(r'app-row-icon')
ICON_ALLOW = {"app_row.html", "backups_apps.html"} # backups_apps: the aligned expander header
failures = []
if not os.path.isdir(TPL):
print("run from controller/ (internal/web/templates not found)")
sys.exit(2)
_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")
for fname, src in files.items():
n_open = len(ROW_OPEN_RE.findall(src))
if fname == "app_row.html":
if n_open != 1:
failures.append("app_row.html: expected the row-opening markup exactly once, found %d" % n_open)
elif n_open:
failures.append("%s: hand-rolled app-row markup (%d occurrence(s)) — render through the app_list_row partial" % (fname, n_open))
if ICON_RE.search(src) and fname not in ICON_ALLOW:
failures.append("%s: app-row-icon outside the partial/allowlist — do not copy the icon markup" % fname)
for fname, pat, why in FORBIDDEN:
src = files.get(fname, "")
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 `<!-- was: {{template "app_list_row" . }} -->` 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 HTML_COMMENT_RE.sub("", files.get(fname, "")):
failures.append("%s: does not render through the app_list_row partial" % fname)
if failures:
print("app_row_dedup_gate: FAIL")
for f in failures:
print(" - " + f)
sys.exit(1)
print("app_row_dedup_gate: OK (row markup single-sourced in app_row.html; %d templates scanned)" % len(files))