Skip to main content

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.
  • ScreeningResultRecord ORM + migration 065: table screening_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_scope pattern.

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_type is 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.