Skip to main content

ADR-0138: Per-actor-type identity dataset (AMLR Art. 22(1)) + registry-grounding marker

Date: 2026-07-25 Status: Accepted Deciders: Adrian (Soft4U BV), Claude Opus 4.8 (implementation agent)

Decision context:

  • Latency: not measured — compute_identity_datasets is a pure in-memory pass over a list of ActorIdentityInput; no DB/graph/network calls added. Safe to defer.
  • Dependency surface: none new. Twelve typed models added to the existing trustrelay-models package (re-exported via app/models/identity_dataset.py); no third-party packages.
  • Debuggability: the report is a pure function of the supplied per-actor inputs. A wrong grounding state is reproduced by constructing the same ActorIdentityInput — no fixtures, no graph. Each field carries its own article_ref, grounding, verified and gap.
  • Reversibility: high. The record is additive and unwired into any consumer; deleting compute_identity_datasets + identity_dataset_requirement + the model file + the package export restores the prior state with no migration and no behaviour change (the three sibling CDD outputs are byte-unchanged).
  • Blast radius: additive only. interpret_cdd_coverage, compute_cdd_register, compute_cdd_trigger_record, get_amlr_coverage and all their consumers are untouched; no persisted schema; no runtime path calls the record yet (consumers are separate surface issues).
  • Alternative considered: reuse the person VerificationRecord / min-2-source gate (verification_gate.py) as the grounding signal — rejected because that gate models a stronger, different obligation (≥2 independent sources for a person attribute) and does not model the per-actor-type Art. 22(1) dataset (which fields are required for an entity vs a person), nor a distinct-from-verified not_assessed field state.

Context

AMLR (Reg. (EU) 2024/1624) Art. 22(1) specifies the identity dataset an obliged entity must collect and verify, and it differs by actor TYPE. Verbatim from the adopted text (verified against the Lex corpus, lex_articles regulation AMLR, article 22):

  • (a) natural person — (i) all names and surnames; (ii) place and full date of birth; (iii) nationalities (or statelessness / refugee or subsidiary protection status where applicable) and the national identification number where applicable; (iv) the usual place of residence and, where available, the tax identification number.
  • (b) legal entity — (i) legal form and name; (ii) address of the registered / official office and the country of creation; (iii) the names of the legal representatives and, where available, the registration number, the tax identification number and the Legal Entity Identifier; (iv) the names of persons holding shares or a directorship position in nominee form.

The Art. 22(1) chapeau extends the obligation beyond the customer to "any person purporting to act on behalf of the customer" (the Art. 20(1)(i) representative) and "the natural persons on whose behalf or for the benefit of whom a transaction or activity is being conducted" (the Art. 20(1)(h) beneficiary); directors and beneficial owners are the other persons whose identity must be collected. A customer may itself be a legal entity or a natural person; the four other actor types are always natural persons.

Two gaps existed. First, there was no typed record of the per-actor-type identity dataset — nothing said "for this director the required dataset is name/DOB/nationality/ residence/national-ID and here is which are present and verified." Second, the existing interpret_cdd_coverage §2(a) identity section models only the legal-entity customer's name/reg-no/form/address and collapses "verified" into a single identity_corroborated boolean; it has no per-field registry-grounding signal and no representation of the regulatorily-important ADR-0058 rule that a central register is a cross-check only (a BO/transparency register grounding is insufficient on its own — it needs a non-central corroborating source). Re-deriving grounding in each downstream consumer would re-introduce the claim-vs-check defect class (an artifact reporting a control's shape — "a value is on file" — instead of its state — "grounded against a non-central registry").

Decision

Add a typed per-actor-type identity-dataset record as the canonical, structured record of the Art. 22(1) dataset required for each actor and its coverage + registry grounding — the "shared oracle" the assistant / PDF / UI consumers will read (those consumers are separate surface issues; this ADR covers only the record + its pure computation).

Typed models in trustrelay-models (trustrelay_models/identity_dataset.py, re-exported via app/models/identity_dataset.py):

  • ActorTypecustomer / director / beneficial_owner / beneficiary / representative.
  • ActorNaturelegal_entity / natural_person; selects the Art. 22(1)(b) vs (a) dataset.
  • IdentityField — the Art. 22(1) fields (name, place/date of birth, nationality, national-id, refugee-status, residence, tax-id, legal-form, registered-address, country-of-creation, legal-representatives, registration-number, LEI, nominee-status).
  • GroundingState — a four-value enum grounded / central_register_only / ungrounded / not_assessed.
  • RequiredIdentityField (field + article_ref + mandatory), IdentityGroundingSource (source + is_central_register), IdentityFieldInput / ActorIdentityInput (caller evidence), IdentityFieldCoverage (the derived per-field state), IdentityDatasetRequirement, ActorIdentityDataset (per-actor coverage + roll-ups), IdentityDatasetReport.

