PR #299 · pramaan-io/pramaan-functions · merged 2026-06-10
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.
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)
| Method | in → out | what 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) → None | stamps the session with the table the caller intends to audit, before the flush |
assert_covered | (session) → None | fails 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 |
Audit route coverage manifest — enumerates mutating routes and the UNAUDITED allowlist. Makes coverage an executable check, not a review-time hope.
| new method | signature |
|---|---|
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 class | signature |
|---|---|
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 method | signature |
|---|---|
_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 method | signature |
|---|---|
_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 method | signature |
|---|---|
emit_catalog_event | async 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 |
snapshot : ChangeSnapshot