ADR-0063: Typed persisted ScreeningResult (ongoing-monitoring evidence trail)
Status: Accepted Date: 2026-06-16 Supersedes: none Superseded by: none Deciders: Adrian (Soft4U), Claude Opus 4.8
Decision context:
- Latency: one insert per (target × list_type × run); a query path for the trail. Negligible.
- Dependency surface: no new packages. Adds a model + ORM + migration 065 + a persist/list service.
- Debuggability: typed, queryable rows with explicit
hit/screened_at/screened_by/list_type(enum). - Reversibility: branch revert +
alembic downgrade -1(drops the table). - Blast radius: additive; the existing ephemeral sanctions dataclass/JSON blob is untouched until the pipeline is repointed.
- Alternative considered: keep the ephemeral blob (rejected — AMLR ongoing monitoring needs a persisted, queryable, timestamped trail).
Context
No canonical persisted typed ScreeningResult existed — sanctions results lived in an ephemeral
dataclass merged into a JSON blob on the investigation; list_type was a free string; hit/screened_at
were implicit; adverse-media findings weren't persisted as typed rows. AMLR ongoing monitoring (§7)
requires recurring, timestamped, queryable screening evidence per target + list type.
Decision
A typed model + an append-only, RLS-scoped table (mirrors the #30 ubo_computations pattern):
ScreeningResult(screening_result.py):target_ref,target_name,list_type: ScreeningListType(eu_sanctions/un_sanctions/ofac/pep/adverse_media),hit,match_score,screened_at,screened_by,details.ScreeningResultRecordORM + migration 065: tablescreening_results, FORCE ROW LEVEL SECURITY (ADR-0023), composite index(tenant_id, case_id, target_ref, list_type).ScreeningResultService.persist_screening_results(one row per result) +list_screening_results(the queryable trail), using the_session_scopepattern.
Consequences
Positive
- A typed, queryable, timestamped screening evidence trail per (target, list_type, run) — the
ongoing-monitoring backbone (pairs with #33 screen-UBOs + #40 periodic re-screening);
list_typeis a constrained enum.
Negative
- A new table + migration; repointing the live sanctions/adverse-media pipeline to write here (replacing the ephemeral blob) is an incremental follow-up (pairs with #40).
Neutral
- The existing ephemeral dataclass/JSON blob remains until the pipeline is repointed.
Alternatives Considered
Alternative 1: Keep the ephemeral sanctions dataclass + JSON blob
Rejected — a blob on the investigation isn't queryable per target/list and isn't a recurring, timestamped record; AMLR ongoing monitoring requires a persisted, queryable evidence trail.