ADR-0069: Regulator-Ready Case-Pack Export & Retention
Status: Accepted
Date: 2026-06-26
Deciders: Adrian (Soft4U) + Claude Opus 4.8
Milestone: M3 — Defensible Case File & Audit Pack (docs/plans/2026-06-26-world-class-product-roadmap.md)
Decision-context
An EU MLRO is personally liable and must, on demand, hand a regulator/auditor a complete,
exportable, tamper-evident dossier that reconstructs a case decision years later (AMLR 5-year
retention; EU AI Act Art. 12 logging completeness). Today the pieces exist as separate downloads —
the KYB compliance report, the MLRO memo / evidence-request / source-appendix (#12-14), ADR-0021
evidence bundles in MinIO, and the immutable audit_events table (ADR-0064). The missing whole is
a one-click case pack: a single archive binding every piece together, with a cryptographic
manifest proving nothing was altered, and an explicit retention tag.
A pack that quietly drops the honest M1/M2 "not assessed" states would be worse than useless — it would manufacture the false comfort M1 was built to forbid. The pack MUST carry the gaps forward.
Decision
A single ZIP, assembled by app/services/case_pack_service.py, downloadable at
GET /api/cases/{workflow_id}/case-pack.zip (auth get_current_user, tenant-scoped, DB-fallback —
no Temporal dependency). Members:
| Member | Source (reused, no data re-assembly) | Role |
|---|---|---|
case-pack.pdf | new case_pack.html over ReportData + AMLR coverage + the artifact index | master dossier / regulator cover — decision+actor+time, honest risk verdict (incl. NOT_ASSESSED), CDD coverage with explicit gaps, findings, discrepancies, screening gaps, UBO-to-natural-person honesty, retention, artifact hashes |
compliance-report.pdf | report_service.generate_compliance_report | full 10-section KYB report |
mlro-memo.pdf | compliance_docs_* (#13) | internal escalation memo |
evidence-request.pdf | compliance_docs_* (#12) | customer EDD letter |
source-appendix.pdf | compliance_docs_* (#14) | primary-source citations |
evidence-bundles/*.json | EvidenceBundleService (MinIO, ADR-0021) | per-agent AI chain-of-thought/provenance |
audit-trail.json | AuditService.get_events (immutable, ADR-0064) | append-only action log |
manifest.json | this service | tamper-evidence + retention |
Tamper-evidence (the contract). For every non-manifest member the manifest records its
filename, role, byte size, and sha256:<hex> of its exact bytes. A top-level pack_hash is
sha256 over the newline-joined, sorted "<filename>:<member_hash>" lines — a Merkle-style root
over the member digests. Recomputing any member's hash from the archived bytes and comparing to the
manifest detects single-bit tampering; recomputing pack_hash detects member add/drop/reorder.
Where an evidence bundle already carries a data_hash (ADR-0021) it is preserved alongside.
Honest absence, not silent omission. When a case has no evidence bundles (MinIO empty/offline),
the manifest still builds and records evidence_bundles_present: false plus a human-readable note —
the absence is stated, never hidden. Same for unavailable AMLR coverage (graph off → "not
computed / not assessed" on the cover, not a fabricated 100%).
Purity / determinism. Assembly splits into a pure core and an I/O orchestrator:
assemble_case_pack(...) and build_manifest(...) take already-fetched data + an injected
generated_at (no datetime.now in any pure builder) and are fully deterministic — identical
member bytes yield an identical pack_hash. Only build_case_pack(...) touches DB/MinIO/clock.
retention_class = "AMLR-5yr".
Consequences
- Positive: one click produces a defensible, self-verifying dossier an auditor can check offline; the M1/M2 honesty (NOT_ASSESSED, CDD gaps, screening gaps, no-natural-person-UBO) is carried into the regulator artifact rather than sanitized away; reuses every existing builder (zero data duplication); the pure core is unit-testable without Docker/Temporal.
- Negative / accepted: WeasyPrint PDFs embed a creation timestamp, so PDF member bytes are not
byte-identical run-to-run — therefore the determinism guarantee is asserted at the
build_manifestlayer (stable input → stablepack_hash) and the integrity guarantee is asserted by recompute-and-match over the actual archived bytes; we do not claim byte-reproducible PDFs. - Retention is a tag, not yet a lifecycle: the pack is stamped
AMLR-5yr; enforced WORM storage / retention scheduling is M5/M8 infra, out of scope here.