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), soescalate_entity_criminal_investigationattaches 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_idthat 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_coveragefor 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_aliasesstill 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_aliasesbuilder 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.