Files
felhom-controller/controller/scripts/template_id_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

102 lines
4.6 KiB
Python

# -*- coding: utf-8 -*-
"""D1 §10 JS element-ID integrity gate.
For every template under internal/web/templates and internal/setup/templates, extract each
getElementById('X') / querySelector('#X') literal used by the file's inline JS and assert an
id="X" exists in the SAME file — or that the ID is created by that file's own JS (innerHTML /
insertAdjacentHTML string containing id="X" / id='X'), or is explicitly allowlisted below with
a justification.
Exit 1 on any unresolved reference. Run from the repo's controller/ directory:
python scripts/template_id_gate.py
"""
import io, os, re, sys
ROOTS = [
os.path.join("internal", "web", "templates"),
os.path.join("internal", "setup", "templates"),
]
# Dynamic-ID exceptions: (template, id-prefix-or-name) -> justification.
# Suffix-parameterized IDs (id + variable) are handled generically below; these are the rest.
ALLOW = {
# layout.html builds the alert/delete/remove modals entirely in JS and later looks them up.
("layout.html", "alert-modal"): "created by showAlert() via innerHTML in the same file",
("layout.html", "delete-modal"): "created by deleteOrphanStack() via innerHTML",
("layout.html", "remove-modal"): "created by removeStack() via innerHTML",
("layout.html", "confirm-delete-btn"): "created inside the delete-modal innerHTML",
("layout.html", "confirm-remove-btn"): "created inside the remove-modal innerHTML",
("layout.html", "delete-hdd-check"): "created inside the delete-modal innerHTML",
("layout.html", "remove-hdd-check"): "created inside the remove-modal innerHTML",
("layout.html", "remove-backup-check"): "created inside the remove-modal innerHTML",
("layout.html", "remove-hdd-keep-warning"): "created inside the remove-modal innerHTML",
("layout.html", "sync-btn"): "lives on stacks.html; syncTemplates() is shared layout JS guarded by if(!btn)return",
("layout.html", "sync-toast"): "lives on stacks.html; guarded null-check",
}
GET_RE = re.compile(r"getElementById\(\s*['\"]([A-Za-z0-9_-]+)['\"]\s*\)")
GET_DYN_RE = re.compile(r"getElementById\(\s*['\"]([A-Za-z0-9_-]+)['\"]\s*\+")
QS_RE = re.compile(r"querySelector\(\s*['\"]#([A-Za-z0-9_-]+)['\"]\s*\)")
ID_ATTR_RE = re.compile(r"""id=["']([A-Za-z0-9_{}\. $-]+)["']""")
ID_IN_JS_RE = re.compile(r"""id=\\?["']([A-Za-z0-9_-]+)\\?["']""")
def check(path):
fname = os.path.basename(path)
src = io.open(path, encoding="utf-8").read()
static_refs = set(GET_RE.findall(src)) | set(QS_RE.findall(src))
dyn_prefixes = set(GET_DYN_RE.findall(src))
# static refs regex also matches the dynamic form's literal — subtract prefixes used with '+'
static_refs -= dyn_prefixes
defined = set(ID_ATTR_RE.findall(src)) | set(ID_IN_JS_RE.findall(src))
defined_prefixes = tuple(d.split("{{")[0] for d in defined if "{{" in d or d.endswith("-"))
problems = []
for ref in sorted(static_refs):
if ref in defined:
continue
# a template-parameterized id like id="field-{{.EnvVar}}" legitimately renders
# ids such as field-SUBDOMAIN — match static refs against those prefixes
if defined_prefixes and ref.startswith(defined_prefixes):
continue
if (fname, ref) in ALLOW:
continue
problems.append("static #%s not defined in %s" % (ref, fname))
for pref in sorted(dyn_prefixes):
# a dynamic lookup 'x-' + var needs SOME id starting with that prefix (template- or JS-created)
if any(d.startswith(pref) for d in defined) or pref in defined_prefixes:
continue
if (fname, pref) in ALLOW:
continue
problems.append("dynamic prefix #%s* not defined in %s" % (pref, fname))
return problems
# R-421 (2026-09-01): any depth, was os.listdir (one level). No template subdirectory exists
# today, so this was green and correct — and would have stayed green the moment anyone added
# templates/partials/. Measured: a planted file there passed every listdir-based gate.
def _html_at_any_depth(root):
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():
bad = []
for root in ROOTS:
for _p in _html_at_any_depth(root):
bad += check(_p)
if bad:
print("INTEGRITY GATE FAILED (%d):" % len(bad))
for b in bad:
print(" -", b)
sys.exit(1)
print("integrity gate OK — every JS element-ID reference resolves within its own template")
if __name__ == "__main__":
main()