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.

Assembling a single-period docket audit report from a ledger slice A ledger slice bounded to one review period feeds a read-only report builder in the center. The builder projects the windowed events into four fixed sections shown on the right: open deadlines as of period end, acknowledgment latency, escalations with tier and outcome, and exceptions covering missed, late, and overridden deadlines. The builder performs no date computation of its own and mutates nothing; a caption notes that the assembled report is sealed with a SHA-256 body digest for reproducibility. 1 Ledger slice one review period 2 Report builder read-only • no date math Open deadlines live at period end • owner Acknowledgment latency reminder → first ack Escalations tier • outcome Exceptions missed • late • override Assembled report sealed with a SHA-256 body digest • reproducible from the same ledger slice

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_ACKNOWLEDGMENT independently 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_computed event with no owner is a real gap, not a formatting quirk. The open-deadlines projection stamps UNASSIGNED rather 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 MANUAL is 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?
The live database holds current state; it is edited continuously as matters progress. A malpractice review needs the state as of a fixed reporting date, proven not to have changed since. The append-only ledger records every event immutably in order, so querying a pinned slice reproduces exactly what the firm held then. Reading the mutable database would give a snapshot no one could later verify against the moment it claims to represent.
Should a deadline that was met after escalation appear as an exception?
The met deadline itself is not a miss, but if it was acknowledged late or required escalation it belongs in the risk sections. The exception fold flags a late acknowledgment even when the deadline was ultimately met, and the escalation section records every tier that fired with its outcome. That lets a reviewer distinguish a comfortably-met deadline from one the automated safety net rescued — a distinction an insurer prices on.
How is acknowledgment latency measured when there were several reminders?
Latency is measured from the first reminder for a deadline to the first acknowledgment of it, in hours, computed in UTC. Pairing the first reminder with the first acknowledgment avoids rewarding a system that sent ten reminders and counting only the last one. If no acknowledgment was recorded in the period, the latency is left unset and the deadline is flagged as a late or missing acknowledgment in the exceptions section.
Does this report ever recompute a deadline date?
No. The builder reads the effective deadline the calculation layer already sealed into the ledger and never applies its own month arithmetic or holiday roll. Keeping reporting free of date computation is a compliance boundary: a reviewer must be able to distinguish what the system computed at the time from what a later report asserts. All date math, including weekend and holiday shifts, stays in the rule engine.

↑ Back to Malpractice Compliance Reporting