ADR-0136: BO record as an AMLR Art. 62 regulated dataset + 28-day / annual data-currency job
Date: 2026-07-25 Status: Accepted Deciders: Adrian (Soft4U BV), Claude Opus 4.8 (implementation agent)
Decision context:
- Latency: not measured. The Art. 62 dataset derivation (
derive_art62_dataset) is a pure in-memory pass over aBeneficialOwnerResult's already-computed fields (path traces,qualified_via,control_bases) — no Neo4j, no DB, no LLM; it runs once per determination insideUBOComputationService.compute, the same place theThresholdSnapshotis stamped. The currency job (MonitoringCheckService.check_bo_dataset_currency) is a singlemax(computed_at)read ofubo_computationsplus (only when overdue) one open-alert dedup read + oneMonitoringAlertinsert — the same shape as the #407 missing-required-documents check. The dominant cost of a UBO computation remains the Neo4j ownership-graph read, unchanged. - Dependency surface: zero new packages. Four additive models in the editable-installed
trustrelay-modelspackage (Art62Provenance,Art62Identity,Art62NatureAndExtent,Art62Dataset) + one additiveBeneficialOwnerResult.art62field; two appended enum members (MonitoringCheckType.bo_dataset_currency,MonitoringTriggerType.bo_dataset_stale) onString(50)-backed columns; one config flag (bo_dataset_currency_monitoring_enabled, default False). Consumers pick the models up with no reinstall. - Debuggability: every set Art. 62 value carries
Art62Provenance(source="ubo_engine.determination",as_of=confirmation timestamp,method), and every unpopulated field carries an explicitgapsmarker (identity:no_verified_source/date_bo_status_acquired:no_source/nature_and_extent.ownership_pct:not_ownership_based). A currency finding names its band (never_confirmed/update_overdue/annual_reconfirmation_overdue), the last-confirmed timestamp, and the age in days, in both theMonitoringEvent.current_stateand the alert'sevidence_refs. A wrong dataset or a wrong overdue flag is answerable from the persisted record alone (EU AI Act Art. 12) without re-deriving. - Reversibility: additive. Every new model field defaults None/empty;
computebehaves identically to pre-#541 except that each result now carries anart62block (the numeric determination is byte-unchanged). The currency check is dark-launched OFF — flag-off is a zero-cost no-op (no DB read, no alert). Reverting is deleting the derivation call + the check method + the field/enum/flag additions — no migration (art62 rides the existing append-onlyubo_computations.resultsJSONB the service already serialises withmode="json"; the currency clock rides the existingubo_computations.computed_at; alerts ride the existingmonitoring_alertstable). - Blast radius: additive-with-defaults. The ownership/control/SMO/arrangement determination in
UBOComputationService.computeis byte-unchanged (test_ubo_service.py+test_ubo_engine.pygreen unmodified beyond one new byte-unchanged assertion); the currency check is a newMonitoringCheckTypebranch gated behind a default-off flag. Three enum-membership guard tests updated to include the two new members (a deliberate, reviewed vocabulary change). - Alternative considered: persist a dedicated
bo_last_confirmed_atcolumn + a new currency table — rejected.ubo_computationsis already an append-only, per-tenant, RLS-scoped record of every BO computation with acomputed_at;max(computed_at)over a case IS the last-confirmed clock. A new column would need a migration, would duplicate a fact the append-only trail already holds, and would risk drifting from the computation it purports to date. Ridingcomputed_atkeeps the clock and the evidence the same row (see the migration note below). - Alternative considered: populate the Art. 62 identity fields (DOB/nationality/residence/TIN) in this
issue — rejected as out of bounded scope. The pure ownership engine has no identity source (it
operates on the
OwnershipGraph), and the identity data lives in the PII-encryptedinvestigation_persons/ verification profile (ADR-0106). Fabricating those fields would violate the presence≠evidence discipline; leaving them as disclosed honest gaps with a tracked follow-up (#634) is the fail-closed choice — exactly as #538's control-population deferred to #630 and #539's arrangement-population deferred to #632.
Context
Issue #541 (AMLR readiness epic #528, Wave 2) has two parts, both grounded in AMLR Art. 62 (the information a beneficial-ownership register must hold) and the AMLR data-currency duties (update within 28 calendar days of a change; re-confirm at least annually).
After #540/#542/#538/#539, BeneficialOwnerResult carries the determination — aggregated percentage
(exact Decimal), qualification, qualified_via/reason_code, threshold snapshot, control fields,
arrangement fields — but none of the Art. 62 dataset dimensions: no direct-vs-indirect split of the
interest, no multi-entity structure description, no natural-person identity (DOB / nationality /
residence / national-id / TIN), no date the BO status was acquired, and no provenance stamp on any of
it. A BO register cannot be populated from a record that lacks these fields.
Separately, there was no data-currency check on the BO record at all. A BO dataset computed once at
onboarding and never refreshed would sit stale indefinitely — the exact failure the AMLR 28-day update
window and annual re-confirmation cadence exist to prevent — with nothing surfacing it. The monitoring
framework (ADRs 0083-0088) already runs cadence-gated sweeps that emit typed MonitoringAlerts through
the ADR-0084 disposition lifecycle; there was simply no check on this axis.
Scope of THIS issue is the model + currency job — widen the record to the Art. 62 shape and populate the fields readily derivable from the determination, and add the currency sweep. The full OSINT/registry/verification population of the identity fields is a separate follow-up (#634, ref #528), exactly as #538→#630 and #539→#632.
Decision
Add an additive Art62Dataset to BeneficialOwnerResult, populate its determination-derived fields
with mandatory provenance (leaving the no-source fields as disclosed honest gaps), and add a
dark-launched monitoring check that flags a BO dataset whose currency has lapsed. Everything is
additive-with-defaults; the check only ever ADDS a finding or ADDS scrutiny; a never-computed dataset is
treated as OVERDUE, never silently current (fail-closed, ADR-0067).
1 — Art62Dataset on BeneficialOwnerResult
A new optional art62: Art62Dataset | None = None field (default None keeps older serialized
ubo_computations.results rows valid). Art62Dataset composes:
nature_and_extent: Art62NatureAndExtent— the AMLR Art. 62(1) DIRECT (single-hop path traces) vs INDIRECT (multi-hop path traces) split of the ownership interest, plus the qualifyingmechanisms(qualified_via+control_bases, deduped + sorted) and a human-readablebasis. An unknown-weight path (#530) is EXCLUDED from the split (same rule as the aggregate) — never counted as a clean 0.direct_pct/indirect_pctare None when the person has no assessable ownership dimension (control-only / SMO-fallback / role-based arrangement BO) — a disclosed gap, recorded ingaps.multi_entity_structure_description— the interposed-entity chain(s) named from the multi-hop path edges (a direct holding contributes none).identity: Art62Identity | Noneanddate_bo_status_acquired— ALWAYS disclosed honest gaps here (None + agapsmarker), because the pure engine has no verified-identity source. Never fabricated.provenance: Art62Provenance— MANDATORY where any value is set (EU AI Act Art. 12), always attached because the nature-and-extent is derived from the determination.gaps: list[str]— one explicit marker per unpopulated field group. An unpopulated field is a DISCLOSED gap, never a clean/absent value (presence≠evidence).
The pure derivation derive_art62_dataset(result, *, as_of) lives in ubo_engine.py (reads only the
result's own fields). UBOComputationService.compute calls it over every determination
(ownership/control/SMO/arrangement) after the ThresholdSnapshot is stamped, with as_of = the
confirmation timestamp (now) — which is also the currency clock via the row's computed_at. Results
serialise into the existing append-only JSONB — no migration.
2 — bo_dataset_currency monitoring check (28-day / annual)
MonitoringCheckType.bo_dataset_currency + MonitoringTriggerType.bo_dataset_stale (both appended;
String(50) columns — no DB migration). MonitoringCheckService.check_bo_dataset_currency reads
max(ubo_computations.computed_at) for the case (the last-confirmed clock) and classifies the currency
band:
never_confirmed— NO computation on file → fail-closed OVERDUE (WARNING). A missing last-confirmed date is NEVER read as current.annual_reconfirmation_overdue— age ≥ 365 days → CRITICAL (the annual re-confirmation duty has lapsed; surfaced loudly).update_overdue— 28 ≤ age < 365 days → WARNING (past the 28-calendar-day update window).current— age < 28 days → INFO no-op, no alert.
An overdue band writes one MonitoringAlert (trigger_type="bo_dataset_stale", ADR-0083 taxonomy /
ADR-0084 lifecycle) with priority 1 (critical) / 2 (warning), the band + last-confirmed timestamp +
age in evidence_refs. Dedup is band-aware (never-suppress): an OPEN alert (status != closed) in
the SAME band suppresses a duplicate, but an escalation to a stricter band (update → annual) fires a
FRESH alert rather than being folded into the open one; re-alert after the ADR-0084 disposition closes
the prior alert. The check is gated by bo_dataset_currency_monitoring_enabled (default OFF —
Calibration-Review-gated to flip; flag-off is a zero-cost no-op). Fail-closed: an unknown tenant or a
read/write error returns an INDETERMINATE WARNING (complete=False), never a benign clean and never a
fabricated alert; it never raises into the scheduler. Wired into run_check dispatch and into the
monitoring_framework_service check descriptors (honest partial maturity naming the identity-field
gap).
Consequences
Positive
- The BO record is now shaped as the AMLR Art. 62 regulated dataset, with the determination-derived dimensions populated and provenance-stamped, and every unpopulated field explicitly disclosed as a gap — a register can be populated from it honestly, and an auditor can see exactly what is evidenced vs. what is a known gap.
- A stale BO dataset now surfaces loudly as a typed data-currency finding on the 28-day / annual cadence, through the same disposition lifecycle as every other monitoring alert; a never-computed dataset is fail-closed overdue, never silently current.
- No migration, no new table, no wall-clock in the numeric determination; the currency clock and the evidence are the same append-only row.
Negative / limitations
- The Art. 62 identity fields (DOB / nationality / residence / national-id / TIN) and the date-BO-status-acquired are disclosed gaps until wired from the PII-encrypted verification data (follow-up #634). The currency check runs per case's BO computation but does not itself trigger a re-computation — it surfaces the lapse; the officer / the AMLR trigger engine drives the refresh.
- The check is dark-launched OFF; flipping it on is Calibration-Review-gated (it feeds a deterministic trigger and writes alerts for every overdue active case).
References
- Issue #541 (AMLR readiness epic #528, Wave 2); follow-up #634 (Art. 62 identity-field population).
- AMLR Reg (EU) 2024/1624 Art. 62 (BO register dataset); 28-day update + annual re-confirmation duties.
- ADR-0067 (fail-closed / never-suppress), ADR-0106 (PII-at-rest encryption), ADR-0083/0084 (monitoring framework + alert disposition lifecycle), ADR-0130/0133/0134/0135 (the preserved UBO determination path).