Pure functions in amlr_section_c.py:

  • identity_dataset_requirement(actor_type, actor_nature=None) — the required Art. 22(1) dataset per actor. The four non-customer actor types resolve to the natural-person dataset by construction; a customer's nature is required (a ValueError is raised rather than silently defaulting, which would mis-derive the dataset).
  • compute_identity_datasets(actor_inputs) — derives, per actor, the required dataset + per-field coverage + grounding.

Registry-grounding marker (the load-bearing decision), fail-closed + ADR-0058:

Evidence for a fieldgroundingverified
≥1 NON-central registry sourcegroundedTrue
central register source(s) onlycentral_register_only (insufficient, ADR-0058)False
a value present, no sourceungrounded (presence ≠ evidence)False
no value, no sourcenot_assessed (fail-closed default)False

verified is True iff grounding == grounded. A central_register_only field carries the honest gap central_register_only_insufficient; a present-but-ungrounded field carries present_but_ungrounded; a missing mandatory field carries mandatory_field_missing (a missing "where available"/"where applicable" conditional field is not itself a gap but is still never verified). The ActorIdentityDataset.fully_grounded roll-up is True iff every mandatory field is grounded via a non-central source — a mandatory field that is ungrounded, central-register-only or not-assessed keeps the actor NOT fully grounded.

This grounding marker is the Art. 22(1) registry-grounding signal (≥1 non-central source, ADR-0058). It is distinct from and does not replace the stronger min-2-independent- source verification GATE (verification_gate.py, ADR-0057), which additionally requires ≥2 independent sources for high-importance person attributes; the two compose (a field can be grounded here yet still block on the min-2 gate).

The record is a sibling of the #544 register / #545 trigger record — it is never bolted onto CddRegister, never touches interpret_cdd_coverage / compute_cdd_register / compute_cdd_trigger_record, so all three of those outputs stay byte-identical.

Consequences

Positive

  • The Art. 22(1) identity dataset is typed per actor type with the correct fields for an entity vs a natural person, each carrying its own article ref and a first-class registry-grounding state — a machine-checkable oracle for downstream consumers.
  • ADR-0058 (central register = cross-check only) is now first-class at the identity-field level: a central_register_only grounding is explicitly flagged insufficient and never reads verified / fully_grounded.
  • Fail-closed / presence-≠-evidence is enforced by construction: a value on file is never verified; an unassessed field reads not_assessed, never a silent clear.
  • The not_assessed (unassessed) vs ungrounded (assessed, no registry) vs central_register_only (insufficient) distinction is machine-checkable — the three failure kinds have different owners (collect it / find a source / find a non-central source).

Negative

  • The record requires the caller to supply per-actor field values + grounding sources that this pure engine does not itself gather; until the population issue lands the record reads not_assessed in production. This is honest but means it under-states real identity data that exists elsewhere in the pipeline (registry decoders, person verification, investigation persons). Population is the follow-up.
  • A second identity representation now coexists with interpret_cdd_coverage §2(a). They are not auto-coupled (unlike the register, which reuses the map) — a future editor wiring population must feed both consistently. Mitigated by both being additive and the §2(a) output being byte-stable.

Neutral

  • No migration and no persisted schema — the record is pure compute over supplied inputs.
  • The record is unwired into any consumer; it has zero runtime effect until the separate surface / population issues consume it.

Alternatives Considered

Alternative 1: Reuse the person VerificationRecord / min-2-source gate as the grounding signal

  • Feed the Art. 22(1) fields through verification_gate.attribute_verification_status.
  • Why rejected: that gate models a different, stronger obligation (≥2 independent sources per person attribute) and only over a fixed set of person attributes; it does not model the per-actor-type Art. 22(1) dataset (which fields are required for an entity vs a person), has no legal-entity field set (LEI / legal form / country of creation / nominee status), and its status enum has no distinct not_assessed field state. The grounding marker here is the weaker, complementary ADR-0058 signal (≥1 non-central source); the two compose rather than substitute.

Alternative 2: Extend interpret_cdd_coverage §2(a) with per-field grounding

  • Add grounding fields to the existing §2(a) dict and a per-actor loop.
  • Why rejected: the §2(a) map is a loose dict[str, dict] scoped to the legal-entity customer only (no directors/UBOs/beneficiaries/representatives, no natural-person customer dataset), and get_amlr_coverage hardcodes the five §2(x) keys — extending it would break byte-compatibility and still not express a per-actor dataset. The typed sibling record lets consumers migrate independently while the legacy output stays byte-stable.