docs+gate: instruction files cannot silently regrow (R-229)
gates / gates (push) Successful in 7s

New shared scripts/instructions_gate.py, registered in controller_gates.py and
agent_gates.py, never copied into a sibling repo (the reuse_refs_check.py
precedent). 20 fixture tests, all asserting the effect: exit code AND that the
message names the file and the reason.

It is a consistency gate, not a budget gate, and the failure message says so. A
/context reading measured the instruction files at 15k tokens against 869k free in
a 1M window -- space is not the constraint, and a future reader must not re-derive
the wrong reason. The 200-line ceiling is adherence guidance; a file nobody can
hold in their head is where contradictions hide, and five were found here.

Checks run against effective text (HTML comments stripped, because they are
stripped before injection): the line ceiling; every .claude/rules/*.md declares
paths: or an explicit unconditional: true; no component version literal; no
TEMPORARY block carrying a past date; and the workspace-root CLAUDE.md is
byte-identical to its versioned copy -- the live file sits outside any git repo,
so that copy is its only version-controlled record.

Two traps recorded so they are not reintroduced: a bare \d+\.\d+\.\d+ matches the
first three octets of every IPv4 (the gate excludes dotted quads, or it fails on
192.168.0.180 in the agent's own file); and unconditional: true is NOT a Claude
Code feature but this project's own marker.

Workspace-root CLAUDE.md 208 -> 182 lines (142 effective), copy kept identical.
The nine-instance invariant table moved into the felhom-testing skill, which
triggers when writing or reviewing a test; all three directive bullets stayed in
the core. felhom.eu/CLAUDE.md got surgical corrections only and is knowingly still
over the ceiling at 227 effective lines -- closing it needs the restructure R-229
defers, said plainly rather than quietly absorbed.

CONTEXT.md gains standing ruling S-35. OPEN-ITEMS.md gains R-229.

Docs only -- no Go, no version bump, nothing built or deployed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJc8sAGRWmavP3rMtdpkr2
This commit is contained in:
2026-08-06 09:38:52 +02:00
parent d30c2a51ed
commit c21bcf84f7
10 changed files with 1125 additions and 333 deletions
+25
View File
@@ -65,6 +65,31 @@ now, it is a regression. **The lesson generalises: "known flake, just re-run it"
it needs the same evidence as any other one.** A test that fails at a stable, explainable rate is
usually telling the truth about a rare input, not misbehaving.
## A comment asserting an invariant needs a test pinning it — the nine instances
The three directives live in the workspace-root `CLAUDE.md` and apply always. This is the evidence
behind them: **nine shipped guarantees the code did not provide**, each surviving review because the
comment read as settled. Read this table when you are about to trust a comment, or write one.
| # | Comment | What it claimed | What the code did |
|---|---|---|---|
| 1 | `EffectiveProtected` | a stack was protected | it was not — the samba false alarm |
| 2 | `newestArchiveOn` | *"errors degrade to unknown, never to no-backup"* | the `(time,bool)` signature made that impossible (R-88 Part 2) |
| 3 | R-97a operator-only | the event *"cannot be routed to a customer"* | only configuration stopped it; fixed by a real `operatorOnlyEvents` register |
| 4 | `classifyRunStates` I1 | *"StateStopped means deliberately stopped by the user"* | quiesce stops stacks the same way — a failed restart was silent (F-CRIT-1) |
| 5 | `inflight.go` | *"a caller that cannot acquire DEFERS"* | the backup caller recorded a failure and paged the operator (F-A1) |
| 6 | `quiesce.go` | the agent's 409 *prevents* "a spurious failure" | on the start path it produced one (F-A1) |
| 7 | `recovery_unit.go` B2 refusal (R-181) | *"the previous unit is untouched and NOTHING was deleted"* | *nothing deleted* held; **untouched was measured false** — the floor was checked ONLY in `captureAllRecoveryUnits`, while the two dump legs wrote the bulk into the same tree first and unguarded, so a 182,272 B tar became 2,147,666,432 B under a manifest that had not moved |
| 8 | `ResolveManagedFloor` (R-216) | *"never push a controller past the agent it depends on"* | it compared the box's agent against the **golden's** MinAgent while serving a **floor** that could point elsewhere. Measured live 2026-08-05: golden 0.192.0/MinAgent 0.113.0, floor 0.200.0, agent 0.120.0 → served, and the box landed on a controller needing 0.125.0. Its customer was then told their correct recovery code was wrong. **The first entry where the false invariant was a GUARD, not a comment alone.** Fixed hub v0.97.0 |
| 9 | `escrow/recover.go` header (R-224) | *"The errors below are DISTINCT on purpose"*, naming **three** situations | there were **four**. A failed FETCH fell through the local-api handler's `default` into the wrong-code answer, so an unreachable hub was reported to the customer as a bad recovery code. Measured live 2026-08-05: **0.0556 s** hub-firewalled and **0.0299 s** agent-stopped, against ~1.0 s for a genuine unseal. **AND A GREEN TEST NAMED IT AND DID NOT PREVENT IT**`TestRecoverOffsiteRepoPassword_FetchErrorIsDistinct` asserted the package's error **string**, one layer below where the merge happened, and a string is not something a caller can branch on. **Mechanism asserted, consequence unpinned.** Fixed agent v0.126.0 + controller v0.202.0 |
**Three of these (4, 5/6 and 7) were found on live hardware**, not by review or unit tests. #4 had a
green, red-proofed suite over a production path broken two independent ways. #7 survived a full green
suite plus three of its own red-proofs, because every one asserted the mechanism inside
`captureAllRecoveryUnits` and none asserted the **consequence** across the whole backup run. The test
that would have caught it is the one #7's fix ships: **fingerprint the tree before and after, and
compare.**
## Live validation doctrine (after unit-land)
Exercise the SERVER-SIDE PIPELINE a real user triggers, end-to-end — never hand-set state around it