Files
felhom.eu/scripts/observations_gate.py
T
admin 574f5df107
gates / gates (push) Failing after 17s
the decoy sweep: 29 gates read, 16 fooled, 10 fixed - and a gate that refuses the next one (R-421)
THE CLASS, now a row: an instrument that matches a LABEL rather than the fact it names. Five
instances - R-410, R-400, R-378, R-419, R-94 - and EVERY ONE was found by accident, by someone
looking at something else. The gates enforce every other rule in this project, including the rule
that findings must be written down rather than left in prose. Nothing had ever checked the gates.

METHOD, and it is the transferable part: for each gate, construct 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 - run the gate, record what it says. No verdict was reached
by reading. Reading is how all five hid.

RESULT: 29 distinct scripts (35 registrations; three are shared across three runners). 19 sound, 4
holes left OPEN with rows, 6 that no plausible decoy could be built for and are named UNTESTED rather
than called sound. A gate nobody tried to fool is UNKNOWN.

SCOPE IS A FACT TOO - the largest single cause, and mundane. Eight gates decided what to look at with
os.listdir, one level. Every one was green AND CORRECT today, and every one would have gone blind the
moment anyone added a subdirectory. mojibake and docker-v already used os.walk, caught the identical
planted file, and are the control that proves the cause was the listing and not the decoy.

IN THIS REPO: hub-confirm and manifest-bearer now walk. observations_gate (R-419, CLOSED) requires a
marker at a line start or after a sentence boundary and strips inline code spans - a note SAYING it
carries no marker no longer satisfies the marker test. closed-register now CONVICTS on a row it
cannot parse instead of warning: FOUR rows were in that state, TWO of them written by the session
that closed them the day before, and every one was exempt from the only check that reads that file.
The rows were repaired first and the conviction added second - registering a failing gate refuses
every push.

THE META-GATE: decoy_coverage_gate.py refuses a gate registered without a decoy or a named exemption.
It convicted ITSELF the moment it was registered, which is how it came to have one. Coverage is a
DECLARATION the gate AST-parses, never a grep - searching a test file for a gate's name would be the
very shape this sweep exists to find. The 20 uncovered gates are listed by name (R-426).

NOT FIXED, each with a row and a decoy asserting TODAY's behaviour so the fix must be deliberate:
R-422 reuse-refs (only 7 extensions; a rotted .md citation is invisible), R-423 site (PAGES is a
hardcoded list of 7), R-424 one-register (a defect parked as `idea`), R-425 offbox-rename (fixed
FILES list). R-427: closed_register_gate checks ONE direction - twelve open rows carry a closed
verdict and were NOT moved, because telling finished from partly-finished is a judgement and R-378
is the record of a machine getting it wrong.

FIVE DECOYS WITHDRAWN AS ILLEGITIMATE, mine, named in the audit. A decoy nobody would write proves
nothing, and manufacturing a finding to fill a row is worse than an honest NO.

No product code. No version bump. No image. No golden owed. All four runners green.
Register: OPEN 172 -> 178, CLOSED 160 -> 161.
2026-09-01 12:39:45 +02:00

