ADR-0147: Licence / regulator-status discovery pass (authority-register-directed, generic, fail-closed)
Date: 2026-07-27 Status: Accepted Deciders: Adrian (Soft4U), Claude (Opus 4.8)
Decision context:
- Latency: not measured because the pass is dark-launched OFF (
licence_discovery_enabled=False) and, when ON, is bounded tolicence_discovery_max_queries(default 6) searches at concurrency 2, reusing the already-metered deep-research search tooling. It runs once per investigation, after the network/deep-research phases. - Dependency surface: no new packages. Reuses
adverse_media_agent._tavily_search+_brightdata_serp_search(existing),reference_data_service(existing),deep_research_discoverytwo-lane helpers (existing), andlicense_registry.verify_vertical_licence(existing). One new reference dataset file + two new app modules; owned lines only. - Debuggability: the pass mutates
result_dict['licences']+ writesresult_dict['licence_discovery_coverage'](mirrorsdeep_research_coverage); every discovered record carriesdiscovered_via,source_url,published_status,attribution_lane. A failure is guard-and-swallowed with alogger.warningand never fails the workflow. - Reversibility: single config flip (
licence_discovery_enabled) — flag-off is a byte-identical no-op. No migration, no schema change. - Blast radius: additive. Flag-off touches nothing. Flag-on only APPENDS licence records (consumed by the existing verifier) + coverage markers; it never edits an existing finding, never adds a scoring path.
- Alternative considered: extend the deep-research pass to also emit licence records (rejected — deep-research emits soft findings, not structured
LicenceRecords into the verification path, and is not authority-register-directed; conflating the two would blur the "no web-sourced green" fail-closed rule).
Context
Closed #721 (structured licence/VAT/case-number extraction) and #724 (gambling-licence
VERIFICATION against the register) cover capturing and verifying a known licence for a
structured jurisdiction (today only EE, via the Äriregister tegevusload extractor,
ADR-0079). There was no discovery pass that actively consults a governing authority
register to find an entity's licence and its regulator status (active / suspended /
revoked) and cross-jurisdiction operating permits.
The gap surfaced on the OB Holding 1 OÜ live investigation (2026-07-27): OB holds an EE gambling operating licence, but the group's LT operator's licence status — a material AMLR Art. 34 higher-risk-vertical signal — was never consulted, because no pass targets the LT Gaming Control Authority register. The user constraint is explicit: the fix must be generic to any regulated vertical and case, not targeted to the OB gambling case.
The forces:
- The verification machinery already exists —
verify_vertical_licenceconsumesinvestigation_data['licences'](LicenceRecorddumps) + the MCC and setsoperating_without_required_licence, which feeds the EXISTINGOPERATING_WITHOUT_REQUIRED_LICENCEEBA deterministic floor (ADR-0020). The missing piece is discovery that populates those licence records, not a new scoring path. - Most authority registers have no queryable API; a discovery pass must be honest about what a bounded web search can and cannot conclude. A web search can surface an adverse status; it cannot authoritatively declare "no licence held" or "licensed & clear".
- Register routing was hardcoded (
license_registry._COUNTRY_REGISTRY_SEARCH), which does not scale to "any vertical, any country" without a data-driven catalog.
Decision
Add a dedicated, dark-launched licence / regulator-status discovery pass
(app/agents/licence_discovery_phase.py::run_licence_discovery), wired in osint_agent
after the deep-research pass and before finalize_investigation_result, behind
settings.licence_discovery_enabled (default False).
Generic by construction (the load-bearing requirement):
- The vertical is resolved from the entity's own MCC (
mcc_display_vertical) + a stated-vertical fallback — never hardcoded to gambling. - The governing register is looked up in a declarative catalog,
config/reference_data/regulator_registers.json(vertical → country → register-entry), loaded throughreference_data_service. Adding a vertical / country / authority is a data edit, documented by a_templaterow. Seeded with real gambling authorities (EE/LT/MT/GB). - The pass logic is vertical-agnostic; genericity is pinned by tests over a synthetic non-gambling vertical so the pass can never regress into gambling-only behaviour.
Fail-closed / never-suppress (ADR-0067), asymmetric emission:
- A discovered INVALID status (revoked/suspended/…), two-lane attributed
(ADR-0078): a subject-identifier-corroborated hit becomes a
subject_matchedLicenceRecordwith a canonical invalid status →verify_vertical_licenceclassifies itexpired→operating_without_required_licence=True→ the EXISTING escalator (works in any jurisdiction; does not depend onlicences_searched). A name-only hit is a labelled candidate (subject_matched=False) that never escalates. - A discovered VALID status is never turned into a verifying record — a web search cannot turn a licence green.
- An unreachable/absent register records an honest
licence_verification_gapcoverage-state finding + alicence_discovery_coveragemarker — never a benign "licence OK". - A
(vertical, country)with a structured connector (EE #721) is deferred to the authoritative extractor — the pass records structured coverage and does not web-search it.
The status vocabulary stays single-sourced in
license_registry._VALID_LICENCE_STATUSES / _INVALID_LICENCE_STATUSES; the catalog only
adds native-language surface terms. Discovered records' actual published status is preserved
verbatim in published_status.
Consequences
Positive
- Closes the discovery gap: a group's cross-jurisdiction operating-licence status (e.g. a revoked LT gambling licence) can now feed the deterministic escalator.
- Fully generic — a new regulated vertical/country is a reviewed data edit, not code.
- No new scoring path; reuses the proven
verify_vertical_licence+ EBA escalator, so the risk math and all ADR-0067 fail-closed invariants are preserved.
Negative
- A bounded web search over free text is a weaker signal than a structured register
consultation; the pass is deliberately conservative (only an identifier-corroborated
adverse status escalates), so it will miss adverse statuses that a structured connector
would catch. Structured coverage remains the goal per (vertical, country); the catalog's
structured_connectorflag marks where it exists. - The escalator-feeding requirements map (
_NONFINANCIAL_VERTICAL_LICENCE_REQUIREMENTS) still lists only gambling; a discovered adverse status for another catalogued vertical surfaces as a candidate/coverage signal but will not auto-fire the escalator until that vertical is added there (a reviewed data edit) — a documented, deliberate limitation.
Neutral
- Adds a 15th reference dataset (
test_reference_data_servicecount updated). - Introduces
licence_discovery_coverageon the investigation result (mirrorsdeep_research_coverage); report-surface rendering of it is a follow-up.
Alternatives Considered
Alternative 1: Extend the deep-research pass to also emit licence records
- The deep-research pass (#723) already web-searches for "licences" as one gap field.
- Why rejected: it emits soft findings, not structured
LicenceRecords into the verification path, and is not authority-register-directed. Folding licence-status discovery into it would blur the fail-closed "no web-sourced green" rule and couple two passes with different output contracts.
Alternative 2: Add live per-authority register connectors instead of a web search
- A structured connector per authority (as EE has) is the authoritative approach.
- Why rejected as the starting point: most authorities have no API; building N connectors
is a large, per-jurisdiction effort. The catalog's
structured_connectorflag is the migration path — a connector, when built, flips the flag and the pass defers to it. The bounded web search is the honest interim that surfaces adverse statuses without fabricating clean ones.
Alternative 3: Do nothing (rely on #721/#724 only)
- Why rejected: #721/#724 cover only structured jurisdictions (EE), so a cross-jurisdiction operating-licence status (the OB LT gap) is never consulted — a silent coverage hole that violates the never-suppress doctrine.