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.
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.