Skip to main content

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:

MemberSource (reused, no data re-assembly)Role
case-pack.pdfnew case_pack.html over ReportData + AMLR coverage + the artifact indexmaster 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.pdfreport_service.generate_compliance_reportfull 10-section KYB report
mlro-memo.pdfcompliance_docs_* (#13)internal escalation memo
evidence-request.pdfcompliance_docs_* (#12)customer EDD letter
source-appendix.pdfcompliance_docs_* (#14)primary-source citations
evidence-bundles/*.jsonEvidenceBundleService (MinIO, ADR-0021)per-agent AI chain-of-thought/provenance
audit-trail.jsonAuditService.get_events (immutable, ADR-0064)append-only action log
manifest.jsonthis servicetamper-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_manifest layer (stable input → stable pack_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.