Story-1162: audit emit coverage + completeness harness (S3)

Story 1162Epic None22 files · +1580/−226

PR #299 · pramaan-io/pramaan-functions · merged 2026-06-10

Why this matters

If a matter is deleted, the record must survive.

A silent audit log is a legal system that cannot defend itself.

Manual coverage erodes the moment the codebase moves.

The fix — If it mutates, the manifest knows — or the flush fails.

How it flows

sequenceDiagram
  autonumber
  participant service as service (the manifest clerk)
  participant manifest as manifest (AUDITED / UNAUDITED)
  participant inspector as inspector (the watchdog)
  participant ledger as ledger (the audit table)
  service->>manifest: is this route listed or exempted?
  service->>ledger: file declaration via builder (summary / diff / tombstone)
  service->>inspector: request to leave dock (flush)
  inspector->>inspector: walk the deck — any business / document entity undeclared?
  inspector->>service: release the ship — or raise AuditCoverageError
  manifest->>manifest: UNAUDITED_ROUTES — the one exempt row (active-firm context switch)

src/shared/audit/watchdog.py + coverage.py

Methodin → outwhat it does
mutating_routes(app) → set[RouteKey]enumerates every POST, PUT, PATCH, DELETE route in the live FastAPI app for the test to assert against
record_pending(session) → Nonestamps the session with the table the caller intends to audit, before the flush
assert_covered(session) → Nonefails the transaction if a business or documents entity is being flushed with no audit row and no exemption
watching(session) → Iterator[Session]context manager that wires the before_flush tripwire onto a session so every transaction is checked automatically

What changed, file by file

Audit route coverage manifest — enumerates mutating routes and the UNAUDITED allowlist. Makes coverage an executable check, not a review-time hope.

new methodsignature
mutating_routes(app) → set[RouteKey]

Before_flush tripwire that fails a business or document mutation with no audit row, so a missing emit can never reach the database.

new classsignature
AuditCoverageError(class)
_audited_table(obj) → str | None
record_pending(session) → None
assert_covered(session) → None
watching(session) → Iterator[Session]
_listener(sess, _flush_context, _instances) → None

Thin snapshot constructors (summary, diff, tombstone, file) reused across ~40 call sites. Keeps call sites one-liners while enforcing a uniform snapshot shape.

new methodsignature
_stringify(value) → str | None
_facts(facts) → list[FactField]
summary_snapshot(facts) → SummarySnapshot
tombstone_snapshot(facts) → TombstoneSnapshot
diff_snapshot(before, after, labels) → DiffSnapshot
file_snapshot(filename, size_bytes, content_type, facts) → FileSnapshot

Matter template service now emits audit on create/edit/delete with `_default_ids` and `_ids` helpers. Closes the silent-emit gap in template configuration flows.

new methodsignature
_default_ids(session, firm_id, template_id) → dict[str, list[str]]
_ids(entity, column) → list[str]

Single catalog emit helper `emit_catalog_event` with a unified snapshot field. So forums, labels, types, and templates all share one registry-compliant shape.

new methodsignature
emit_catalog_eventasync def emit_catalog_event( session: AsyncSession, *, kind: str, verb: str, entity_id: UUID, name: str, before: Mapping[str, Any] | None = None, after: Mapping[str, Any] | None = None, field_labels: Mapping[str, str] = CATALOG_FIELD_LABEL
New fields: snapshot : ChangeSnapshot

Files changed (22)

File±
docs/walkthroughs/story-1162-walkthrough.html+159
src/domains/document/service.py+19 −19
src/domains/filing_type/service.py+15 −24
src/domains/forum/service.py+25
src/domains/matter/identity_service.py+36 −20
src/domains/matter/redesign_service.py+106 −41
src/domains/matter/service.py+51 −38
src/domains/matter_type/template_service.py+73
src/domains/participant_label/service.py+33
src/domains/proceeding/service.py ★+117 −61
src/domains/proceeding_type/service.py+33
src/shared/audit/builders.py+86
src/shared/audit/catalog.py+58
src/shared/audit/coverage.py ★+211
src/shared/audit/watchdog.py ★+115
tests/integration/test_app_documents.py+10 −3
tests/integration/test_app_filing_types.py+10 −7
tests/integration/test_app_matters.py+31 −13
tests/integration/test_audit_reused_action_routes.py+145
tests/integration/test_audit_watchdog_integration.py+113
tests/unit/test_audit_route_coverage.py+68
tests/unit/test_audit_watchdog.py+66