Skip to main content

ADR-0093: Tier-B per-entity / per-director adverse-media recall

Date: 2026-07-06 Status: Accepted Deciders: Adrian (Soft4U), Claude Opus 4.8

Context

Adverse-media retrieval (ADR-0077/0078) was scoped to the subject entity plus its alias/brand/group names. The EVOI network scan (ADR-0019/0025) discovers related legal entities (with reg-no / LEI) and natural persons and screens them for sanctions/PEP — but never runs adverse-media (regulatory fines, criminal/enforcement investigations, litigation) against them. So group- and person-level adverse history was missed even though the targets were already known (issue #170; the OB Holding €8.4m Lithuanian fine, the McLoughlin SKS365 history). Tier A (PR #171) surfaced the honest "not assessed for related entities and named persons" marker; this is the Tier-B content fix.

A load-bearing constraint: the subject adverse-media call runs before the network phase (osint_agent.py), so network targets do not exist at that point — adding them to that call is a silent no-op. And adverse media is off the EVOI cost ledger (that budget covers only €0.008 NorthData scans), so an uncapped per-target fan-out could blow up cost/latency.

Decision

Add a network-aware adverse-recall pass (app/agents/adverse_recall_phase.py, run_network_adverse_recall) that runs after the network phase. It enumerates prioritised targets from the network roster (app/services/adverse_recall_targets.py), caps the fan-out with a dedicated off-ledger budget, and for each selected related entity runs a scoped run_adverse_media_agent pass (the entity's own country gives native ET/LT recall; its persons ride the call as directors). Attribution follows the ADR-0078 two lanes:

  • verified lane — a related entity with a real reg-no/LEI is added to result_dict['verified_group_aliases'] (subsidiary + reg-no only, mirroring the conservative _GROUP_OPERATING_ROLES), so escalate_entity_criminal_investigation attaches its criminal findings to the subject via the subject-via-verified-group-chain tier.
  • unverified_candidate lane — every network-discovered person (names only) and every name-only entity is a labelled candidate, never folded into subject risk on name alone (ADR-0073 R9 name-collision guard / ADR-0078 person GDPR guard). The critical guard: an entity_id that fell back to the entity name is not treated as an identifier.

The Tier-A recall-gap marker becomes coverage-aware: full coverage emits no gap; partial coverage shrinks to the residual ("screened N of M; K budget-skipped: …"); a degraded search propagates material_check_incomplete so the ADR-0089 ratchet refuses to lower a floor on incomplete coverage.

Dark launch: settings.adverse_recall_enabled defaults False — the pass is a no-op that leaves the Tier-A marker in place until a live validation run confirms recall quality and cost, at which point the flag is flipped (a Calibration-Review-gated change, since it feeds the deterministic escalator). adverse_recall_max_targets (default 8) caps the fan-out.

Decision context:

  • Latency: none while off. When on, bounded by adverse_recall_max_targets × (Tavily + proactive SERP on high-signal queries) — the slow path, which is exactly why it is capped and flagged.
  • Dependency surface: no new packages; two new modules + a one-call hook + a coverage branch in the marker; reuses the existing engine, escalator, and budget pattern.
  • Debuggability: the enumerator/budget are pure functions with 10 unit tests; the phase is guarded (non-fatal) and records adverse_recall_coverage for the audit trail.
  • Reversibility: single flag flip to disable; the code path is additive.
  • Blast radius: off by default → zero change until enabled. When enabled it can only ADD findings/scrutiny; the two-lane guards prevent a name-only hit from reaching subject risk.
  • Alternative considered: fold network entities into the subject's single alias call — rejected (the ordering trap makes it a no-op, and it would share the subject's country, losing per-target native-language recall).

Consequences

Positive

  • Closes the compliance-material recall gap: group- and person-level adverse findings are now searched for the exact targets the network scan discovered, with native-language queries per jurisdiction.
  • Honest coverage: the "not assessed" marker shrinks to the true residual instead of always firing; skipped targets are named for the MLRO.
  • Fail-safe: two-lane attribution + the name-as-identifier guard mean a person/name-only hit can never escalate subject risk on name alone.

Negative

  • Real external-API spend when enabled (multi-provider, multi-language) — mitigated by the hard target cap and the dark-launch flag.
  • The pass is dark-launched, so the gap is closeable but not yet closed in production — flipping the flag needs a live validation run + calibration review.
  • Person recall depth is shallow: network persons ride the entity call as directors; standalone persons without a discovered entity are not yet searched (follow-up).

Neutral

  • The budget is separate from the EVOI ledger (adverse media is not on it); a future unification is possible but not required.
  • derive_verified_group_aliases still restricts the CRITICAL tier to subsidiaries; this ADR does not broaden it.

Alternatives Considered

Alternative 1: Extend the subject's existing alias loop with network targets

  • Add network entities to the _am_aliases builder in _safe_adverse_media.
  • Why rejected: that call runs before the network phase, so the targets do not exist yet — a silent no-op. It also shares the subject's country, losing per-target native-language recall.

Alternative 2: Enable it immediately (no dark-launch flag)

  • Ship it on by default.
  • Why rejected: it feeds the deterministic escalator and is real external spend; flipping it on without a live validation run + calibration review risks over-escalation and cost surprises. The flag lets the code land, be tested, and be enabled deliberately.