ADR-0086: Expected-Activity & Purpose Baseline Activation (Completing ADR-0062)
Date: 2026-07-03 Status: Proposed Deciders: Adrian (Soft4U), Claude Fable 5 (AMLA remediation design session)
Decision context:
- Latency: one indexed single-row read (
purpose_profilesby case_id) added to the decision path, the memo builder, and the case-pack builder — sub-millisecond against local PostgreSQL, not measured because it is one PK-adjacent lookup on surfaces that already perform dozens of reads. Theprofile_deviationmonitoring check adds one profile read + one financials read per monitored entity per cycle, inside an already-batch Temporal activity. - Dependency surface: zero new packages. One new RLS table (next sequential Alembic revision),
one new service (
purpose_profile_service.py), one new API pair (GET/PUT /api/cases/{id}/purpose-profile), portal question wiring, one new EBA factor, one new monitoring check type. Reusestrustrelay_models.purpose_profile(ADR-0062),missing_purpose_requirements(currently dead),financial_analysis(ADR-0048),MakerCheckerService(ADR-0070). - Debuggability: a blocked approval returns a 409 with
error: missing_purpose_requirementsand the explicit missing-attribute list (same shape as the ADR-0059open_ubo_discrepancyand ADR-0065dissolved_entity409s incase_decisions.py); a deviation alert carries the declared-vs-observed values in its payload; a profile-less monitored entity yields an indeterminate WARNING that names the absent profile, never a silent skip. - Reversibility: additive throughout — drop the gate wiring and the check registration and the
prior behaviour returns; the table and migration can stay inert. No
CaseStatuschange, no Temporal workflow signature change. Estimated < 1 day to revert by branch. - Blast radius:
case_decisions.pyapproval path (new gate, same pattern as three existing gates),decision_memorandum_service.py§Business Rationale (read source swap with fallback),case_pack_service.py(new section),eba_risk_matrix.pychannel dimension (one factor appended under max-of-factors — can only raise, never lower, a dimension score), monitoring check registry (+profile_deviation), portal questions. Onboarding flow for low/medium-risk cases without a profile is behaviourally unchanged except for a new warning finding. - Alternative considered: keep the untyped
additional_datakeys and just add writers — rejected because it reproduces the dual-representation defect class fixed in PR #176 (two unreconciled vocabularies for the same fact) and gives the monitoring deviation check no queryable, validated baseline to diff against.
Context
ADR-0062 (2026-06-25, issue #44) introduced typed purpose/expected-activity models for AMLR
(EU) 2024/1624 Art. 20(1)(c) — "assessing and, as appropriate, obtaining information on the
purpose and intended nature of the business relationship". The models shipped and nothing
else did. The 2026-07-03 evidence-audited gap analysis
(docs/research/2026-07-03-amla-ongoing-monitoring-gap-analysis.md §3.1 "Activity baseline",
§3.2.5) verified, with file:line evidence:
PurposeProfile/ExpectedActivity/StatedPurpose(packages/trustrelay-models/src/trustrelay_models/purpose_profile.py, re-exported atbackend/app/models/purpose_profile.py) are never instantiated anywhere in app code. There is no persistence table, no capture endpoint, no portal question.- The Officer Decision Memorandum's §Business Rationale
(
decision_memorandum_service.py:516-549,_build_business_rationale) readsadditional_datakeys —business_purpose,expected_geography,expected_monthly_turnover_eur,channel_mix,source_of_funds,source_of_wealth,compensating_controls— that no code path ever writes. The section renders empty strings with acoveredflag that can be satisfied by MCC classification alone, so the memo silently presents an unassessed purpose as covered. missing_purpose_requirements(purpose_requirements.py:11-19) — the SoF-at-standard/high, SoW-at-high tier rule — has zero callers. Consequence: a HIGH/CRITICAL (EDD-tier) case can be approved with empty source-of-funds and source-of-wealth, an AMLR Art. 20(1)(c) requirement the codebase encodes as a rule and then never enforces.
The AMLA draft ongoing-monitoring guidelines (Art. 26 AMLR, public hearing 2026-07-02) make this the prerequisite gap: Guideline 2's position is that without an expected-activity baseline captured at CDD, deviations cannot be identified, and that peer-group comparison cannot substitute for an individual customer profile. Trust Relay's monitoring spine (ADR-0066) can therefore never satisfy Guideline 2's deviation-monitoring expectation — there is nothing to deviate from. The baseline is also the handoff artifact a customer's transaction-monitoring system consumes; Trust Relay deliberately is not a TM system (architecture doc §Out of scope), but the CDD-side capture obligation is squarely in scope.
This ADR covers Wave 4 of the AMLA remediation architecture
(docs/superpowers/specs/2026-07-03-amla-remediation-architecture.md §2.3 purpose_profiles,
§2.4 purpose_profile_service, §3 W4). Companion ADRs: 0083 (trigger taxonomy — defines
MonitoringTriggerType.profile_deviation this wave's check emits into), 0084 (alert
disposition — where deviation alerts are dispositioned), 0070 (maker-checker — reused for the
gate override).
Decision
Activate the ADR-0062 models end-to-end: persist them, capture them, gate on them, render them, and monitor against them. Six parts, all using the architecture doc §2 vocabulary verbatim.
1. Persist: purpose_profiles table (architecture §2.3, W4 migration). Create the next
sequential Alembic revision (never a hardcoded revision id) adding purpose_profiles:
id, tenant_id, case_id, stated_purpose (the ADR-0062 StatedPurpose enum values),
expected_activity JSONB (monthly_volume, monthly_value_eur, geographies[],
counterparties[], channels[]), source_of_funds, source_of_wealth,
is_intermediary bool, sub_merchant_profile JSONB, captured_by, captured_at.
FORCE ROW LEVEL SECURITY, tenant-scoped like screening_results (ADR-0023); every INSERT
sets tenant_id explicitly — no server_default (the PR #177 confirm-website lesson,
architecture §1.4). The JSONB shape extends ADR-0062's ExpectedActivity with channels[];
the Pydantic model gains that field rather than the table diverging from the model.
2. Capture: purpose_profile_service.py (architecture §2.4) +
GET/PUT /api/cases/{id}/purpose-profile (architecture §2.5) + portal questions. Officers
capture the profile at case creation (dashboard form on the case-create flow); the customer
portal's question step enriches it (expected volumes/values/geographies/channels, SoF/SoW
narrative). The service is the single write path (validates against the typed model, stamps
captured_by/captured_at, RBAC-gated for the officer endpoint). Portal wording is
requirement-stating only — tipping-off discipline (AMLD 2015/849 Art. 39, ADR-0071,
architecture §1.6) forbids any suspicion-revealing framing.
3. Gate: wire missing_purpose_requirements into the approval path. In the
case_decisions.py decision flow, before the officer_decision signal fires (the Temporal
workflow stays contained, exactly as ADR-0059/0065/0070 gate at the service/API layer):
- High risk (HIGH/CRITICAL, EDD tier): hard 409 when
missing_purpose_requirements(profile, tier)is non-empty or no profile exists for the case (fail-closed — an absent profile is missing everything, never treated as nothing-required). Response body carrieserror: missing_purpose_requirementsplus the missing-attribute list. Override exists but is audited maker-checker:FourEyesContextgains anoverride_purpose_requirementsflag andrequires_four_eyes(maker_checker.py:82) gains the trigger reasonpurpose_requirements_override, so the override routes throughPENDING_SECOND_APPROVALwith both actors on the immutableaudit_eventsspine (ADR-0064/0070). No single-officer bypass. - Standard risk: warning finding, not a block — the missing attributes are recorded as a finding on the case and rendered in memo/pack, preserving officer discretion where AMLR demands SoF but the ADR-0070 SoD rationale does not apply.
4. Render truthfully: memo + case pack. _build_business_rationale
(decision_memorandum_service.py:516-549) reads the purpose_profiles row first (typed),
falling back to the legacy additional_data keys only when no row exists, and its covered
flag becomes honest: covered only when a captured profile (or explicit legacy keys) exists —
MCC classification alone no longer marks purpose as covered (ADR-0067 "not assessed ≠ clear").
The regulator case pack (ADR-0069, case_pack_service.py) gains the profile as a rendered
section — it is the baseline artifact a supervisor or the buyer's TM team asks for.
5. Intermediary risk factor. is_intermediary + sub_merchant_profile feed a new
intermediary factor in the EBA matrix channel dimension (_score_channel_dimension,
eba_risk_matrix.py:784), per EBA/GL/2021/02 distribution-channel/intermediation risk
factors. Because dimension scores are the max of normalized factor scores, the new factor can
only raise or leave unchanged a dimension score — structurally compliant with the
never-suppress principle (architecture §1.1). Factor thresholds are tenant-configurable like
the existing 21 factors and subject to the calibration-rationale regime (ADR-0088/W6).
6. Monitor: profile_deviation check v1. Append profile_deviation to
MonitoringCheckType (trustrelay_models/monitoring.py:20-31) and emit
MonitoringTriggerType.profile_deviation (architecture §2.1, ADR-0083) into the W1 trigger
router. v1 compares, per monitored entity on the existing ContinuousMonitoringWorkflow
cadence (no new schedules, architecture §2.6):
- Declared turnover vs registry-filed financials: annualized
expected_activity.monthly_value_euragainst the filed-accounts revenue already retrieved by the financial-analysis layer (ADR-0048; sourced-or-honest-gap per ADR-0079/0080). A divergence beyond the configured band raises aprofile_deviationalert carrying both values. - Declared geographies vs registry-observed country set:
expected_activity.geographiesagainst the countries observed in the investigation/registry evidence (operational and UBO country sets the EBA input already assembles). - Fail-closed: no profile captured (the entire pre-W4 back catalogue), or financials unavailable/capability-gapped for the jurisdiction, → indeterminate WARNING naming what could not be assessed — never a benign pass, never a silent skip (ADR-0067/0068 contract).
Explicitly not in v1: payment-flow or volume-count deviation — Trust Relay ingests no transaction data (architecture §Out of scope); the captured baseline is the export the customer's TM system consumes for that half of Guideline 2.
Verification per architecture §4: test_no_false_reassurance.py (or sibling) gains the
invariants "empty SoF at high risk blocks" and "no profile → indeterminate WARNING, not
pass"; the OB Holding fixture (CRITICAL/90 oracle) exercises the high-risk 409 + maker-checker
override path; testcontainers PostgreSQL for the RLS table (no mocks); golden suite stays
green.
Consequences
Positive
- AMLR Art. 20(1)(c) becomes enforced, not merely encoded: a high-risk case can no longer be approved with empty SoF/SoW, and the override that permits it is a two-person, immutably audited event.
- AMLA Guideline 2's prerequisite exists: an individual, typed, tenant-scoped expected-activity baseline that deviations can be measured against — and the exportable handoff artifact a buyer's TM system needs.
- The memo's §Business Rationale and the case pack stop presenting unwritten fields as covered — closes a data-honesty defect of the same class as the PR #176 risk-display split.
- Three pieces of verified dead code (
missing_purpose_requirements, the ADR-0062 models, the memo'sadditional_datareads) gain their intended callers instead of accreting.
Negative
- Capture friction: officers get new form fields at case creation and high-risk approvals gain a hard prerequisite; until tenants adjust their intake habits, some approvals will bounce off the 409 and need profile completion or a four-eyes override round-trip.
- Deviation check v1 is deliberately crude: annualized declared turnover vs the latest filed annual accounts ignores growth-stage companies, seasonality, FX, and stale filings — a measurable false-positive rate is expected, and every one demands a W2 disposition with rationale. The band thresholds will need calibration cycles (W6) before the signal is trustworthy.
- The entire pre-W4 approved back catalogue has no profile, so the monitoring population will
emit a wave of indeterminate
profile_deviationWARNINGs on day one. This is the honest fail-closed outcome, but it is alert-queue load until CDD-refresh outreach (W3,cdd_refresh_service) back-fills profiles. - Two representations of the profile now exist (Pydantic model + table row + a transitional
additional_datafallback) that must be kept in sync; the fallback is retirement debt with a named owner (delete once no tenant case relies on legacy keys).
Neutral
- The Temporal workflow definition is untouched — capture, gate, and render all live at the
API/service layer; no
workflow.patched()guards needed (zero customers). - The EBA intermediary factor rides the existing versioned risk-config machinery; tenants that never flag intermediaries see no scoring change.
sub_merchant_profileis captured and rendered in W4 but its downstream use (per-sub-merchant screening, KBC establishment-unit asks) is a later wave; this ADR only reserves the typed slot.
Alternatives Considered
Alternative 1: keep the additional_data keys and just add writers
- Populate
business_purpose/expected_monthly_turnover_eur/ etc. oncases.additional_datafrom the portal and dashboard, leaving the memo reader as-is. - Why rejected: untyped JSONB keys have no validation (a free-text turnover string cannot be diffed against filed financials), no RLS-table query surface for the monitoring sweep, and no enum discipline — and it entrenches a second vocabulary for facts ADR-0062 already typed, the exact dual-representation defect class that produced the PR #176 risk-display bug and gap-analysis bug §3.2.4 (three unreconciled cadence tables).
Alternative 2: capture the profile only in the decision memorandum
- Add memo form fields the officer fills at decision time; the profile lives as prose in the generated PDF and its stored section payload.
- Why rejected: a document is not a baseline — the
profile_deviationcheck cannot diff declared turnover against a PDF paragraph; the ADR-0082 post-workflow-retrievability lesson showed decision-time-only artifacts are not queryable afterwards; and decision-time capture is too late for the SoF/SoW gate, which must evaluate before the approval it blocks.
Alternative 3: defer deviation detection entirely to the customer's TM system
- Capture the profile (parts 1–5) but ship no
profile_deviationcheck; hand the baseline to the buyer's transaction-monitoring stack and let it own all deviation logic. - Why rejected: the two v1 comparisons are non-transactional — registry-filed financials and registry country sets are data Trust Relay already retrieves and the TM system does not; AMLA Guideline 2 treats behavioural/registry deviation as part of ongoing monitoring (Art. 26), which is exactly the ADR-0066 spine's remit. The scope boundary excludes payment flows, not registry-evidence deviation.
Alternative 4: do nothing
- Leave the models dead and rely on officer diligence for purpose assessment.
- Why rejected: not viable — a high-risk approval with empty SoF/SoW is demonstrably possible
today (
missing_purpose_requirementshas zero callers), the memo asserts coverage of fields nothing writes, and comply-or-explain convergence on the AMLA guidelines (NCA declarations Q1 2027, AMLR applying 2027-07-10) makes "no baseline exists" an obligation-not-demonstrable finding for every tenant.