Skip to main content

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) produces sha256:<hex> — the tamper-evidence primitive.
  • build_manifest(...) records each member's SHA-256 digest and size, then computes a top-level pack_hash = SHA-256 over the concatenated member digests (a flat Merkle root). manifest.json is 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)

DocumentEndpointNotes
Evidence requestGET /cases/{workflow_id}/documents/evidence-request.pdfGated by the SAR-first customer-contact gate (returns 409 until the MLRO assessment exists)
MLRO memoGET /cases/{workflow_id}/documents/mlro-memo.pdfIncludes the HARD-STOP action and Art. 69 consistency framing
Source appendixGET /cases/{workflow_id}/documents/source-appendix.pdfPer-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:

StepEndpoint
DraftPOST /cases/{workflow_id}/decision-memorandum
UpdatePUT /cases/{workflow_id}/decision-memorandum/{memo_id}/draft
AI justification assistPOST /cases/{workflow_id}/decision-memorandum/{memo_id}/suggest-justification
SignPOST /cases/{workflow_id}/decision-memorandum/{memo_id}/sign
Get (HTML or PDF)GET /cases/{workflow_id}/decision-memorandum/{memo_id}
ListGET /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.