681cc663ef
gates / gates (push) Failing after 13s
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
82 lines
3.1 KiB
Python
82 lines
3.1 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""D1 §10 Python emoji gate — Windows grep silently fails to match multibyte emoji (the D0
|
||
grep-gate zero was a false negative). This scans templates by Unicode codepoint.
|
||
|
||
Run from controller/: python scripts/emoji_gate.py
|
||
Exit 1 if any emoji/pictograph remains in web+setup templates.
|
||
"""
|
||
import io, os, sys, unicodedata
|
||
|
||
ROOTS = [
|
||
os.path.join("internal", "web", "templates"),
|
||
os.path.join("internal", "setup", "templates"),
|
||
]
|
||
|
||
# Codepoint ranges that count as "emoji / pictographs / dingbats" for UI-copy purposes.
|
||
# Deliberately does NOT flag Hungarian letters, typographic quotes/dashes, the middle dot (·),
|
||
# arrows used as affordances (→ ↗ ↻ ↑ ↓), the multiplication sign (×), or box-drawing.
|
||
def is_emoji(ch):
|
||
o = ord(ch)
|
||
ranges = [
|
||
(0x1F300, 0x1FAFF), # Misc symbols & pictographs, emoticons, transport, supplemental, symbols-ext
|
||
(0x2600, 0x26FF), # Misc symbols (☀ ⚙ ⚠ ☁ …)
|
||
(0x2700, 0x27BF), # Dingbats (✅ ✂ ✈ ✏ ✓? no — see allow)
|
||
(0x1F000, 0x1F0FF), # Mahjong/dominoes/cards
|
||
(0xFE00, 0xFE0F), # Variation selectors (emoji presentation)
|
||
(0x1F1E6, 0x1F1FF), # Regional indicators
|
||
]
|
||
if any(a <= o <= b for a, b in ranges):
|
||
return True
|
||
return False
|
||
|
||
# Dingbat codepoints that are legitimate UI glyphs (checkmarks/crosses used as plain text marks,
|
||
# not emoji). We keep these OUT of the ban — they render as monochrome text, not color emoji.
|
||
ALLOW = set("✓✗✔✘•●○■▶") # ✓ ✗ ✔ ✘ • ● ○ ■ ▶
|
||
|
||
|
||
def scan(path):
|
||
hits = []
|
||
for lineno, line in enumerate(io.open(path, encoding="utf-8"), 1):
|
||
for ch in line:
|
||
if ch in ALLOW:
|
||
continue
|
||
if is_emoji(ch):
|
||
try:
|
||
name = unicodedata.name(ch)
|
||
except ValueError:
|
||
name = "U+%04X" % ord(ch)
|
||
hits.append((lineno, ch, name))
|
||
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 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))
|
||
if total:
|
||
print("EMOJI GATE FAILED: %d emoji found" % total)
|
||
sys.exit(1)
|
||
print("emoji gate OK — no emoji in web/setup templates")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|