Skip to main content

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 a BeneficialOwnerResult's already-computed fields (path traces, qualified_via, control_bases) — no Neo4j, no DB, no LLM; it runs once per determination inside UBOComputationService.compute, the same place the ThresholdSnapshot is stamped. The currency job (MonitoringCheckService.check_bo_dataset_currency) is a single max(computed_at) read of ubo_computations plus (only when overdue) one open-alert dedup read + one MonitoringAlert insert — 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-models package (Art62Provenance, Art62Identity, Art62NatureAndExtent, Art62Dataset) + one additive BeneficialOwnerResult.art62 field; two appended enum members (MonitoringCheckType.bo_dataset_currency, MonitoringTriggerType.bo_dataset_stale) on String(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 explicit gaps marker (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 the MonitoringEvent.current_state and the alert's evidence_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; compute behaves identically to pre-#541 except that each result now carries an art62 block (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-only ubo_computations.results JSONB the service already serialises with mode="json"; the currency clock rides the existing ubo_computations.computed_at; alerts ride the existing monitoring_alerts table).
  • Blast radius: additive-with-defaults. The ownership/control/SMO/arrangement determination in UBOComputationService.compute is byte-unchanged (test_ubo_service.py + test_ubo_engine.py green unmodified beyond one new byte-unchanged assertion); the currency check is a new MonitoringCheckType branch 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_at column + a new currency table — rejected. ubo_computations is already an append-only, per-tenant, RLS-scoped record of every BO computation with a computed_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. Riding computed_at keeps 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-encrypted investigation_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 qualifying mechanisms (qualified_via + control_bases, deduped + sorted) and a human-readable basis. An unknown-weight path (#530) is EXCLUDED from the split (same rule as the aggregate) — never counted as a clean 0. direct_pct/indirect_pct are None when the person has no assessable ownership dimension (control-only / SMO-fallback / role-based arrangement BO) — a disclosed gap, recorded in gaps.
  • multi_entity_structure_description — the interposed-entity chain(s) named from the multi-hop path edges (a direct holding contributes none).
  • identity: Art62Identity | None and date_bo_status_acquired — ALWAYS disclosed honest gaps here (None + a gaps marker), 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).