AUDIT-WP-0009 — record T04/T06/T07 as done
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2069992@bnt-lap001
Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
This commit is contained in:
tegwick 2026-09-10 16:43:44 +02:00
parent b098fb12ca
commit 8a2a09deae

View file

@ -164,10 +164,32 @@ occurred. Twelve tests in `tests/test_senders.py`.
```task
id: AUDIT-WP-0009-T04
status: todo
status: done
priority: high
state_hub_task_id: "f36470af-4019-54b2-96d7-e049d867c7db"
```
Done 2026-09-10. `audit_core/stream_findings.py`, surfaced at
`GET /v1/stream-findings`, documented in `docs/stream-completeness.md`.
A heartbeat is an **ordinary event** — same envelope, same append-only custody,
same chain, no special table. A heartbeat stored outside the chain would be the
one record in this store that could be back-dated.
Declared **per class**, not per source, on the sender registration
(`heartbeat_classes`). `informed-decision` raised the shape first and it
generalises: a per-source heartbeat from a mixed-volume emitter is satisfied by
its chattiest class and says nothing about the quiet, security-relevant one,
which is the only reason heartbeats exist.
`no_heartbeat_since_registration` is a distinct finding kind rather than a skip
— a source that declared a heartbeat and never sent one is the case most likely
to be a broken integration, and it is exactly what a "compare against last
seen" implementation drops silently. A wildcard source is refused rather than
guessed at: there is no determinate set of streams to expect a heartbeat from.
Explicitly not T05's territory. Cadence describes expected *rate* and belongs
to the §17 Taxonomy schema; this says how often a source promises to say
*nothing to report* for a class that may legitimately be silent.
Heartbeat ingestion and missing-heartbeat findings. The required form for
low-volume load-bearing classes, and the only control covering adversarial
omission for exactly the rare negative events — revocation, denial, containment
@ -191,10 +213,29 @@ alternatives.
```task
id: AUDIT-WP-0009-T06
status: todo
status: done
priority: medium
state_hub_task_id: "0a8d6eed-75dd-5aaa-bf86-60be9dadca03"
```
Done 2026-09-10. `GET /v1/reconciliation?source=&tenant=&since=&until=`,
per-class counts, `event_counts` on both the SQLite and Postgres backends.
Counts only — never payloads.
**The read-model tension, resolved rather than ignored.** The task said respect
`may_read`. But every registered sender holds `may_read: false`, so read
literally that makes the §9.6 reconciliation obligation undischargeable by
every source actually registered — a strange end for a rule audit-core argued
for. The resolution: a source asking how many of *its own* events audit-core
holds is not reading the archive, because it learns nothing it did not itself
emit. So the surface is scoped to the caller's own permitted sources and
tenants and carries no payloads; another source's counts, or an unscoped
aggregate, stay behind `may_read` and full tenant scope.
Two refusals: another source's counts return **403, not an empty count**, since
a zero would read as "we hold none of yours" — a false answer to a question
about completeness. And there is **no default window**: a count whose bounds the
caller did not choose is not comparable to anything the caller computed, and
would be quoted as though it were.
Reconciliation surface: per-source, per-class event counts over a bounded
window, readable by the source itself, so an emitter can compare audit-core's
count against its own state transitions and treat divergence as a finding.
@ -204,10 +245,23 @@ scoping, and no payloads in a counts response.
```task
id: AUDIT-WP-0009-T07
status: todo
status: done
priority: medium
state_hub_task_id: "f572dfeb-0d1b-58d6-be83-405125189028"
```
Done 2026-09-10. `GET /v1/stream-findings`, following the existing
findings-surface conventions rather than adding a new shape: `may_read` plus
full tenant scope, because findings span every registered sender and carry no
tenant key to filter on — the same rule as dead letters, secret findings and
integrity.
INTENT principle 10 had the principle and no mechanism. It has one now.
Every finding and every reconciliation response carries a `means` field stating
the bound — a missing heartbeat is not proof of suppression, and agreement on
counts proves neither completeness nor that any event occurred. On the response
rather than in a document nobody opens beside it, because these are the numbers
most likely to be quoted out of context in someone else's conformance argument.
Give stream-completeness findings a home. `/v1/dead-letters` and
`/v1/secret-findings` exist; a cadence miss (T05) and a missing heartbeat (T04)
have nowhere to surface. INTENT principle 10 already says a degraded audit