ADR-0094: Brand-directed operating-subsidiary discovery
Date: 2026-07-06 Status: Accepted Deciders: Adrian (Soft4U), Claude Opus 4.8
Context
The EVOI network scan (ADR-0019/0025) discovers related entities by following NorthData
relatedCompanies, which models control / ownership. Ownership flows upward from a
holding SPV to its ultimate owners. Verified live during the OB Holding 1 OÜ validation run:
the network scan reached the full Luxembourg holding chain (Odyssey / Hippocrate / Noal SPVs)
and the Estonian operating parent (Olympic Entertainment Group AS) — but the scanned roster was
LU (6) / EE (3) / MT (1), zero LT. A direct NorthData query confirmed why:
Olympic Entertainment Group AS's relatedCompanies returns only its LU parents and its
auditor (PwC) — it does not list its operating subsidiaries. So the Lithuanian operating
casino, where the €8.4m AML fine sits, is never discovered. It shares a brand (OlyBet /
Olympic Casino) with the subject, not a direct ownership edge, so no amount of deeper ownership
traversal reaches it.
The same NorthData, queried by name search for the brand, returns the operating entities (15 hits for "OlyBet"). The gap is therefore a discovery-modality gap, not a depth or budget one: ownership traversal cannot find a sibling that is linked by brand, not by an ownership edge.
This closes the analytical half of issue #170's "corporate-group recall" follow-up (the recall half is ADR-0093 Tier-B).
Decision
Add a brand-directed discovery pass (app/agents/brand_discovery_phase.py,
run_brand_directed_discovery, with the pure gating in
app/services/brand_directed_discovery.py) that runs AFTER the network phase and BEFORE the
Tier-B adverse-recall pass. It derives the group's brand tokens (reusing
_derive_brand_tokens), keeps only distinctive ones, runs a bounded NorthData name search
per token, filters the hits, and appends the survivors to the network roster as
brand_discovered entries — which then flow into Tier-B per-entity adverse-media recall.
Because these entities feed the risk pipeline, the pass is conservative by construction:
- distinctive-token gating — only coined tokens (≥5 chars, not a generic corporate word: "olympic", "casino", "holding", "europe", "group", "entertainment" are all filtered) trigger a search. Searching a generic word returns thousands of unrelated entities (the IOC, every "… Casino …"); "olybet"/"odyssey" survive.
- vertical gating — only runs for verticals where a cross-jurisdiction operating graph is compliance-material (gambling/gaming/casino/betting).
- two-lane attribution (ADR-0078) — a brand-name match is an
unverified_candidate(never subject risk on a shared word; ADR-0073 R9), promoted toverifiedonly when a shared director or registered address corroborates a real corporate link. The name-as-identifier guard rejects a reg-no that equals the name. - token cap (
brand_directed_discovery_max_tokens, default 3).
Dark launch: settings.brand_directed_discovery_enabled defaults False — a no-op until
a live validation run confirms precision and cost, at which point the flag is flipped (a
Calibration-Review-gated change, since discovered entities feed the deterministic escalator).
Decision context:
- Latency: none while off. When on, ≤
max_tokensNorthData searches (€0.008 each) + the Tier-B adverse-media recall on any survivors — bounded and vertical-gated. - Dependency surface: no new packages; a pure gating module + an orchestration phase + a
one-call hook; reuses
_derive_brand_tokens,NorthDataService.search_company, and the Tier-B roster path. - Debuggability: the gating/attribution are pure functions with 8 unit tests; the phase is
guarded (non-fatal) and records
brand_discovery_coverage. - Reversibility: single flag flip; the code path is additive.
- Blast radius: off by default → zero change until enabled. When on, a discovered entity can only ADD scrutiny, and the two-lane guard prevents a brand-name match from reaching subject risk on its own.
- Alternative considered: increase EVOI network depth — rejected (NorthData does not list the operating subsidiaries at any depth; a deeper ownership traversal cannot find a brand sibling).
Consequences
Positive
- Closes the discovery-modality gap: a group's operating subsidiaries in other jurisdictions are found by brand and fed into per-entity adverse-media recall, where the OB Holding LT fine lives.
- Reuses the existing brand-token + two-lane-attribution machinery; no new external dependency.
- Conservative: vertical- and distinctive-token-gated, dark-launched, and never escalates the subject on a shared brand word alone.
Negative
- Brand search has an inherent false-positive surface (common words) — mitigated by the distinctive-token filter, the brand-in-name check, and the unverified-candidate default, but not eliminated; this is exactly why it is dark-launched and calibration-gated.
- Discovered entities are screened for adverse media (Tier-B) but not yet run through the full network sanctions/PEP scan — a follow-up.
- Real external-API spend when enabled.
Neutral
- The pass mutates the network roster in place; discovery coverage is recorded separately from the Tier-B recall coverage.
Alternatives Considered
Alternative 1: Increase EVOI network depth / budget
- Scan more of the ownership graph, deeper.
- Why rejected: the live NorthData check shows the operating subsidiaries are simply not in the ownership graph at any depth (Olympic Entertainment lists only parents + auditor). More depth spends budget climbing further up the ownership chain, away from the operating siblings.
Alternative 2: Enable immediately (no dark-launch flag)
- Ship it on by default.
- Why rejected: it feeds the deterministic escalator and has a real false-positive surface (brand words); flipping it on without a live precision/cost validation risks over-discovery. The flag lets the code land, be unit-tested, and be enabled deliberately after review.
Revision Notes
The original Decision (dark-launched brand-directed pass, distinctive-token gating, two-lane attribution) stands unchanged. These notes record post-decision calibration of the token-seed selection, discovered on live OB Holding runs.
2026-07-18 — bigram calibration: advisor/geo precision + gambling single-name recall (#426)
Issue #417 added a bigram brand rescue to distinctive_brand_bigrams: a compound brand
whose stem the unigram filter correctly kills ("Olympic Casino" — 'olympic' is generic) is
recovered as an adjacent-pair search seed under strict co-occurrence evidence (stem recurs in
≥2 distinct group names, leads ≥1, plausible tail), capped at 2 and riding within the fixed
token budget. The first live run of that feature (wf_1b76902d78e4) exposed two mis-calibrations
on real OB data — a precision leak and a recall miss caused by the same guard:
- Precision leak: it emitted
pricewaterhousecoopers eastern— the auditor's regional entity (PwC Eastern Europe B.V.), not a brand. The advisor stem 'pricewaterhousecoopers' recurs ≥2× and leads a name, and 'eastern' passed the len≥4/not-structural-tail tail check. - Recall miss (the oracle): it did not emit
olympic casino/olympic entertainment. 'olympic' appears in only ONE mined source name (OLYMPIC ENTERTAINMENT GROUP AS), so the ≥2-distinct-names stem guard blocked the exact LT operating casino (€8.4m fine) we hunt — the same guard that let the auditor noise through.
Fix (bigram path only; unigram gating, the matcher, and the two-lane attribution are unchanged):
- Precision — exclude advisors and geo tails. The advisor/auditor/PE-SPV stems (PwC,
Deloitte, KPMG, EY, Alter Domus, TMF,
*coshells) are lifted into a dedicated_ADVISOR_SPV_STOP_TOKENSset (still unioned into the generic set, so unigram behaviour is byte-identical). A bigram whose first word is in that set is never emitted — an advisor is noise regardless of the trailing word. A bigram whose second word is a geo stop-token (_GEO_STOP_TOKENS: country names + compass/region words, now alsoeurope/european) is never emitted — it names where a subsidiary sits, not who it is.pricewaterhousecoopers easternnow dies on both rules. - Recall — gambling-STEM rescue (revised by #427, see below). (Original #426 attempt:
relax the bigram guard to ≥1 under a gambling vertical so
olympic entertainmentemits from the singleOlympic Entertainment Group AS. Superseded — see #427.)
Guard preserved: this only changes which seeds are searched; a discovered entity is still a brand-name match in the unverified-candidate lane unless a shared director/address corroborates it, and it can only ADD scrutiny.
2026-07-18 — gambling-stem rescue supersedes the single-name bigram relaxation (#427)
Codex review of the #426 fix (verified against the code) found it functionally incomplete.
The #426 relaxation emitted olympic entertainment — but that bigram is derived from the
parent name (Olympic Entertainment Group AS), and build_discovery_candidates requires
all of a token's words in a hit. So the oracle target Olympic Casino Group Baltija UAB
(the LT €8.4m-fine operating casino: olympic ✔, entertainment ✘) is rejected. The parent's
second word is not the casino's second word — no parent-derived bigram can ever match the
operating entity. Net: #426 rediscovered the already-known parent and still missed the casino.
Fix — a gambling-STEM rescue (distinctive_gambling_stems), the actual recall mechanism:
- The #426 bigram gambling relaxation is reverted —
distinctive_brand_bigramsreturns to a vertical-agnostic ≥2-distinct-names guard (the advisor-stem and geo-tail precision exclusions from #426 stay). It still rescues genuine ≥2-name compounds where the second word is distinctive. - Under a gambling vertical (the phase's existing
_is_discovery_verticalgate) a bare, generic- but-brandable stem that leads ≥1 group name (olympic— generic-dropped as a unigram, not an advisor/geo/structural/gambling-keyword word) is emitted as a single-stem search seed, capped at 1 (the highest-FP-surface search kind). Search seeds are now typedSearchToken(query, kind), threading the token kind throughselect_search_tokens→ the phase search loop →build_discovery_candidates; a same-stem bigram is dropped as redundant (the stem search ⊇ the bigram), and the stem gets a reserved budget slot by sacrificing the weakest unigram. build_discovery_candidatesgains atoken_kind="gambling_stem"match rule: a hit qualifies when the stem AND a gambling keyword both appear (_GAMBLING_KEYWORDS— casino/gaming/ betting/lottery/… + nativekasiino/kasino). Precision comes from the keyword, not an exact second word:Olympic Casino Group Baltija UABis kept;Olympic Committee of MaltaandOlympic Airwaysare rejected. Searching the bare genericolympicis precision-safe only because (a) the phase gates on the gambling vertical and (b) the matcher demands a gambling keyword.
Candidates still land unverified_candidate (ADR-0078); cap + token budget unchanged; the stem is
one bounded search. Tests: backend/tests/test_brand_directed_discovery.py (37 total; new coverage
for the gambling-stem derivation + exclusions, the stem+keyword matcher keeping the casino /
rejecting the committee + airways, token-kind metadata flow, budget stem-slot reservation with
same-stem-bigram dedup, and the phase-level OB oracle end-to-end discovering the LT casino).
Revision — 2026-07-18 (#428, live re-validation wf_8f1e1c91359e): gambling-stem search carries the casino anchor
#427's mechanism fired but still missed the LT casino live: it searched the bare stem
olympic, which NorthData relevance-ranks to generic Olympic <legalform> shells — the operating
casino UAB Olympic Casino Group Baltija is never returned in the top hits, so the (correct)
matcher never sees it. Probing NorthData, Olympic Casino returns the whole group (Baltija LT +
EE/RO/SK). Fix: the gambling-stem search query now carries the anchor ("<stem> casino",
_GAMBLING_STEM_SEARCH_ANCHOR); the matcher is unchanged (stem = tok_words[0]). Also corrected
the #427 phase fixture that mocked NorthData returning the casino for the bare olympic query —
the fixture-vs-reality gap that let it ship green. Tests: 39 total (+2, 2 fixtures corrected).