Skip to main content

ADR-0134: Control-via-other-means derivation engine (acting-in-concert, nominee, board/veto, 50%+1 threshold)

Date: 2026-07-25 Status: Accepted Deciders: Adrian (Soft4U BV), Claude Opus 4.8 (implementation agent)

Decision context:

  • Latency: not measured — the two new passes (_apply_concert, _apply_nominee) reuse the existing _aggregate_ownership_to / _enumerate_control DFS over the same in-memory graph the engine already walks; both are no-ops (return on the first line) when the graph declares no concert group / nominee edge, which is every pre-#538 graph. The dominant cost remains the Neo4j ownership-graph read in UBOComputationService, unchanged.
  • Dependency surface: zero new packages. Two new Pydantic models (ConcertParty, NomineeRelationship) + five additive fields on BeneficialOwnerResult + two list fields on the OwnershipGraph dataclass, all in the editable-installed trustrelay-models package; consumers pick them up with no reinstall.
  • Debuggability: every derived controller carries a legible basis. control_bases names the DISTINCT control mechanism(s) per person (veto_rights / appoint_remove_board / ... / majority_ownership / acting_in_concert / nominee); a synthesized ControlTrace documents each concert/nominee attribution; audit_note names the concert group + combined % or the nominee→nominator attribution. A wrong determination is answerable from the persisted result alone ("via which mechanism, and on what declared signal?") without re-deriving (EU AI Act Art. 12).
  • Reversibility: additive. Every new field defaults empty/False; the two passes are guarded no-ops without their declared inputs. Reverting is deleting the two passes + the field additions — no migration (the results ride the existing ubo_computations.results JSONB).
  • Blast radius: additive-with-defaults. qualified_via/reason_code are byte-unchanged for every pre-#538 case (the 25-test test_ubo_engine.py suite is green unmodified); the new bases only appear when a concert group / nominee edge is declared. The derived

    50%-ownership control hop is relabelled majority_votingmajority_ownership, which no consumer read (verified by grep — the only majority_voting reader is the INBOUND graph_service._map_control_type, which maps registry strings to ControlEdge, not the engine's derived hop). amlr_section_c._EVIDENCED_BASES gains the two new BO bases so a concert/nominee BO counts toward CDD coverage (never-suppress).

  • Alternative considered: fold acting-in-concert / nominee attribution into the existing _apply_art54 pass — rejected; Art. 53(3) concert aggregation and Art. 53(4) nominee attribution are distinct mechanisms with distinct declared inputs and distinct audit markers, and conflating them into the Art. 54 coexistence pass would blur the very taxonomy this ADR exists to keep distinct.

Context

Issue #538 (AMLR readiness epic #528, Wave 2 — the BO-graph flagship) replaces an inert control schema and a single ">50% ownership implies control" shortcut with a real control-derivation engine. AMLR Art. 53 recognises control by means other than ownership; the pre-#538 UBOComputationEngine carried the machinery but flattened it:

  1. ControlType flattening. The ControlType enum's five values (MAJORITY_VOTING / APPOINT_REMOVE_BOARD / VETO_RIGHTS / CONTROL_PROFIT_DISTRIBUTION / OTHER_DOMINANT_INFLUENCE) were carried on the ControlEdge/ControlHop but every control determination collapsed to a single qualified_via=["control"] — a person controlling via veto rights was indistinguishable in the result from one appointing the board, defeating Art. 12 traceability.

  2. No acting-in-concert. AMLR Art. 53(3): parties acting in concert are assessed on their COMBINED holding. The engine had no way to express a declared concert group, so four minority holders jointly holding >50% were each read as a below-threshold non-owner — a missed controller.

  3. No first-class nominee. AMLR Art. 53(4): a nominee holds for a nominator; the NOMINATOR is the beneficial owner. The engine had no nominee relationship, so a classic nominee-director structure recorded the front (or nobody) rather than the real principal.

  4. Threshold conflation. The >50% ownership test produced a control hop labelled majority_voting — the SAME string as the explicit MAJORITY_VOTING ControlType — so 50%+1 control-through-ownership was not distinguishable from a declared voting-majority right, and it was not reported distinctly from the 25% ownership-INTEREST threshold or the ADR-0127 sanctions >50% test (taxonomy-conflation is a tracked Calibration-Review defect class).

Scope of THIS issue is the engine derivation + the input model — the pure, testable heart, exercised with hand-built OwnershipGraph fixtures. Deriving the concert-group / nominee inputs FROM OSINT/registry (graph-population) is a separate follow-up (issue #630, ref #528).

The already-correct Decimal arithmetic (#540 / ADR-0130 — exact Decimal, never Decimal(float)), the typed-absence handling (#530 / ADR-0067 fail-closed), and the per-computation threshold snapshot (#542 / ADR-0133) are preserved.

Decision

Extend UBOComputationEngine (ubo_engine.py) with the four control mechanisms, keeping binary reachability but making the BASIS legible, and add the two declared-signal inputs to the model. All arithmetic stays exact Decimal under the fixed context; every mechanism only ever ADDS controllers/BOs (never removes one), and the two declared mechanisms (concert, nominee) fire ONLY on their declared signal (never inferred → never a fabricated BO).

1 — ControlType-aware derivation (basis legible per type)

BeneficialOwnerResult gains control_bases: list[str] — the sorted, deduped set of the control mechanisms that conferred control on the person: the ControlType values of the contributing control-chain hops (veto_rights, appoint_remove_board, …) plus the distinct markers below. qualified_via stays ["control"] (byte-compatible), but the per-type basis is now legible in control_bases and on each ControlHop.control_type in control_paths.

2 — Acting-in-concert aggregation (Art. 53(3))

New ConcertParty model (party_id, member_ids, basis, label) on OwnershipGraph.concert_groups. _apply_concert sums the members' combined KNOWN ownership toward the subject (exact Decimal); if it strictly exceeds the >50% majority-control threshold, every natural-person member resolves as a controller via acting_in_concert (company members count toward the combined sum but are not themselves beneficial owners — only natural persons are). A member who independently qualified keeps their basis and GAINS acting_in_concert; a member whose sole basis is the concert gets qualified_via=["acting_in_concert"]. Concert is a declared legal signal (a shareholders' agreement / voting pact) — never inferred from arithmetic, so it can never fabricate a beneficial owner. A group at exactly 50% does not confer control (strict >).

3 — First-class nominee relationship (Art. 53(4))

New NomineeRelationship model (nominee_id, nominator_id, formal_arrangement, evidence_ref) on OwnershipGraph.nominee_edges. _apply_nominee computes the nominee's aggregated ownership + control chains to the subject and ATTRIBUTES them to the NOMINATOR as the beneficial owner (qualified_via=["nominee"], the holding carried across — never dropped); the nominee is retained but labelled is_nominee=True with nominee_for naming the nominator, and its qualification is corrected so it is not mistaken for the beneficial owner. Revealing the previously-hidden principal is net MORE scrutiny, never less. Fail-closed: a declared nominee that holds/controls nothing of the subject attributes nothing (never a fabricated nominator BO).

4 — 50%+1 control-through-ownership as a DISTINCT third threshold (Art. 53(2))

BeneficialOwnerResult gains two distinct, separately-reported booleans: ownership_interest_met (meets the 25%/jurisdiction ownership-INTEREST threshold → BO via ownership, Art. 52) and majority_control_via_ownership (meets the DISTINCT >50% "50%+1" control-through-ownership threshold, Art. 53(2)). The 50%+1 test is computed on the aggregated exact-Decimal holding (aggregated > _majority_dec), so it also catches a person exceeding 50% via multiple sub-majority paths, and it adds the distinct majority_ownership control basis. These are the FIRST TWO of three thresholds kept structurally distinct; the THIRD — the ADR-0127 sanctions 50%-rule (sanctioned_ownership.py) — is a separate module. An entity can be a 25%-interest BO, a 50%+1 controller, or both — each labelled, none conflated. The 50%+1 threshold is a fixed regulatory constant (the EU 50%-rule), NOT the jurisdiction-configurable 25% snapshot (#542), and is deliberately not routed through ThresholdSnapshot.

Consequences

Positive

  • A control determination now names the mechanism (veto / board / concert / nominee / 50%+1) it rests on — Art. 12 traceability, not a flat "control".
  • Four minority holders acting in concert, or a hidden principal behind a nominee, are now identified as beneficial owners where the pre-#538 engine missed them entirely.
  • The three ownership/control thresholds (25% interest, 50%+1 control, sanctions 50%-rule) are reported distinctly and can never be conflated in a determination.
  • Every mechanism is strictly additive scrutiny; the concert/nominee mechanisms are gated on a declared signal, so they cannot fabricate a beneficial owner.

Negative

  • BeneficialOwnerResult widens by five fields and the OwnershipGraph by two lists; consumers that snapshot the full result see the new keys (all default-safe).
  • The engine cannot yet POPULATE concert groups / nominee edges from OSINT/registry — that is a declared follow-up (issue #630). Until then these inputs arrive only from a caller that constructs them explicitly, so the production path is behaviourally unchanged.

Neutral

  • The derived >50%-ownership control hop is relabelled majority_votingmajority_ownership; no consumer read that string (the only majority_voting reader maps INBOUND registry strings to ControlEdge, unaffected).
  • No migration: the widened results ride the existing append-only ubo_computations.results JSONB (the service already serialises with mode="json").

Alternatives Considered

Alternative 1: Encode per-type control in qualified_via

  • Push veto_rights / appoint_remove_board directly into qualified_via.
  • Why rejected: the existing contract pins qualified_via == ["control"] across the suite and downstream (amlr_section_c, dashboard_agent); a distinct additive control_bases gives the per-type legibility without breaking that contract.

Alternative 2: Infer acting-in-concert from arithmetic (e.g. co-located minority holders)

  • Treat a cluster of minority holders as a concert group automatically.
  • Why rejected: acting-in-concert is a LEGAL determination (a declared coordinated exercise of rights); inferring it fabricates beneficial owners on arithmetic alone — the presence-≠-evidence / false-BO defect. Concert requires a declared ConcertParty.

Alternative 3: Drop the nominee from the results entirely

  • Remove the nominee once its holding is attributed to the nominator.
  • Why rejected: never-suppress requires the underlying holding be retained. The nominee is kept, labelled is_nominee with nominee_for, and only its qualification is corrected — the record and the holding survive; the real owner is added.

Alternative 4: Route the 50%+1 threshold through the #542 ThresholdSnapshot

  • Snapshot the majority-control threshold like the 25% ownership-interest threshold.
  • Why rejected: the 50%+1 line is a fixed EU regulatory constant, not a jurisdiction/effective-dated rule; snapshotting it would imply a configurability it does not have and blur it against the 25% interest threshold the snapshot governs.