R-404 CLOSED with the ruling; R-417 CLOSED by cause removal; R-418/419/420 filed
gates / gates (push) Successful in 17s

THE RULING WAS NEITHER OPTION AS FRAMED. Both offered answers - narrow the gate, or leave it and
write waivers - argued about the gate, and the gate was never the problem.

DIAGNOSIS, from live source: golden_currency_gate.py never looks at the push. It compares the
controller's newest CHANGELOG heading against this repo's bake evidence and returns the same
verdict whatever you are pushing - correct for a standing invariant, wrong as a push gate. And
controller_gates.py had NO golden-currency entry at all. So the repo where a release happens never
checked, and the repo that cannot create the debt was refused on every push. 18 of the last 24
pushes here touched no code - measured, and the new classifier agrees EXACTLY - most of them by
construction, because the controller's code is in one repo and its register lives in this one. SIX
of those 18 were bake records, so the push that PAYS the debt is itself documents-only: the gate
was blocking its own cure.

Not the waiver its docstring prescribes: that clause was written for a release nobody wants a
golden for. 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.

RULING: block the push that can create the debt, notify the push that cannot.

The gate's logic, exit codes and wording are BYTE-IDENTICAL. Only the consequence changed, for one
gate, on one kind of push, with a loud ADVISORY block so nothing goes quiet.

R-242's vouch half is amended in place to say it is UNTOUCHED and still open - a baked-but-unvouched
golden still passes both the gate and the new notice. Do not read R-404's closure as closing it.

FILED: R-418 - this runner's docstring listed ELEVEN gates while THIRTEEN were registered;
one-register and closed-register ran undocumented since 2026-08-24. Enumeration fixed here, the
correspondence is still unenforced. R-419 - observations_gate.py accepts an item whose body merely
CONTAINS "NOT-A-FINDING", even in prose disclaiming it; found by accident when a planted test
observation passed and my live validation proved nothing. R-420 - controller_gates.py could not
express a non-blocking gate at all before today.