272 lines
13 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Observations gate — an observation with no row behind it refuses the push.
Run from the repo root: python3 scripts/observations_gate.py [path/to/REPORT.md]
Exit 0 clean · 1 convicted (an observation is neither filed nor declared) · 2 inconclusive.
WHY THIS EXISTS, and what it cost to learn.
On 2026-08-23 a session measured, on live hardware, that **only the first broken app per hour reaches
the operator** — the notification cooldown keys on the event type and not on the app. It was real, it
was reproducible, and it was written in a numbered item under `## Observations` in `REPORT.md`. **It
was written nowhere else.** There was no register row. `REPORT.md` is overwritten every session by
this project's own convention, so the finding had a lifetime of exactly one session.
That is R-341's shape one surface over — a commitment recorded in prose that nothing enforces — and
it is why gate 10 exists. The same argument applies here and nothing was reading this section.
**The instruction invited it.** `documentation/PROMPT-TEMPLATE.md` §15 asked for "observations:
out-of-scope items noticed, documented, not acted on". "Documented" was satisfied by the paragraph.
The template was corrected in the same session that added this gate; **this gate is the mechanism
that correction points at**, because a rule without a mechanism is a wish.
── THE RULE ─────────────────────────────────────────────────────────────────────────────────
Every numbered item in a `REPORT.md` observations section must carry exactly one explicit marker:
* ``FILED: R-NNN`` — this observation is filed, as that row. The row MUST resolve in
`OPEN-ITEMS.md` or `CLOSED-ITEMS.md`.
* ``NOT-A-FINDING:`` — deliberately not filed, followed by a reason on the same item.
── WHY A MARKER AND NOT "MENTIONS AN R-NUMBER", WHICH IS WHAT WAS ASKED FOR ──────────────────
The specification for this gate said an item may "cite an R-NNN that resolves". **That rule would
have passed the exact item this gate was built to catch**, and the discrepancy is recorded here
rather than quietly resolved:
"This is R-182's known cooldown-key shape; it was harmless while app_start_failed was
undeliverable and is not any more. Not fixed here."
`R-182` resolves. It is cited as an **analogy** — the family the defect belongs to — not as the row
that files it. No parser can tell a citation-as-precedent from a citation-as-filing by reading prose,
and a gate that guesses would either miss this item or convict every item that mentions history.
So the marker is explicit and the burden is one token. **The cost is a format requirement on one
section of one file; the benefit is that the failure mode which produced this gate cannot recur
silently.** This is the same trade `due_checks_gate.py` made: dates moved out of prose and into a
machine-readable block, inside the register so no sidecar can drift.
── BOUNDARIES, stated so they are not silently re-decided ────────────────────────────────────
* **No observations section → PASS, quietly.** Most pushes do not touch `REPORT.md`, and a gate
that taxes every push is one that gets disabled within a week. Absence of the section is not
evidence of a hidden finding.
* **No `REPORT.md` at all → INCONCLUSIVE.** This gate is registered per-repo precisely because the
repo has one; if it has vanished, that is worth a word rather than a silent pass.
* **A section present but with no parseable items → INCONCLUSIVE, naming what it could not read.**
Fail closed on ambiguity in the RULE, fail open on ambiguity in the PARSE — but INCONCLUSIVE is
never a silent pass, and the runner reports 2 distinctly for exactly that reason.
* **REFUSES, does not warn.** Gate 10's reasoning applies unchanged: a warning is the thing that
gets scrolled past, and this repo has the census to prove it.
* **Both markers, or two of one, on a single item → conviction.** An item that is both filed and
declared-not-a-finding is not a parse problem, it is an undecided author.
* **A passing run still says what it looked at** (workspace standing rule 3): it prints the item
count and how each was satisfied, so a gate that silently examined nothing is visible.
Stdlib only; no network, no subprocess — `--fast`, so it runs in BOTH the pre-push hook and CI. A
non-fast gate would run in neither, which is the R-29 failure the runner ended.
"""
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# `## 10. Observations — recorded, not acted on`, `### Observations`, `## Observations:` …
HEADING_RE = re.compile(r"^(#+)\s*(?:\d+[.)]\s*)?observations\b", re.IGNORECASE)
ITEM_RE = re.compile(r"^\s*(\d+)[.)]\s+(.*)$")
# ⚠ THE MARKER IS A MARKER, NOT A MENTION (R-419, fixed 2026-09-01).
#
# These were `\bFILED:` and `\bNOT-A-FINDING:` searched over the whole item body, so ANY occurrence
# counted — including one inside a sentence ABOUT the markers. Measured, and by accident: an
# observation reading *"it carries no `FILED:` and no `NOT-A-FINDING:` marker"* was reported
# `OK 1. NOT-A-FINDING` and a real push of mine went green over an unfiled finding. **The gate's
# whole job is to force an explicit choice, and a sentence disclaiming the choice counted as making
# it.** That is the substring-for-field class this project has now shipped five times.
#
# A marker must now be the START of a line (after optional list/emphasis punctuation), which is
# where a real marker is written and where a mention inside prose never is. `**FILED: R-417**` at
# the end of a sentence is the common real shape, so a marker is also accepted after a sentence
# boundary — but never mid-sentence and never inside backticks.
_MARK = r"(?:^|(?<=[.!?)]\s)|(?<=[.!?)]\s\s))[\s>*_\-]*"
FILED_RE = re.compile(_MARK + r"\*{0,2}FILED:\*{0,2}\s*(R-\d+)",
re.IGNORECASE | re.MULTILINE)
NOT_A_FINDING_RE = re.compile(_MARK + r"\*{0,2}NOT-A-FINDING:\*{0,2}\s*(\S.*)$",
re.IGNORECASE | re.MULTILINE)
# A marker written inside backticks is being TALKED ABOUT, never used. Strip inline code spans
# before matching — this is what makes the R-419 decoy fail.
CODE_SPAN_RE = re.compile(r"`[^`]*`")
REGISTERS = [
os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md"),
os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"),
]
def find_registers():
"""Locate the registers. They live in felhom.eu; sibling repos reach across, as other gates do."""
found = [p for p in REGISTERS if os.path.isfile(p)]
if found:
return found
sibling = os.path.join(os.path.dirname(ROOT), "felhom.eu", "documentation", "backlog")
return [p for p in (os.path.join(sibling, "OPEN-ITEMS.md"),
os.path.join(sibling, "CLOSED-ITEMS.md")) if os.path.isfile(p)]
def known_rows(paths):
"""Every R-number that has a row in the registers."""
rows = set()
for p in paths:
with open(p, encoding="utf-8") as fh:
for line in fh:
m = re.match(r"^\|\s*\*\*(R-\d+)\*\*\s*\|", line)
if m:
rows.add(m.group(1).upper())
return rows
def observation_items(text):
"""(items, heading) — each item is (number, its full text). heading is None when absent."""
lines = text.split("\n")
start = None
depth = 0
for i, line in enumerate(lines):
m = HEADING_RE.match(line)
if m:
start, depth = i, len(m.group(1))
break
if start is None:
return None, None
body = []
for line in lines[start + 1:]:
hm = re.match(r"^(#+)\s", line)
if hm and len(hm.group(1)) <= depth:
break
body.append(line)
items, cur = [], None
for line in body:
m = ITEM_RE.match(line)
if m:
if cur:
items.append(cur)
cur = [m.group(1), m.group(2)]
elif cur is not None:
if line.strip() == "" and cur[1].endswith("\n\n"):
continue
cur[1] += "\n" + line
if cur:
items.append(cur)
return items, lines[start].strip()
def main():
# The argument is a REPO ROOT (how the sibling runners invoke every shared gate) or, for tests
# and one-off checks, a REPORT.md path directly. Both are accepted so the controls in this
# session's evidence and the runner call the same code.
report = os.path.join(ROOT, "REPORT.md")
if len(sys.argv) > 1:
arg = sys.argv[1]
report = os.path.join(arg, "REPORT.md") if os.path.isdir(arg) else arg
if not os.path.isfile(report):
print("OBSERVATIONS GATE INCONCLUSIVE: no REPORT.md at %s" % report)
print(" This gate is registered here because this repo keeps one. If it was removed "
"deliberately, unregister the gate in the runner rather than leaving it unreadable.")
sys.exit(2)
with open(report, encoding="utf-8") as fh:
text = fh.read()
items, heading = observation_items(text)
if items is None:
print("observations gate OK — %s has no observations section (nothing to check)"
% os.path.basename(report))
sys.exit(0)
if not items:
print("OBSERVATIONS GATE INCONCLUSIVE: found the section %r but no numbered items under it."
% heading)
print(" Items must be a numbered list (`1.`, `2.` …). If the section is deliberately empty, "
"remove the heading — an empty section reads as coverage while providing none.")
sys.exit(2)
rows = known_rows(find_registers())
if not rows:
print("OBSERVATIONS GATE INCONCLUSIVE: could not read any register rows from "
"OPEN-ITEMS.md / CLOSED-ITEMS.md — cannot verify that a cited R-number resolves.")
sys.exit(2)
convictions = []
satisfied = []
for num, body in items:
body = CODE_SPAN_RE.sub("", body) # R-419: a marker inside backticks is a mention
filed = FILED_RE.findall(body)
declared = NOT_A_FINDING_RE.findall(body)
first_line = body.split("\n")[0].strip()
short = (first_line[:78] + "…") if len(first_line) > 78 else first_line
if filed and declared:
convictions.append((num, short,
"carries BOTH `FILED:` and `NOT-A-FINDING:` — decide which it is"))
continue
if len(filed) > 1:
convictions.append((num, short,
"carries %d `FILED:` markers (%s) — one observation, one row"
% (len(filed), ", ".join(filed))))
continue
if filed:
r = filed[0].upper()
if r not in rows:
convictions.append((num, short,
"`FILED: %s` does not resolve — no row for %s in OPEN-ITEMS.md "
"or CLOSED-ITEMS.md" % (r, r)))
else:
satisfied.append("%s. FILED %s" % (num, r))
continue
if declared:
reason = declared[0].strip()
if len(reason) < 12:
convictions.append((num, short,
"`NOT-A-FINDING:` carries no reason — the reason is the whole "
"point of the marker"))
else:
satisfied.append("%s. NOT-A-FINDING" % num)
continue
convictions.append((num, short, "neither `FILED: R-NNN` nor `NOT-A-FINDING: <reason>`"))
# Print the evidence unconditionally — a gate that only speaks when it fails teaches nobody what
# it is watching (workspace standing rule 3).
print(" report : %s" % report)
print(" section : %s" % heading)
print(" observation items : %d" % len(items))
for s in satisfied:
print(" OK %s" % s)
if convictions:
print("")
print("OBSERVATIONS GATE FAILED: %d observation(s) with nothing behind them." % len(convictions))
for num, short, why in convictions:
print("")
print(" item %s: %s" % (num, short))
print(" %s" % why)
print("")
print("An observation that lives only in REPORT.md has a lifetime of ONE SESSION — this file "
"is overwritten every time. That is how the cooldown-grain finding was lost on "
"2026-08-23 and had to be re-derived the next day.")
print("Fix: add `FILED: R-NNN` naming the row you opened for it, or `NOT-A-FINDING: <why "
"this is not worth a row>`. Opening the row is the default; declaring is the exception "
"and needs its reason stated.")
sys.exit(1)
print("observations gate OK — every observation is either filed or explicitly declared")
if __name__ == "__main__":
main()