ADR-0055: UBO Senior Managing Official (SMO) fallback
Status: Accepted Date: 2026-06-15 Supersedes: none Superseded by: none Deciders: Adrian (Soft4U), Claude Opus 4.8
Decision context:
- Latency: one extra Neo4j read (active directors) in
fetch_ownership_graph, plus an O(directors) post-pass in the engine. Negligible; only the post-pass list-build runs, and only when no qualified UBO exists. - Dependency surface: no new packages. Adds
SmoCandidatetotrustrelay-models,audit_notetoBeneficialOwnerResult,smo_candidatestoOwnershipGraph, a post-pass to the engine, one query to the adapter. - Debuggability: each SMO result carries
reason_code="smo_fallback"and anaudit_notestating the ownership/control bases were exhausted — self-explaining for EU AI Act Art. 12. - Reversibility: branch revert. All model changes are additive (no field-type changes), so no consumer breaks.
- Blast radius: additive — the post-pass only appends when no qualified UBO exists; the ownership
(ADR-0053) and control (ADR-0054) traversals are untouched. One behavioural change:
get_amlr_coverage§2(c)ubo_identifiednow becomes satisfied via the SMO fallback for entities with directors but no owner/controller (intended AMLR behaviour). - Alternative considered: elect only the single most-senior official (rejected — title seniority is not reliably orderable across jurisdictions; see Alternatives).
Context
ADR-0053 (ownership) and ADR-0054 (control) determine UBOs from the two AMLR bases. But AMLR requires a UBO of last resort: when no natural person qualifies via ownership or control, the senior managing official(s) are designated as beneficial owner. The engine previously returned no qualified result in that case, leaving the entity with "0 UBOs" — non-compliant.
Directors / senior management are already in the graph as HAS_DIRECTOR edges (Company→Person, role,
active = invalid_at IS NULL), populated by the registry ETL. They are the SMO candidate pool. Only
citation strings for the fallback existed (decision_memorandum_service.py:741,
dashboard_agent.py:953) — no logic.
Article citation note (honesty). The issue (#32) references AMLR Art. 53; the existing codebase citation for the SMO fallback is Art. 42(1) (
decision_memorandum_service.py:741). We cite Art. 42(1) as primary (consistent with existing code) and record the issue's Art. 53 reference here. The behaviour — SMO as UBO of last resort — is unambiguous; only the exact article number is uncertain, and this ADR does not assert false certainty on it.
Decision
Add an SMO fallback post-pass to the pure engine, fired only when ownership + control yield zero
qualified UBOs:
- Models (
trustrelay-models/ubo.py):SmoCandidate(person_id, person_name, role);OwnershipGraph.smo_candidates;BeneficialOwnerResult.audit_note. - Engine (
ubo_engine.py): after the ownership+control per-person loop, ifnot any(r.qualified for r in results) and graph.smo_candidates, append oneBeneficialOwnerResultper candidate withqualified=True,qualified_via=["smo_fallback"],reason_code="smo_fallback",aggregated_pct=0.0, and anaudit_noterecording that the ownership (Art. 51) and control (Art. 52) bases were exhausted. The guard keys onqualified— a below-threshold near-miss (emitted unqualified) does not suppress the fallback; a control-only or ownership UBO does. - Elect all active directors (plural) — not a single "most senior."
- Adapter (
graph_service.fetch_ownership_graph): read active directors (HAS_DIRECTOR,invalid_at IS NULL, tenant-scoped) intosmo_candidates, keyedperson:<name>.
Consequences
Positive
- Closes the AMLR SMO gap; an entity with no qualifying owner/controller no longer reports "0 UBOs."
audit_noterecords why the fallback fired (explainability).- Reuses existing
HAS_DIRECTORdata — no new ETL. - Additive models — no consumer breaks.
Negative
- Elects all active directors rather than a single "most senior" — may over-list for large boards (acceptable; narrowing is a follow-up).
ubo_identified(§2(c)) broadens to count the SMO fallback — intended, but a behavioural change to coverage thatreason_code="smo_fallback"keeps auditable.
Neutral
- SMO results persist in
ubo_computationslike any other (model_dumpround-trips).
Alternatives Considered
Alternative 1: Elect only the single most-senior official
Rank directors by role/title and elect one.
- Why rejected: title seniority is not reliably orderable across jurisdictions and languages (Gérant, Managing Director, Geschäftsführer, Bestuurder, …). AMLR uses the plural; electing all active directors is non-arbitrary and defensible. A single-election refinement can follow if a customer needs it.
Alternative 2: Put the fallback in UBOComputationService rather than the pure engine
- Why rejected: SMO election is regulated determination logic and belongs with the ownership/control arithmetic in the pure, unit-testable engine (same rationale as ADR-0053/0054). The service stays orchestration + persistence.
Alternative 3: Mark the SMO result qualified=False (informational only)
- Why rejected: AMLR treats the SMO as the beneficial owner when none is found via ownership/control —
it is a real determination, not a note.
qualified=Truewithreason_code="smo_fallback"is the faithful model; downstream CDD must treat the SMO as the identified beneficial owner.