docs(v0.223.0): REPORT, CONTEXT rulings, README severity contract
gates / gates (push) Successful in 11s

REPORT overwritten: the 1.1 sweep in full (one bad severity, nine legitimate
"warn" strings that are healthcheck statuses), the hub manifest's real location
since the task's premise was wrong, all five red-proofs with the layer each
guard sits at, the live walk in six steps with the hub's own records quoted, and
the absent-intent count (0 of 8).

Three things are reported that a tidier account would omit: red-proof 5 passed
first time because the mutation was INERT; Scenario G was silently refused twice
behind an HTTP 200; and the live Scenario A does NOT prove the customer gate,
because demo-hp has no prefs row at all.

CONTEXT records the severity vocabulary as a ruling with its mechanism, the
intent ruling with its three-way handling of unknown, both fences, and two traps
worth more than the fixes: a 200 can be a refusal, and a passing red-proof can
mean an inert mutation.

README: the event table said `app_start_failed | warn` - the defect, written
down as if correct. Now `warning`, with the vocabulary contract and who receives
what. `disk_critical` also corrected from `error` to `critical`, which is what
fillwatch has always sent.
This commit is contained in:
2026-08-23 12:06:45 +02:00
parent 9832760027
commit 1da2c9c6c6
3 changed files with 273 additions and 204 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.222.0 — R-384: a dead database raised no alarm; R-383; and R-386 filed)
Last updated: 2026-08-23 (v0.223.0 — R-329: the alarm reached nobody; R-386: ask the field that knows)
> **2026-08-23 — v0.223.0 (R-329 + R-386), and a defect that only became visible once another was fixed.**
>
> **[RULING] The severity a controller sends is the HUB's vocabulary: exactly
> `{info, warning, error, critical}`.** Anything else is **coerced to `info` at ingest, silently**, and
> `severityNotifies` drops `info` **before both** delivery legs. `app_start_failed` emitted `"warn"`.
> **Measured on the live hub DB: 91 such events stored all-time, ZERO notification rows ever.**
>
> **[FACT] This was the SECOND occurrence, and the first one's comment had recorded the lesson.**
> `DiskAlertKind.Severity` emitted `"warn"` until v0.215.0. **A comment is not a guard** — the guard is
> now an AST walk over the whole controller. **grep cannot do this job:** `"warn"` is a legitimate
> *healthcheck status* in `internal/monitor` and `internal/selftest`; the sweep hit nine such strings
> and exactly one defect. The walk cannot follow a variable, so the **six** dynamic call sites are
> registered by name with the values each can take — **an unlisted limit is not a limit, it is a hole**.
> The guard found two of those six that the hand sweep had missed.
>
> **[FACT] It hid because another defect hid it.** R-384's ordering bug meant `app_start_failed` could
> not fire at all, so a broken severity had nothing to break. **Fixing one defect made another
> reachable** — and the same shape appeared again downstream: the operator cooldown key carries no app
> identifier, so **only the first app-down per hour now e-mails the operator** (R-182's shape, newly
> load-bearing, filed not fixed).
>
> **[RULING] `app_start_failed`: operator always, customer OFF by default.** `processOperator` never
> consults customer preferences, so one word fixed the operator leg and left the customer leg where the
> ruling wanted it. **Deliberately NOT in `operatorOnlyEvents`** — that would make the new toggle
> visible, flickable and structurally incapable of delivering.
>
> **[RULING, R-386] "The customer stopped this" is a RECORD, never an inference.** `aggregateState`
> folds `StateExited` into the stopped counter, so an out-of-band stop and a customer's Stop are
> byte-identical on the Docker side — no state test can separate them. Ask `DesiredState`, which has
> exactly one writer. `Stopped` → no alarm; `Running` → **alarm**; **absent → UNKNOWN, keep the old
> behaviour AND announce it**, because reading absent as "nobody asked" would e-mail about every app
> anyone ever stopped, fleet-wide, on the first cycle after upgrade. **The backfill cannot help — it
> seeds `Running` only from an observed-UP reading.**
>
> **[MECHANISM] `IntentUnknown` + an INFO line naming the apps.** A rule without a mechanism is a wish.
> Measured on `demo-hp`: **0 of 8** deployed apps carry an absent intent.
>
> **[FENCE] Adding a `DesiredState` WRITER is the fenced act; reading is fine.** And `failedRestart`
> must still lift a `Stopped` intent, or F-CRIT-1 re-opens.
>
> **[TRAP, cost three attempts] An HTTP 200 can be a REFUSAL.** The settings save answers 200 while
> rendering the empty-email wipe-guard error. Scenario G's before/after hashes matched twice because
> **nothing was saved**, not because nothing changed. And the email `<input>` spans three lines, so a
> single-line grep reads it empty. **Assert the refusal banner is ABSENT before believing a save.**
>
> **[TRAP] A red-proof that passes may mean an INERT mutation.** `if next <= prev` → `if next < prev`
> in fillwatch changes nothing, because an earlier `if next == prev { continue }` already removed the
> equal case. Check the mutation applied before believing either verdict.
>
> **[RULING] The compound-toggle split's risk was the MIGRATION, not the split.** A save whose event
> SET is unchanged now stores the existing slice **verbatim**, so byte-identity is by construction —
> without that guard the defaults case reorders, and the red-proof caught it.
> **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.**
>