Files
felhom.eu/scripts/test_gate_decoys.py
T

489 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 re
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, a row with no state cell at all, and (RULE 3, "
"2026-10-03) a FINISHED row left in OPEN-ITEMS.md — with R-87's shape, a READY "
"row whose prose says 'closed' about another row, asserted to PASS"),
"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; and "
"(2026-10-03) an old-shape row under the new header, a near-miss category, an "
"old rank tag as Sev, an undefined state word, and a pipe outside backticks"),
"decoy-coverage": "a gate registered in a runner with no decoy and no exemption (its red-proof)",
"script-tests": ("R-885: FIVE cases in scripts/test_script_tests_gate.py, run from here: a suite that PRINTS "
"OK and exits 1 (label without fact), a failing suite three levels deep (scope is a walk), "
"an empty scripts/ tree (checked nothing), the genuine article (must pass), the nested mark"),
"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"))
# --- closed-register RULE 4 (2026-10-05, R-416): the same id twice in CLOSED-ITEMS.md ---------------
# The id is read from the file's FIRST closed row at run time, so the decoy duplicates a real id rather than an
# invented one that could never collide.
def _first_closed_id():
for line in io.open(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"), encoding="utf-8"):
m = re.match(r"^\| \*\*(R-\d+[a-z]?)\*\* \|", line)
if m:
return m.group(1)
decoy("closed-register/duplicate-closed-id", "closed_register_gate.py",
append_to(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"),
u"\n| **%s** | The same id again. | CLOSED 2026-10-05 | none |\n" % _first_closed_id()))
# --- closed-register RULE 3 (2026-10-03): a FINISHED row left in the OPEN register ---------------
# On 2026-10-03 the open register held 113 rows whose leading verdict was finished — a quarter of the
# file. The decoy is that exact shape: a row a session closed in place and never moved. The genuine
# article beside it is R-87's shape from R-378: a READY row whose prose says "closed" about ANOTHER
# row — it must pass, or the gate repeats the 2026-08-22 sweep's mistake one file over.
# The row is built from the register's CURRENT header, so the decoy follows a column change rather
# than silently testing a shape the file no longer has.
_OPEN_REG = os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md")
def _open_row(rid, state):
cols = None
for line in io.open(_OPEN_REG, encoding="utf-8"):
if line.startswith("| ID |"):
cols = [c.strip() for c in line.strip().strip("|").split("|")]
fill = {"ID": "**%s**" % rid, "Category": "Process & tooling", "Sev": "P4",
"What": "**A decoy finding.**", "State": state, "Owner": "CC"}
return u"\n" + u"| " + u" | ".join(fill.get(c, u"—") for c in cols) + u" |\n"
decoy("closed-register/finished-row-in-open", "closed_register_gate.py",
append_to(_OPEN_REG, _open_row("R-906", "**CLOSED 2026-10-03 — shipped in v9.9.9**")))
decoy("closed-register/open-row-closed-word (BY DESIGN)", "closed_register_gate.py",
append_to(_OPEN_REG, _open_row("R-907", "**READY — RE-RANKED UP 2026-08-03 (R-86 closed)**")),
expect="accept")
# --- one-register (2026-10-03): an open ROADMAP row whose id carries a LETTER SUFFIX ------------
# The gate's id pattern was `R-(\d+)`, so R-27b, R-27c and R-50b were never read. Fixing it convicted
# R-50b at once — a finding that had lived only in ROADMAP.md. This decoy is that shape. (one-register
# stays in decoy_coverage_gate's EXEMPT list for R-424's hole — a finding filed under `idea` — which
# this decoy does not close.)
decoy("one-register/suffix-id-row", "one_register_gate.py",
append_to(os.path.join(ROOT, "documentation", "backlog", "ROADMAP.md"),
u"\n| R-905b | **A finding filed only here.** | S | READY — 2026-10-03 | none |\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))
# ── script-tests (R-885) ─────────────────────────────────────────────────────────────────────────
#
# Its decoys are whole fake repos, so they live in their own file and are RUN from here (guide-quote's shape).
ran += 1
_st = subprocess.run([sys.executable, os.path.join("scripts", "test_script_tests_gate.py")],
cwd=ROOT, capture_output=True, text=True)
if _st.returncode != 0:
fails.append("script-tests: its decoy suite FAILED — a decoy did not convict\n%s"
% (_st.stdout + _st.stderr)[-800:])
else:
_n = _st.stdout.strip().splitlines()[-1] if _st.stdout.strip() else "?"
print(" ok %-20s %s" % ("script-tests", _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)
_HEADER = "| ID | Category | Sev | What | State | Blocked on | Next action | Owner |"
_HEALTHY = "\n".join([
_HEADER,
"|---|---|---|---|---|---|---|---|",
"| **R-901** | Backup & restore | P3 | **A finding.** Its text mentions `owner: CC | and a literal pipe` on purpose. | **READY — owner: CC** | — | — | CC |",
"| **R-902** | Process & tooling | P4 | **Another finding.** | **CLOSED 2026-09-01** | — | — | CC |",
"",
])
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")
# decoy 4 (2026-09-30) — a row whose id carries a LETTER SUFFIX (R-88a, R-88b, R-209a are real) with its state cell
# eaten: the old `R-\d+` pattern did not read it as a row at all, so it passed unchecked
_rsg_case("suffix-row-eaten-state",
_HEALTHY.rstrip() + "\n| **R-903a** | **A split finding.** |\n".replace(" |\n", "\n"),
must_convict=True, expect_rule="RULE 1")
# 2026-10-03 — RULES 5–8: the columns, the category, the severity and the state word. Each decoy is a
# shape a session filing a row by hand would really produce.
_R901 = "| **R-901** | Backup & restore | P3 |"
# decoy 5 — a row filed with no category (the old 3-column habit, under the new header)
_rsg_case("old-shape-row-under-new-header",
_HEALTHY.rstrip() + "\n| **R-903** | **A finding filed the old way.** | **READY** |\n",
must_convict=True, expect_rule="RULE 5")
# decoy 6 — a near-miss category (the brief's own short name, not the defined one)
_rsg_case("unknown-category",
_HEALTHY.replace(_R901, "| **R-901** | Backup | P3 |"),
must_convict=True, expect_rule="RULE 6")
# decoy 7 — an old rank tag in the Sev cell instead of P1–P4
_rsg_case("old-rank-tag-as-sev",
_HEALTHY.replace(_R901, "| **R-901** | Backup & restore | P3-LOW |"),
must_convict=True, expect_rule="RULE 7")
# decoy 8 — a state word nobody defined (one of the 18 real ones the triage found)
_rsg_case("undefined-state-word",
_HEALTHY.replace("**READY — owner: CC**", "**PLUMBING COMPLETE** (controller v0.196.0)"),
must_convict=True, expect_rule="RULE 8")
# decoy 9 — a literal pipe in prose OUTSIDE backticks, which renders as an extra column
_rsg_case("pipe-outside-backticks",
_HEALTHY.replace("**A finding.**", "**A finding.** owner: CC | operator"),
must_convict=True, expect_rule="RULE 5")
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)