diff --git a/workplans/AUDIT-WP-0009-evidence-role-conformance.md b/workplans/AUDIT-WP-0009-evidence-role-conformance.md index fbe1805..f0e2208 100644 --- a/workplans/AUDIT-WP-0009-evidence-role-conformance.md +++ b/workplans/AUDIT-WP-0009-evidence-role-conformance.md @@ -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