Skip to main content

The Regulator Case-Pack — An MLRO's Field Guide

Audience. This page is written for a seasoned European enterprise MLRO who is deciding one thing: would this system's case-pack survive a competent-authority inspection? It explains, for each artifact, what it is, why it exists (the regulatory driver and the ADR that governs it), how the generator actually assembles it (real sections, real code paths), the MLRO lens (how to read it, the red flags to hunt, how it defends an audit), and the honest caveats — every fail-closed behaviour and every known gap is preserved, not sanitised.


1. Orientation — what the case-pack is and what guarantees it makes

The regulator case-pack is a single downloadable ZIP dossier that reconstructs one case decision for a regulator or auditor years after the fact. It does not invent data: it binds together artifacts that already exist elsewhere in the system — the KYB compliance report, the #12–#14 compliance documents (evidence-request letter, MLRO memo, source appendix), the SAR/STR assessment, the per-agent AI evidence bundles, and the immutable audit_events trail — under a master cover document (case-pack.pdf) and a cryptographic manifest.json.

It is served at GET /api/cases/{workflow_id}/case-pack.zip (permission CASE_READ), and it is the M3 keystone of the "defensible case file" (ADR-0069).

The three guarantees

GuaranteeMechanismWhere it lives
Tamper-evidenceEvery non-manifest member is sealed with a sha256:<hex> digest; a top-level pack_hash is a Merkle-style root over the sorted member digests. Single-bit tampering or member add/drop/reorder is detectable offline.build_manifest, content_sha256
Fail-closed honestyThe pack carries forward every "NOT ASSESSED" / "GAP" / "BLOCKED" state. It never sanitises a gap into false comfort and never certifies "clear" when no check ran (ADR-0067). Load failures raise 503, they do not produce a reassuring empty pack._verify_pack_integrity, fail-closed loaders
5-year retention + accountable exportStamped retention_class = "AMLR-5yr". The export writes a case_pack_exported audit event capturing WHO exported the full-PII dossier, WHEN, and the pack_hash of the exact bytes handed out._record_export_audit

Governing regulation

RegulationObligation the pack satisfies
AMLR — Reg (EU) 2024/1624Art. 77 / Art. 56 record-keeping (5-yr retention of CDD + transaction records); Art. 20/22/34/51-52 CDD/EDD substance carried by the enclosed documents; Art. 69 FIU reporting (SAR).
AMLD6 — Dir (EU) 2024/1640Art. 39 tipping-off boundary (enforced on the customer-facing letter and the SAR-first gate); the MLRO role and documented risk-based decision.
EU AI Act (Annex III high-risk)Art. 11 technical documentation; Art. 12 logging/traceability; Art. 13 transparency (disclose what was not assessed); Art. 14 human oversight; Art. 15 accuracy.
GDPRArt. 30 records-of-processing (the export itself is logged); Art. 22 (a human, not the AI, owns the decision).
FATFRec. 20 (report suspicion), Rec. 21 (tipping-off/confidentiality) — presupposed by the tamper-evident audit trail.

The immutability of that audit trail is itself DB-enforced (ADR-0064): a BEFORE UPDATE OR DELETE trigger fires an unconditional exception for all roles including superuser, UPDATE/DELETE are REVOKEd from the app role, and the cases → audit_events foreign key is RESTRICT (not CASCADE) so a case with history cannot be hard-deleted (delete_case returns 409).


2. The documents in the pack

The ZIP contains, in assembly order: case-pack.pdf (cover), compliance-report.pdf, mlro-memo.pdf, evidence-request.pdf, source-appendix.pdf, audit-trail.json, conditionally sar-assessment.pdf, zero-or-more evidence-bundles/{iteration}__{agent}.json, and manifest.json. Each member reuses an existing builder — there is no re-assembly of data, which is what lets the same facts render identically across documents.


2.1 case-pack.pdf — the master cover dossier

What it is. The single-document front sheet that a regulator reads first: it states the decision of record and its actor, the honest risk verdict, CDD data coverage, top findings, screening, ownership-to-natural-persons, purpose, discrepancies, post-approval monitoring, and an index of every enclosed artifact with its SHA-256.

Why it exists. AMLR Art. 77 record-keeping + EU AI Act Art. 11/12 (a high-risk system must produce reconstructable technical documentation and logs). It inherits the ADR-0067 honesty contract: it must carry M1/M2 gap states forward. Governing ADRs: 0069 (this export), 0064 (immutability), 0067 (fail-closed), plus 0021/0070/0086/0088 for its sub-sections.

How it is structured. Rendered from a CasePackDoc dataclass via case_pack.html, in template order:

#SectionWhat it asserts (and its honest guard)
1Decision & actorTag reads "Decision of record" only if decided_by/decided_at exist, else "Recommended disposition". The actor comes from _decision_actor, which reads an explicit allowlist (officer_decision, decision_memorandum_signed, decision_second_approval_granted) — never a substring — so a SAR sar_mlro_approved/rejected event can never be misread as the case decision. For a four-eyes decision it attributes both actors: "{checker} (second approver), {maker} (maker)".
2Honest risk verdictrisk_level / tier / blurb, including the literal NOT_ASSESSED.
3CDD Data CoveragePer AMLR §2(a)-(e) domains with explicit NOT ASSESSED / GAP badges. If the knowledge graph is unavailable it renders "not computed" — never a fabricated 100%.
4Risk FindingsTop 8, with severity counts.
5Sanctions / PEP / Adverse-Media ScreeningSurfaces a "screening gap" note when a screening_gap signal is present.
6Ownership to Natural PersonsUBO rows, or the honest note "No natural-person beneficial owner is evidenced… AMLR §2(c) is materially incomplete."
7Purpose & Expected ActivityADR-0086 purpose profile.
8DiscrepanciesOpen vs resolved.
9Post-Approval Monitoring TrailADR-0088 monitoring events, screening evidence trail, alert dispositions; honest note when not-yet-approved or never-monitored.
10Enclosed Artifacts indexfilename / role / sha256 / size + the retention & tamper-evidence statement.

Determinism split: the pure core takes an injected generated_at (no datetime.now); only the outer build_case_pack touches DB/MinIO/clock.

The MLRO lens.

  1. Verify integrity offline first (see §2.7 for the exact sha256sum procedure). A mismatch means tampering or member add/drop/reorder.
  2. Read the honest-gap surfaces as the point of the pack, not blemishes. "NOT ASSESSED" on a CDD domain, a "screening gap" note, a BLOCKED SAR, or "No natural-person beneficial owner is evidenced" is the system behaving correctly — ADR-0067 forbids reporting clear when no check ran.
  3. Red flags: (a) evidence_bundles_present: false with no explanatory note, or a 100% CDD with no graph; (b) a "Decision of record" tag with a blank decided_by, or a decision attributed to a SAR sign-off actor (the allowlist is designed to prevent both); (c) a missing sar-assessment.pdf on a triggered case — its absence there is itself a red flag; (d) a case_pack_exported event whose pack_hash does not match the ZIP you hold means it is not the authentic export.

