Skip to main content

ADR-0054: UBO via control — ControlEdge dimension (binary reachability)

Status: Accepted Date: 2026-06-15 Supersedes: none Superseded by: none Deciders: Adrian (Soft4U), Claude Opus 4.8

Decision context:

  • Latency: control adds one bounded DFS per natural person over control hops (sparse, ≤10 deep) plus one Neo4j read for explicit CONTROLS edges in fetch_ownership_graph. Negligible on real KYB graphs; not separately measured (same shape as the ownership traversal already in ADR-0053).
  • Dependency surface: no new packages. Adds ControlType/ControlEdge/ControlHop/ControlTrace to trustrelay-models and _enumerate_control to the pure engine.
  • Debuggability: control determinations carry control_paths (the ordered control chain) for audit, mirroring path_traces for ownership; the pure engine stays unit-testable in isolation.
  • Reversibility: branch revert. The qualified_via str→list change is the only non-additive edit, and it has no external consumers (verified).
  • Blast radius: additive — new models + a new engine traversal + a merge step in compute + one extra Neo4j read. _enumerate_paths (ownership) is untouched; get_ownership_tree (UI) untouched.
  • Alternative considered: model control as a 100%-weight ownership edge through the summation (rejected — control is not additive/multiplicative; see Alternatives).

Context

ADR-0053 made UBO ownership a real multi-path computation, but control — an independent basis for beneficial ownership under AMLR Art. 52 — was still absent. control_type existed only as a free-form string ("DeJure"/"DeFacto") on the company→company IS_SUBSIDIARY_OF edge, surfaced read-only in Entity 360. No code path conferred UBO status from control, so a natural person who exercises control — majority voting, appoint/remove the board majority, veto rights, control over profit distribution, or other dominant influence — with 0% ownership was invisible to the engine.

AMLR Art. 52 treats control as a separate test from ownership: such a person is a beneficial owner regardless of ownership percentage.

Decision

Extend the ADR-0053 engine with a control dimension computed alongside ownership:

  1. ControlEdge (first-class model): source → target, control_type (enum: majority_voting, appoint_remove_board, veto_rights, control_profit_distribution, other_dominant_influence), evidence_ref (placeholder for a VerificationRecord, deferred to the verification issues #34/#36). OwnershipGraph gains control_edges.
  2. Binary control reachability (_enumerate_control): from each natural person, DFS over control hops to the subject with a per-path visited-set (cycle-safe, depth-capped). A control hop is an explicit ControlEdge OR an OwnershipEdge with fraction > 0.50 (>50% ownership is de jure control, so control propagates through majority-owned intermediates). Reaching the subject ⇒ UBO via control. This is independent of the ownership 25% threshold.
  3. Merge into one determination: compute runs both dimensions per person and sets qualified_via: list[str] (["ownership"], ["control"], or ["ownership","control"]), qualified = bool(qualified_via), and a truthful reason_code ("control" / "ownership_<pct>" / "ownership_<pct>+control"). control_paths records the conferring chain(s).
  4. Adapter: fetch_ownership_graph also reads explicit CONTROLS edges into control_edges (free-form control_type mapped to the enum; legacy DeFactoOTHER_DOMINANT_INFLUENCE). The

    50%-implies-control derivation needs no new data — it falls out of existing ownership edges.

Consequences

Positive

  • Control becomes a real, independent AMLR UBO basis; a 0%-ownership board-appointer is flagged.
  • qualified_via as a list captures persons qualifying on both bases — richer, truthful audit.
  • Reuses the ADR-0053 DFS/cycle machinery; control mirrors ownership traversal.
  • 50%-implies-control surfaces control from existing data with no new ETL.

Negative

  • qualified_via type change (str→list) touches the ADR-0053 result model + its tests (bounded; no external consumers).
  • Person→entity explicit control edges are data-starved until registry ETL populates them (a deliberate follow-up, like ADR-0053's person→intermediate ownership edges).
  • evidence_ref is a placeholder until #34/#36 deliver a real VerificationRecord.

Neutral

  • The free-form control_type string on IS_SUBSIDIARY_OF stays for the Entity 360 UI; the engine reads it but does not require its removal.

Alternatives Considered

Alternative 1: Model control as a 100%-weight ownership edge through the summation

Represent each control relationship as OwnershipEdge(fraction=1.0, kind="control") and run it through the ADR-0053 percentage summation.

  • Why rejected: control is not additive or multiplicative. Two veto rights do not sum to 200%, and a control hop through a 30%-owned intermediate still confers full control. Forcing control into percentage arithmetic produces meaningless aggregates. Control requires binary reachability — a distinct traversal.

Alternative 2: A separate compute_control() returning its own result list

  • Why rejected: a person can qualify on both bases, and AMLR wants one beneficial-owner determination per person. Two parallel result lists would force every caller to dedupe persons. Merging into a single BeneficialOwnerResult with a qualified_via list is the faithful model.

Alternative 3: Treat ≥50% (inclusive) as control

  • Why rejected: de jure control requires a majority — strictly more than 50%. A 50/50 split gives neither holder control. The engine uses fraction > 0.50 (strict), verified by a boundary test.