Generating a Docket Audit Report for Malpractice Review
A docket audit report for malpractice review is a single-period, read-only rollup of the audit ledger that presents four fixed sections — the deadlines open at period end, how promptly each was acknowledged, every escalation that fired, and every exception that occurred — so a reviewer can judge the firm’s docket control without touching the underlying system.
This is the concrete query-and-render job behind the broader Malpractice Compliance Reporting practice: where that reference fixes the reproducibility, scope, and retention rules, this page implements the report for one review window against a fixed ledger slice. It reads the hash-chained event store defined by the Core Docketing Architecture & Deadline Types reference and never computes a deadline itself — it reports the dates the calculation layer already sealed.
Report Anatomy: Four Sections, One Period
A malpractice reviewer is not reading source code; they are checking whether the control system behaved. The report answers four questions in a fixed order, each a pure projection over the period’s ledger entries.
| Section | Question answered | Ledger events read |
|---|---|---|
| Open deadlines | What was still live at period end, and who owned it? | deadline_computed without a terminal met/missed by the period end |
| Acknowledgment latency | How fast did the owner confirm receipt of each alert? | reminder_sent paired with the first acknowledged for the same deadline |
| Escalations | Which deadlines needed the safety net, and to what tier? | escalated, with tier and outcome |
| Exceptions | What failed or nearly failed? | missed, late acknowledgments, and discretionary overrides |
The report is deliberately narrow — one period, one matter population, four sections — because a narrow report is auditable. A reviewer can hold the entire scope in mind and confirm nothing was quietly omitted.
Minimal Reproducible Implementation
The builder takes a list of validated ledger entries and a half-open review window, filters to the window in UTC, and returns a typed report object. It is pure: same entries plus same window yields the same report. The entry model mirrors the one persisted by the docketing pipeline; only the fields this report reads are shown.
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo
from pydantic import BaseModel, Field
UTC = ZoneInfo("UTC")
class LedgerEntry(BaseModel):
"""One immutable audit-ledger record read by the report builder."""
seq: int
matter_id: str
deadline_id: str # stable id of the specific deadline
jurisdiction: str = Field(pattern=r"^[A-Z]{2}$")
event_type: str # deadline_computed | reminder_sent | acknowledged | escalated | met | missed | override
deadline_category: str
effective_deadline: date | None = None
owner: str | None = None
tier: int | None = None # escalation tier, when event_type == escalated
outcome: str | None = None # escalation outcome, e.g. RESOLVED | MANUAL
recorded_at: datetime # UTC, timezone-aware
@dataclass
class AckLatency:
deadline_id: str
matter_id: str
reminder_at: datetime
acknowledged_at: datetime | None
latency_hours: float | None # None if never acknowledged in-period
@dataclass
class DocketAuditReport:
period_start: datetime
period_end: datetime
matter_population: int
open_deadlines: list[dict[str, str]] = field(default_factory=list)
ack_latencies: list[AckLatency] = field(default_factory=list)
escalations: list[dict[str, str]] = field(default_factory=list)
exceptions: list[dict[str, str]] = field(default_factory=list)
Windowing and the four projections
Filtering is done once, in UTC, against a half-open [start, end) window so a boundary event lands in exactly one period. Each projection then folds the windowed slice.
def _in_window(e: LedgerEntry, start: datetime, end: datetime) -> bool:
return start <= e.recorded_at.astimezone(UTC) < end
def _open_deadlines(entries: list[LedgerEntry], period_end: datetime) -> list[dict[str, str]]:
"""Computed deadlines with no terminal met/missed event by period end."""
terminal: set[str] = {
e.deadline_id for e in entries
if e.event_type in ("met", "missed") and e.recorded_at.astimezone(UTC) < period_end
}
rows = [
{
"deadline_id": e.deadline_id,
"matter_id": e.matter_id,
"jurisdiction": e.jurisdiction,
"category": e.deadline_category,
"effective_deadline": e.effective_deadline.isoformat() if e.effective_deadline else "",
"owner": e.owner or "UNASSIGNED",
}
for e in entries
if e.event_type == "deadline_computed" and e.deadline_id not in terminal
]
return sorted(rows, key=lambda r: r["effective_deadline"])
def _ack_latencies(entries: list[LedgerEntry]) -> list[AckLatency]:
"""Pair the first reminder for each deadline with its first acknowledgment."""
first_reminder: dict[str, datetime] = {}
first_ack: dict[str, datetime] = {}
matter_of: dict[str, str] = {}
for e in sorted(entries, key=lambda x: x.recorded_at):
matter_of.setdefault(e.deadline_id, e.matter_id)
ts = e.recorded_at.astimezone(UTC)
if e.event_type == "reminder_sent":
first_reminder.setdefault(e.deadline_id, ts)
elif e.event_type == "acknowledged":
first_ack.setdefault(e.deadline_id, ts)
latencies: list[AckLatency] = []
for deadline_id, reminded in first_reminder.items():
acked = first_ack.get(deadline_id)
hours = round((acked - reminded).total_seconds() / 3600, 2) if acked else None
latencies.append(AckLatency(
deadline_id=deadline_id, matter_id=matter_of[deadline_id],
reminder_at=reminded, acknowledged_at=acked, latency_hours=hours,
))
return latencies
Escalations, exceptions, and assembly
The escalation and exception folds surface the risk signal, then build_audit_report composes the whole. A late acknowledgment — one that exceeded the firm’s acknowledgment SLA — is an exception even when the deadline was ultimately met, because it means the safety net, not the owner, carried the matter.
ACK_SLA_HOURS: float = 24.0 # acknowledge within one business day of first reminder
def _escalations(entries: list[LedgerEntry]) -> list[dict[str, str]]:
rows = [
{
"deadline_id": e.deadline_id,
"matter_id": e.matter_id,
"tier": str(e.tier or 0),
"outcome": e.outcome or "UNKNOWN",
"at": e.recorded_at.astimezone(UTC).isoformat(),
}
for e in entries if e.event_type == "escalated"
]
return sorted(rows, key=lambda r: (r["matter_id"], r["tier"]))
def _exceptions(entries: list[LedgerEntry], latencies: list[AckLatency]) -> list[dict[str, str]]:
exceptions: list[dict[str, str]] = []
for e in entries:
if e.event_type == "missed":
exceptions.append({"deadline_id": e.deadline_id, "matter_id": e.matter_id,
"kind": "MISSED"})
elif e.event_type == "override":
exceptions.append({"deadline_id": e.deadline_id, "matter_id": e.matter_id,
"kind": "DISCRETIONARY_OVERRIDE"})
for lat in latencies:
if lat.latency_hours is None or lat.latency_hours > ACK_SLA_HOURS:
exceptions.append({"deadline_id": lat.deadline_id, "matter_id": lat.matter_id,
"kind": "LATE_ACKNOWLEDGMENT"})
return exceptions
def build_audit_report(entries: list[LedgerEntry], period_start: datetime,
period_end: datetime, matter_population: int) -> DocketAuditReport:
"""Assemble a single-period docket audit report from a ledger slice."""
if period_start.tzinfo is None or period_end.tzinfo is None:
raise ValueError("period bounds must be timezone-aware (UTC)")
windowed = [e for e in entries if _in_window(e, period_start, period_end)]
latencies = _ack_latencies(windowed)
return DocketAuditReport(
period_start=period_start,
period_end=period_end,
matter_population=matter_population,
open_deadlines=_open_deadlines(windowed, period_end),
ack_latencies=latencies,
escalations=_escalations(windowed),
exceptions=_exceptions(windowed, latencies),
)
The report object is now a deterministic function of its inputs. Serializing it with sorted keys and hashing the result yields the reproducible body_sha256 the Malpractice Compliance Reporting attestation binds a signer to. Rendering to a human-readable PDF/A or a machine CSV is a separate presentation step over this same object; the numbers never change between formats.
Known Gotchas and Compliance Traps
- Reading the ledger head instead of a pinned slice. Regenerating a past report against the current ledger head silently includes corrections appended after the period. A malpractice reviewer needs the report the firm held on the reporting date. Query the ledger as-of the original period end and pass that fixed slice into the builder, never the live table.
- Counting a late acknowledgment as a clean result. A deadline met after the owner blew the acknowledgment SLA still met — but the safety net carried it, which is exactly the signal an insurer wants. The exception fold flags
LATE_ACKNOWLEDGMENTindependently of whether the deadline was ultimately met, so a met-but-late case never reads as trouble-free. - Naive datetimes at the window boundary. An entry recorded without a timezone can drift into the wrong period, double-counting or vanishing. The builder rejects naive period bounds outright and normalizes every entry to UTC before comparison, so a boundary event lands in exactly one half-open window.
- Treating an unassigned open deadline as covered. A
deadline_computedevent with noowneris a real gap, not a formatting quirk. The open-deadlines projection stampsUNASSIGNEDrather than dropping the row, so an ownerless live deadline is visible on the report face — the standard-of-care question a reviewer asks first. - Escalation outcome inferred from tier alone. A tier-3 escalation that resolved is a working safety net; a tier-3 that ended in
MANUALis a control that ran out of automated road. The report records the outcome verbatim from the ledger rather than assuming a high tier means failure, so the escalation section reflects what actually happened.
Frequently Asked Questions
Why does the report read the ledger instead of the live docketing database?
Should a deadline that was met after escalation appear as an exception?
How is acknowledgment latency measured when there were several reminders?
Does this report ever recompute a deadline date?
Related
- Malpractice Compliance Reporting — the reproducibility, scope, attestation, and retention rules this report is built to satisfy.
- Core Docketing Architecture & Deadline Types — the append-only, hash-chained ledger the builder queries.
- Escalation Routing Workflows — the source of the escalation events summarized in the report.
- Security & Access Control Boundaries — who may run and release the report.
↑ Back to Malpractice Compliance Reporting