Honest caveats. PDF members are not byte-reproducible run-to-run (WeasyPrint embeds a creation timestamp — an accepted negative in ADR-0069); the guarantee is recompute-and-match over the archived bytes, determinism is asserted at the build_manifest layer (stable input → stable pack_hash), not byte-identical PDFs. Retention is a tag, not yet a lifecycle (WORM/scheduling deferred). _record_export_audit is best-effort/non-fatal — a transient audit-DB fault does not block the handoff, so a pack may occasionally exist without its export event; verify the event on the immutable trail. The §2(a)-(e) labels are an operational mapping to GraphService.get_amlr_coverage, not verbatim AMLR text.


2.2 compliance-report.pdf — the KYB risk-assessment narrative

What it is. The human-readable risk-assessment the MLRO reads and files to understand why the entity carries the risk level it does. It is a pure rendering of a fully-typed ReportData object (ReportDataBuilder.build() does all the assembly; generate_compliance_report only renders).

Why it exists. Three drivers: EBA GL/2021/02 + AMLR/6AMLD risk-based approach (ADR-0020 — the rating must be structured, documented, and a single critical dimension must not be diluted away, hence the weighted-max floor); the fail-closed honesty contract (ADR-0067 — never present a benign "LOW" when no check ran); and deterministic scoring + one-way ratchet (ADR-0089 — identical evidence → identical rating; a re-screen may raise or maintain but never silently lower a persisted baseline). EU AI Act Art. 12/13/15 also apply.

How it is structured — the rating fields.

FieldHow it is builtThe honest guard
risk_score_extract_risk_score() pulls a genuine numeric from investigation_results.Returns None, not 0.0, when absent — the fail-closed sentinel.
risk_level / risk_tier / blurb_build_risk_verdict() sources a base level, else derives from score (>=0.7 HIGH, >=0.4 MEDIUM, a 0–1 scale), then applies a severity floor mirroring ADR-0020: any critical finding floors to ≥ HIGH, any high finding to ≥ MEDIUM.With no score, no recognised base level and no critical/high finding, returns literal NOT_ASSESSED / tier "—" + "Risk could not be assessed… a compliance officer must complete the assessment." risk_tier maps LOW→SDD, MEDIUM→CDD, HIGH/CRITICAL→EDD.
findings_by_severityBuckets into critical/high/medium/low/verified.findings_raw_count preserves the pre-dedup count so the report reconciles against the immutable audit trail.
domain_ragRED if any critical/high, AMBER if any medium, else GREEN, across 4 domains (Company Registration, Directors & UBO, Financial Health, Compliance Screening).
basis_of_rating_build_basis_of_rating() — the "Basis of overall rating" sentence: names the single determinative driver (first critical, else first high, else top signal), labels others "contributing factors, not determinative" (capped 3).On NOT_ASSESSED: honest sentence naming no driver. On assessed-but-clean: returns None (line omitted) — never a fabricated driver.

The disposition verb (DECLINE/DEFER/PROCEED WITH CONDITIONS/PROCEED) comes from the same recommend_verb() the MLRO memo uses, so the two documents can never drift. The cover KPI band shows the risk-level word (not a numeric /100), confirmed sanctions/PEP counts, UBO count or a "Gap — ownership chain unresolved" cell (0 UBOs is a gap, never a clean zero), and adverse-media count.

The MLRO lens — read in this order.

  1. Cover word + Recommendation pill must agree. "Overall Risk: High / EDD" beside a "PROCEED" verb is an internal contradiction — challenge it.
  2. The "Basis of overall rating" line is your single most defensible sentence at inspection. It must name a real determinative finding that also appears in Key Findings. Absent on an elevated case, or naming a driver you can't find, is a red flag.
  3. Trust NOT_ASSESSED. If you see LOW/green on an entity with a criminal or sanctions signal in the sections below, the floor failed — do not sign.
  4. Cross-check the severity floor (a critical finding must never sit above a sub-HIGH rating — ADR-0020 anti-dilution), the "Gap" ownership cell, and findings_raw_count vs the retained count (confirms nothing was silently dropped in de-dup).

Honest caveats. The cover shows the word, not the 0–100 EBA composite (computed upstream in eba_risk_matrix.py); _derive_risk_level here uses a 0–1 scale — verify which scale a given case's persisted score is on. The ADR-0089 ratchet and temperature=0 determinism are enforced in the entity-baseline/scoring layers, not inside report_data_builder — the report faithfully renders the persisted value, so its determinism is only as good as the upstream ratchet; verify the baseline-governance path for any case where a re-screen appears to have lowered risk. The severity floor here is a presentation-layer mirror, not the authoritative EBA computation. generate_compliance_report returns b"" if WeasyPrint yields None (an empty-PDF failure mode to guard operationally). "10-section" is the docstring label; the live compliance_report.html renders exactly 13 h2 sections (confirmed 2026-07-04) — the docstring undercounts.


