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_datasetsis a pure in-memory pass over a list ofActorIdentityInput; no DB/graph/network calls added. Safe to defer. - Dependency surface: none new. Twelve typed models added to the existing
trustrelay-modelspackage (re-exported viaapp/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 ownarticle_ref,grounding,verifiedandgap. - 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_coverageand 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-verifiednot_assessedfield 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):
ActorType—customer/director/beneficial_owner/beneficiary/representative.ActorNature—legal_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 enumgrounded/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 (aValueErroris 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 field | grounding | verified |
|---|---|---|
| ≥1 NON-central registry source | grounded | True |
| central register source(s) only | central_register_only (insufficient, ADR-0058) | False |
| a value present, no source | ungrounded (presence ≠ evidence) | False |
| no value, no source | not_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_onlygrounding is explicitly flagged insufficient and never readsverified/fully_grounded. - Fail-closed / presence-≠-evidence is enforced by construction: a value on file is never
verified; an unassessed field readsnot_assessed, never a silent clear. - The
not_assessed(unassessed) vsungrounded(assessed, no registry) vscentral_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_assessedin 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_assessedfield 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), andget_amlr_coveragehardcodes 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.