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_controlDFS over the same in-memory graph the engine already walks; both are no-ops (returnon 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 inUBOComputationService, unchanged. - Dependency surface: zero new packages. Two new Pydantic models (
ConcertParty,NomineeRelationship) + five additive fields onBeneficialOwnerResult+ two list fields on theOwnershipGraphdataclass, all in the editable-installedtrustrelay-modelspackage; consumers pick them up with no reinstall. - Debuggability: every derived controller carries a legible basis.
control_basesnames the DISTINCT control mechanism(s) per person (veto_rights/appoint_remove_board/ ... /majority_ownership/acting_in_concert/nominee); a synthesizedControlTracedocuments each concert/nominee attribution;audit_notenames 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.resultsJSONB). - Blast radius: additive-with-defaults.
qualified_via/reason_codeare byte-unchanged for every pre-#538 case (the 25-testtest_ubo_engine.pysuite is green unmodified); the new bases only appear when a concert group / nominee edge is declared. The derived50%-ownership control hop is relabelled
majority_voting→majority_ownership, which no consumer read (verified by grep — the onlymajority_votingreader is the INBOUNDgraph_service._map_control_type, which maps registry strings toControlEdge, not the engine's derived hop).amlr_section_c._EVIDENCED_BASESgains 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_art54pass — 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:
-
ControlType flattening. The
ControlTypeenum's five values (MAJORITY_VOTING/APPOINT_REMOVE_BOARD/VETO_RIGHTS/CONTROL_PROFIT_DISTRIBUTION/OTHER_DOMINANT_INFLUENCE) were carried on theControlEdge/ControlHopbut every control determination collapsed to a singlequalified_via=["control"]— a person controlling via veto rights was indistinguishable in the result from one appointing the board, defeating Art. 12 traceability. -
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.
-
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.
-
Threshold conflation. The
>50%ownership test produced a control hop labelledmajority_voting— the SAME string as the explicitMAJORITY_VOTINGControlType — 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
BeneficialOwnerResultwidens by five fields and theOwnershipGraphby 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_voting→majority_ownership; no consumer read that string (the onlymajority_votingreader maps INBOUND registry strings toControlEdge, unaffected). - No migration: the widened results ride the existing append-only
ubo_computations.resultsJSONB (the service already serialises withmode="json").
Alternatives Considered
Alternative 1: Encode per-type control in qualified_via
- Push
veto_rights/appoint_remove_boarddirectly intoqualified_via. - Why rejected: the existing contract pins
qualified_via == ["control"]across the suite and downstream (amlr_section_c,dashboard_agent); a distinct additivecontrol_basesgives 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_nomineewithnominee_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.