ADR-0135: Look-through legal-arrangement UBO regimes + Art. 60 discretionary state machine + wiring the dead arrangement engine
Date: 2026-07-25 Status: Accepted Deciders: Adrian (Soft4U BV), Claude Opus 4.8 (implementation agent)
Decision context:
- Latency: not measured —
compute_arrangement_ubosis a pure in-memory pass over the parties of an arrangement (no Neo4j / no DB); it runs inUBOComputationService.computeonly when the caller passesarrangements=[...], which no production caller does yet (graph-POPULATION is follow-up #632). The recursion into a corporate party reuses the existingUBOComputationEngine.computeover an already in-memoryOwnershipGraph; depth is bounded atMAX_ARRANGEMENT_DEPTH=10with a cycle guard. The dominant cost of a real UBO computation remains the Neo4j ownership-graph read, unchanged. - Dependency surface: zero new packages. Three new enums (
ArrangementPartyState,DiscretionaryKind)ArrangementType.CIU, one new model (BeneficiaryClass), one new model on the ubo side (ArrangementStateTransition), six additiveArrangementPartyfields + one additiveLegalArrangementfield + two additiveBeneficialOwnerResultfields — all in the editable-installedtrustrelay-modelspackage; consumers pick them up with no reinstall.legal_arrangement.pygains a one-directional import ofOwnershipGraphfromubo.py(ubo.py does NOT import legal_arrangement — theArrangementStateTransitionfields are plain strings), so there is no import cycle.
- Debuggability: every arrangement BO carries a legible basis.
qualified_viais["arrangement_role"]for a role-based BO (byte-compatible with the pre-#539 determination) and gainsarrangement_lookthroughfor a natural person reached by recursing through a corporate party;reason_codenames the role + Art. 60 state (arrangement_beneficiary_selected/_default_active/_potential);arrangement_transitioncarries the timestamped Art. 60 transition (from/to state +activates_bo+ ISOoccurred_at);audit_notenames the applicable article (Art. 58 trust / Art. 59 foundation / Art. 61 CIU) and, for a look-through BO, the corporate party it was reached through. A wrong determination is answerable from the persisted result alone (EU AI Act Art. 12) without re-deriving. - Reversibility: additive. Every new field defaults empty/None/ACTIVE;
computebehaves identically whenarrangementsis None/empty. Reverting is deleting thecompute_arrangement_ubosconcatenation block + the field additions — no migration (arrangement results ride the existing append-onlyubo_computations.resultsJSONB the service already serialises withmode="json"). - Blast radius: additive-with-defaults. The non-arrangement ownership/control path in
UBOComputationService.computeis byte-unchanged whenarrangementsis None (the 20-testtest_ubo_service.pyand 40-testtest_ubo_engine.pysuites are green unmodified); arrangement results only appear when a caller constructs and passesLegalArrangementobjects.amlr_section_c._EVIDENCED_BASESgainsarrangement_role+arrangement_lookthroughso an arrangement BO counts toward §2(c) CDD coverage (never-suppress). - Alternative considered: force-fit arrangements into the ownership
OwnershipGraph+ the EBA percentage engine — rejected; a trust/foundation BO is qualified by ROLE (Art. 58, no threshold), so re-using the %-based engine would either fabricate a fake 100% or drop the party under the 25% test; the role-based determination is a distinct regime that must stay distinct.
Context
Issue #539 (AMLR readiness epic #528, Wave 2) completes the legal-arrangement (trust/foundation/CIU) beneficial-ownership path. AMLR Art. 54-61 determines the beneficial owners of a legal arrangement by ROLE — a settlor, trustee, protector, beneficiary, founder, or governing-body member is a beneficial owner regardless of any ownership percentage (Art. 58 for trusts, Art. 59 for foundations and similar arrangements, Art. 61 lex specialis for collective investment undertakings). This is a fundamentally different regime from the Art. 51-53 percentage-and-control ownership engine.
The model (LegalArrangement / ArrangementParty / ArrangementRole, ADR-0061) and the pure
role-based computation (arrangement_ubo.compute_arrangement_ubos) already existed — but the compute
was dead code: its only caller was its own test. Three regulatory dimensions were entirely absent:
-
No merge into the UBO result.
UBOComputationService.computereturned only the ownership/ control/SMO results from the graph engine. A trust's role-based beneficial owners were never surfaced, even when a caller had aLegalArrangement. -
No Art. 60 discretionary state machine. For a DISCRETIONARY trust the beneficial-owner determination is STATEFUL: an object of a power becomes a beneficial owner when selected or when it benefits; a default taker becomes a beneficial owner when the trustee does NOT exercise discretion. The flat "every listed party is a BO" model could not express "not yet a BO, but must not be dropped" — the exact fail-closed case AMLR discretionary trusts exist to catch.
-
No multi-layer recursion. A foundation whose trustee is a corporate entity must resolve THROUGH to the natural-person beneficial owners (Art. 57(2)/58(3)); the flat model stopped at the corporate entity — a silent chain-break that reports a corporate wall as if it were the answer.
Scope of THIS issue is the engine/model core — the pure, testable determination exercised with
hand-built LegalArrangement fixtures. Deriving the arrangement inputs FROM OSINT/registry
(graph-population: trust/foundation nodes + party edges → a fetch_arrangements reader) is a separate
follow-up (issue #632, ref #528), exactly as #538's control-via-other-means graph-population was
deferred to #630.
The already-correct exact-Decimal ownership arithmetic (#540 / ADR-0130), the effective-dated
threshold snapshot (#542 / ADR-0133), and the control-via-other-means derivation (#538 / ADR-0134) on
the percentage path are preserved untouched — arrangements are role-based and carry no percentage, so
they never touch that path.
Decision
Wire compute_arrangement_ubos into UBOComputationService.compute and extend it with the Art. 60
state machine, multi-layer recursion, a beneficiary-class gap, and type-selected regimes. Everything is
additive-with-defaults; every mechanism only ever ADDS a beneficial owner or ADDS scrutiny, never
removes one; a state or chain that cannot be resolved is surfaced as not_assessed, never silently
dropped (fail-closed, ADR-0067).
1 — Wire the dead compute into UBOComputationService.compute
compute gains an additive arrangements: list[LegalArrangement] | None = None parameter. After the
ownership/control/SMO engine.compute(graph) results are computed and stamped with the
ThresholdSnapshot, the arrangement UBOs are concatenated first-class into the returned list. They are
role-based (no percentage), so they are deliberately NOT stamped with the ownership ThresholdSnapshot
(threshold_snapshot stays None — an honest "no percentage threshold applied"). When arrangements
is None/empty the result set is byte-unchanged. Arrangement results serialise into the existing
append-only ubo_computations.results JSONB — no migration.
2 — Art. 60 discretionary-trust state machine
ArrangementParty gains state: ArrangementPartyState (active / potential / selected /
default_active, default active) and discretionary_kind: DiscretionaryKind (none /
object_of_power / default_taker, default none). A plain named party is active + none —
byte-compatible with the pre-#539 unconditional-BO determination. A discretionary party originates
potential; two transition functions advance it:
activate_object_of_power(party, at):POTENTIAL→SELECTED(the object of a power was selected / benefited);activate_default_taker(party, at):POTENTIAL→DEFAULT_ACTIVE(the trustee did not exercise discretion).
Both are fail-closed: an illegal transition (wrong discretionary_kind or wrong current state) raises
ValueError and leaves the party UNCHANGED — never a silent activation. Each stamps
state_changed_at. compute_arrangement_ubos emits, for every discretionary party, a timestamped
ArrangementStateTransition (role, kind, from/to state, ISO occurred_at = the real event time when
known else the observation time, and activates_bo). An active/selected/default_active party is
a qualified BO; a still-potential party is surfaced as a NOT-yet-excluded potential BO
(qualified=False, ownership_outcome="not_assessed", reason_code=arrangement_<role>_potential) —
never dropped.
3 — Multi-layer recursion (Art. 57(2)/58(3))
ArrangementParty gains is_natural_person: bool = True and two recursion inputs for a non-natural
party: nested_arrangement: LegalArrangement | None (the party is itself a trust/foundation → recurse
compute_arrangement_ubos) and nested_ownership: OwnershipGraph | None (the party is a company →
resolve via UBOComputationEngine.compute). Natural persons reached this way are tagged
arrangement_lookthrough in qualified_via and carry an audit note naming the corporate party they
were reached through. A non-natural party with neither input, or one that exceeds
MAX_ARRANGEMENT_DEPTH=10 or forms a cycle, is surfaced as not_assessed
(arrangement_<role>_entity_unresolved) — never a silent stop at the entity.
4 — Beneficiary class as an explicit gap
LegalArrangement gains beneficiary_classes: list[BeneficiaryClass]. An enumerated class
(enumerable=True with members) resolves each member like a named party; an unenumerable class emits a
single explicit gap row (qualified=False, ownership_outcome="not_assessed",
reason_code=arrangement_beneficiary_class_gap) naming the class — never a clean "no BO".
5 — Type selects the regime (legible in the audit trail)
ArrangementType.CIU is added. _REGIME_BY_TYPE maps the type to its AMLR article — trust → Art. 58,
foundation / similar → Art. 59, CIU → Art. 61 (lex specialis) — which is rendered into every result's
audit_note, keeping the applicable rule set legible per determination.
amlr_section_c._EVIDENCED_BASES gains arrangement_role + arrangement_lookthrough so an arrangement
beneficial owner counts toward §2(c) CDD coverage (never-suppress); a potential party is
qualified=False and is correctly excluded from the evidenced set.
Consequences
Positive
- A trust/foundation/CIU's role-based beneficial owners are now first-class in the UBO result, no longer dead code; the applicable article (58/59/61) is legible per determination.
- Discretionary trusts are handled statefully and honestly: an object of a power / default taker is a timestamped transition, and an unactivated potential BO is surfaced for monitoring, never dropped.
- A corporate trustee resolves through to the natural-person beneficial owners; a chain that hits an
unresolvable entity is a documented
not_assessedgap, not a silent corporate wall. - An unenumerable class of beneficiaries is an explicit gap — the "class we cannot see" is visible.
Negative
ArrangementPartywidens by six fields,LegalArrangementby one,BeneficialOwnerResultby two; consumers that snapshot the full result see the new keys (all default-safe).- The engine cannot yet POPULATE
LegalArrangementobjects from OSINT/registry — that is a declared follow-up (issue #632). Until then arrangements arrive only from a caller that constructs them explicitly, so the production path is behaviourally unchanged. compute_arrangement_ubosgains a dependency onUBOComputationEngine(lazy-imported) for the corporate-party recursion, coupling the two previously-independent modules.
Neutral
legal_arrangement.pynow importsOwnershipGraphfromubo.py(one-directional; ubo.py keeps the Art. 60 transition fields as plain strings, so no cycle).- No migration: the arrangement results ride the existing append-only
ubo_computations.resultsJSONB.
Alternatives Considered
Alternative 1: Force arrangements into the ownership OwnershipGraph + the percentage engine
- Represent a trust's parties as ownership edges and run the existing EBA percentage engine.
- Why rejected: an Art. 58 beneficial owner is qualified by ROLE with NO threshold; modelling a settlor as a 100% owner fabricates a percentage that does not exist, and modelling them below 25% would drop a genuine beneficial owner. The role-based regime is legally distinct and must stay a distinct pass.
Alternative 2: Treat an unactivated discretionary party as a full beneficial owner
- Qualify every object of a power / default taker as a confirmed BO regardless of state.
- Why rejected: it over-states — an object of a power that has never been selected is not yet a
beneficial owner under Art. 60. But dropping it would suppress a signal, so the chosen middle is a
surfaced
not_assessedpotential BO: scrutiny added, nothing hidden, nothing over-claimed.
Alternative 3: Stop at a corporate trustee and report a corporate wall
- Emit the corporate trustee as the terminal party without recursing.
- Why rejected: Art. 57(2)/58(3) require resolving through to the natural persons; stopping at the
entity reports a corporate wall as the answer — a silent chain-break. The chosen design recurses, and
where it genuinely cannot resolve, emits an explicit
not_assessedgap rather than a false terminal.
Alternative 4: Populate arrangements from OSINT/registry in this issue
- Build the trust/foundation graph reader and the
fetch_arrangementsproducer now. - Why rejected: the same scoping decision as #538 (graph-population deferred to #630). The engine/model core is the testable heart; the producer is a separate, larger surface (registry endpoints, entity resolution) tracked as follow-up #632.