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
CONTROLSedges infetch_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/ControlTracetotrustrelay-modelsand_enumerate_controlto the pure engine. - Debuggability: control determinations carry
control_paths(the ordered control chain) for audit, mirroringpath_tracesfor ownership; the pure engine stays unit-testable in isolation. - Reversibility: branch revert. The
qualified_viastr→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:
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 aVerificationRecord, deferred to the verification issues #34/#36).OwnershipGraphgainscontrol_edges.- 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 explicitControlEdgeOR anOwnershipEdgewithfraction > 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. - Merge into one determination:
computeruns both dimensions per person and setsqualified_via: list[str](["ownership"],["control"], or["ownership","control"]),qualified = bool(qualified_via), and a truthfulreason_code("control"/"ownership_<pct>"/"ownership_<pct>+control").control_pathsrecords the conferring chain(s). - Adapter:
fetch_ownership_graphalso reads explicitCONTROLSedges intocontrol_edges(free-formcontrol_typemapped to the enum; legacyDeFacto→OTHER_DOMINANT_INFLUENCE). The50%-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_viaas 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_viatype 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_refis a placeholder until #34/#36 deliver a realVerificationRecord.
Neutral
- The free-form
control_typestring onIS_SUBSIDIARY_OFstays 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
BeneficialOwnerResultwith aqualified_vialist 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.