Case-Pack Export & Compliance Documents
The compliance-document suite turns a decided case into the artifacts an MLRO hands to a
regulator or FIU: a tamper-evident case pack, a signed decision memorandum, an
MLRO memo, an evidence request, a source appendix with per-source provenance,
and the SAR reportability assessment. Every document assembles from the same
ReportData object (app/services/report_data_builder.py), so the verdict, findings, and
evidence are identical across documents — a single source of truth rather than several
generator paths that can disagree.
Design basis: ADR-0069 (regulator-ready pack) and ADR-0021 (evidence bundles).
The regulator case pack (GET /cases/{workflow_id}/case-pack.zip)
app/services/case_pack_service.py builds a ZIP whose integrity is anchored by a SHA-256
manifest and a Merkle-style pack_hash:
content_sha256(bytes)producessha256:<hex>— the tamper-evidence primitive.build_manifest(...)records each member's SHA-256 digest and size, then computes a top-levelpack_hash= SHA-256 over the concatenated member digests (a flat Merkle root).manifest.jsonis never a member of its own hash.- The pack schema is versioned (
trust-relay/case-pack-manifest/1).
Anyone can later recompute each member's hash and the pack_hash and compare against
manifest.json to prove the pack has not been altered since sealing.
Decision Conclusion section (ADR-0097, #347)
For a decided case the master dossier (case-pack.pdf) renders a "Decision Conclusion
(ADR-0097)" section directly under the decision-of-record box. It is assembled by the pure
_decision_conclusion() helper from the same immutable audit_events list the pack
already encloses as audit-trail.json (single source, no extra query): the officer's
verbatim rationale (officer_decision.details.reason), the decision verb / deciding
officer / iteration, and — from decision_conclusion_recorded — the regulatory basis,
conditions (or "None recorded"), conclusion id and recorded-at audit anchors.
Decided-detection reuses the exact _DECISION_EVENT_TYPES allowlist behind
_decision_actor() — never a second heuristic. An undecided case omits the section
entirely; a decided legacy case with no structured conclusion on the trail gets honest
"not recorded" lines, never fabricated content. The deterministic disposition narrative
above the section is unchanged.
Honest gaps are carried, not hidden
The pack is fail-closed and honest about absence. A case missing evidence bundles
records that absence in the manifest rather than silently shipping a thinner pack; the SAR
assessment inclusion is fail-closed (the build fails rather than omitting it silently); and
the network adverse-media recall gap finding (see below) travels into the pack so a
CLEAR screening result is never presented as "no adverse media."
:::warning Integrity anchoring is client-recomputable, not yet server-notarised
The pack_hash proves internal consistency of a downloaded pack, but the export endpoint
does not itself persist an audit event anchoring the emitted pack_hash server-side. The
known gaps register tracks this and the related "audit-trail load must
fail loud, not empty" items surfaced by the review.
:::
Compliance documents (app/api/compliance_docs.py)
| Document | Endpoint | Notes |
|---|---|---|
| Evidence request | GET /cases/{workflow_id}/documents/evidence-request.pdf | Gated by the SAR-first customer-contact gate (returns 409 until the MLRO assessment exists) |
| MLRO memo | GET /cases/{workflow_id}/documents/mlro-memo.pdf | Includes the HARD-STOP action and Art. 69 consistency framing |
| Source appendix | GET /cases/{workflow_id}/documents/source-appendix.pdf | Per-source provenance (see below) |
Source-appendix provenance semantics (PR #171)
Each source in the appendix carries a Collected timestamp and a Content hash, and a
legend distinguishes two hash kinds honestly (templates/source_appendix.html,
compliance_docs_builder.py):
- Captured-content hash — a SHA-256 over content actually retained (registry PDFs, evidence-bundle snapshots). This is a fingerprint of the remote content.
- Recorded-citation hash — for a consulted OSINT source with no retained snapshot, the hash is a SHA-256 over the recorded citation (name/type/note). The legend states plainly that this is "an integrity anchor for the record, not a fingerprint of remote content."
This is a deliberate honesty choice: the system does not fabricate a content fingerprint for data it did not capture.
Decision memorandum (app/api/decision_memorandum.py)
The Officer Decision Memorandum is a signed, 9-section justification with a full lifecycle:
| Step | Endpoint |
|---|---|
| Draft | POST /cases/{workflow_id}/decision-memorandum |
| Update | PUT /cases/{workflow_id}/decision-memorandum/{memo_id}/draft |
| AI justification assist | POST /cases/{workflow_id}/decision-memorandum/{memo_id}/suggest-justification |
| Sign | POST /cases/{workflow_id}/decision-memorandum/{memo_id}/sign |
| Get (HTML or PDF) | GET /cases/{workflow_id}/decision-memorandum/{memo_id} |
| List | GET /cases/{workflow_id}/decision-memoranda |
The GET returns an A4 PDF rendered from templates/decision_memorandum.html.j2 via
WeasyPrint when ?format=pdf or Accept: application/pdf is sent (PR #169); otherwise HTML.
The signed memo carries a content_hash over its content for tamper-evidence.
Adverse-media recall gap (honest coverage finding)
report_data_builder.py injects a Finding(category="adverse_media_recall_gap", severity="medium") whenever the network scan identified related entities or named persons
that were sanctions/PEP-screened but not individually adverse-media-searched. This makes
the coverage boundary explicit in the report and the pack: a clean screening result is
labelled as not adverse-media-assessed for the related network, never as "no adverse
media found." See ADR-0067 for the fail-closed
"not assessed" contract this implements.
EU AI Act conformity record
Related but distinct: GET /api/conformity/ai-act.pdf (app/api/conformity.py,
app/services/ai_act_conformity_service.py) serves a data-driven Art. 11–15 conformity
record with honest per-obligation satisfied / partial / gap status. See
EU AI Act Compliance.