Heartbeat per event class, not per source

audit-core's completeness contract landed, and our single per-source beat
is the shape it rules inadequate: it is discharged by whichever class is
busy, so a revocation stream that has gone silent looks identical to a
quiet one — and revocation is the only silence here that matters.

Emit one nothing-to-report assertion per declared class, all four in one
transaction so a partial emission cannot report some classes healthy and
others stalled. Carry type audit-core.heartbeat with class and assertion
on data. Pin that the first beat goes out at startup rather than an
interval later, since a declared-but-never-sent class is their
no_heartbeat_since_registration finding and not a skip.

Declare heartbeat_classes and the reconciliation surface in the source
registration, including the residual neither control covers: a
compromised emitter suppresses the event and its own heartbeat together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyybaE7DUXrWYrhbnESCTe

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1275879@bnt-lap001
Assistant-Session: eb464208-f821-41b2-bc5a-a6c33d92a8ad
This commit is contained in:
tegwick 2026-09-10 20:36:21 +02:00
parent c8f85c6d76
commit bfb1e66646
6 changed files with 213 additions and 18 deletions

View file

@ -513,6 +513,32 @@ the same tenant. This closes those source-input waits, not live registration.
Platform still owes linked receiver/sender custody and the protected token;
T04 stays wait for admission and live drain/reconciliation evidence.
2026-09-10: audit-core landed the detection half (`AUDIT-WP-0009` T04/T06/T07
at `b098fb1`, contract in their `docs/stream-completeness.md`), so the sender
side is no longer describing a cadence that does not operate. Implemented the
per-class heartbeat contract: `HEARTBEAT_CLASSES` covers `issuance`, `use`,
`supersession` and `revocation`, each asserting `nothing-to-report` for itself,
all four written in one transaction. The previous single per-source beat was
exactly the shape audit-core rules inadequate — it is discharged by whichever
class is busy, so a silent `revocation` stream hides behind `issuance`, and
`revocation` is the only silence here that matters. Heartbeats now carry
`type: audit-core.heartbeat` with `class` and `assertion` on `data` rather than
inside our details object. The first beat is emitted at startup, not one
interval later, because a declared-but-never-sent class is their
`no_heartbeat_since_registration` finding rather than a skip — `heartbeat_due`
already returned true on an empty outbox and that is now pinned by test.
`docs/audit-source-registration.md` declares `heartbeat_classes` at 172800s for
all four (twice the deployed 86400s default, so one missed cycle is not a
finding and two are), records the reconciliation surface and both bounds, and
restates the residual audit-core stated on every response: counts and
heartbeats cover loss, outage, drain failure and accident, and neither covers
this engine lying about itself, since a compromised emitter suppresses the
event and its own heartbeat together. Tests: per-class emission including
`revocation`, atomicity under outbox failure, the envelope placement, and that
an ordinary event gains no heartbeat fields. 155 pass. T04 stays `wait`: live
drain still needs the sender registration and credential admission, and the
`heartbeat_classes` values above are a request until audit-core accepts them.
## Prove one live PEP consumption path
```task