Third gap in the M6 audit.
Gap: the old monitor kept opened/first-fail/last-fail/resolved apart; the rebuild tracked open and resolved only. Start-of-failure ≠ time-of-notice is the entire point of incident forensics.
What changed: first_fail_at, opened_at, last_fail_at, resolved_at
are all committed separately. The design choice: first_fail_at is derived
inside the commit transaction from committed check rows — the oldest
consecutive failure since the last pass/missed — so it stays correct across
restarts and is never fabricated from scheduler memory. last_fail_at
re-stamps on every failing check while open. Pre-existing rows keep NULL
rather than invented history. All four ride the API IncidentView, the webhook
payload, the CLI, and the dashboard incident rows.
Proof: the store test asserts exact slot relationships — a five-slot streak
yields first=slot 1, opened=slot 3, last=slot 5. Live: on a refused-port
monitor first_fail_at landed 30 seconds before opened_at — the failure
started two slots before the threshold noticed.