462ab4a5ff
gates / gates (push) Successful in 29s
THE REPAIR. Last night's append regex ate the state cells of R-446 and R-458, left them as a stray
fourth cell on duplicated copies of R-626 and R-625, and split the table with blank lines. The
register read 317 rows for 315 findings. Both cells restored from the cells that carried them, the
two duplicates deleted, and 15 blank lines that split the register into 12 separate markdown tables
removed. Every row's text is byte-identical afterwards, proven by diff; no row added or removed.
THE RED-PROOF FOUND AN OLDER INSTANCE: R-254 lost its state cell on 2026-08-08 (59527d0) and had
rendered without a State column for 45 days. Its own verdict sentence was the cell; it has it back.
THE GATE. scripts/register_shape_gate.py, gate 14 in repo_gates.py and reached by the pre-push
hook: a row that does not end with `|` (an eaten state cell), a duplicated id, or a blank line
splitting the table. Four decoys in test_gate_decoys.py — three convicting on the exact damage
shapes, one asserting a healthy register still passes. The red-proof corrected the gate twice: a
first draft counted CELLS and convicted 125 innocent rows (register cells carry literal `|` in
prose and shell snippets, so a row cannot be split on `|`), and it skipped malformed rows before
counting ids, reporting 5 duplicates where there were 2. It then immediately caught a blank line
left by this session's own next insert.
R-618's rank now reads P1-HIGH in both its title and its state cell.
HETZNER ANSWERED, AND I FIRST SAID THEY HAD NOT. The operator supplied ticket #2026090103040671.
Q1: with the MAIN account, files and directories can be downloaded from a snapshot (Storage Box
docs govern, not the Storage Share FAQ); a restore reverts the whole box. Q2: --append-only is
enforced by pinning `command="rclone serve restic --stdio --append-only path/to/repo"` to the key
in authorized_keys — so append-only IS expressible on a Storage Box, which is what R-95 was blocked
on. Neither is measured; R-436's due check now asks for the measurement, not the question.
My error is filed as R-628 and is precise: the query was fine — a control returns 201 threads and
reaches SENT and TRASH — and the thread genuinely is not in this mailbox. What was invented was the
step from "absent here" to "Hetzner has not answered". An absent record in one place cannot answer
what someone else did. The rule is now in the Gmail-access memory.
R-629: the drill repo inherited has_actions from the migrate call and mailed the operator 47 CI
failures overnight, into the mailbox that was carrying real off-site alarms. Actions disabled and
verified; 09 §6.5 now makes it a step of creating a drill repo. The 47 mails are the operator's to
clear: subject:"gates FAILED in admin/app-catalog-drill".
Gates: repo_gates.py --fast — all 16 OK, including the new one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
388 lines
20 KiB
Python
388 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""test_gate_decoys.py — can this gate be fooled by a LABEL? (R-421)
|
|
|
|
WHY THIS FILE EXISTS. Four times in one week a gate turned out to be matching a name instead of the
|
|
thing it named — R-410 (a `mkdir` turned the release gate green), R-400 (seven debug controls that
|
|
answered nothing), R-378 (a status word inside a sentence), R-419 (a phrase inside prose, including
|
|
prose saying the marker was absent). **All four were found by accident.** The gates are the machinery
|
|
that enforces everything else in this project, and they were the one part nothing checked.
|
|
|
|
A DECOY IS THE LABEL WITHOUT THE FACT. Each test below constructs one, runs the real gate, and
|
|
asserts it CONVICTS. Where a decoy would pass, that is a live hole.
|
|
|
|
⚠ A DECOY MUST BE THE SHAPE A REAL SESSION WOULD PRODUCE. R-419 was not found by an absurd input —
|
|
it was found by a genuine note explaining that it carried no marker. That is the standard. A decoy
|
|
nobody would ever write proves nothing, and saying so is a result.
|
|
|
|
⚠ EVERY TEST ASSERTS BOTH DIRECTIONS where it can. A gate that rejects the decoy AND rejects the
|
|
genuine article is worse than the hole it replaced.
|
|
|
|
Fixtures are planted in the real tree and removed in a `finally`. The suite asserts the tree is
|
|
unchanged at the end.
|
|
|
|
Run from the repo root: python3 scripts/test_gate_decoys.py
|
|
Exit 0 all decoys rejected · 1 a decoy passed (a live hole).
|
|
"""
|
|
import io
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
# ── WHAT THIS FILE COVERS ────────────────────────────────────────────────────────────────────────
|
|
# Read by scripts/decoy_coverage_gate.py, which AST-parses this literal rather than grepping for
|
|
# gate names — a substring search for coverage would be the very shape this sweep exists to find.
|
|
# A gate named here MUST have a decoy below that has been seen to fail.
|
|
COVERS = {
|
|
"hub-confirm": "a native confirm() in templates/partials/ (scope was os.listdir)",
|
|
"manifest-bearer": "a bearer literal in manifests/overlays/ (scope was os.listdir)",
|
|
"observations": "R-419: prose SAYING it carries no marker, plus both genuine markers",
|
|
"reuse-refs": "a cited .go path that does not exist; the .md hole is asserted as R-422",
|
|
"golden-currency": "R-410: an empty directory with a perfect name, checked by what it COUNTED",
|
|
"closed-register": "a verdict cell reading open, and a row with no state cell at all",
|
|
"register-shape": ("R-627: a row whose state cell was EATEN (no trailing pipe), a DUPLICATED "
|
|
"id, and a blank line splitting the table — each the exact shape the "
|
|
"2026-09-21 append produced; plus the genuine article, which must pass"),
|
|
"decoy-coverage": "a gate registered in a runner with no decoy and no exemption (its red-proof)",
|
|
"guide-quote": ("R-596: SEVEN cases in scripts/test_guide_quote_gate.py, run from here so "
|
|
"this suite stays the single entry point. The load-bearing one is "
|
|
"name-for-fact: a guide that lists every key in a table and quotes none of "
|
|
"their sentences. Also: a prefix of the real sentence, the OLD Hungarian "
|
|
"quote, a reworded bundle, a deleted key, and the scope case — an absent "
|
|
"controller clone must be INCONCLUSIVE, never a pass"),
|
|
"hub-copy": ("R-558: an English retrieval promise in the NEW bundle (the sentences moved "
|
|
"out of templates.go, so the surface list had to move with them), the same "
|
|
"in Hungarian, and an INNOCENT control using the identical verbs without the "
|
|
"capability claim — the control is the half that matters, because the first "
|
|
"stem list convicted 141 honest sentences"),
|
|
}
|
|
fails = []
|
|
ran = 0
|
|
|
|
|
|
def gate(script, args=()):
|
|
p = subprocess.run([sys.executable, os.path.join("scripts", script)] + list(args),
|
|
cwd=ROOT, capture_output=True, text=True)
|
|
return p.returncode, p.stdout + p.stderr
|
|
|
|
|
|
def decoy(name, script, plant, args=(), expect="convict"):
|
|
"""plant() is a callable returning a cleanup callable."""
|
|
global ran
|
|
ran += 1
|
|
cleanup = plant()
|
|
try:
|
|
rc, out = gate(script, args)
|
|
finally:
|
|
cleanup()
|
|
want_nonzero = (expect == "convict")
|
|
if (rc != 0) != want_nonzero:
|
|
fails.append("%s: decoy %s (rc=%d)\n%s" %
|
|
(name, "PASSED - LIVE HOLE" if want_nonzero else "was wrongly convicted",
|
|
rc, out[-700:]))
|
|
else:
|
|
print(" ok %-20s %s" % (name, "decoy rejected" if want_nonzero else "genuine accepted"))
|
|
|
|
|
|
def plant_file(path, content):
|
|
def _plant():
|
|
made = []
|
|
d = os.path.dirname(path)
|
|
if d and not os.path.isdir(d):
|
|
os.makedirs(d)
|
|
made.append(d)
|
|
io.open(path, "w", encoding="utf-8").write(content)
|
|
|
|
def _clean():
|
|
if os.path.exists(path):
|
|
os.remove(path)
|
|
for m in reversed(made):
|
|
if os.path.isdir(m) and not os.listdir(m):
|
|
os.rmdir(m)
|
|
return _clean
|
|
return _plant
|
|
|
|
|
|
def append_to(path, extra):
|
|
def _plant():
|
|
backup = io.open(path, encoding="utf-8").read()
|
|
io.open(path, "w", encoding="utf-8").write(backup + extra)
|
|
return lambda: io.open(path, "w", encoding="utf-8").write(backup)
|
|
return _plant
|
|
|
|
|
|
T = os.path.join(ROOT, "hub", "internal", "web", "templates")
|
|
M = os.path.join(ROOT, "manifests")
|
|
REP = os.path.join(ROOT, "REPORT.md")
|
|
|
|
print("decoys — felhom.eu")
|
|
|
|
# --- hub-confirm: a native confirm() one directory down (R-421) -------------------------------
|
|
# Shape 1, name-for-fact: the gate used os.listdir, so its SCOPE was a directory listing rather
|
|
# than the set of templates. There are no subdirectories today; adding templates/partials/ is an
|
|
# ordinary act and the gate would have stayed green.
|
|
decoy("hub-confirm/subdir", "hub_confirm_gate.py",
|
|
plant_file(os.path.join(T, "partials", "decoy.html"),
|
|
u'<button onclick="confirm(\'biztos?\')">x</button>\n'))
|
|
|
|
# --- manifest-bearer: a bearer literal one directory down --------------------------------------
|
|
decoy("manifest-bearer/subdir", "manifest_bearer_gate.py",
|
|
plant_file(os.path.join(M, "overlays", "decoy.yaml"),
|
|
u"apiVersion: v1\ndata:\n token: %s\n" % ("a1b2c3d4" * 8)))
|
|
|
|
# --- observations: R-419 itself, and the genuine markers beside it -----------------------------
|
|
# Shape 2, substring-for-field. THE decoy that found this class: an honest note SAYING it has no
|
|
# marker satisfied the marker test.
|
|
decoy("observations/R-419", "observations_gate.py",
|
|
append_to(REP, u"\n## Observations\n\n1. **A real finding.** It carries no `FILED:` marker "
|
|
u"and no `NOT-A-FINDING:` marker, deliberately.\n"), args=(ROOT,))
|
|
decoy("observations/genuine-FILED", "observations_gate.py",
|
|
append_to(REP, u"\n## Observations\n\n1. **A real finding.** Something broke. "
|
|
u"**FILED: R-419**\n"), args=(ROOT,), expect="accept")
|
|
decoy("observations/genuine-NAF", "observations_gate.py",
|
|
append_to(REP, u"\n## Observations\n\n1. **A real finding.** Odd. **NOT-A-FINDING: my own "
|
|
u"typo, corrected in the same minute.**\n"), args=(ROOT,), expect="accept")
|
|
|
|
# --- reuse-refs: a cited path that does not exist ----------------------------------------------
|
|
# The .go case is REJECTED. The .md case is a KNOWN HOLE (R-422) and is asserted as such below, so
|
|
# this file records the hole rather than pretending it is covered.
|
|
decoy("reuse-refs/missing-go", "reuse_refs_check.py",
|
|
append_to(os.path.join(ROOT, "REUSE.md"),
|
|
u"\n- see `hub/internal/api/does_not_exist.go`\n"), args=(ROOT,))
|
|
|
|
# --- KNOWN HOLE, asserted so it cannot be forgotten (R-422) ------------------------------------
|
|
# reuse_refs_check.py's PATH_RE matches only go|py|html|css|yml|yaml|sh. A rotted .md citation is
|
|
# invisible. This asserts the CURRENT behaviour so the day it is fixed, this test fails and is
|
|
# updated deliberately — a hole that nothing asserts is a hole nobody remembers.
|
|
decoy("reuse-refs/missing-md (KNOWN HOLE R-422)", "reuse_refs_check.py",
|
|
append_to(os.path.join(ROOT, "REUSE.md"),
|
|
u"\n- see `documentation/architecture/99-does-not-exist.md`\n"),
|
|
args=(ROOT,), expect="accept")
|
|
|
|
# --- golden-currency: R-410's own decoy, re-run here so the sweep owns it too ------------------
|
|
def _mkdir_decoy():
|
|
d = os.path.join(ROOT, "documentation", "tests", "golden-9.9.9-2026-01-01")
|
|
os.makedirs(d)
|
|
return lambda: os.path.isdir(d) and os.rmdir(d)
|
|
|
|
|
|
def check_golden_names_the_fake():
|
|
"""golden-currency exits 0 either way when currency is fine — the QUESTION is what it counted."""
|
|
global ran
|
|
ran += 1
|
|
cleanup = _mkdir_decoy()
|
|
try:
|
|
_rc, out = gate("golden_currency_gate.py")
|
|
finally:
|
|
cleanup()
|
|
if "newest golden baked : 9.9.9" in out:
|
|
fails.append("golden-currency: an EMPTY directory was counted as a bake — R-410 has regressed")
|
|
elif "NOT counted as bakes" not in out:
|
|
fails.append("golden-currency: the empty directory was neither counted nor REPORTED; a "
|
|
"half-finished bake must be visible, not silently ignored")
|
|
else:
|
|
print(" ok %-20s empty dir rejected AND named" % "golden-currency")
|
|
|
|
|
|
check_golden_names_the_fake()
|
|
|
|
# --- closed-register: the verdict cell is the predicate, deliberately (R-378) -------------------
|
|
# NOT a hole: R-378's whole lesson is that an open word ANYWHERE in a row convicts rows that are
|
|
# genuinely closed. This asserts the deliberate behaviour so a future "fix" has to argue with it.
|
|
decoy("closed-register/body-word (BY DESIGN)", "closed_register_gate.py",
|
|
append_to(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"),
|
|
u"\n| **R-903** | Work continues and it is still READY in the body. | CLOSED 2026-09-01 | none |\n"),
|
|
expect="accept")
|
|
# A 4-column row (| ID | Title | Shipped | Evidence |) whose VERDICT cell reads open.
|
|
decoy("closed-register/verdict-word", "closed_register_gate.py",
|
|
append_to(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"),
|
|
u"\n| **R-904** | A finished thing. | READY - still being worked on | none |\n"))
|
|
|
|
# R-421: a row this gate cannot PARSE used to be a warning, and the gate then printed OK. Four rows
|
|
# were in that state — two of them written by the session that closed them the day before this
|
|
# sweep — so they were exempt from the only check that reads this file. An unreadable row is now a
|
|
# conviction. This is the sweep's own shape one level up and it is why the decoy is kept.
|
|
decoy("closed-register/unreadable-row", "closed_register_gate.py",
|
|
append_to(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"),
|
|
u"\n| **R-905** | A row with no state cell at all. |\n"))
|
|
|
|
# --- hub-copy: an ENGLISH retrieval promise, planted in the bundle (R-558) -----------------------
|
|
#
|
|
# TWO SHAPES AT ONCE, and the second is why this decoy exists at all.
|
|
#
|
|
# Shape "scope is a fact": until hub v0.118.0 every customer sentence was a Go literal in
|
|
# templates.go. Slice 3 moved all of them into locales/*.json. If the gate's CUSTOMER_SURFACES had
|
|
# not moved with them, all four declared files would still exist, the gate would still report
|
|
# success, and it would be scanning a file with no customer sentences left in it.
|
|
#
|
|
# Shape "half a guard": the stems were Hungarian only. A promise written in the new English bundle
|
|
# would have been invisible to a gate that had just been taught to read the file it sits in.
|
|
#
|
|
# The planted sentence is the shape a real translator would write — the English of a claim this
|
|
# product genuinely cannot keep (R-304: nothing reads a retained key, and a customer's correct old
|
|
# code is reported as wrong).
|
|
_EN_BUNDLE = os.path.join(ROOT, "hub", "internal", "i18n", "locales", "en.json")
|
|
|
|
|
|
def _plant_en_promise():
|
|
backup = io.open(_EN_BUNDLE, encoding="utf-8").read()
|
|
d = json.loads(backup)
|
|
d["mail.event.offbox_repo_reset"] = ("The remote backup store has been reset. Your earlier "
|
|
"backups can still be restored with your old recovery code.")
|
|
io.open(_EN_BUNDLE, "w", encoding="utf-8").write(json.dumps(d, ensure_ascii=False, indent=2) + "\n")
|
|
return lambda: io.open(_EN_BUNDLE, "w", encoding="utf-8").write(backup)
|
|
|
|
|
|
decoy("hub-copy/en-promise", "hub_copy_gate.py", _plant_en_promise)
|
|
|
|
|
|
# ...and the CONTROL, which is the half that makes the decoy mean something: an ordinary English
|
|
# sentence using the very same words WITHOUT the capability claim must NOT be convicted. The first
|
|
# version of these stems matched the bare verbs and convicted 141 honest sentences, including
|
|
# "Disaster recovery has started" and the name of the Restore page.
|
|
def _plant_en_innocent():
|
|
backup = io.open(_EN_BUNDLE, encoding="utf-8").read()
|
|
d = json.loads(backup)
|
|
d["mail.event.offbox_repo_reset"] = ("The remote backup store has been reset. Disaster recovery "
|
|
"has finished and the Restore page is available again.")
|
|
io.open(_EN_BUNDLE, "w", encoding="utf-8").write(json.dumps(d, ensure_ascii=False, indent=2) + "\n")
|
|
return lambda: io.open(_EN_BUNDLE, "w", encoding="utf-8").write(backup)
|
|
|
|
|
|
decoy("hub-copy/en-innocent", "hub_copy_gate.py", _plant_en_innocent, expect="accept")
|
|
|
|
|
|
# And the scope half, stated as its own decoy: a HUNGARIAN promise in the bundle. Before v0.118.0
|
|
# this text lived in templates.go and was scanned; if the bundle were not a declared surface, moving
|
|
# the sentence would have moved it out of reach of a gate that was already watching it.
|
|
def _plant_hu_promise():
|
|
hu = os.path.join(ROOT, "hub", "internal", "i18n", "locales", "hu.json")
|
|
backup = io.open(hu, encoding="utf-8").read()
|
|
d = json.loads(backup)
|
|
d["mail.event.offbox_repo_reset"] = ("A tárolót visszaállítottuk. A régi mentéseidet a korábbi "
|
|
"helyreállítási kóddal visszaállíthatod.")
|
|
io.open(hu, "w", encoding="utf-8").write(json.dumps(d, ensure_ascii=False, indent=2) + "\n")
|
|
return lambda: io.open(hu, "w", encoding="utf-8").write(backup)
|
|
|
|
|
|
decoy("hub-copy/hu-in-bundle", "hub_copy_gate.py", _plant_hu_promise)
|
|
|
|
|
|
# --- decoy-coverage: the meta-gate's own red-proof, kept as a test --------------------------------
|
|
# It must convict a gate registered in a runner with no decoy and no exemption. Without this the
|
|
# meta-gate is itself an unchecked instrument, which is the joke this whole sweep exists to avoid.
|
|
# It also convicted ITSELF the moment it was registered, which is how this decoy came to be written.
|
|
ran += 1
|
|
_RUNNER = os.path.join(ROOT, "scripts", "repo_gates.py")
|
|
_b = io.open(_RUNNER, encoding="utf-8").read()
|
|
_anchor = ' ("observations", os.path.join(SCRIPTS, "observations_gate.py"), [ROOT], True, False),'
|
|
try:
|
|
assert _anchor in _b, "the runner's shape changed — this decoy can no longer be built"
|
|
io.open(_RUNNER, "w", encoding="utf-8").write(_b.replace(
|
|
_anchor, _anchor + '\n ("decoy-red-proof", os.path.join(SCRIPTS, "nope.py"), [], True, False),', 1))
|
|
_rc, _out = gate("decoy_coverage_gate.py", (ROOT,))
|
|
finally:
|
|
io.open(_RUNNER, "w", encoding="utf-8").write(_b)
|
|
if _rc == 0:
|
|
fails.append("decoy-coverage: a NEW gate with no decoy and no exemption was ACCEPTED — the "
|
|
"meta-gate cannot see the thing it exists for\n%s" % _out[-500:])
|
|
elif "decoy-red-proof" not in _out:
|
|
fails.append("decoy-coverage: it convicted, but did not NAME the uncovered gate")
|
|
else:
|
|
print(" ok %-20s a new gate with no decoy is convicted BY NAME" % "decoy-coverage")
|
|
|
|
# ── guide-quote (R-596) ──────────────────────────────────────────────────────────────────────────
|
|
#
|
|
# Its decoys build whole fake workspaces (a guide plus a sibling controller clone), which does not
|
|
# fit the plant/restore shape above — so they live in their own file and are RUN from here. The
|
|
# decoy-coverage gate reads COVERS in this file, so this is the seam that keeps that entry honest:
|
|
# if the separate suite stops passing, this one fails, and the COVERS line stops being a label.
|
|
ran += 1
|
|
_gq = subprocess.run([sys.executable, os.path.join("scripts", "test_guide_quote_gate.py")],
|
|
cwd=ROOT, capture_output=True, text=True)
|
|
if _gq.returncode == 2:
|
|
fails.append("guide-quote: its decoy suite could not run (no controller clone beside this one) — "
|
|
"INCONCLUSIVE is not coverage\n%s" % (_gq.stdout + _gq.stderr)[-500:])
|
|
elif _gq.returncode != 0:
|
|
fails.append("guide-quote: its decoy suite FAILED — a decoy did not convict\n%s"
|
|
% (_gq.stdout + _gq.stderr)[-800:])
|
|
else:
|
|
_n = _gq.stdout.strip().splitlines()[-1] if _gq.stdout.strip() else "?"
|
|
print(" ok %-20s %s" % ("guide-quote", _n))
|
|
|
|
# ── register-shape (R-627) ───────────────────────────────────────────────────────────────────────
|
|
#
|
|
# THE DECOYS ARE THE REAL DAMAGE, not invented shapes. On 2026-09-21 an append regex ate two rows'
|
|
# state cells, left the cell behind as a stray extra cell on a DUPLICATE of another row, and put a
|
|
# blank line between each pair. The register then reported 317 rows for 315 findings. Each decoy
|
|
# below is one of those three shapes, written as a session would actually produce it.
|
|
#
|
|
# AND THE GENUINE ARTICLE IS ASSERTED TOO: a gate that rejects the damage and also rejects a healthy
|
|
# register is worse than the hole it replaces.
|
|
import importlib.util as _ilu
|
|
_spec = _ilu.spec_from_file_location("rsg", os.path.join(ROOT, "scripts", "register_shape_gate.py"))
|
|
_rsg = _ilu.module_from_spec(_spec)
|
|
_spec.loader.exec_module(_rsg)
|
|
|
|
_HEALTHY = "\n".join([
|
|
"| ID | What | State |",
|
|
"|---|---|---|",
|
|
"| **R-901** | **A finding.** Its text mentions `owner: CC | and a literal pipe` on purpose. | **READY — owner: CC** |",
|
|
"| **R-902** | **Another finding.** | **CLOSED 2026-09-01** |",
|
|
"",
|
|
])
|
|
|
|
def _rsg_case(name, text, must_convict, expect_rule=None):
|
|
global ran
|
|
ran += 1
|
|
fp = os.path.join(ROOT, "scripts", ".decoy-register-%s.md" % name)
|
|
try:
|
|
with open(fp, "w", encoding="utf-8") as fh:
|
|
fh.write(text)
|
|
rows, ids, bad = _rsg.check(fp)
|
|
convicted = bool(bad)
|
|
if convicted != must_convict:
|
|
fails.append("register-shape [%s]: expected %s, got %s (%s)"
|
|
% (name, "CONVICT" if must_convict else "PASS",
|
|
"CONVICT" if convicted else "PASS",
|
|
"; ".join(w for _, _, w in bad)[:200] or "no findings"))
|
|
elif expect_rule and not any(expect_rule in w for _, _, w in bad):
|
|
fails.append("register-shape [%s]: convicted, but not on %s — on %s"
|
|
% (name, expect_rule, "; ".join(w.split(" — ")[0] for _, _, w in bad)))
|
|
else:
|
|
print(" ok %-20s %s" % ("register-shape", name))
|
|
finally:
|
|
if os.path.exists(fp):
|
|
os.remove(fp)
|
|
|
|
# the genuine article must PASS — including a row carrying a literal `|` in its prose, which is why
|
|
# there is no cell-count rule
|
|
_rsg_case("genuine", _HEALTHY, must_convict=False)
|
|
|
|
# decoy 1 — the state cell eaten (R-446 / R-458 / R-254's shape)
|
|
_rsg_case("eaten-state-cell",
|
|
_HEALTHY.replace("| **CLOSED 2026-09-01** |", "").rstrip().rstrip("|").rstrip() + "\n",
|
|
must_convict=True, expect_rule="RULE 1")
|
|
|
|
# decoy 2 — the same finding filed twice (R-625 / R-626's shape)
|
|
_rsg_case("duplicate-id",
|
|
_HEALTHY.rstrip() + "\n| **R-901** | **The same finding again.** | **READY** |\n",
|
|
must_convict=True, expect_rule="RULE 3")
|
|
|
|
# decoy 3 — a blank line splitting the table
|
|
_rsg_case("blank-splits-table",
|
|
_HEALTHY.replace("| **R-902**", "\n| **R-902**"),
|
|
must_convict=True, expect_rule="RULE 4")
|
|
|
|
print()
|
|
if fails:
|
|
for f in fails:
|
|
print("FAIL: %s" % f)
|
|
print("\n%d decoy(s) of %d exposed a hole" % (len(fails), ran))
|
|
sys.exit(1)
|
|
print("all %d felhom.eu decoys behaved — labels do not satisfy these gates" % ran)
|