2.3 evidence-request.pdf — the customer EDD evidence-request letter (doc #12)

What it is. The only customer-facing artefact in the pack. It tells the customer exactly which CDD/EDD documents to supply, why (regulatory basis per item), by when (14 days), and in what form (certified true copies, ≤3 months old). It exists to close the identification/verification/BO/SoF-SoW gaps that OSINT could not resolve from public sources — and it requests only what is actually missing. Assembled as a pure function from the same ReportData as the main report, so the ask is guaranteed consistent with the file.

Why it exists. AMLR CDD/EDD: Art. 20 (purpose/nature, ownership-and-control, SoF/means), Art. 22 (identify/verify customer and directors/SMOs), Art. 34 (EDD + 5-yr window), Art. 51-52 (BOs ≥25%). Emission is risk-based and coverage-driven off AMLR Art. 28 coverage — each ask fires only where a genuine gap exists (proportionality, SDD vs EDD). Its customer-facing nature triggers the AMLD6 Art. 39 tipping-off boundary, which is why it carries only neutral regulatory vocabulary and why issuance is blocked behind the SAR-first gate. ADRs: 0071 (tipping-off + SAR-first gate), 0069 (pack copy), 0068 (honest gaps).

How it is structured. An EvidenceRequestDoc: header, neutral intro, an ordered list of EvidenceSection blocks (each with EvidenceItem rows of title + detail + basis = the Article citation), a certified-copy note, a 14-day deadline, and a closing that onboarding cannot complete until review concludes. Up to eight sections, each only when a real gap exists (keyed to AMLR Art. 28 coverage via _section_coverage; <1.0 = gap):

§SectionFires whenBasis
1Corporate Registry & Legal Statusregistry gap§2(a) / Art. 22
2Ownership & Beneficial OwnershipBO gap§2(c) / Art. 51-52
3Group Structure & Affiliationsgroup gap§2(e) / Art. 20
4Licensing & AuthorisationsEDD / high MCC tierArt. 20(1)
5Regulatory & Legal HistoryEDD tier OR adverse-media OR any critical/high signal — includes a neutral group-wide 5-year confirmation ask (ADR-0071/R3)Art. 34
6Financial Statements & Fundingno financial years / status unknownArt. 20(1)(c) SoF
7Directors & Senior Managementdirector gap§2(b) / Art. 22
8Nature & Purpose of Businesspurpose gap§2(d) / Art. 20(1)(a)(b)

If no section qualifies, _core_cdd_sections() guarantees a four-ask floor (registry/ownership/directors/purpose) — the letter is never blank.

The MLRO lens. Read it as your documented, risk-based CDD/EDD ask register. Every item's basis citation is your audit defence that each request is justified and proportionate (not a fishing expedition). Confirm proportionality to tier: an EDD case should show licensing (4), 5-year history (5), and financials (6); a low-risk case should not. The load-bearing tipping-off control: the letter must contain zero internal risk/severity/suspicion/escalation language (enforced by builder + tests + the model contract) — scan it and confirm no finding, score, or "suspicious" wording leaked in (AMLD Art. 39). The §5 group-wide confirmation ask is deliberate: it invites the customer to disclose (or demonstrably fail to disclose) any enforcement/asset measure in neutral words, creating an evidentiary record either way. Red flags: (a) the letter was issued while a criminal/enforcement/sanctions predicate is open and the SAR/STR assessment is incomplete — the customer endpoint 409-blocks this via assert_customer_contact_allowed, no officer override; if you see an issued letter on such a file, verify the gate fired; (b) missing certified-copy freshness/translation clause; (c) a blank/near-blank letter (the core-CDD fallback should prevent it).

Do not confuse the pack copy with proof of customer contact. The pack's embedded copy is intentionally NOT SAR-gated — it is routed through the builder (regulator/officer use), not the issuance endpoint. Only the letter sent via assert_customer_contact_allowed is evidence the customer was contacted.

Honest caveats. The basis Article strings are hard-coded literals in the builder, not resolved from the Lex corpus — verify they track current AMLR text if articles renumber. When the graph is unavailable the builder silently uses report-level fallback heuristics, which can differ from graph-driven emission — spot-check that a graph outage doesn't under-/over-request. The neutral-language guarantee is enforced by tests (test_tipping_off.py) — verify the assertion actually scans detail/intro/basis for risk tokens. The SAR-first gate protects only the customer-issuance endpoint — confirm no other path renders and sends this letter without passing the gate.


2.4 mlro-memo.pdf — the internal MLRO decision memorandum (doc #13)

What it is. The internal, legally-privileged escalation memo that puts the automated assessment in front of a human MLRO/Legal for a documented onboarding decision. It states a recommendation (DECLINE/DEFER/PROCEED WITH CONDITIONS/PROCEED), the verified factors driving it, where the machine was corrected by a human/deterministic overlay, the decision requested, and sequenced next steps. It is deliberately a recommendation to a human, not an autonomous decision — the Art. 14 human-oversight artifact.

Why it exists. AMLD6 MLRO role + AMLR documented risk-based CDD/EDD decision (5-yr trail) + EU AI Act Art. 14 human oversight. Two PR #166 governance hardenings are load-bearing: (1) the SAR/STR HARD STOP — when a criminal-investigation/enforcement signal is present, ACTION 1 forces the FIU-reportability assessment (AMLR Art. 69 + national FIU rules) before any customer contact, sequencing the EDD request behind it so no outward step tips off (AMLD Art. 39) — implementing ADR-0071's SAR-first gate; (2) Art. 69 consistency — the recommendation verb comes from the single shared recommend_verb() and the classification is the canonical EBA level (_canonical_risk_classification), so the memo can never contradict the master dossier. Verification states honour ADR-0057 (min-2-source) and ADR-0082 (deterministic corroboration overlay, named as an engine action, never an MLRO judgement).

How it is structured (build_mlro_memo_doc assembles MlroMemoDoc; generate_mlro_memo renders it):

  1. Classification banner — default "CONFIDENTIAL — LEGALLY PRIVILEGED".
  2. Header block — Prepared by · Subject entity (+reg no) · Risk classification (canonical EBA level · CDD/SDD/EDD tier) · CDD coverage % · Reference.
  3. Recommendation — verdict phrase from recommend_verb(n_crit, n_high, has_medium) + recommendation_basis (states the escalation-vs-aggregate relationship explicitly, e.g. "escalation recommended on the strength of the factor(s), not a higher aggregate tier").
  4. Highest Verified Factor — the single lead blocker: title, severity, description, source, regulatory basis, verification_state, and an honest overlay-note when ADR-0082 corroboration promoted it to CONFIRMED over a different raw state.
  5. Supporting Verified Factors — remaining ranked factors, same shape.
  6. Where the Automated Assessment Was AdjustedMemoCorrection list (severity re-labels, sanctions-mislabel, BO-coverage overstatement) each with issue / automated_output / correction / direction.
  7. Decision Requested + Next steps — when _criminal_enforcement_finding fires (category or token list: eppo/olaf/prosecutor/asset freeze/money laundering/…), "ACTION 1 — owner: MLRO · status: OPEN · HARD STOP" is inserted first and the EDD request is re-worded as a routine, no-suspicion CDD request sequenced after it.
  8. Items Requiring Re-confirmation — every factor stamped REQUIRES RE-CONFIRMATION.
  9. Footer — classification + generated_at + reference.

The MLRO lens — read top-down as your own escalation you must sign. First confirm the header trio agrees with the rest of the pack: the risk classification must not drift from the dossier (enforced in code). The recommendation verb is finding-driven — a single high-severity factor can warrant DECLINE/DEFER even at a MEDIUM aggregate; read the basis line to confirm the escalation logic, not just the tier. Treat verification_state as the trust dial: CONFIRMED with an ADR-0082 overlay-note means "corroborated across ≥2 independent publishers with a URL" — legitimate, but the note lets you tell it from a natively-CONFIRMED fact; REQUIRES RE-CONFIRMATION entries are your homework before you file anything. The Corrections section is your inspection defence — it shows the AI output was actively adjusted (presence≠evidence, sanctions-label conflation, BO-coverage overstatement). Red flags: an ACTION 1 HARD STOP means a criminal/enforcement predicate exists — complete and document the SAR/STR assessment before any customer contact; an evidence request already issued "in parallel" with a criminal signal is a tipping-off breach (AMLD Art. 39). A sanctions-label correction means a "sanctions" alert was actually adverse-media — it must be formally re-screened and closed. "No natural-person beneficial owner evidenced" is a hard EDD blocker, not a formality.

Honest caveats. The neutral standing-EDD ask (R3) lives in the sibling evidence-request builder, not inside the memo; the memo's own tipping-off-safe wording is the next-step that re-sequences EDD behind the HARD STOP. The assembly function is build_mlro_memo_doc (the task's "generate_mlro_memo" is only the render function). Criminal-enforcement detection is a deterministic scan whose token list (_CRIMINAL_ENFORCEMENT_TOKENS, compliance_docs_builder.py:903) is English-only (eppo / olaf / prosecutor / money laundering / criminal investigation / indict / arrest / raid) — a purely non-English enforcement disclosure would not match this memo-side scan; native-language recall lives in the OSINT layer (ADR-0077), not here. prepared_by defaults to "Compliance Analyst", but the endpoint does supply the authenticated user (user.display_name or user.user_id, compliance_docs.py:135) — the default applies only if both are empty, so oversight attribution is genuine. The "CONFIDENTIAL — LEGALLY PRIVILEGED" banner (rendered on the SAR assessment, sar_assessment.html:18) is a template label, not an enforced access control; the real gate is the CASE_READ permission on the export endpoint (ADR-0069/0074).


2.5 sar-assessment.pdf — the SAR/STR internal assessment & determination

What it is. The regulator-ready artifact that turns the Step-3 reportability tripwire"does this case warrant a SAR/STR to the FIU?" — into its own auditable, always-enclosed document. It records the MLRO's documented "should we file?" judgment, the underlying facts with their verification state, the tipping-off boundary, and — printed on the document's face, top and bottom — the fail-closed gate governing whether customer contact (Step 6) is unlocked or BLOCKED. A pure render of a serialized SAR row + the case ReportData: no free-text narration, every field traceable to a persisted value. Classification fixed to "CONFIDENTIAL — LEGALLY PRIVILEGED · RESTRICTED ACCESS".

Why it exists. ADR-0071 (SAR/STR lifecycle draft→pending_mlro→approved→submitted→acknowledged, no-skip; MLRO four-eyes on filing; AMLD Art. 39 tipping-off) + ADR-0067 (fail-closed). Anchors on the banner: AMLR Art. 69 (FIU reporting), AMLD6 Art. 39 (tipping-off), EU AI Act Art. 14 (a natural person — the MLRO — owns the determination the AI surfaced), FATF Rec. 20/21. It exists because a high-risk pack must be structurally incapable of silently dropping the SAR question: enclosure is forced whenever a criminal/enforcement/sanctions/asset-freeze trigger is present, even with no assessment yet done.

How it is structured. A legal banner + a "STOP — Read First" gate box lead, before eight lettered sections (build_sar_assessment_docsar_assessment.html):

§SectionContent
AIdentity & Accesssubject, reg no, case opened, assessor, access-restricted-to, doc reference
BTriggertrigger_categories (humanised) + trigger_summary
CUnderlying Facts & Verification Statetable of every finding: claim, source, Corroborated Yes/No, and a CONFIRMED / REQUIRES RE-CONFIRMATION / NOT VERIFIED badge — computed by the same _memo_verification_assessment engine the MLRO memo and source appendix use, so the three can never disagree
DSuspicionsuspected_offence, grounds_for_suspicion
EReportability Determinationband REQUIRED / NOT_REQUIRED / FURTHER_INFO_NEEDED → headline (default "NOT YET DETERMINED"); reasoning, FIU reference, filed-at
FTipping-offAMLD Art. 39 considerations + boundary-confirmed Yes/No/—
GOnboarding Interactiondecline_sar_filed / decline_no_sar / defer_edd / other — with the standing caveat that an onboarding decision is independent of the filing duty
HSign-offMLRO, assessment-by, completed-at, legal reviewer

The gate string is single-sourced: sar_assessment_complete(sar)_SAR_GATE_UNLOCKED ("COMPLETE & SIGNED → Step 6 customer contact UNLOCKED") or _SAR_GATE_BLOCKED ("INCOMPLETE → customer contact REMAINS BLOCKED"). Two unlock paths: (1) a signed structured record_assessment — determination present, assessment_completed_at set, and not further_info_needed (both REQUIRED and NOT_REQUIRED unlock); or (2) the lifecycle reached an MLRO-decided state (approved/rejected/submitted/acknowledged). draft, pending_mlro, further_info_needed, and any unsigned/absent determination stay BLOCKED.

The MLRO lens. Read the STOP box first — it is the tipping-off tripwire. BLOCKED means: do not contact the customer, request documents, or communicate any decline until the determination is signed. This same predicate is enforced at runtime — the SAR-first gate 409-blocks REJECT/FOLLOW_UP/evidence-request/portal/chat on a criminal predicate, no officer override — and both the paper and the runtime call the same sar_assessment_complete(), so they cannot diverge. Section E is the heart: confirm a real determination and reasoning exist, not the honest "NOT YET DETERMINED" default. Section C is your credibility check: a determination resting on facts still marked REQUIRES RE-CONFIRMATION or NOT VERIFIED must be re-checked against primary sources (presence ≠ evidence). Cross-read E and G: a decline does not discharge a filing obligation; REQUIRED can co-exist with a proceeding relationship. Red flags: gate UNLOCKED with no assessment_completed_at or no legal reviewer; determination REQUIRED but the lifecycle never reached submitted/acknowledged; trigger categories present but Section D suspicion blank. Audit defence: the whole lifecycle is on the immutable trail (ADR-0064) with distinct event types per transition, four-eyes on mlro_approve/mlro_reject (raiser ≠ MLRO, assert_distinct_approver), and the artifact is force-enclosed even when BLOCKED — so you can show a regulator the question was never suppressed.

Honest caveats. The FIU export (goAML draft) can be skipped at raise time when no country profile exists (e.g. EE) — the SAR is still raisable and the assessment still enclosed, but a linked goAML draft may be absent; verify goaml_draft_id when the pack claims a filing was prepared. load_sar_assessment degrades to None on any error, and callers treat None as BLOCKED when a trigger is present — deliberate fail-closed, so a transient DB/RLS error and a genuinely-absent assessment are indistinguishable on the face (both correctly read BLOCKED — no false "clear" — but the reason is not surfaced). record_assessment (the documented judgment) is decoupled from the lifecycle and does not itself require a second actor — the four-eyes gate lives only on mlro_approve/mlro_reject (the filing decision); confirm the filing four-eyes trail in audit_events, not the assessment doc alone.


2.6 source-appendix.pdf — the primary-source citation & provenance record (doc #14)

What it is. The tamper-evident provenance ledger of the pack. For every material claim (all risk signals + any verified fact that carries a source) it records the claim text, cited source(s) with type/URL, per-source collection time and a content hash, a three-state verification verdict, and any severity correction. It lets an examiner trace each assertion back to its origin and see, honestly, which claims are not yet confirmed.

Why it exists. EU AI Act Art. 12 (traceability — each output traceable to its inputs) and Art. 13 (transparency — disclose what was and was not assessed). It operationalises the MEMORY principle "may ADD scrutiny, never suppress a signal, and any scrutiny-reducing output must be evidence-traceable." AMLR Art. 28 (risk-based CDD across the group) + Art. 18 (EDD) underpin the network adverse-recall gap finding. ADRs: 0069, 0082 (corroboration overlay — appendix and memo must never diverge on verification state), 0057 (min-2-source), PR #171.

How it is structured (build_source_appendix_doc):

  1. Cover note — auto-derived AppendixCoverNote: verification_date, total_claims, confirmed/recheck/not_verified counts, open_items (every non-CONFIRMED claim), and a one-line provenance_summary whose per-state counts come from the same summary dict as the closing table — they can never disagree.
  2. Legend — explains the two provenance fields honestly (see caveats).
  3. Summary table — counts in exactly the three canonical VERIFICATION_STATES (CONFIRMED, NOT VERIFIED, REQUIRES RE-CONFIRMATION).
  4. Claim-by-claim evidence — one CitationEntry per de-duplicated material claim: claim text; verification badge; CitationSource rows (source_type upper-cased · name · URL · Collected · Content hash); optional Correction line.
  5. Items to re-confirm before filing — the REQUIRES RE-CONFIRMATION claims.

Verification verdict (_appendix_verification): NOT VERIFIED if the resolution note says unverifiable/could-not/contradict/not-found; CONFIRMED if a citable URL AND ≥2 independent publishers (ADR-0082 overlay + ADR-0057 bar via _distinct_publisher_count, which collapses same-publisher variants); REQUIRES RE-CONFIRMATION if severity was re-labelled, or the category is in the recheck set, or a source exists but no URL; NOT VERIFIED if uncited.

The MLRO lens. Read the cover note firstprovenance_summary and open_items tell you at a glance how many claims are still not confirmed at filing (an all-CONFIRMED pack should never invent a gap; a pack with open items must list them here). Cross-check against the closing table — same dict, so any divergence is a red flag of tampering or a rendering bug. Verify the badges are consistent with the MLRO memo and SAR assessment — all three use the same engine, so a claim CONFIRMED here but "requires re-confirmation" in the memo is a defect (ADR-0082/Codex P2). Scrutinise CONFIRMED-by-overlay claims: the overlay upgrades only with a URL AND ≥2 independent publishers — treat single-source escalations as still open. The decisive red flag to hunt: a CLEAR sanctions/PEP screen being read as "no adverse media." The system surfaces this itself via the MEDIUM adverse_media_recall_gap finding — confirm it appears whenever there are related entities or named persons (directors/UBOs), because per-entity/per-director adverse-media recall (tier B, issue #170) is NOT run — the interim control is a manual MLRO addendum. For an inspection, this document is what proves each assertion is input-traceable (Art. 12) and that the system honestly reported what it did not assess (Art. 13), with reproducible content hashes.

Honest caveats. content_hash is NOT a fingerprint of remote source content for investigation-consulted OSINT sources — those retain no raw snapshot, so the hash is a reproducible SHA-256 over the recorded citation (claim|source|verification-state). It proves the record was not altered and regenerates identically, but it does not prove the remote page still says what was cited. Only evidence-bundle/snapshotted sources carry a true content hash + real capture time. collected_at can be "—" when no capture time is known. The network adverse-recall gap is tier A only — the group and persons are listed and flagged as not-adverse-media-searched, but the actual per-entity/per-director recall (tier B) is not implemented (issue #170); verify the manual addendum was done. The gap finding returns None (absent) when there are no related entities and no persons — confirm that absence is genuinely "nothing to recall against," not a data-load failure. URL detection is best-effort.


2.7 audit-trail.json + manifest.json — the immutable log and the tamper-evidence seal

What they are. audit-trail.json is a verbatim, time-ordered export of the case's audit_events rows — the DB-enforced immutable event log (ADR-0064). manifest.json is the tamper-evidence seal — it lists every member's SHA-256 + size and computes the Merkle-style pack_hash root. The trail is the content (what the system did, and who did it); the manifest is the proof that no page was altered after sealing.

Why they exist. AMLR Art. 77 (5-yr record-keeping) + EU AI Act Art. 12 (logging/traceability). ADR-0064 makes the trail tamper-evident at the database layer — a BEFORE UPDATE OR DELETE trigger audit_events_immutable() fires for all roles incl. superuser, REVOKE UPDATE, DELETE from trustrelay_app, and the FK is RESTRICT so a case with history cannot be hard-deleted. The manifest/pack_hash answers a second question — "is this the unaltered pack you exported?" — and the export is written back as a case_pack_exported event carrying the pack_hash for GDPR Art. 30 / AMLR traceability.

How they are structured. content_sha256(bytes)sha256:<hex> is the single primitive. audit-trail.json = {case_id, workflow_id, count, events: [...]} — each event {event_type, details, timestamp} exactly as AuditService.get_events() returns, ordered by created_at ascending. manifest.json carries schema: "trust-relay/case-pack-manifest/1", case/workflow/tenant ids, generated_at, retention_class: "AMLR-5yr", evidence_bundles_present, member_count, members[] (each {filename, role, description, sha256, size_bytes}, sorted by filename), pack_hash, and notes[] (the honest-gap lines). manifest.json is written into the ZIP but is never a member of itself.

Real event types on the trail include investigation_completed (per iteration, with risk_score), risk_escalated (from_score/to_score, from_tier/to_tier, escalator, checkpoint, subject_attribution_basis), sanctions_fp_tier1_evaluated (FP-suppression evidence trail), the decision_* family, the sar_* lifecycle family, ubo_*, screening_*, cross_tenant_access (super-admin impersonation, ADR-0081), and finally case_pack_exported.

The MLRO lens — verify tamper-evidence offline with only sha256sum and a text editor:

  1. Unzip.
  2. For each manifest.members entry, run sha256sum <filename> and confirm it equals the recorded sha256. (This is exactly what the system's own _verify_pack_integrity() does at seal time — recompute every member hash, assert the member set and every digest match, else CasePackIntegrityError and the pack is never shipped.)
  3. Recompute the root: sort "<filename>:sha256:<hex>" lines, newline-join, hash — must equal pack_hash.
  4. Cross-check that pack_hash against the case_pack_exported event's details.pack_hash on the firm's live immutable trail — this binds the paper artifact to a WHO/WHEN export record the firm cannot alter.

In audit-trail.json: confirm count matches the events length; walk the timestamps for a coherent story — investigation_completedrisk_escalated (does the escalation carry subject_attribution_basis? a CRITICAL with null basis is a red flag) → decision_* → any sar_*. Red flags: a high-risk case with no risk_escalated or no sar_* events; a manifest notes[] line saying a SAR is enclosed BLOCKED or bundles are absent (these are honest-absence disclosures, not omissions — ADR-0067 forbids silently dropping them); member_count ≠ members length; a cross_tenant_access event you cannot account for. How it defends an inspection: the DB-layer immutability (trigger fires for superuser too, FK RESTRICT blocks deletion) means "we could have edited the log" is not a live objection — a correction would require a deliberate, itself-traceable superuser action to drop the trigger.

Honest caveats. The trigger + REVOKE + FK RESTRICT are the deployment-level guarantee; in CI/local the REVOKE is guarded for a missing role — verify the trigger and REVOKE are actually present in the target Postgres before relying on "the log cannot be edited." get_events scopes to case_id (and, when tenant_id is passed, an RLS session) — the trail is per-case, not the whole tenant log; a case-less action (e.g. a GDPR DSR event with case_id NULL) will not appear. _record_export_audit is best-effort/non-fatal — a missing case_pack_exported anchor is a known non-fatal path, not proof of tampering (verify against server logs). pack_hash is a Merkle-style root (one hash over the sorted concatenation), not a full Merkle tree — it detects any change but does not localise which member changed (the per-member sha256 list does that). The manifest is not cryptographically signed (no PKI/HSM) — tamper-evidence rests on SHA-256 + the independent immutable case_pack_exported anchor; verify whether a signing/notarisation layer is required by your regulator. audit-trail.json exports details as raw JSONB — the pack is a full-PII dossier by design (that is why the export is itself audited).


3. The lifecycle of an investigation

The pack is a snapshot of a deterministic Temporal workflow (ComplianceCaseWorkflow.run) whose defining shape (ADR-0018) is that OSINT runs before the customer is ever asked for a document — the system only requests what public registries cannot already supply. The lifecycle is the substrate that produces every risk score, finding, and audit event the pack later reads.

The state machine (named states)

DRAFT → CREATED → PRE_INVESTIGATION → DOCUMENT_GAP_ANALYSIS → REQUIREMENTS_REVIEW
→ AWAITING_DOCUMENTS → DOCUMENTS_RECEIVED → PROCESSING → VALIDATING_DOCUMENTS
→ GENERATING_TASKS → REVIEW_PENDING
→ [PENDING_SECOND_APPROVAL (maker-checker, ADR-0070)]
→ { APPROVED | APPROVED_WITH_RESTRICTIONS | REJECTED | ESCALATED
| FOLLOW_UP_REQUIRED → AWAITING_DOCUMENTS (loop, iteration++) | FAILED }
PhaseStateWhat runsRisk checkpoint
Pre-investigationCREATEDcase row + workflow start; case_created audit event carries origin (onboarding vs AMLA review, ADR-0083)
PRE_INVESTIGATIONcountry registry agents (KBO/NBB/INPI/ARES) + official-PDF download to MinIO; Docling conversion spawned async (non-blocking); run_osint_investigation; dedup; classify_mcc (authoritative industry finding, supersedes OSINT vertical); reconcile_nace_vs_mcc; reinject_established_findings (ADR-0089)Checkpoint 1 reassess_risk post_osint (floored EBA composite carried back)
run_verification_checks (directors capped at 60, sanctions/PEP); PEPPOL (BE only); confidence scoringCheckpoint 2 reassess_risk post_verification
DOCUMENT_GAP_ANALYSIScross_reference_evidence (corroboration matrix + discrepancies by severity); analyze_document_gaps (coverage vs template, customer-required docs, automation_tier); upsert_entity_baseline (floored composite + risk level, monotonic ratchet); populate_goaml_entities; then score_investigation_quality (LLM-judge, flags <0.3 critical/<0.5 low), generate_follow_up_tasks, populate_knowledge_graph
Human gateREQUIREMENTS_REVIEWautomation_tier (computed upward-only, fail-safe to FULL_REVIEW): AUTONOMOUS auto-approves; ASSISTED waits 15 min then auto-releases; FULL_REVIEW waits indefinitely for signal_requirements_approved (officer may add/remove/modify docs)
Iteration loopAWAITING_DOCUMENTSwaits for signal_documents_submitted with max_timeline_days (default 60) timeout → FAILED
PROCESSINGprocess_documents (Docling → markdown)
VALIDATING_DOCUMENTSvalidate_documents — a deterministic adverse-content scan (ADR-0075) reads extracted markdown for criminal/AML/freeze/SoF + payment-account-outside-licence. If a required requirement has no valid doc → auto re-upload tasks + bounce back to AWAITING_DOCUMENTS (validation_bounce_back, never reaches the officer). On pass: extract_document_data (UBOs), refresh_synthesis_after_documentsCheckpoint 3 reassess_risk post_document_adverseuploaded docs can escalate to CRITICAL
REVIEW_PENDINGpersists state, waits for signal_officer_decision (state-guarded — a decision signalled in any other state is ignored + audited)
DecisionterminalAPPROVE / APPROVE_WITH_RESTRICTIONS (+MerchantRestrictions + reason) / REJECT / ESCALATE / FOLLOW_UP (→ loops, iteration++). Each writes an attributed officer_decision (officer_id/name), consolidates episodic memory. Max iterations → FAILED

The decision gates are enforced in the API layer (app/api/case_decisions.py) before the signal is sent — see §4. They route high-risk approvals to PENDING_SECOND_APPROVAL and 409-block on open-UBO (ADR-0059), dissolved-entity (ADR-0065), the SAR-first predicate (ADR-0071), and expired-ID (ADR-0087) conditions.

Where each pack document is produced along the lifecycle

  • compliance-report.pdf / case-pack.pdf — render on-demand from the investigation_results + baseline persisted through checkpoints 1–3 and the decision.
  • evidence-request.pdf — the ask is computed at DOCUMENT_GAP_ANALYSIS; the customer-facing copy is gated at the AWAITING_DOCUMENTS transition (SAR-first gate).
  • mlro-memo.pdf / sar-assessment.pdf — produced around REVIEW_PENDING/decision; the SAR assessment is force-enclosed whenever a trigger signal exists at any point.
  • source-appendix.pdf — renders the full provenance of every signal accumulated across all phases.
  • audit-trail.json — the running log of every transition and checkpoint; sealed into the manifest at export.

The MLRO lens. Read the case as a chain of custody, not a verdict. Confirm the investigation-first ordering held (OSINT/registry evidence must pre-date the portal opening). Check the automation_tier and why: AUTONOMOUS/ASSISTED bypass or time-box human review, so for any PEP/adverse/HIGH-EBA case you expect FULL_REVIEW — a HIGH-risk case at AUTONOMOUS is a red flag. Trace the three checkpoints: the authoritative rating is the floored EBA composite, not the pre-floor LLM synthesis (raw_synthesis_risk_score is preserved but under-states — a criminal-investigation case floors to 90/CRITICAL regardless of a softer synthesis). Verify checkpoint 3 fired if documents were uploaded — documents can raise risk; a case where an adverse disclosure did not escalate needs scrutiny. On the decision, confirm attribution and gate compliance. Red flags: an officer_decision recorded while status ≠ REVIEW_PENDING (should have been ignored); a validation_bounce_back loop that quietly consumed iterations; directors_list truncated at the 60 cap on a large board (silent under-screening); max_iterations_reached FAILED masking an unresolved case.

Honest caveats. The gates are enforced in case_decisions.py/customer_contact_gate/maker_checker, not inside the workflow — the workflow's signal_officer_decision only records a decision already cleared by the API. PENDING_SECOND_APPROVAL is driven by the API/maker_checker, not an explicit workflow branch. The risk-reassessment fires at four points — post_osint (checkpoint 1), post_verification (checkpoint 2), and post_network + post_document_adverse (compliance_case.py:1891 and :1941); the doc's "checkpoint 3" groups the last two and both fire — a network reassessment, then the post-document adverse-content scan. max_timeline_days handling is confirmed simplified: remaining_days = max_timeline_days (compliance_case.py:1431) does not subtract elapsed time (an in-code "Tier 2 calculates from start" TODO), so an SLA countdown can over-state the time remaining. The subject-criminal floor is confirmed 90 / CRITICAL (ADR-0082), applied in the reassess_risk / activities escalation path.


4. The four-eyes principle

A distinct-actor segregation-of-duties control that stops a single officer from unilaterally letting a high-risk relationship through or overriding a fail-closed gate. It is a baseline AMLR internal-controls / EBA governance expectation, and it is reused as the MLRO sign-off gate on SAR/STR filing.

When it triggers

The pure predicate requires_four_eyes(FourEyesContext) → FourEyesVerdict returns required=False unless the decision is an approval and at least one trigger holds. Each trigger contributes a named reason (which becomes your audit evidence of which control fired):

Trigger reasonFires whenADR
high_risk_approvalrisk_level ∈ {high, critical}0070
edd_tier_approvaldd_tier ∈ {edd, enhanced, enhanced_due_diligence}0070
ubo_discrepancy_overrideapprove over an open UBO discrepancy0059
dissolved_entity_overrideapprove a dissolved entity0065
purpose_requirements_overrideapprove with missing purpose/SoF/SoW0086
expired_document_overrideaccept-expired-document on a high-risk case0087
risk_divergence_overrideaudited entity-risk downgrade0089
context_unavailablethe case row can't be read — fail-closed, gated, never cleared as low-risk0070

Reject / escalate / follow-up and low-risk approvals are never gated — no friction where SoD is not required. risk_level/dd_tier are read honestly from additional_data ('' when absent, never fabricated).

The PENDING_SECOND_APPROVAL detour

On a triggering decision the deciding officer becomes the maker; the decision does not fire. MakerCheckerService.create_pending() writes a row to the append-only, RLS/tenant-scoped pending_decision_approvals table (migration 067) capturing maker_user_id/name, intended_decision, the verbatim decision_payload (replayed later), justification, and trigger_reasons; sets cases.status = PENDING_SECOND_APPROVAL; and writes audit_events type decision_pending_second_approval. It does not fire officer_decision.

resolve_pending() loads the pending row, then runs assert_distinct_approver(maker, checker) before any side effect — a blank/missing checker raises AmbiguousApproverError, checker == maker raises SelfApprovalError (both → HTTP 403, fail-closed). On approve it re-validates the case is still PENDING_SECOND_APPROVAL (else supersedes and raises StalePendingApprovalError → 409, so the maker's decision can't be auto-applied to a new iteration the checker never saw), replays the stored DecisionRequest under the maker's identity, marks the row approved, and writes decision_second_approval_granted carrying both ids, both names, decision, justification, checker note, trigger reasons, timestamp. Reject returns the case to REVIEW_PENDING and writes decision_second_approval_rejected.

Who may be the second approver

GateChecker requirement
Decision four-eyesDistinct-actor only, gated by Permission.CASE_DECIDE. Role-based checking was explicitly deferred (ADR-0070 Alternative 2) — the checker is a different officer, not necessarily senior/MLRO.
SAR filing four-eyesThe checker IS role-gated to the MLROSARService.mlro_approve/mlro_reject reuse assert_distinct_approver so the MLRO must be a different actor from the raiser (pending_mlro → approved/rejected).

The SAR-first MLRO gate

Independently of maker-checker, a REJECT or FOLLOW_UP on a criminal/enforcement predicate is 409-blocked until a completed, signed MLRO SAR/STR assessment exists — no officer override (AMLD Art. 39 tipping-off defence). This is the same sar_assessment_complete() predicate printed on the SAR assessment's face (§2.5), so paper and runtime cannot diverge.

The MLRO lens. Read the two-person trail from audit_events: decision_pending_second_approval (who decided, and the trigger_reasons list — your evidence of which control fired) followed by decision_second_approval_granted (who authorized, both names/ids, timestamp, note). For a defensible file you want the maker ≠ checker pairing visible and the trigger reasons to match the tier. Red flags: (1) the decision checker only needs the same CASE_DECIDE permission as any officer — a two-junior-officer pairing is technically compliant but weak SoD; verify your tenant's role assignments make it meaningful (contrast SAR filing, which is MLRO-gated). (2) The gate is behind settings.maker_checker_enabled (default on) — confirm it is on in production; when off, high-risk approvals fire single-officer with only a legacy override entry. (3) The signal is replayed under the maker's identity, so decided_by on the case is the maker — the authorizing checker is only in audit_events/the pending row; make sure your case-pack surfaces both (the _decision_actor allowlist in §2.1 does render "{checker} (second approver), {maker} (maker)"). (4) A decision_second_approval_superseded event is the healthy outcome when a follow-up moved the case on — it proves the checker could not rubber-stamp stale evidence. (5) context_unavailable gating means an unreadable case is treated as high-risk — expect occasional gated approvals that are infra, not risk.

How it defends an inspection. Self-approval is structurally impossible (the guard runs before any side effect); the override of every fail-closed gate is itself forced through a second pair of eyes; and the complete who/why/when sits on an append-only, tenant-isolated, 5-year-retained audit spine.

Honest caveats. The decision-stage checker is distinct-actor only (not senior-role), gated by CASE_DECIDE on both decision endpoints; the SAR-filing checker is confirmed role-gated — the mlro_approve route requires Permission.SAR_APPROVE (sar.py:157). The decision memo does surface both actors — it preserves the full maker→signer chain via drafted_by_* fields, distinct from the signer (decision_memorandum_service.py:121,144). HIGH_RISK_LEVELS / EDD_TIERS are confirmed module-level frozensets (maker_checker.py:36,38) — not runtime tenant-configurable. The gate depends on additional_data.risk_level being populated at decision time — a case whose risk was never persisted there reads '' and won't trigger high_risk_approval on its own (EDD/override/context_unavailable paths still catch many such cases); confirm your deployment's risk-persistence path.


5. How this defends an audit — and where the honest gaps are

Regulatory coverage map

ObligationHow the pack satisfies itArtifact / mechanism
AI Act Art. 11 (technical documentation)The case-pack.pdf cover + evidence bundles reconstruct the high-risk system's assessment of one case.case-pack.pdf, evidence-bundles/*.json
AI Act Art. 12 (logging/traceability)Verbatim immutable audit_events + per-claim source provenance with reproducible content hashes.audit-trail.json, source-appendix.pdf
AI Act Art. 13 (transparency)The system discloses what it did and did not assess — NOT_ASSESSED badges, screening-gap notes, the adverse-media recall gap.case-pack.pdf §3/§5, source-appendix.pdf
AI Act Art. 14 (human oversight)The MLRO memo is a recommendation a human signs; four-eyes and the REQUIREMENTS_REVIEW/REVIEW_PENDING gates put a natural person in the loop.mlro-memo.pdf, sar-assessment.pdf, maker-checker
AI Act Art. 15 (accuracy)Deterministic state machine + floored EBA re-scoring + one-way risk ratchet (ADR-0089).lifecycle, compliance-report.pdf
AMLR Art. 20/22/34/51-52 (CDD/EDD/BO)Risk-based, citation-bearing evidence-request; ownership-to-natural-persons section with honest gaps.evidence-request.pdf, case-pack.pdf §6
AMLR Art. 69 (FIU reporting)The SAR/STR assessment turns the reportability question into an always-enclosed, signed artifact.sar-assessment.pdf
AMLR Art. 77 / 5-yr retentionretention_class = "AMLR-5yr" stamp + immutable trail + FK RESTRICT preventing deletion.manifest.json, audit-trail.json
AMLD6 Art. 39 (tipping-off)Neutral-language customer letter + the no-override SAR-first gate on customer contact.evidence-request.pdf, customer-contact gate
GDPR Art. 30 / Art. 22The full-PII export is itself audited (WHO/WHEN/pack_hash); a human, not the AI, owns the decision.case_pack_exported event, mlro-memo.pdf
FATF Rec. 20/21Mandatory-suspicion reporting + tipping-off confidentiality, on a tamper-evident trail.sar-assessment.pdf, manifest.json

Be honest about the known gaps

These are deliberately disclosed — the system's own doctrine is that a stated gap is safer than a false "clear":

  1. Retention is a tag, not yet a lifecycle. The pack is stamped AMLR-5yr, but enforced WORM storage / retention scheduling is deferred (ADR-0069; M5/M8 infra). You have the label, not yet the immutable-storage lifecycle.
  2. The manifest is not cryptographically signed. Tamper-evidence rests on SHA-256 + the independent immutable case_pack_exported anchor — not on a PKI/HSM signature or notarisation. Verify whether your regulator requires a signing layer.
  3. PDF members are not byte-reproducible run-to-run (WeasyPrint timestamp). The guarantee is recompute-and-match over the archived bytes, not identical pack_hash across two exports of the same case.
  4. Verified-list screening is stubbed. Real sanctions/PEP/adverse-media verified-list screening (kyc_screening.py) and real identity verification (eID Easy/itsme/eIDAS, eid_easy_service.py) are mock/stub-gated — they raise NotImplementedError when their *_MOCK_MODE flag is false (never silently faked). OSINT retrieval is real; the verified-list client is not. Live company-status monitoring is a stub but fail-closed (returns an indeterminate WARNING, never a benign no-change).
  5. Per-entity/per-director adverse-media recall (tier B) is not implemented (issue #170). The source appendix lists the group and persons and flags them as not-searched (tier A), but the actual recall is a manual MLRO addendum — verify it was done.
  6. SSO/SCIM federation is design-only (ADR-0076, Proposed); ISO 27001/42001/27701 are targets, not certifications; data-residency controls are not yet enforced. These are the AI-Act conformity record's own declared known-gaps (ADR-0072).
  7. _record_export_audit is best-effort/non-fatal — a pack may occasionally ship without its case_pack_exported anchor row. A missing anchor is a known non-fatal path, not proof of tampering — verify against server logs.
  8. The DB-layer immutability guarantee is deployment-dependent. The trigger + REVOKE + FK RESTRICT are the production guarantee; in CI/local the REVOKE is guarded for a missing role — verify the trigger and REVOKE are present in your target Postgres before relying on "the log cannot be edited."

Bottom line for the inspecting MLRO. The pack is built to make the right things structurally impossible to omit: it cannot silently drop the SAR question on a triggered case, cannot report "clear" where no check ran, cannot render a decision without attributing its actor(s), cannot ship a ZIP that fails its own manifest, and cannot let a single officer override a fail-closed gate. The residual risk is not in what it hides — it hides nothing — but in the maturity of the surrounding infrastructure (WORM retention, PKI signing, verified-list screening clients, tier-B recall). Those are named here so you can compensate with policy and manual controls until they land.