the decoy sweep: 29 gates read, 16 fooled, 10 fixed - and a gate that refuses the next one (R-421)
gates / gates (push) Failing after 17s
gates / gates (push) Failing after 17s
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.
This commit is contained in:
@@ -91,6 +91,14 @@ today that is all of them. **A missing gate script is a FAILURE, never a skip.**
|
||||
`due_checks_gate.py` refuses the push when a dated check in `OPEN-ITEMS.md`'s `DUE-CHECKS` block has
|
||||
come due (R-341). **It is not a scheduler** — it fires on the next push, not on the date.
|
||||
|
||||
**A gate ships with a decoy test that has been seen to fail (R-421).** A decoy is the LABEL without
|
||||
the FACT — a directory with the right name and no bake log, a note whose prose mentions the marker it
|
||||
lacks. `scripts/decoy_coverage_gate.py` refuses a new gate that has neither a decoy nor a named
|
||||
exemption carrying its row. The four shapes, the 2026-09-01 sweep that fooled 16 of 29 gates, and the
|
||||
decoys withdrawn as illegitimate: `documentation/audits/AUDIT-gate-decoys-2026-09-01.md` and
|
||||
`felhom-controller/.claude/rules/gates.md`. **Scope is a fact too** — prefer `os.walk` over
|
||||
`os.listdir`, and a glob over a hand-maintained list.
|
||||
|
||||
`site_gates.py` is a *gate*, not a runner — do not model new work on it;
|
||||
`app-catalog-felhom.eu/scripts/catalog_gates.py` is the canonical runner (R-161).
|
||||
|
||||
|
||||
+38
@@ -14,6 +14,44 @@
|
||||
> language, one screen, no identifiers in the prose. Same subjects, different readers; merging them
|
||||
> would make one of the two audiences stop reading. `STATUS.md` is also a **view of `OPEN-ITEMS.md`**
|
||||
> and holds nothing of its own; this file does hold its own content, namely the standing rulings below.
|
||||
## The checks were the one thing nothing checked — 16 of 29 could be fooled by a label (2026-09-01, R-421)
|
||||
|
||||
**THE CLASS: an instrument that matches a LABEL rather than the fact it names.** Five instances, and
|
||||
every single one was found by accident, by someone looking at something else: R-410 (a `mkdir` turned
|
||||
the release gate green), R-400 (seven debug controls answering nothing), R-378 (a status word inside a
|
||||
longer sentence), R-419 (a phrase inside prose — including prose stating the marker was absent), R-94
|
||||
(a test comparing a constant to itself). **The gates enforce every other rule in this project,
|
||||
including the rule that findings must be written down. Nothing had ever checked the gates.**
|
||||
|
||||
**The method, and it is the transferable part: write the decoy.** 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, and record what it
|
||||
says. **A last column answered from reading is worth nothing**; that is precisely how all five hid.
|
||||
|
||||
**Result: 29 scripts, 19 sound, 4 holes left open with rows, 6 that no plausible decoy could be built
|
||||
for and are named as UNTESTED rather than called sound.** Sixteen were fooled; ten were fixed the
|
||||
same day.
|
||||
|
||||
**The largest single cause was mundane and worth remembering: SCOPE IS A FACT TOO.** Eight gates
|
||||
decided what to look at with `os.listdir` — one directory level. Every one was green *and correct*,
|
||||
because no subdirectory exists today; every one would have gone blind the moment anyone added
|
||||
`templates/partials/`, which is an ordinary act. `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. **Prefer `os.walk` over `os.listdir`, and a glob over a hand-maintained list of files** —
|
||||
three of the four remaining holes (R-423 `PAGES`, R-425 `FILES`, and R-410 itself) are hand-maintained
|
||||
lists that silently narrowed as the thing they described grew.
|
||||
|
||||
**A decoy nobody would write proves nothing, and saying so is a result.** Five of mine were withdrawn
|
||||
as illegitimate rather than counted — an inert Compose `x-` field, a CHANGELOG heading that did not
|
||||
actually hide the release, a half-built decoy that left the real call in place, a malformed table row
|
||||
that convicted for a structural reason, and several planted files carrying nothing the gate hunts for.
|
||||
Manufacturing a finding to fill a row is worse than an honest NO.
|
||||
|
||||
**The rule that keeps the answer true:** a gate ships with a decoy test that has been seen to fail.
|
||||
`scripts/decoy_coverage_gate.py` refuses a new gate that has neither one nor a named exemption
|
||||
carrying its row — and it convicted *itself* the moment it was registered, which is how it got one.
|
||||
The 20 gates not yet covered are listed by name (R-426), so the debt is visible and shrinks.
|
||||
|
||||
## A guard aimed at the wrong repository trains everyone to bypass it (2026-09-01, R-404 / R-417)
|
||||
|
||||
**The ruling was neither option as framed.** The question on the table was whether a documents-only
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# AUDIT — can this check be fooled by a label? The decoy sweep (2026-09-01, R-421)
|
||||
|
||||
**Question asked of every gate:** *could I satisfy this with a convincing label instead of the real
|
||||
thing?* Answered by construction — a decoy is written, the gate is run, and the verdict recorded.
|
||||
**No row's last column was answered from reading.**
|
||||
|
||||
## The survey
|
||||
|
||||
**29 distinct scripts, 35 registrations** (`reuse-refs`, `instructions`, `observations` are one
|
||||
script each, registered in three runners). This matches the task's count of 29.
|
||||
|
||||
| gate | runner(s) | meant to prove | actually matches | shape | decoy passed **before**? |
|
||||
|---|---|---|---|---|---|
|
||||
| emoji | ctrl | no emoji in UI copy | codepoints, in `listdir` scope | 1 | **YES** |
|
||||
| native-confirm | ctrl | no OS-modal dialogs | JS call regex, `listdir` scope | 1 | **YES** |
|
||||
| app-row-dedup | ctrl | one row markup | regex + `'…' not in src`, `listdir` | 1, 2 | **YES** (×2) |
|
||||
| template-id | ctrl | JS ids resolve | id sets, `listdir` scope | 1 | **YES** |
|
||||
| secret-markup | ctrl | no secret in markup | template actions, `listdir` | 1 | **YES** |
|
||||
| retrieval-promise | ctrl | promises are registered | stems, `listdir` scope | 1 | **YES** |
|
||||
| hub-confirm | eu | no OS-modal dialogs | JS call regex, `listdir` scope | 1 | **YES** |
|
||||
| manifest-bearer | eu | no bearer literals | 64-hex regex, `listdir` scope | 1 | **YES** |
|
||||
| observations | eu, ctrl, agent | a finding is filed | `FILED:` anywhere in the body | 2 | **YES** (R-419) |
|
||||
| debug-routes | ctrl | controls resolve | raw text, comments included | 2, 3 | **YES** |
|
||||
| closed-register | eu | closed rows are closed | verdict cell — **but skipped unparseable rows** | 2 | **YES** |
|
||||
| site | eu | pages are well-formed | a 7-entry `PAGES` list | 1 | **YES → R-423** |
|
||||
| one-register | eu | open work is registered | the state cell; `idea` escapes | 2 | **YES → R-424** |
|
||||
| offbox-rename | ctrl | branding is retired | a fixed 3-entry `FILES` list | 1 | **YES → R-425** |
|
||||
| reuse-refs | eu, ctrl, agent | citations resolve | paths — only 7 extensions | 1 | **YES → R-422** |
|
||||
| mojibake | ctrl | no double-encoded UTF-8 | bytes, via `os.walk` | 5 | NO — **control** |
|
||||
| docker-v | ctrl | `-v` mounts are safe | argv, via `os.walk` | 5 | NO |
|
||||
| image-pins | catalog | no floating tags | real `image:` refs | 5 | NO |
|
||||
| golden-currency | eu | a golden was baked | `GOLDEN_SHA256` in the bake log | 5 | NO (R-410 fixed) |
|
||||
| golden-notice | ctrl | ditto, other direction | imports the gate above | 5 | NO |
|
||||
| instructions | eu, ctrl, agent | instruction files stay sane | effective text | 5 | NO |
|
||||
| hub-copy | eu | retired names are gone | `os.walk` over hub/internal | 5 | NO |
|
||||
| release-complete | agent | a release is complete | git tag + ancestry + HTTP HEAD | 5 | NO |
|
||||
| hostinstall | eu | installer invariants | — | ? | **UNKNOWN** |
|
||||
| wire-contract | eu | emitted fields decode | — | ? | **UNKNOWN** |
|
||||
| due-checks | eu | dated checks fire | — | ? | **UNKNOWN** |
|
||||
| published | agent | versions are published | — | ? | **UNKNOWN** |
|
||||
| image-resolvable | catalog | images exist | `docker manifest inspect` | 5 | **UNKNOWN** |
|
||||
| volume-persistence | catalog | data survives | runs containers, diffs | 5 | **UNKNOWN** |
|
||||
|
||||
**19 sound · 4 holes left open with rows · 6 unknown.** Ten holes were fixed in this session.
|
||||
|
||||
## The one cause behind eight of them
|
||||
|
||||
Eight gates decided their **scope** with `os.listdir`, one directory level. No template or manifest
|
||||
subdirectory exists today, so every one was green **and correct** — and would have stayed green the
|
||||
moment anyone added `templates/partials/`, which is an ordinary act. `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.**
|
||||
|
||||
## Holes left open, with rows
|
||||
|
||||
| row | gate | why not fixed here |
|
||||
|---|---|---|
|
||||
| **R-422** | reuse-refs | `PATH_RE` matches 7 extensions; a rotted `.md` citation is invisible. Widening it needs a false-positive pass over 4 repos. |
|
||||
| **R-423** | site | `PAGES` is a hardcoded list of 7. Fix is to glob `website/*.html`, which needs the per-page exemptions rethought. |
|
||||
| **R-424** | one-register | a defect parked under state `idea` escapes. Declared in the gate's own docstring as residual hole 1. |
|
||||
| **R-425** | offbox-rename | fixed 3-entry `FILES` list; a new offbox file is unscanned. |
|
||||
| **R-426** | — | owns the meta-gate's 20-name exemption list. |
|
||||
|
||||
Each is asserted **as it behaves today** where a test could hold it, so the day it is fixed the
|
||||
assertion fails and is updated deliberately. A hole nothing asserts is a hole nobody remembers.
|
||||
|
||||
## Decoys withdrawn as illegitimate — mine, and named
|
||||
|
||||
§2.1 sets the standard: a decoy nobody would write proves nothing. These were withdrawn rather than
|
||||
counted, because counting them would have manufactured findings.
|
||||
|
||||
1. **image-pins / `x-image: nginx:latest`** — `x-` fields are inert in Compose. The label had no fact
|
||||
behind it *either way*. Replaced with a positive control (a real untagged `image:` line → convicted).
|
||||
2. **release-complete / a non-version heading on top of CHANGELOG.md** — `HEAD_RE.search` scans the
|
||||
whole file, so the real release is still found and named `v0.130.0`. The gate is sound.
|
||||
3. **app-row-dedup / one commented-out partial call** — `dashboard.html` has **two**; replacing one
|
||||
left the other live. The gate was right and my decoy was half-built.
|
||||
4. **one-register / a 3-column decoy row** — it convicted for a *structural* reason, not the one under
|
||||
test. Rebuilt at the real 5-column shape, and the hole then reproduced.
|
||||
5. **template-id, secret-markup, retrieval-promise, hub-copy / wrong content** — my first planted
|
||||
files carried nothing those gates hunt for, so "passed" meant nothing. Rebuilt with real triggers.
|
||||
@@ -26,8 +26,9 @@
|
||||
|
||||
---
|
||||
|
||||
| **R-404** | **DECISION — should a documents-only push be subject to the golden-currency gate? RULED 2026-09-01: NEITHER option as framed. Block the push that can create the debt; notify the push that cannot.** The two options on the table were *narrow the gate* and *leave it and build a waiver*, and both were wrong for the same reason: they argued about the GATE, and the gate was never the problem. **The DIAGNOSIS, measured from live source, is that the check was aimed at the wrong repository.** `golden_currency_gate.py` never looks at the push at all — it compares the controller's newest CHANGELOG heading against this repo's bake evidence and returns the same verdict whatever you are pushing, which is correct for a standing invariant and wrong as a push gate. Meanwhile `controller_gates.py` had NO golden-currency entry, so **the repo where a release happens never checked, and the repo that cannot create the debt enforced it on every push.** 18 of the last 24 pushes here touched no code — MEASURED, and the classifier agrees exactly — most of them for a structural reason: the controller's code is in one repo and its register, architecture and status live in this one, so **every controller change produces a documents-only push here by construction.** Six of those 18 were bake records — **the push that PAYS the debt is itself documents-only, so the gate was blocking its own cure.** **WHY NOT THE WAIVER** the gate's own docstring prescribes: that clause was written for *a release nobody wants a golden for*. The case that actually occurred (R-417) was *a release we did want a golden for, on a night the runbook forbade baking*. A waiver would have recorded a lie. **SHIPPED:** `scripts/push_scope.py` (allow-list; every uncertainty answers `code`), a fifth `exemptible` field in `repo_gates.py` + `--scope`, a new **ADVISORY** verdict printed in its own block, the pre-push hook reading git's stdin, the same rule in CI from the push event payload, and `felhom-controller/controller/scripts/golden_notice.py` — a NON-BLOCKING notice at the moment a release is committed. **The gate's own logic, exit codes and wording are byte-identical**; only the consequence changed. The exemption is ONE gate wide and `TestR3`/Scenario C pins it, red-proved by widening it. Proven live on the real hook: docs+debt → ADVISORY, pushed; code+debt → refused; docs+debt+a second gate → refused for that gate alone | **CLOSED 2026-09-01** — ruled and shipped |
|
||||
| **R-417** | **A drill night that forbids baking a golden made `golden_currency_gate.py` red, so pushing the drill's own evidence needed `--no-verify` — the very signal CI e-mails about.** Measured 2026-09-01: five consecutive felhom.eu CI runs red (jobs 469/470/471/473/476), all mine, all on step 3 `Run the gate entry point`; job 478 green the moment the golden-0.232.0 evidence was committed. Cause confirmed by isolation — moving that directory aside reproduces exit=1, restoring it gives exit=0. **The gate was right every time**: 0.231.0 and 0.232.0 were released with no golden carrying them. **CAUSE REMOVED, not worked around** (R-404): a drill's pushes are documents-only, so the conviction now prints as a loud ADVISORY and the push proceeds — in the hook AND in CI, so a drill night no longer produces red runs indistinguishable from real ones. The expectation is now written where the next drill author reads it (`documentation/runbooks/target-selection.md`), which is the half I had left out. | **CLOSED 2026-09-01** — by R-404 |
|
||||
| **R-419** | **`observations_gate.py` accepted an observation whose body merely CONTAINED the string `NOT-A-FINDING`, even in prose disclaiming it.** Found by accident on 2026-09-01 when a planted test observation reading *"it carries no `FILED:` and no `NOT-A-FINDING:` marker"* was reported `OK 1. NOT-A-FINDING` and a real push 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. **FIXED 2026-09-01:** a marker must now start a line or follow a sentence boundary, and inline code spans are stripped before matching — a marker inside backticks is being talked about, never used. | **CLOSED — FIXED + PINNED** (2026-09-01, R-421 sweep) | verified in BOTH directions: the decoy and a backticked mention are convicted; a real `**FILED: R-419**` and a real `**NOT-A-FINDING: ...**` still pass. Decoy kept in `felhom.eu/scripts/test_gate_decoys.py` |
|
||||
| **R-404** | **DECISION — should a documents-only push be subject to the golden-currency gate? RULED 2026-09-01: NEITHER option as framed. Block the push that can create the debt; notify the push that cannot.** The two options on the table were *narrow the gate* and *leave it and build a waiver*, and both were wrong for the same reason: they argued about the GATE, and the gate was never the problem. **The DIAGNOSIS, measured from live source, is that the check was aimed at the wrong repository.** `golden_currency_gate.py` never looks at the push at all — it compares the controller's newest CHANGELOG heading against this repo's bake evidence and returns the same verdict whatever you are pushing, which is correct for a standing invariant and wrong as a push gate. Meanwhile `controller_gates.py` had NO golden-currency entry, so **the repo where a release happens never checked, and the repo that cannot create the debt enforced it on every push.** 18 of the last 24 pushes here touched no code — MEASURED, and the classifier agrees exactly — most of them for a structural reason: the controller's code is in one repo and its register, architecture and status live in this one, so **every controller change produces a documents-only push here by construction.** Six of those 18 were bake records — **the push that PAYS the debt is itself documents-only, so the gate was blocking its own cure.** **WHY NOT THE WAIVER** the gate's own docstring prescribes: that clause was written for *a release nobody wants a golden for*. The case that actually occurred (R-417) was *a release we did want a golden for, on a night the runbook forbade baking*. A waiver would have recorded a lie. **SHIPPED:** `scripts/push_scope.py` (allow-list; every uncertainty answers `code`), a fifth `exemptible` field in `repo_gates.py` + `--scope`, a new **ADVISORY** verdict printed in its own block, the pre-push hook reading git's stdin, the same rule in CI from the push event payload, and `felhom-controller/controller/scripts/golden_notice.py` — a NON-BLOCKING notice at the moment a release is committed. **The gate's own logic, exit codes and wording are byte-identical**; only the consequence changed. The exemption is ONE gate wide and `TestR3`/Scenario C pins it, red-proved by widening it. Proven live on the real hook: docs+debt → ADVISORY, pushed; code+debt → refused; docs+debt+a second gate → refused for that gate alone | **CLOSED 2026-09-01** — ruled and shipped | full text and the ruling: `git show 1e6c387:documentation/backlog/CLOSED-ITEMS.md`; the diagnosis is in `CONTEXT.md` and `scripts/CHANGELOG.md`; `scripts/push_scope.py` + `test_repo_gates_scope.py` |
|
||||
| **R-417** | **A drill night that forbids baking a golden made `golden_currency_gate.py` red, so pushing the drill's own evidence needed `--no-verify` — the very signal CI e-mails about.** Measured 2026-09-01: five consecutive felhom.eu CI runs red (jobs 469/470/471/473/476), all mine, all on step 3 `Run the gate entry point`; job 478 green the moment the golden-0.232.0 evidence was committed. Cause confirmed by isolation — moving that directory aside reproduces exit=1, restoring it gives exit=0. **The gate was right every time**: 0.231.0 and 0.232.0 were released with no golden carrying them. **CAUSE REMOVED, not worked around** (R-404): a drill's pushes are documents-only, so the conviction now prints as a loud ADVISORY and the push proceeds — in the hook AND in CI, so a drill night no longer produces red runs indistinguishable from real ones. The expectation is now written where the next drill author reads it (`documentation/runbooks/target-selection.md`), which is the half I had left out. | **CLOSED 2026-09-01** — by R-404 | five red CI jobs 469/470/471/473/476, green at 478; reproduced by isolation (`documentation/audits/AUDIT-gate-decoys-2026-09-01.md` records the technique) |
|
||||
| **R-361** | **The pre-restore safety dump overwrote the app's own DB dump, and the comment beside it said it could not.** Shipped in controller v0.221.0 (+v0.221.1). Evidence: `audits/DRILL-r361-2026-08-22/evidence/`. **Reasoning kept:** *`DumpOne` writes `<stack>-<dbtype>.sql` — the app's canonical dump, the name the replay loop matches EXACTLY — so nothing else may ever be written to it.* The fix is a DESTINATION, not a rename: `DumpOneTo` takes the final path and derives its own `.tmp` from it, so neither the destination nor the scratch file can collide with a nightly dump running beside it. **`DumpOne`'s signature did not move** — it has callers outside this concern. **The manifest no longer lists the undo copies:** every consumer of `Manifest.DBDumps` was grepped and named — three, all inside `recovery_unit.go`, none reading it for recovery. **AND THAT CHANGE MADE ANOTHER UNREACHABLE:** a stable `db_dumps` let `CaptureRecoveryUnit`'s already-current early return fire, and the undo-copy prune sat after it — four copies on disk against a cap of three, counted live. The prune now runs ABOVE the check; it is housekeeping on the dump directory and is independent of whether the manifest needs rewriting. **PROVEN LIVE the only way it can be:** the canonical dump's sha256, unchanged across a restore — `docmost` `5d35678349bb…`, `bookstack` `7837aa5de295…`, both byte-identical before and after. A test asserting merely that the undo copy exists passes just as well when the app's backup was destroyed. | **CLOSED — SHIPPED + PROVEN-LIVE** (controller v0.221.1, 2026-08-23) | full text: `git show a8caa0fdde7c:documentation/backlog/OPEN-ITEMS.md` |
|
||||
| **R-379** | **The pre-restore undo copy was valid, was named to the customer, and no product action could apply it.** Shipped in controller v0.220.0 (+v0.220.1, v0.220.2). Evidence: `audits/DRILL-r379-rollback-2026-08-22/evidence/`. **Reasoning kept:** *R-379 and R-380 were ONE failure with ONE fix — both ended with a half-restored database and the only difference was whether it looked broken.* **The undo set is matched on THE RUN'S OWN STAMP, never on the `pre-restore-` prefix** (four copies coexisted on one app in one afternoon; a prefix match replays an arbitrary older state) **and never just the first file** (a two-database app would have had one restored and the other left half-written). **The rollback RE-DISCOVERS the container** — the undo file is stable, the container is not: the DB-only start re-creates it, and v0.220.0's own first live run held an app for 30 s of `waitDBReady` against a dead id while its data was recoverable. **No unit test saw that: they all inject the import seam and never look at container identity.** | **CLOSED — SHIPPED + PROVEN-LIVE** (controller v0.220.1, 2026-08-22; docmost and bookstack both rolled back to byte-identical prior state) | full text: `git show 4e488321bfd1:documentation/backlog/OPEN-ITEMS.md` |
|
||||
| **R-380** | **A failed MariaDB replay left a partially-applied database behind an app reporting `health=healthy`.** Shipped in controller v0.220.0. Evidence: `audits/DRILL-r379-rollback-2026-08-22/evidence/13-step2-verify.txt`. **Reasoning kept:** **no engine flag closes this** — `--single-transaction` was added to the Postgres import and does make it all-or-nothing, but **MariaDB's DDL is not transactional**, so a partial apply there is unavoidable at the engine. The flag is a belt; the rollback is the fix, and this row must not be read as saying otherwise. Proven live: `bookstack`'s `migrations` table back at **102 rows**, the exact cell the defect was measured in. | **CLOSED — SHIPPED + PROVEN-LIVE** (controller v0.220.0, 2026-08-22) | full text: `git show 4e488321bfd1:documentation/backlog/OPEN-ITEMS.md` |
|
||||
@@ -235,8 +236,8 @@ Compressed here to title, shipping version, evidence, and the sentences that sta
|
||||
hub, never in a doc.
|
||||
- **A doc comment claiming a guard exists is why nobody looks for the missing guard** (R-360). Correct
|
||||
such a sentence in place; do not delete it.
|
||||
| **R-399** | **How deep should the off-site integrity check go — Viktor ruled full depth.** Shipped in controller **v0.228.0**, 2026-08-31. Evidence: `felhom-controller/REPORT.md` (v0.228.0) — restic argv observed from the guest at both depths on `demo-hp`. **Reasoning kept:** *the structure check does not detect a size-preserving pack corruption — measured 2026-08-30, plain `restic check` reported `no errors were found` and exited 0 over a damaged pack that every read-data form caught. That is the reason for the default and it is what should stop anyone turning it back down to save four seconds.* *An empty value means "not configured", therefore the default; `off` is the off token, because a setting with no off switch is not a setting.* *A malformed value falls back to the DEFAULT, never to structure — falling back to structure would silently remove the protection on a typo, which is R-357's shape.* **Superseded by R-401** for anything about a large store. Original text: `git show 300d7e8:documentation/backlog/OPEN-ITEMS.md` |
|
||||
| **R-400** | **A third of the debug page posted to endpoints that did not exist — and three of the seven fetched on page LOAD.** Shipped in controller **v0.228.0**, 2026-08-31. 24 referenced / 17 dispatched became 18 / 18. `backup/crossdrive` implemented (proven live: real Tier-2 copies for three apps); `backup/infra`, `hub/infra-push`, `dr/infra-status`, `storage/watchdog-status` and both `storage/simulate-*` deleted with their panels and JavaScript. **Reasoning kept:** *implement or delete FIRST, register the gate SECOND — a registered-but-failing gate refuses every push.* *Keep `handleDebugAPI`'s exact-match switch with its `NotFound` default; a prefix match would have made the defect invisible instead of merely silent.* *A panel left behind renders nothing forever, which is how this class hides.* *A debug control that simulates or mutates storage state is deleted unless a live need can be shown — that is where drives get unenrolled and data gets stranded.* Enforced by `controller/scripts/debug_route_gate.py`, both directions, red-proofed. Original text: `git show 300d7e8:documentation/backlog/OPEN-ITEMS.md` |
|
||||
| **R-399** | **How deep should the off-site integrity check go — Viktor ruled full depth.** Shipped in controller **v0.228.0**, 2026-08-31. Evidence: `felhom-controller/REPORT.md` (v0.228.0) — restic argv observed from the guest at both depths on `demo-hp`. **Reasoning kept:** *the structure check does not detect a size-preserving pack corruption — measured 2026-08-30, plain `restic check` reported `no errors were found` and exited 0 over a damaged pack that every read-data form caught. That is the reason for the default and it is what should stop anyone turning it back down to save four seconds.* *An empty value means "not configured", therefore the default; `off` is the off token, because a setting with no off switch is not a setting.* *A malformed value falls back to the DEFAULT, never to structure — falling back to structure would silently remove the protection on a typo, which is R-357's shape.* **Superseded by R-401** for anything about a large store. Original text: `git show 300d7e8:documentation/backlog/OPEN-ITEMS.md` | **CLOSED — SHIPPED** (controller v0.228.0, 2026-08-30) | controller v0.228.0; `documentation/tests/r359-integrity-2026-08-30/` |
|
||||
| **R-400** | **A third of the debug page posted to endpoints that did not exist — and three of the seven fetched on page LOAD.** Shipped in controller **v0.228.0**, 2026-08-31. 24 referenced / 17 dispatched became 18 / 18. `backup/crossdrive` implemented (proven live: real Tier-2 copies for three apps); `backup/infra`, `hub/infra-push`, `dr/infra-status`, `storage/watchdog-status` and both `storage/simulate-*` deleted with their panels and JavaScript. **Reasoning kept:** *implement or delete FIRST, register the gate SECOND — a registered-but-failing gate refuses every push.* *Keep `handleDebugAPI`'s exact-match switch with its `NotFound` default; a prefix match would have made the defect invisible instead of merely silent.* *A panel left behind renders nothing forever, which is how this class hides.* *A debug control that simulates or mutates storage state is deleted unless a live need can be shown — that is where drives get unenrolled and data gets stranded.* Enforced by `controller/scripts/debug_route_gate.py`, both directions, red-proofed. Original text: `git show 300d7e8:documentation/backlog/OPEN-ITEMS.md` | **CLOSED — SHIPPED** (controller v0.228.0, 2026-08-30) | controller v0.228.0; `controller/scripts/debug_route_gate.py` + its decoy in `test_gate_decoys.py` |
|
||||
| **R-102** (was **C9-F4**) | **Tier-2 wrote a full `recovery-unit/` mirror on every run and no code path read it** - `RecoveryUnitPath` joined a hard-coded `backups/primary/`, so in the one failure Tier-2 exists for the surviving copy was unopenable. Shipped in controller **v0.229.0**: four unit-directory-relative path primitives in `appbackup`, `RestoreFromRecoveryUnitAt(stack, unitDir)`, `RestoreTier2Unit`. Evidence: `audits/DRILL-r102-tier2-unit-2026-08-31/`. **Reasoning kept:** *THE SOURCE MOVES; THE DESTINATION DOES NOT* - `unitDir` changes only where a unit is READ from; data still lands in the live volumes and the live database container, resolved by `GetAppDrivePath` exactly as the capture is, because a restore that also relocated an app's data would be a migration wearing a restore's label. And: *a directory that exists is not a package* - the Tier-2 route refuses fail-closed unless the mirror carries a parseable manifest. | **CLOSED 2026-08-31 - controller v0.229.0, PROVEN-LIVE with the primary unit moved aside** (`07` §8 row 3b -> PROVEN, 28.65 s; row 4 stays PARTIAL - the drive-loss JOURNEY is still unexercised) | full text: `git show 1623a4d5b5d5:documentation/backlog/OPEN-ITEMS.md` |
|
||||
| **R-103** (was **C9-F1b**) | **The Tier-2 no-coverage refusal named the working action but did not route to it** - it sent the customer to a button on another page for data that R-102 made restorable on the page they were already looking at. Shipped in controller **v0.229.0**: `POST /backup/tier2/unit-restore` and „Teljes visszaállítás a másolatból” on the Tier-2 row. Evidence: `audits/DRILL-r102-tier2-unit-2026-08-31/`. **Reasoning kept:** *a destructive operation reached from a non-destructive surface must carry the difference in the CONFIRM, not in the label* - the two actions stay two buttons because they are two promises, and the confirm names the copy's date, differently when that date is only an attempt clock (R-101). And: *two questions, two predicates* - `CanRestore()` was NOT widened to cover the unit; one predicate answering two questions is R-356, which refused 40 running apps for months. And: `tier2UnitNotCoveredMsg` was NOT deleted, because it is appended where the FILE restore ran and is still exactly true of it. | **CLOSED 2026-08-31 - controller v0.229.0, PROVEN-LIVE** (the refusal now carries `tier2UnitAvailableMsg`, verified at the endpoint) | full text: `git show 1623a4d5b5d5:documentation/backlog/OPEN-ITEMS.md` |
|
||||
| **R-87** | **The restic tier was never restore-tested — RE-SCOPED by its own spike to "prove the off-site snapshot still CONTAINS a recoverable unit".** Shipped in controller **v0.231.0** + hub **v0.110.0**. Evidence: `tests/r87-offsite-proof-2026-08-31/`; reasoning: `audits/SPIKE-restic-restore-test-2026-08-31.md`. **Reasoning kept:** *The weekly check proves the stored bytes are the bytes we stored; it cannot tell us we stored the WRONG thing.* *The acceptance rule has TWO parts and the obvious one is a trap — "everything declared is present" passes a hollow unit, which is the shape it exists to catch.* *The expectation comes from INSIDE the unit, never the live box: the snapshot may predate the app's shape.* *The volume half is an EXISTENCE check and not a name match — the naming held on all eight real units, but "held on eight" is not "derivable" (R-355), and half a rule that is true beats a whole rule that is invented.* *THREE outcomes: pass, fail, and cannot-judge — collapsing the third hides a gap in one direction and alarms on our own blind spot in the other.* *It proves the snapshot CONTAINS a recoverable unit; it does NOT prove a restore puts data back into a running app — §8 matrix row 4 was deliberately NOT moved.* *The proof's scratch is a SEPARATE root because the job deletes on every path, and sharing the customer's root would mean a nightly job deleting a copy the customer is looking at.* | **CLOSED 2026-08-31 — SHIPPED + PROVEN-LIVE** (controller v0.231.0, hub v0.110.0) | full text: `git show 303129e:documentation/backlog/OPEN-ITEMS.md` |
|
||||
|
||||
@@ -593,8 +593,14 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server`
|
||||
| **R-413** | **R-87's proof caught a naturally-produced hollow snapshot, end to end, unattended — the validation yesterday's session could only do with a declared hand-built fixture.** 2026-08-31 soak, demo-hp. After R-412's chain left `opengist`'s newest off-site snapshot hollow, the nightly proof rotated to it and returned **`verdict:"fail"`, `reason:"volumes_expected_none_captured"`, missing `opengist_data`**, logged *"READABLE AND EMPTY — the store is not damaged; the backup does not contain this app's data"*, and pushed **one** `offsite_proof_empty` at severity `error`. The four apps ahead of it in the rotation all passed, so the discrimination is real and not a constant fail. **This is recorded as a row rather than only as a report line because it upgrades a claim:** the capability map's R-87 row cites a CONSTRUCTED failing case; it can now cite a natural one. | **CLOSED 2026-08-31 — the claim it upgrades is recorded** | R-87, R-412 | Nothing to build. When the capability map is next touched, cite this instead of the constructed case. | CC |
|
||||
| **R-416** | **`closed_register_gate.py` still has no within-register duplicate-id rule.** R-406 closed by renumbering the only collision (R-133 → R-415), so the register is clean today and nothing stops the next one. The rule was deliberately NOT added in the same commit: with its only real subject removed, the red-proof would have had to be a planted fixture rather than the live defect, and this project's own standard is that a guard ships with a proof against something real. **Now that the register is clean it can be added safely** — a fresh duplicate would be the first thing it ever sees. | **OPEN — LOW** | R-406, R-405 | Add a third rule to `closed_register_gate.py`: no `R-` id may appear twice within either register. Ship it with a planted red-proof, and note that suffixed ids (R-88a/R-88b, R-209/R-209a) are distinct and must NOT be convicted. | CC |
|
||||
| **R-418** | **`repo_gates.py`'s docstring listed ELEVEN gates while THIRTEEN were registered** — `one-register` (R-369) and `closed-register` (R-405) ran on every push from 2026-08-24 to 2026-09-01 while being documented nowhere. Found while adding the `exemptible` field. This is the drift `felhom-controller/.claude/rules/gates.md` already warns about in its own runner (*"it has already drifted once — it said seven while nine were registered"*), recurring in the sibling repo that the warning does not load for. FIXED in the same commit: all thirteen listed, plus a line saying the table is the list and this is a pointer to it. **Nothing enforces the correspondence** — a gate added tomorrow drifts again, and the fix is a test that compares the docstring list against `len(GATES)`. | **OPEN — the enumeration is fixed, the mechanism is not** |
|
||||
| **R-419** | **`observations_gate.py` accepts an observation whose body merely CONTAINS the string `NOT-A-FINDING`, even in prose explaining that it carries no marker.** MEASURED 2026-09-01 and by accident: a deliberately unmarked observation planted to make the gate convict during Scenario C's live validation was reported `OK 1. NOT-A-FINDING`, because the sentence *"it carries no `FILED:` and no `NOT-A-FINDING:` marker"* satisfied the substring test. **The gate's whole job is to force an explicit choice, and a sentence disclaiming the marker counted as the marker.** This is the `strings.Contains` class the AST-walk lesson (R-408) is about, one repo over. Not a hypothetical: it silently passed a real push of mine before I noticed. Fix: require the marker at the START of a line or item, not anywhere in the body. | **OPEN** |
|
||||
| **R-420** | **`controller_gates.py` could not express a NON-BLOCKING gate before 2026-09-01** — every registered gate's non-zero exit failed the run, so the only way to add a check was to give it the power to refuse a push. That is the wrong trade for a notice that must fire at the moment a release is committed, when the golden legitimately cannot exist yet. **The capability was added rather than the notice compromised** (a fifth `blocking` field, False for exactly one gate; the felhom.eu runner already had the shape from its `--scope` work). Recorded because the ABSENCE was invisible: nobody had wanted a non-blocking gate before, so nothing said it was impossible. `felhom.eu/scripts/repo_gates.py` still has no `blocking` field — it has `exemptible`, which is a different idea (scope-dependent, not permanent). If a permanently-advisory gate is ever wanted there, it needs the same addition. | **OPEN — noted, not needed yet** |
|
||||
| **R-421** | **THE CLASS: an instrument that matches a LABEL rather than the fact it names — five instances, every one found by accident.** R-410 (a `mkdir` turned the release gate green), R-400 (seven debug controls answering nothing), R-378 (a status word inside a sentence), R-419 (a phrase inside prose, including prose saying the marker was ABSENT), R-94 (a test comparing a constant to itself). **The gates are the machinery that enforces everything else in this project, and they were the one part nothing had ever checked.** The 2026-09-01 decoy sweep read all 29 scripts and fooled **16**. Ten were fixed the same day; four remain with rows (R-422..R-425); six could not be given a plausible decoy and are named. **The shapes, so the next one is cheap to recognise:** (1) name-for-fact — it matches a path or directory NAME while the fact lives inside the file; (2) substring-for-field — it matches a token anywhere in a body instead of in the field that carries it; (3) declaration-for-reachability — it checks a thing is declared, not that it RESOLVES; (4) constant-for-measurement — it compares a value against itself. **The single largest cause was mundane:** eight gates set their SCOPE with `os.listdir` (one level), so every one was green and correct today and would have gone blind the moment anyone added a subdirectory. `decoy_coverage_gate.py` now refuses a new gate that ships without a decoy. | **OPEN — the class row; it stays open as the place the next instance is recorded** |
|
||||
| **R-422** | **`reuse_refs_check.py` only checks citations whose extension is one of `go py html css yml yaml sh`.** A cited `.md` path that does not exist is invisible — MEASURED 2026-09-01: `documentation/architecture/99-does-not-exist.md` added to `REUSE.md` passed, while the `.go` control was correctly convicted. REUSE.md and the CLAUDE.md files cite `.md` paths routinely, so this is the common case, not an exotic one. Fix: widen `PATH_RE`, then walk the false positives it produces across all four repos — that pass is the work, not the regex. The decoy is kept in `scripts/test_gate_decoys.py` asserting TODAY's behaviour, so the day this is fixed the test fails and is updated deliberately. | **OPEN** |
|
||||
| **R-423** | **`site_gates.py` checks a hardcoded `PAGES` list of seven files; a new page is not scanned at all.** MEASURED 2026-09-01: a new `website/decoy-page.html` carrying an emoji and no nav or analytics passed. `felhom.eu/CLAUDE.md` already tells the author to add new pages to the list by hand — which is the R-410 shape written down as a procedure. Fix: glob `website/*.html` and rethink the per-page exemptions (`ANALYTICS_EXEMPT` and friends) so the list becomes a list of EXCEPTIONS rather than a list of what is checked. | **OPEN** |
|
||||
| **R-424** | **`one_register_gate.py`: a real defect parked under the roadmap state `idea` is invisible to it.** MEASURED 2026-09-01 with a correctly-shaped 5-column row. This is **declared** in the gate's own docstring as residual hole 1 — *"the state column is a human judgement, and a defect written under `idea` looks exactly like a proposal to this gate"* — so it is honest, not hidden. Recorded here because a hole declared only in a docstring is not in the register, which is this project's own standing rule. No cheap fix: distinguishing a defect from a proposal mechanically is the thing the gate cannot do. | **OPEN — declared, not hidden; recorded so it is not re-derived** |
|
||||
| **R-425** | **`offbox_rename_gate.py` scans a fixed three-entry `FILES` list.** MEASURED 2026-09-01: `NAS-mentés` in a new `backups_offbox_extra.html` passed. The scope was correct when written and silently narrows every time the feature grows a file. Fix: scan the offbox feature's files by pattern, or assert the FILES list against a discovered set so a new file fails until it is classified. | **OPEN** |
|
||||
| **R-426** | **The decoy-coverage exemption list — 20 registered gates that ship WITHOUT a decoy test, each named.** `scripts/decoy_coverage_gate.py`'s `EXEMPT` map is debt, and this row owns it so it lives in the register and not only in a Python literal. **Four kinds:** (a) genuinely covered in the 2026-09-01 sweep but not yet moved into a suite — `hub-copy`, `instructions`, `docker-v`, `image-pins`; (b) blocked by an open hole and therefore un-assertable as rejecting — `site` (R-423), `one-register` (R-424), `offbox-rename` (R-425); (c) shared scripts whose decoy lives in `felhom.eu` and is counted there — `reuse-refs`, `instructions`, `observations` in the controller and agent runners; (d) **no plausible decoy constructed yet** — `hostinstall`, `wire-contract`, `due-checks`, `published`, `image-resolvable`, `volume-persistence`. Group (d) is the honest unknown: six gates whose soundness is UNTESTED, not established. **The list is green today and shrinks; a NEW gate with no decoy fails immediately.** | **OPEN — 20 names; group (d) is six untested gates** |
|
||||
| **R-427** | **`closed_register_gate.py` checks ONE direction only: an open word in a CLOSED row. The mirror — a CLOSED verdict on a row still sitting in `OPEN-ITEMS.md` — is unchecked, and there are TWELVE.** MEASURED 2026-09-01 during the decoy sweep, by reading the leading verdict of every open row with the gate's own predicate: **R-385, R-387, R-341, R-378, R-405, R-88a, R-88b** read unambiguously closed; **R-123, R-190, R-352** read `PARTLY CLOSED` / `MITIGATION SHIPPED` and almost certainly belong where they are. **The rows were NOT moved by this session** — telling a finished row from a partly-finished one is a judgement, and R-378 is itself the record of what happens when a machine makes that judgement on a substring (six still-open rows moved out of the register). **This is R-405's finding mirrored:** that row exists because R-87 sat in the CLOSED file while its state read READY, and the gate written for it looks only the way it was bitten. Fix: the same leading-verdict predicate applied to `OPEN-ITEMS.md`, reporting rather than convicting until the twelve are adjudicated by a person — a gate registered while twelve rows fail it would refuse every push. | **OPEN — 12 rows named; the adjudication is Viktor's, the gate is mine** |
|
||||
|
||||
<!-- DUE-CHECKS-BEGIN — machine-readable. Parsed by scripts/due_checks_gate.py.
|
||||
One row per dated check. The R-number must have a row above. Dates are UTC.
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
## the decoy sweep — can this gate be fooled by a label? (2026-09-01, R-421) — NOT A RELEASE
|
||||
|
||||
**No product code, no version bump, no image, no golden.** A scripts change is not a release.
|
||||
|
||||
Four times in one week a gate turned out to match a NAME instead of the thing it named — R-410 (a
|
||||
`mkdir` turned the release gate green), R-400 (seven debug controls answering nothing), R-378 (a
|
||||
status word inside a sentence), R-419 (a phrase inside prose, including prose saying the marker was
|
||||
absent). **All four found by accident.** The gates enforce everything else here and were the one part
|
||||
nothing had checked.
|
||||
|
||||
**All 29 gate scripts read and decoyed. 16 were fooled.** 10 fixed here, 4 left with rows
|
||||
(R-422..R-425), 6 could not be given a plausible decoy and are named (R-426 group d).
|
||||
|
||||
**The largest single cause was mundane:** eight gates set their SCOPE with `os.listdir` (one level).
|
||||
Green and correct today; blind the moment anyone adds `templates/partials/`. `mojibake` and
|
||||
`docker-v` already used `os.walk`, caught the identical planted file, and are the control that
|
||||
proves the cause was the listing rather than the decoy.
|
||||
|
||||
Full survey table, and the five decoys withdrawn as illegitimate (mine, named):
|
||||
`documentation/audits/AUDIT-gate-decoys-2026-09-01.md`.
|
||||
|
||||
**In this repo:** `hub-confirm` and `manifest-bearer` now walk instead of listing one directory;
|
||||
`observations_gate` (R-419) requires a marker at a line start or after a sentence boundary and strips
|
||||
inline code spans, so 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. New: `scripts/test_gate_decoys.py` (12 decoys) and
|
||||
`scripts/decoy_coverage_gate.py`, registered LAST, which convicted itself until given its own decoy.
|
||||
|
||||
## push_scope.py v1.0.0 + repo_gates.py --scope — block who can act, notify who cannot (2026-09-01, R-404/R-417)
|
||||
|
||||
**No product code, no version bump, no image, and deliberately NO GOLDEN** — creating one would be an
|
||||
|
||||
@@ -98,7 +98,18 @@ def main():
|
||||
convicted, warnings, checked = [], [], 0
|
||||
for n, rid, state, line in rows(CLOSED):
|
||||
if state is None:
|
||||
warnings.append((n, rid, "row is not four columns — no state cell to read"))
|
||||
# R-421 (2026-09-01): A ROW THIS GATE CANNOT READ IS A CONVICTION, NOT A WARNING.
|
||||
#
|
||||
# This was a warning, and the gate then printed OK. Four rows were in that state —
|
||||
# R-399 and R-400 for days, and R-404 and R-417 written malformed by the session that
|
||||
# closed them the day before this sweep. **Every one of them was exempt from the only
|
||||
# check that reads this file**, and the gate said OK each time. That is the sweep's own
|
||||
# shape one level up: an instrument that reports a pass over rows it never examined.
|
||||
#
|
||||
# The rows were repaired first and the conviction added second — registering a failing
|
||||
# gate refuses every push, which is the ordering instructions_gate learned the hard way.
|
||||
convictions.append((n, rid, "row is not four columns, so it has NO STATE CELL and this "
|
||||
"gate cannot judge it — an unreadable row is never a pass"))
|
||||
continue
|
||||
checked += 1
|
||||
verdict = leading_verdict(state)
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""decoy_coverage_gate.py — every registered gate ships with a decoy test (R-421).
|
||||
|
||||
Usage: python3 scripts/decoy_coverage_gate.py <repo-root> [<repo-root> ...]
|
||||
Exit 0 covered-or-registered · 1 a gate has neither a decoy nor an exemption · 2 inconclusive.
|
||||
|
||||
WHY THIS 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 answering
|
||||
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 here, and they were the one part nothing checked.** The 2026-09-01 sweep
|
||||
read all 29 and fooled 16 of them.
|
||||
|
||||
A survey fixes what it finds once. This makes the next one impossible to add quietly: **a NEW gate
|
||||
with no decoy fails immediately**, and the gates not yet covered are listed BY NAME below, each with
|
||||
its row, so the remaining debt is visible and shrinking rather than forgotten in a Python literal.
|
||||
|
||||
R-329's shape, deliberately, because this project already trusts it: walk everything, register the
|
||||
exceptions by name, and let a new one fail rather than slip through.
|
||||
|
||||
⚠ COVERAGE IS A DECLARATION, NOT A GREP. Each decoy suite exports `COVERS = {gate: why}` and this
|
||||
gate AST-parses that literal. Searching the test file for a gate's name would be exactly the
|
||||
substring-for-fact shape this whole sweep exists to find — the gate that checks for label-matching
|
||||
must not itself match on a label.
|
||||
"""
|
||||
import ast
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
# ── EXEMPTIONS — each carries its row and one line saying why it is not covered YET ─────────────
|
||||
# Dated 2026-09-01. This list is DEBT, not a settled state: R-426 owns it and names every entry.
|
||||
EXEMPT = {
|
||||
# felhom.eu
|
||||
("felhom.eu", "site"):
|
||||
"R-423 — PAGES is a hardcoded list of 7 files; a new page is unscanned. The decoy passes "
|
||||
"TODAY, so a test asserting rejection would be a lie. Fix is to glob website/*.html.",
|
||||
("felhom.eu", "one-register"):
|
||||
"R-424 — a real defect parked under state `idea` is invisible. Declared in the gate's own "
|
||||
"docstring as residual hole 1; the decoy passes today.",
|
||||
("felhom.eu", "hostinstall"):
|
||||
"R-426 — no plausible decoy constructed yet. It asserts installer invariants against a "
|
||||
"shell script; a legitimate decoy needs a shape a real edit would produce.",
|
||||
("felhom.eu", "wire-contract"):
|
||||
"R-426 — 607 lines comparing emitted fields to receiver structs across two repos; a decoy "
|
||||
"needs a Go edit, which this sweep was forbidden from making.",
|
||||
("felhom.eu", "hub-copy"):
|
||||
"COVERED IN THE SWEEP, not yet in a suite: a retired name planted in a NEW hub template was "
|
||||
"REJECTED (it uses os.walk). R-426 tracks moving that decoy into the suite.",
|
||||
("felhom.eu", "due-checks"):
|
||||
"R-426 — no legitimate decoy constructed; the attempt in the sweep was a no-op and its "
|
||||
"verdict was withdrawn rather than reported.",
|
||||
("felhom.eu", "instructions"):
|
||||
"COVERED IN THE SWEEP, not yet in a suite: a version literal in effective text was REJECTED. "
|
||||
"R-426 tracks moving it in.",
|
||||
# felhom-controller
|
||||
("felhom-controller", "docker-v"):
|
||||
"COVERED IN THE SWEEP, not yet in a suite: an unallowlisted `-v` host path in a new Go file "
|
||||
"was REJECTED (it uses os.walk). R-426 tracks moving it in.",
|
||||
("felhom-controller", "offbox-rename"):
|
||||
"R-425 — the FILES list is fixed, so banned branding in a NEW offbox file is unscanned. The "
|
||||
"decoy passes today.",
|
||||
("felhom-controller", "reuse-refs"):
|
||||
"shared script; its decoy lives in felhom.eu/scripts/test_gate_decoys.py.",
|
||||
("felhom-controller", "instructions"):
|
||||
"shared script; see felhom.eu. R-426.",
|
||||
("felhom-controller", "observations"):
|
||||
"shared script; its decoy (R-419) lives in felhom.eu/scripts/test_gate_decoys.py.",
|
||||
# felhom-agent — no decoy suite yet
|
||||
("felhom-agent", "reuse-refs"): "shared script; decoy in felhom.eu. R-426.",
|
||||
("felhom-agent", "instructions"): "shared script; decoy in felhom.eu. R-426.",
|
||||
("felhom-agent", "observations"): "shared script; decoy in felhom.eu. R-426.",
|
||||
("felhom-agent", "published"):
|
||||
"R-426 — a network gate; a decoy needs a fake registry, which this sweep did not build.",
|
||||
("felhom-agent", "release-complete"):
|
||||
"R-426 — the sweep's decoy (a non-version heading on top of CHANGELOG.md) was WITHDRAWN: "
|
||||
"HEAD_RE.search scans the whole file, so the real release is still found and named. The "
|
||||
"gate looked sound; no legitimate decoy has been constructed yet.",
|
||||
# app-catalog
|
||||
("app-catalog-felhom.eu", "image-pins"):
|
||||
"COVERED IN THE SWEEP: a real untagged `image:` line was REJECTED, and the `x-image:` decoy "
|
||||
"was WITHDRAWN as illegitimate (x- fields are inert in Compose). R-426 tracks a suite.",
|
||||
("app-catalog-felhom.eu", "image-resolvable"):
|
||||
"R-426 — needs a container runtime and the network; not a --fast gate.",
|
||||
("app-catalog-felhom.eu", "volume-persistence"):
|
||||
"R-426 — 877 lines that actually run containers and diff them; not a --fast gate.",
|
||||
}
|
||||
|
||||
RUNNERS = {
|
||||
"felhom.eu": os.path.join("scripts", "repo_gates.py"),
|
||||
"felhom-controller": os.path.join("controller", "scripts", "controller_gates.py"),
|
||||
"felhom-agent": os.path.join("scripts", "agent_gates.py"),
|
||||
"app-catalog-felhom.eu": os.path.join("scripts", "catalog_gates.py"),
|
||||
}
|
||||
SUITES = {
|
||||
"felhom.eu": os.path.join("scripts", "test_gate_decoys.py"),
|
||||
"felhom-controller": os.path.join("controller", "scripts", "test_gate_decoys.py"),
|
||||
"felhom-agent": os.path.join("scripts", "test_gate_decoys.py"),
|
||||
"app-catalog-felhom.eu": os.path.join("scripts", "test_gate_decoys.py"),
|
||||
}
|
||||
|
||||
|
||||
def literal_from(path, name):
|
||||
"""AST-parse a module-level literal assignment. Never imports — importing a test RUNS it."""
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
try:
|
||||
tree = ast.parse(io.open(path, encoding="utf-8").read())
|
||||
except SyntaxError as e:
|
||||
return ("ERROR", "%s does not parse: %s" % (path, e))
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
for t in node.targets:
|
||||
if isinstance(t, ast.Name) and t.id == name:
|
||||
try:
|
||||
return ast.literal_eval(node.value)
|
||||
except Exception:
|
||||
return ("ERROR", "%s in %s is not a literal" % (name, path))
|
||||
return None
|
||||
|
||||
|
||||
def gates_of(runner_path):
|
||||
"""The GATES table's LABELS, AST-read — the runner is never imported and never literal_eval'd.
|
||||
|
||||
Only the label is taken, deliberately. Three of the four runners build their script paths with
|
||||
`os.path.join(...)`, so the table as a whole is not a literal — but every label is a plain string
|
||||
constant, which is all this gate needs. Evaluating the whole row would make this gate fail on
|
||||
three repos for a reason that has nothing to do with coverage.
|
||||
"""
|
||||
if not os.path.isfile(runner_path):
|
||||
return None
|
||||
try:
|
||||
tree = ast.parse(io.open(runner_path, encoding="utf-8").read())
|
||||
except SyntaxError:
|
||||
return None
|
||||
for node in tree.body:
|
||||
if not isinstance(node, ast.Assign):
|
||||
continue
|
||||
if not any(isinstance(t, ast.Name) and t.id == "GATES" for t in node.targets):
|
||||
continue
|
||||
if not isinstance(node.value, (ast.List, ast.Tuple)):
|
||||
return None
|
||||
out = []
|
||||
for row in node.value.elts:
|
||||
if isinstance(row, (ast.Tuple, ast.List)) and row.elts:
|
||||
first = row.elts[0]
|
||||
if isinstance(first, ast.Constant) and isinstance(first.value, str):
|
||||
out.append(first.value)
|
||||
return out
|
||||
return None
|
||||
|
||||
|
||||
def main(argv):
|
||||
roots = argv[1:] or ["."]
|
||||
problems, inconclusive, lines = [], [], []
|
||||
total = covered = exempt = 0
|
||||
|
||||
for root in roots:
|
||||
root = os.path.abspath(root)
|
||||
name = os.path.basename(root)
|
||||
if name not in RUNNERS:
|
||||
inconclusive.append("unknown repo %r — no runner registered for it" % name)
|
||||
continue
|
||||
runner = os.path.join(root, RUNNERS[name])
|
||||
if not os.path.isfile(runner):
|
||||
inconclusive.append("%s: runner not found at %s" % (name, runner))
|
||||
continue
|
||||
labels = gates_of(runner)
|
||||
if labels is None:
|
||||
inconclusive.append("%s: could not read the GATES table from %s" % (name, runner))
|
||||
continue
|
||||
covers = literal_from(os.path.join(root, SUITES[name]), "COVERS") or {}
|
||||
if isinstance(covers, tuple):
|
||||
inconclusive.append("%s: %s" % (name, covers[1]))
|
||||
covers = {}
|
||||
|
||||
for label in labels:
|
||||
total += 1
|
||||
if label in covers:
|
||||
covered += 1
|
||||
lines.append(" COVERED %-22s %-20s %s" % (name, label, covers[label][:60]))
|
||||
elif (name, label) in EXEMPT:
|
||||
exempt += 1
|
||||
lines.append(" exempt %-22s %-20s %s" % (name, label, EXEMPT[(name, label)][:60]))
|
||||
else:
|
||||
problems.append((name, label))
|
||||
|
||||
print("decoy-coverage gate — %d registered gate(s): %d with a decoy, %d registered exempt, "
|
||||
"%d UNACCOUNTED" % (total, covered, exempt, len(problems)))
|
||||
for l in sorted(lines):
|
||||
print(l)
|
||||
|
||||
if inconclusive:
|
||||
print()
|
||||
for i in inconclusive:
|
||||
print(" INCONCLUSIVE: %s" % i)
|
||||
if not problems:
|
||||
print("\ndecoy-coverage gate INCONCLUSIVE — an undetermined result is never a pass")
|
||||
return 2
|
||||
|
||||
if problems:
|
||||
print()
|
||||
print("CONVICTED — these gates have neither a decoy test nor a registered exemption:")
|
||||
for repo, label in problems:
|
||||
print(" %s / %s" % (repo, label))
|
||||
print()
|
||||
print("A gate ships with a decoy test that has been SEEN TO FAIL. Construct the label")
|
||||
print("without the fact, run the gate, and assert it convicts — then name the gate in that")
|
||||
print("repo's scripts/test_gate_decoys.py COVERS map.")
|
||||
print("If no plausible decoy exists, say so: add it to EXEMPT here with a row number and one")
|
||||
print("line of reason. An honest exemption is a result; a silent gap is how R-410 happened.")
|
||||
return 1
|
||||
|
||||
print("\ndecoy-coverage gate OK — every registered gate has a decoy or a named exemption (R-426)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -16,20 +16,35 @@ ROOT = os.path.join("hub", "internal", "web", "templates")
|
||||
NATIVE = re.compile(r"(?<![A-Za-z0-9_$])(?:confirm|prompt)\(\s*[^)\s]")
|
||||
|
||||
|
||||
def templates():
|
||||
"""Every .html under ROOT, AT ANY DEPTH.
|
||||
|
||||
Was `os.listdir(ROOT)`, which is one level only. There are no 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 2026-09-01 by planting a template with a native
|
||||
`confirm()` in a new `partials/` directory: the gate passed it (AUDIT-gate-decoys, R-421).
|
||||
"""
|
||||
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 fn in sorted(os.listdir(ROOT)):
|
||||
if not fn.endswith(".html"):
|
||||
continue
|
||||
path = os.path.join(ROOT, fn)
|
||||
for path in templates():
|
||||
rel = os.path.relpath(path, ROOT)
|
||||
for lineno, line in enumerate(io.open(path, encoding="utf-8"), 1):
|
||||
if NATIVE.search(line):
|
||||
total += 1
|
||||
print("%s:%d %s" % (fn, lineno, line.strip()[:120].encode('ascii', 'backslashreplace').decode()))
|
||||
print("%s:%d %s" % (rel, lineno, line.strip()[:120].encode('ascii', 'backslashreplace').decode()))
|
||||
if total:
|
||||
print("HUB CONFIRM GATE FAILED: %d native confirm()/prompt() call(s) remain" % total)
|
||||
sys.exit(1)
|
||||
print("hub confirm gate OK — no native confirm()/prompt() in hub templates")
|
||||
print("hub confirm gate OK — no native confirm()/prompt() in hub templates (%d scanned, any depth)"
|
||||
% len(templates()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -30,10 +30,16 @@ KNOWN_BACKLOG = {"felhom.secret.yaml"}
|
||||
|
||||
def main():
|
||||
total = 0
|
||||
for fn in sorted(os.listdir(ROOT)):
|
||||
if not fn.endswith((".yaml", ".yml")):
|
||||
continue
|
||||
path = os.path.join(ROOT, fn)
|
||||
# AT ANY DEPTH. Was os.listdir(ROOT), one level only — measured 2026-09-01 to miss a
|
||||
# bearer-shaped literal in manifests/overlays/ (R-421). There are no subdirectories today; an
|
||||
# overlays/ or base/ directory is an ordinary thing to add, and the gate would have stayed green.
|
||||
paths = []
|
||||
for dirpath, _dirs, names in os.walk(ROOT):
|
||||
for fn in sorted(names):
|
||||
if fn.endswith((".yaml", ".yml")):
|
||||
paths.append(os.path.join(dirpath, fn))
|
||||
for path in sorted(paths):
|
||||
fn = os.path.basename(path)
|
||||
for lineno, line in enumerate(io.open(path, encoding="utf-8", errors="replace"), 1):
|
||||
for m in BEARER.finditer(line):
|
||||
masked = m.group(0)[:8] + "..." + m.group(0)[-4:]
|
||||
|
||||
@@ -76,8 +76,28 @@ 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+(.*)$")
|
||||
FILED_RE = re.compile(r"\bFILED:\s*(R-\d+)", re.IGNORECASE)
|
||||
NOT_A_FINDING_RE = re.compile(r"\bNOT-A-FINDING:\s*(\S.*)$", re.IGNORECASE | re.MULTILINE)
|
||||
# ⚠ 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"),
|
||||
@@ -184,6 +204,7 @@ def main():
|
||||
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()
|
||||
|
||||
@@ -139,6 +139,13 @@ GATES = [
|
||||
# R-389 — a live finding lived in a REPORT.md observations paragraph and nowhere else, and
|
||||
# REPORT.md is overwritten every session. Fast: stdlib file reads.
|
||||
("observations", os.path.join(SCRIPTS, "observations_gate.py"), [ROOT], True, False),
|
||||
# R-421 — every registered gate across all four repos ships with a decoy test, or is
|
||||
# named in the exemption list with its row. Registered LAST, after every runner was green:
|
||||
# a failing gate refuses every push, which is what instructions_gate learned the hard way.
|
||||
("decoy-coverage", os.path.join(SCRIPTS, "decoy_coverage_gate.py"),
|
||||
[ROOT, os.path.join(os.path.dirname(ROOT), "felhom-controller"),
|
||||
os.path.join(os.path.dirname(ROOT), "felhom-agent"),
|
||||
os.path.join(os.path.dirname(ROOT), "app-catalog-felhom.eu")], True, False),
|
||||
]
|
||||
|
||||
VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
#!/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 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",
|
||||
"decoy-coverage": "a gate registered in a runner with no decoy and no exemption (its red-proof)",
|
||||
}
|
||||
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"))
|
||||
|
||||
# --- 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")
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user