ADR-0066: Live risk-paced UBO re-screening + general-population monitoring
Status: Accepted Date: 2026-06-16 Supersedes: none Superseded by: none Deciders: Adrian (Soft4U), Claude Opus 4.8
Decision context:
- Latency: live OpenSanctions/PEP calls during monitoring runs, gated by a per-case cadence check so
most cases skip on most runs; per-case execution is already sequential in the workflow. The cadence
gate is a single timestamp comparison + one indexed
screening_resultslookup per case. - Dependency surface: no new packages, no new table. Reuses
screen_entities(#33 pipeline) and thescreening_resultsevidence trail (#45, migration 065). Adds one field toRiskAssessment+ two pure helpers + one read activity. - Debuggability: a re-screen emits a
monitoring_eventsrow (severity/change) and appendsscreening_resultsrows (timestamped, per person/list); a failed screen surfaces as awarningevent, never a silent skip. - Reversibility: branch revert; additive (the retired customs-only activity is the only deletion, and only the monitoring workflow referenced it).
- Blast radius: the monitoring workflow now iterates the general onboarded population;
check_ubo_screeninggoes from no-op to live. No schema migration. - Alternative considered: fixed per-tenant cron with no per-case cadence (rejected — not risk-based, re-screens everything every run).
Context
Ongoing monitoring (AMLR Art. 21) was scaffolding for the data that matters. check_ubo_screening was an
explicit mock — UBOs were never re-screened against sanctions/PEP lists. ContinuousMonitoringWorkflow
iterated active customs cases only (template_id LIKE '%customs%' AND status='APPROVED'), so the
general onboarded KYB population was never monitored. The "perpetual KYC" capability (NBB 2026/02) was not
real. The building blocks existed: the live screening pipeline (screen_entities, #33) and a timestamped,
RLS-scoped, append-only screening evidence trail (screening_results, #45).
Decision
- Risk-based cadence. Add
review_cadence_daystoRiskAssessment(auto-derived from the risk tier via a@model_validator). Purecadence_days_for_tiermaps EDD→90, CDD→180, SDD→365 days — all far within the AMLR Art. 26(2) full-review ceilings (EDD 12mo, CDD/SDD 60mo); these are sanctions-re-screen intervals, intentionally tighter. Pureis_rescreen_due(last_screened_at, cadence_days, now)gates each case: never-screened or elapsed ⇒ due. - Live
check_ubo_screening. When due, merge directors + UBOs (#33 canonical set), build queries, callscreen_entitiesunder a tenant-scoped session (live OpenSanctions/PEP + 3-tier suppression), map post-suppression hits →ScreeningResultrows, persist viaScreeningResultService(#45 trail), and return aMonitoringEventwhose severity reflects the hits (sanctions⇒critical, PEP⇒warning). Not due ⇒ INFO no-op. Engine failure ⇒warningevent (never a silent skip). - General population. New
fetch_active_monitoring_casesactivity selectsstatus IN ('APPROVED','APPROVED_WITH_RESTRICTIONS')with an explicit tenant predicate (admin session bypasses RLS), no customs filter, returning each case'srisk_tier. The workflow calls it; the customs-only activity is retired (only the workflow referenced it). - Evidence trail. Re-screens append to
screening_results(timestamped) — no new migration.
Consequences
Positive
- Real recurring, risk-paced sanctions/PEP re-screening of subject + UBOs across all onboarded cases, with a timestamped append-only evidence trail (AMLR Art. 21/26, EU AI Act Art. 12). Completes perpetual KYC.
Negative
- Live screening cost/latency during monitoring runs — bounded by the cadence gate and sequential per-case execution. A monitoring run's wall-time grows with the number of due cases.
Neutral
- Customs cases become a subset of the monitored population (still covered).
RiskAssessmentgains a derived field (back-compat via validator; existing serialized assessments default it from tier on load).
Alternatives Considered
Alternative 1: Fixed per-tenant cron, no per-case cadence gate
Rejected — re-screens every onboarded case on every run regardless of risk; wasteful and not risk-based as the acceptance criteria require.
Alternative 2: New ubo_rescreen_events table
Rejected — screening_results (#45) is already the append-only, RLS-scoped, timestamped screening evidence
trail. A parallel table would fragment the evidence.
Alternative 3: Screen UBOs only (not directors)
Rejected — #33 established merge_screening_persons(directors, ubos) as the canonical natural-person
screening set; director records carry the DOB/nationality discriminators that reduce false positives.