Skip to main content

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_ubos is a pure in-memory pass over the parties of an arrangement (no Neo4j / no DB); it runs in UBOComputationService.compute only when the caller passes arrangements=[...], which no production caller does yet (graph-POPULATION is follow-up #632). The recursion into a corporate party reuses the existing UBOComputationEngine.compute over an already in-memory OwnershipGraph; depth is bounded at MAX_ARRANGEMENT_DEPTH=10 with 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 additive ArrangementParty fields + one additive LegalArrangement field + two additive BeneficialOwnerResult fields — all in the editable-installed trustrelay-models package; consumers pick them up with no reinstall. legal_arrangement.py gains a one-directional import of OwnershipGraph from ubo.py (ubo.py does NOT import legal_arrangement — the ArrangementStateTransition fields are plain strings), so there is no import cycle.
  • Debuggability: every arrangement BO carries a legible basis. qualified_via is ["arrangement_role"] for a role-based BO (byte-compatible with the pre-#539 determination) and gains arrangement_lookthrough for a natural person reached by recursing through a corporate party; reason_code names the role + Art. 60 state (arrangement_beneficiary_selected / _default_active / _potential); arrangement_transition carries the timestamped Art. 60 transition (from/to state + activates_bo + ISO occurred_at); audit_note names 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; compute behaves identically when arrangements is None/empty. Reverting is deleting the compute_arrangement_ubos concatenation block + the field additions — no migration (arrangement results ride the existing append-only ubo_computations.results JSONB the service already serialises with mode="json").
  • Blast radius: additive-with-defaults. The non-arrangement ownership/control path in UBOComputationService.compute is byte-unchanged when arrangements is None (the 20-test test_ubo_service.py and 40-test test_ubo_engine.py suites are green unmodified); arrangement results only appear when a caller constructs and passes LegalArrangement objects. amlr_section_c._EVIDENCED_BASES gains arrangement_role + arrangement_lookthrough so 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:

  1. No merge into the UBO result. UBOComputationService.compute returned 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 a LegalArrangement.

  2. 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.

  3. 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) : POTENTIALSELECTED (the object of a power was selected / benefited);
  • activate_default_taker(party, at) : POTENTIALDEFAULT_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_assessed gap, not a silent corporate wall.
  • An unenumerable class of beneficiaries is an explicit gap — the "class we cannot see" is visible.

Negative

  • ArrangementParty widens by six fields, LegalArrangement by one, BeneficialOwnerResult by two; consumers that snapshot the full result see the new keys (all default-safe).
  • The engine cannot yet POPULATE LegalArrangement objects 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_ubos gains a dependency on UBOComputationEngine (lazy-imported) for the corporate-party recursion, coupling the two previously-independent modules.

Neutral

  • legal_arrangement.py now imports OwnershipGraph from ubo.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.results JSONB.

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_assessed potential 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_assessed gap rather than a false terminal.

Alternative 4: Populate arrangements from OSINT/registry in this issue

  • Build the trust/foundation graph reader and the fetch_arrangements producer 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.