Skip to main content

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 to licence_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_discovery two-lane helpers (existing), and license_registry.verify_vertical_licence (existing). One new reference dataset file + two new app modules; owned lines only.
  • Debuggability: the pass mutates result_dict['licences'] + writes result_dict['licence_discovery_coverage'] (mirrors deep_research_coverage); every discovered record carries discovered_via, source_url, published_status, attribution_lane. A failure is guard-and-swallowed with a logger.warning and 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_licence consumes investigation_data['licences'] (LicenceRecord dumps) + the MCC and sets operating_without_required_licence, which feeds the EXISTING OPERATING_WITHOUT_REQUIRED_LICENCE EBA 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):

  1. The vertical is resolved from the entity's own MCC (mcc_display_vertical) + a stated-vertical fallback — never hardcoded to gambling.
  2. The governing register is looked up in a declarative catalog, config/reference_data/regulator_registers.json (vertical → country → register-entry), loaded through reference_data_service. Adding a vertical / country / authority is a data edit, documented by a _template row. Seeded with real gambling authorities (EE/LT/MT/GB).
  3. 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_matched LicenceRecord with a canonical invalid status → verify_vertical_licence classifies it expiredoperating_without_required_licence=True → the EXISTING escalator (works in any jurisdiction; does not depend on licences_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_gap coverage-state finding + a licence_discovery_coverage marker — 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_connector flag 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_service count updated).
  • Introduces licence_discovery_coverage on the investigation result (mirrors deep_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.
  • 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_connector flag 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.