Skip to main content

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 + date comparisons. annotate_eba_result runs 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/dataclasses are 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), the factor_id, the annex_ref, and the as_of date, 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.py are untouched — the matrix score/level/hashes are byte-unchanged (pinned by a byte-stability test). The only pre-existing test touched is test_reference_data_service.py, whose two assertions hardcoded "12 datasets" (now 13).
  • Alternative considered: encode the factors as code constants inside eba_risk_matrix.py rather 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 the config/reference_data/ envelope pattern already provides (source/source_url/source_date provenance, per-entry effective_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:

  1. 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.
  2. 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 flagged in_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_mappings bind 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/ + ReferenceDataService pattern.
  • The dataset count in ReferenceDataService moved 12 → 13; the two hardcoded assertions in test_reference_data_service.py were 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.