faef8f8a91
Operator ruling 2026-10-12 (S27, 09 §3 decision 222). The operator approves the Hungarian text before release. A box whose first ingestion failed now serves one bilingual waiting page and retries every minute instead of the wizard. Own commit so the lead can hold it back. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
101 lines
4.5 KiB
Python
101 lines
4.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""D1 §10 JS element-ID integrity gate.
|
|
|
|
For every template under internal/web/templates (the setup wizard templates are deleted, R-554), 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"),
|
|
]
|
|
|
|
# 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()
|