docs(v0.222.0): REPORT, CONTEXT decisions, README state table + the ordering
gates / gates (push) Successful in 11s

REPORT.md overwritten with the full run: baselines and the hub's four numbers,
the four red-proofs with the mutation and observed text for each, the five
IsDownState consumers walked and named, the live walk in full with the old and
new heartbeat lines quoted side by side, and the halt.

CONTEXT records the decision - a dead supervised member is asked about before a
failing healthcheck, because they are different questions and the second was
answering the first - plus the fence that IsDownState did not move, the trap
that three existing subtests pinned the defect, and R-386.

README gains the `degraded` row, which the state table never had, and a note
that the ORDER is load-bearing. Points at the new alarm-ladder architecture doc.
This commit is contained in:
2026-08-23 07:58:09 +02:00
parent 5da11c4480
commit 14137efac5
3 changed files with 320 additions and 80 deletions
+55 -1
View File
@@ -7,7 +7,61 @@
>
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
Last updated: 2026-08-23 (v0.221.1 — R-361: taking the undo copy destroyed the app's own backup)
Last updated: 2026-08-23 (v0.222.0 — R-384: a dead database raised no alarm; R-383; and R-386 filed)
> **2026-08-23 — v0.222.0 (R-384 + R-383), and a bigger hole found by a measurement that was told not to fix it.**
>
> **[DECISION] A dead SUPERVISED member is asked about BEFORE a failing healthcheck, because they are
> different questions and the second was answering the first.** `aggregateState` returned
> `StateUnhealthy` the moment `unhealthy > 0`, and the R-51 mixed-case block that asks "is a supervised
> member dead?" sat below it. A two-container app whose database exits goes `unhealthy` seconds later
> *because it cannot reach that database* — so **the symptom the fault causes was what suppressed the
> alarm for it.** The supervised test is now hoisted above the unhealthy/starting/restarting returns.
>
> **[DECISION] "Some members are up" means ANY member not in the down bucket** — running, unhealthy,
> starting or restarting. The old guard was `running > 0` counting `StateRunning` alone, which made the
> R-51 block **unreachable in precisely the case it was written for**: an unhealthy survivor beside a
> dead database counted as nothing being up. Either half alone leaves the defect standing, and the two
> red-proofs convict independently.
>
> **[FENCE, unchanged] `IsDownState` is byte-identical and `unhealthy` stays excluded.** An unhealthy
> container is RUNNING; folding it in reintroduces the flapping that exclusion exists to stop. **No new
> state was minted** — `StateDegraded` already meant this and every consumer already handled it. The
> fix is an ORDER, not a widening.
>
> **[FACT] The register's own suggested remedy was wrong.** R-384's row proposed a sustained-`unhealthy`
> threshold on the `crashLoopAfter` model. The defect needed no threshold at all. **A register's
> "recommended fix" is a hypothesis written before the diagnosis, and must be re-derived from source.**
>
> **[TRAP] Three existing subtests pinned the DEFECT as settled behaviour.**
> `TestAggregateState_UnchangedBranches` asserted an unhealthy/starting/restarting member beat an
> `exited` peer that was on `unless-stopped`. They were amended (down member given a benign policy,
> which is the only case where that sentence was ever true) and the change is reported, not buried.
> **A green suite can be green about the wrong thing.**
>
> **[FINDING — R-386, OPEN, NOT FIXED] A single-container app stopped out of band raises NO alarm, and
> a comment states the opposite.** `aggregateState` folds `StateExited` into the `stopped` counter, so
> an all-down stack returns `StateStopped` and **`StateExited` never survives aggregation**;
> `classifyRunStates` then whitelists `stopped` as a deliberate user stop. The comment at
> `cmd/controller/main.go` claiming an out-of-band `docker compose stop` "still alerts" is **measured
> false** — `privatebin`, 9 scans, 0 events, 0 banner. **Case #10 of "a comment asserting an invariant
> the code does not provide".** The task asked for this as a MEASUREMENT and forbade a fix; it is filed.
>
> **[DECISION, R-383] A message may not assert a file exists without asking the disk.** The
> double-failure sentence named the undo copy as present, built from the returned path — and a missing
> file is one of the two ways that rollback fails. `undoCopyPhrase` now reads from disk; a zero-length
> dump counts as MISSING; and the absent case still names WHERE the file should have been, because
> R-351's lesson is that a refusal naming nothing forces someone to remember what the product knows.
>
> **[DECISION] The alarm ladder now has an owning document** —
> `felhom.eu/documentation/architecture/08-alarm-ladder.md`. Until 2026-08-23 no document owned it; the
> rules lived as comments in four packages, each locally correct, with the ordering between them legible
> only by reading one function top to bottom. **That absence is why R-384 survived review.**
>
> **[GOTCHA] `app_start_failed` still ships severity `warn` (R-329)**, which is not in the hub's
> vocabulary and coerces silently to `info`, e-mailing nobody, while the POST returns 200. R-384 moved
> this event from unreachable to load-bearing, so the severity bug now matters.
> **2026-08-23 — v0.221.0/.1 (R-361), and two negatives worth as much as the fix.**
>