Register: OPEN 171 -> 172, CLOSED 158 -> 160.
This commit is contained in:
2026-09-01 12:01:10 +02:00
parent 1f74427fd2
commit 1e6c387a0b
6 changed files with 116 additions and 18 deletions
+32
View File
@@ -14,6 +14,38 @@
> language, one screen, no identifiers in the prose. Same subjects, different readers; merging them > 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`** > 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. > and holds nothing of its own; this file does hold its own content, namely the standing rulings below.
## 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
push should be subject to the golden-currency gate, and both answers offered — *narrow the gate*, or
*leave it and write waivers* — argued about the gate. **The gate was never the problem.**
The diagnosis, measured from live source rather than reasoned: `golden_currency_gate.py` **never
looks at the push**. It compares the controller's newest CHANGELOG heading against this repo's bake
evidence and returns the same verdict whatever you are pushing — correct for a standing invariant,
wrong as a push gate. And `controller_gates.py` had **no golden-currency entry at all**. So the
repository where a release actually happens never checked, while the repository that cannot create
the debt enforced it on every push. **18 of the last 24 pushes here touched no code** — measured, and
the classifier built for this agrees exactly — most of them by construction, because the controller's
code lives in one repo and its register, architecture and status live in this one. Worse: **six of
those eighteen were bake records**, so the push that pays the debt is itself documents-only and the
gate was blocking its own cure. `--no-verify` had been reached for thirteen times, each with a
recorded reason, which is what a correctly-bypassed guard produces.
**The ruling: block the push that can create the debt, notify the push that cannot.** The gate's
logic, exit codes and wording are byte-identical; only the consequence changed, for one gate, on one
kind of push, with a loud ADVISORY block so nothing goes quiet. A notice now fires in the controller
repo at the moment a release is committed — advisory in every case, because at that moment the golden
legitimately cannot exist yet.
**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 was *a release we did want a golden for,
on a night the drill runbook forbade baking*. A waiver would have recorded a lie.
**What did NOT change, and must not be presumed:** nothing gates the **vouch**. A baked-but-unvouched
golden still passes both the gate and the new notice — R-242's remaining half, still open, for the
unchanged and forced reason that the vouched version exists only in the hub's database.
## Skills cover the PROCESS domain too, from named MIT sources with named exclusions (2026-08-25) ## Skills cover the PROCESS domain too, from named MIT sources with named exclusions (2026-08-25)
**[RULING] `felhom.eu/skills/` now holds two kinds of skill and the distinction is deliberate.** The **[RULING] `felhom.eu/skills/` now holds two kinds of skill and the distinction is deliberate.** The
+15 -14
View File
@@ -37,28 +37,29 @@ nothing.*
two register lines in the hub (already live). No customer action, no data migration, no two register lines in the hub (already live). No customer action, no data migration, no
credential change. credential change.
4. **Whether a documents-only push should still be checked for a missing golden** (R-404). We have now 4. **Whether to change the hub password** (R-350). I printed it into my own session log on 20 August.
skipped that check **eight times**, each time for a written reason: it runs on every push to the
website/documentation repository, including pushes that change nothing a machine installs.
**A guard we correctly skip eight times is teaching us to skip it.**
**The case for narrowing it:** a documents-only push cannot be the one that finishes a release, so
only checking pushes that touch real code would fire on exactly the risky ones and end the habit.
**The case against:** the check was earned — a release went out while machines were still being
installed with the previous one, three times in three days — and narrowing a guard is how the thing
it was built for comes back.
**If you do nothing:** nothing breaks, the skipping stays routine, and the count keeps rising.
I have NOT changed it; this is yours to decide and mine to build.
5. **Whether to change the hub password** (R-350). I printed it into my own session log on 20 August.
Not in git, not in any saved file — in the log on this machine. **If you do nothing:** it stays as Not in git, not in any saved file — in the log on this machine. **If you do nothing:** it stays as
it is, at the risk you accept by leaving it. I can change it without ever showing you the new one. it is, at the risk you accept by leaving it. I can change it without ever showing you the new one.
6. **`demo-hp`'s network setup does not match our own notes** (R-338) — the machine works, the page is 5. **`demo-hp`'s network setup does not match our own notes** (R-338) — the machine works, the page is
wrong, or the other way round. **If you do nothing:** the page keeps misleading the next session, wrong, or the other way round. **If you do nothing:** the page keeps misleading the next session,
as it misled one by an hour. as it misled one by an hour.
## Decided — and what would reopen each ## Decided — and what would reopen each
- **The missing-golden warning is now pointed at the people who can act on it (R-404). DECIDED
2026-09-01, and built the same day.** The warning was aimed at the wrong repository: the one where
a release actually happens never checked at all, while the one that only holds documents was
refused on every push — including the push that RECORDS a golden bake, which is the very act that
clears the warning. So the check was blocking its own cure, and we had skipped it thirteen times.
**What changed:** the release repository now prints a reminder the moment a release is committed
(it never blocks — you cannot bake a golden for a version you have not pushed yet), and the
documents repository still runs the check on every push and still says so loudly, but only refuses
a push that touches real code. **Every other check still blocks everything, always.** Nothing was
silenced and no product code changed. **Reopens if:** a release ever ships without a golden and
nobody noticed — that would mean the reminder is not reaching anyone, and the answer would be to
make the release repository refuse rather than remind.
- **Getting old backups back yourself: NOT BUILT, deliberately.** **Reopens if:** a real customer - **Getting old backups back yourself: NOT BUILT, deliberately.** **Reopens if:** a real customer
asks. *(R-312)* asks. *(R-312)*
- **The unopenable old copy on `demo-felhom`: KEPT as a test fixture** — the only state in existence - **The unopenable old copy on `demo-felhom`: KEPT as a test fixture** — the only state in existence
+2
View File
@@ -26,6 +26,8 @@
--- ---
| **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-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-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-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` | | **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` |
File diff suppressed because one or more lines are too long
+49
View File
@@ -1,3 +1,52 @@
## 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
odd way to finish a task about golden debt.
**The diagnosis, because the fix is not a weakening and should not be mistaken for one.**
`golden_currency_gate.py` never looks at the push: it compares the controller's newest CHANGELOG
heading against this repo's bake evidence and returns the same verdict whatever you are pushing.
That is right for a standing invariant and wrong as a push gate. `controller_gates.py` had no
golden-currency entry at all. **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; the new classifier agrees exactly), most of them by construction, because the controller's
code is in one repo and its register lives in this one. Six of those 18 were bake records, so **the
push that pays the debt is itself documents-only and the gate was blocking its own cure.**
- **`push_scope.py`** — classifies a push `code` or `docs` from an **allow-list** of document paths.
Everything else, including any new top-level directory, is code. **Every uncertainty answers
`code`**: first push (all-zero remote sha), deletion, force-push, merge commit, empty range,
unreadable stdin, absent classifier. Two input modes feed one classifier — `--range` for the hook,
`--files-from` for CI — so there is one definition of "document" and not two. Reasoning goes to
stderr; only the verdict word goes to stdout.
- **`repo_gates.py`** — fifth `exemptible` field (True for `golden-currency` **only**), `--scope=`,
and a new **ADVISORY** verdict printed in its own block after the table. `run_gate` now tees rather
than captures, so the advisory quotes the gate's own numbers instead of re-deriving them. An
unscoped run is byte-identical to before. **`--scope=banana` is refused, not assumed.**
- **`.githooks/pre-push`** — reads git's ref updates from stdin (measured against git 2.47.3:
`<local ref> <local sha> <remote ref> <remote sha>`, one line per ref) and passes the scope through.
Stdin is consumed exactly once, into a variable.
- **`.gitea/workflows/gates.yml`** — the same rule in CI. CI checks out `--depth 1` so it has no
range; the file list comes from the push event payload and feeds the same classifier. Every failure
path writes `code`, so this step can only make CI as strict as it is now, never looser.
**THE GATE ITSELF IS UNTOUCHED** — its logic, exit codes and wording are byte-identical. Only the
consequence changed, and only for one gate, on one kind of push.
**Tests.** `test_push_scope.py` (P1–P5) and `test_repo_gates_scope.py` (R1–R6). **Scenario C — a
documents push with a NON-exemptible gate convicting must still be refused — was written FIRST and
is the acceptance.** Red-proofs run and recorded: swapping the allow-list for a deny-list breaks P3
(all seven unknown paths become documents); marking every gate exemptible breaks R3.
**Proven live on the real hook**, against a throwaway local remote so no test commits reached Gitea:
docs + debt → ADVISORY printed, push accepted; code + debt → refused with today's wording; docs +
debt + a second gate convicting → refused, `CONVICTED: observations` alone. The evidence directory
was moved aside to create the debt and restored afterwards; tree byte-identical, gate green.
**Also fixed here:** this file's own runner docstring listed ELEVEN gates while THIRTEEN were
registered (R-418) — `one-register` and `closed-register` ran on every push undocumented since
2026-08-24.
## closed_register_gate.py v1.0.0 — CLOSED-ITEMS.md holds closed work only (2026-08-31, R-405) ## closed_register_gate.py v1.0.0 — CLOSED-ITEMS.md holds closed work only (2026-08-31, R-405)
**One row corrected, one gate added, no product code touched.** R-87 ("the restic tier is never **One row corrected, one gate added, no product code touched.** R-87 ("the restic tier is never
+14 -1
View File
@@ -18,7 +18,20 @@ Gates, in order (all must pass; **non-zero exit on any failure**):
8. wire-contract every emitted field is decodable by its receiver (G-1) 8. wire-contract every emitted field is decodable by its receiver (G-1)
9. hub-copy the hub's customer-facing words, against the retired-name list (R-324) 9. hub-copy the hub's customer-facing words, against the retired-name list (R-324)
10. due-checks a dated check in OPEN-ITEMS.md that has come due (R-341) 10. due-checks a dated check in OPEN-ITEMS.md that has come due (R-341)
11. observations a REPORT.md observation with no register row behind it (R-389) 11. one-register open work living outside OPEN-ITEMS.md (R-369)
12. closed-register a CLOSED row whose verdict still reads open, or an id in both (R-405)
13. observations a REPORT.md observation with no register row behind it (R-389)
**THE `GATES` TABLE BELOW IS THE LIST; THIS IS A POINTER TO IT.** It drifted once already —
it read eleven while thirteen were registered, from 2026-08-24 until 2026-09-01, so
`one-register` and `closed-register` ran on every push while being documented nowhere
(R-418). Add a gate here in the same commit, or delete this list rather than let it lie.
SCOPE (R-404, 2026-09-01). `--scope=docs` marks a push whose whole range touches documents.
It changes ONE thing: a CONVICTION by a gate whose fifth `exemptible` field is True — today
`golden-currency`, and only it — prints as ADVISORY and does not refuse the push. The gate
still RUNS and still CONVICTS; its verdict, exit codes and wording are untouched. Every
other gate refuses every push, in every scope. An unscoped run behaves exactly as before.
WHY 11 IS HERE (2026-08-24, R-389). On 2026-08-23 a session measured on live hardware that only the WHY 11 IS HERE (2026-08-24, R-389). On 2026-08-23 a session measured on live hardware that only the
FIRST broken app per hour reaches the operator — the notification cooldown keys on the event type, FIRST broken app per hour reaches the operator — the notification cooldown keys on the event type,