ADR-0141: AMLR Annex I/II/III factors as a versioned, effective-dated reference dataset + signal mapping
Date: 2026-07-25 Status: Accepted Deciders: Adrian (Soft4U BV), Claude Opus 4.8 (implementation agent)
Decision context:
- Latency: not measured — the loader reads an already-cached reference dataset
(
get_reference_data().get_dataset("amlr_annex_factors"), one JSON file loaded once at startup) and does dict lookups +datecomparisons.annotate_eba_resultruns a handful of set-membership reference-data lookups per country. Negligible against the OSINT investigation and Neo4j reads that dominate a case. No user-visible latency change. - Dependency surface: zero new packages —
json/datetime/dataclassesare stdlib. One new JSON reference dataset and one new pure service module. No model/DB change. - Debuggability: every citation carries its
basis(amlr/pre_effective_predecessor/amlr_not_yet_in_force), thefactor_id, theannex_ref, and theas_ofdate, so "which Annex factor was cited, under which instrument, as of when?" is answerable from the enrichment alone. An unmapped signal carries a note naming the real statutory basis. - Reversibility: additive. The module is new and read-only; the JSON is a new file. Nothing reads the enrichment yet (UI/goAML wiring is the separate #608 surface issue), so reverting is deleting two files and one test — no migration, no data change.
- Blast radius: additive-with-defaults.
eba_risk_matrix.py/risk_matrix_service.pyare untouched — the matrix score/level/hashes are byte-unchanged (pinned by a byte-stability test). The only pre-existing test touched istest_reference_data_service.py, whose two assertions hardcoded "12 datasets" (now 13). - Alternative considered: encode the factors as code constants inside
eba_risk_matrix.pyrather than a versioned JSON dataset — rejected because the statutory factor list must be a versioned, effective-dated artifact (Art. 20(2)-(3) + EU AI Act Art. 12 reproducibility), which is exactly what theconfig/reference_data/envelope pattern already provides (source/source_url/source_date provenance, per-entryeffective_date).
Context
Issue #551 (AMLR readiness epic #528, Wave 4). AMLR (Reg. (EU) 2024/1624) Art. 20(2)-(3)
require the customer-risk assessment to take into account the higher-risk factors of
Annex III, the lower-risk factors of Annex II, and (Art. 10) the indicative risk
variables of Annex I. The platform already operationalises these through the EBA
7-dimension matrix (eba_risk_matrix.py, EBA/GL/2021/02, ADR-0020) — but nothing let a risk
determination cite which statutory Annex factor a given risk driver corresponds to. The
Annex factor text existed only inside the Lex corpus (CELEX 32024R1624), not as a queryable,
versioned, effective-dated factor registry mapped to the platform's own risk signals.
Two properties matter for compliance:
- Versioned + effective-dated + reproducible — the AMLR applies 2027-07-10; a determination dated before then must not cite a not-yet-applicable article as if in force (the presence-≠-in-force defect ADR-0133/#542 closed for the UBO threshold). The Annex factor set must resolve its citation as of a date.
- Fail-closed honesty (ADR-0067) — a higher-risk (Annex III) factor a fired signal maps to must never be suppressed; a signal with no Annex II/III factor (PEP, a sanctions hit, adverse media — all real signals governed by distinct AMLR provisions) must yield an honest no-mapping, never a fabricated factor.
Decision
Make the AMLR Annex I/II/III factors a versioned, effective-dated reference dataset and add a pure loader + signal mapping that composes with — and never changes — the EBA matrix.
1 — The dataset (config/reference_data/amlr_annex_factors.json)
A standard reference-data envelope (list_key/type_id/data/source/source_url/
source_date) whose data carries a version (amlr-annex-2024-1624-v1), an
effective_date (2027-07-10), and the factors under annex_i / annex_ii / annex_iii.
Each factor: factor_id (annex_iii_1_f), annex, point ((1)(f)), annex_ref
(AMLR Annex III point (1)(f)), category, direction (higher/lower/variable),
text (verified verbatim against the Lex corpus AMLR Annexes), effective_date, and a
predecessor_ref (the AMLD (Directive (EU) 2015/849) Annex II/III section, or null for an
AMLR-added factor with no AMLD equivalent). The dataset also carries the
signal_mappings (platform signal → factor ids) and unmapped_signal_notes (the honest
no-mapping notes) so the whole mapping is one versioned artifact.
2 — The loader + effective-dated resolution (app/services/amlr_annex_factors.py)
resolve_annex_factor(factor_id, as_of) selects the citation in force as of a date
(mirrors ADR-0133/#542):
- on/after 2027-07-10 → the AMLR annex citation (
basis="amlr",in_force=True); - before go-live, factor with an AMLD predecessor → the in-force AMLD citation
(
basis="pre_effective_predecessor",in_force=True) — never a not-yet-applicable AMLR article as if in force. The predecessor is cited at the annex-section level (not point-level), because the AMLR re-lettered several geographic points; asserting a point-for-point AMLD correspondence would be the ADR-0121 mis-citation class. - before go-live, an AMLR-added factor (III (1)(g)/(h)/(i), III (2)(e);
predecessor_ref = null) → the AMLR citation but honestly flaggedin_force=False(basis="amlr_not_yet_in_force"). The factor is still surfaced (never-suppress); only the citation's in-force status is disclosed.
3 — Signal → Annex-factor mapping (fail-closed)
map_signal_to_annex(signal, as_of) resolves a signal through the versioned
signal_mappings. A signal with no mapping returns mapped=False with a note from
unmapped_signal_notes (or a generic honest gap) — never a fabricated factor. The
deliberately-unmapped signals name their real basis:
- PEP → distinct statutory EDD trigger, AMLR Art. 42-44 — not an Annex factor.
- sanctions hit / sanctioned ownership → TFS obligation / AMLR Art. 20(1)(d) — Annex III (3)(d) concerns sanctioned countries, not sanctioned persons.
- adverse media / criminal investigation → an Annex I reputation risk variable + Art. 20(1), not an enumerated Annex III higher-risk factor.
A standing contract test asserts every signal_mappings target exists in the dataset (a
mapping can never cite a non-existent factor).
4 — Composition with the EBA matrix (additive, byte-compatible)
annotate_eba_result(eba_result, eba_input, as_of) reads an already-computed
EBARiskResult and its EBARiskInput, derives the fired platform signals
(derive_signals_from_eba — input booleans nominee/bearer/complex-ownership, geographic
signals classified from the same reference lists the matrix scores against, and the
matrix's own auto_escalations), and returns a JSON-safe enrichment
(higher_risk_factors/lower_risk_factors/unmapped_signals + version/effective_date/
as_of). It changes no score, does not mutate either argument, and the EBA matrix
output (dimensions/overall_score/risk_level/hashes/auto_escalations) is byte-unchanged
(pinned by tests). Never-suppress: every mapped Annex III factor appears in
higher_risk_factors. Fail-closed: an unmapped fired signal (e.g. PEP) appears in
unmapped_signals with its honest note, never folded into a higher-risk factor.
Consequences
Positive
- A risk determination can now cite which AMLR Annex factor a driver corresponds to ("higher-risk per AMLR Annex III point (1)(f)"), grounded in a versioned, effective-dated dataset — Art. 20(2)-(3) + EU AI Act Art. 12 traceability.
- The citation is contemporaneous: before 2027-07-10 it reads the in-force AMLD predecessor (or honestly flags an AMLR-added factor as not-yet-in-force), never a not-yet-applicable article as if in force.
- The presence-≠-evidence / never-fabricate discipline is structural: PEP, sanctions, and adverse media — signals with no Annex factor — produce an honest gap naming the real basis, not a fabricated Annex citation.
Negative
- The
signal_mappingsbind code-level signal names to the dataset; a future rename of a platform signal without updating the dataset would silently stop mapping it. Mitigated by the contract test (targets must exist) and the honest-gap default (an unknown signal is reported, never fabricated) — but the signal-name coupling is real and is why the mapping lives in the versioned dataset (visible + reviewable) rather than scattered in code. - The enrichment is unwired — no consumer reads it yet (UI/goAML/case-pack surfacing is the bounded follow-up #608). It is a tested, reviewable substrate, not a user-visible feature this ADR ships.
Neutral
- No migration — a JSON reference dataset + pure compute, riding the existing
config/reference_data/+ReferenceDataServicepattern. - The dataset count in
ReferenceDataServicemoved 12 → 13; the two hardcoded assertions intest_reference_data_service.pywere updated in lockstep.
Alternatives Considered
Alternative 1: Encode the Annex factors as constants in eba_risk_matrix.py
- Put the factor list + mapping in the scorer module.
- Why rejected: the statutory factor list is a versioned, effective-dated artifact with provenance and per-entry effective dates — exactly what the reference-data envelope provides. Burying it in the scorer would couple a slow-changing legal dataset to the scoring code and lose the effective-dating + provenance the compliance story needs.
Alternative 2: Point-level AMLD predecessor citations
- Give each factor a point-level AMLD predecessor (e.g. AMLD Annex III point (3)(d)).
- Why rejected: the AMLR re-lettered several Annex III geographic points relative to AMLD, so a point-for-point predecessor would mis-cite (the ADR-0121 mis-citation class — a citation that resolves but is legally wrong). Section-level AMLD citation is honest without asserting a false precision.
Alternative 3: Map PEP / sanctions / adverse media to the nearest Annex III factor
- Force every strong signal to some Annex III factor for coverage.
- Why rejected: PEP (Art. 42-44), sanctions (Art. 20(1)(d) / TFS law), and adverse media (Annex I reputation variable) are governed by distinct provisions; mapping them to an Annex III factor would be a fabricated citation. The honest no-mapping (ADR-0067) naming the real basis is correct and demonstrates the fail-closed contract.