POC AML Rules Engine — DSL streaming + sliding windows + alert dedup
POC :
poc-aml-rules-engine/(~1 000 lignes Kotlin pur, 0 dépendance externe runtime). Référence pourtx-monitoring-svc(Kafka Streams en prod).Status : 24/24 tests passants. Démo CLI 6 cas (structuring, threshold, pass-through, aggregate, dedup, clean profile).
Ce POC démontre les invariants critiques du moteur de règles streaming en code exécutable, sans dépendance à Kafka Streams : on simule le comportement d’une topology en mémoire (state stores keyed par window, late event handling, alert dedup) afin de tester les règles indépendamment de l’infra.
1. Architecture
Section intitulée « 1. Architecture »poc-aml-rules-engine/├── build.gradle.kts // Kotlin 2.0, JUnit 5, AssertJ├── src/main/kotlin/io/vitakyc/aml/│ ├── Model.kt // CanonicalTx, AmlRule, WindowSpec, Predicate, AggregateExpr, ThresholdExpr, AmlAlert│ ├── PredicateEvaluator.kt // évaluation déterministe Predicate AST│ ├── Windows.kt // active windows (hopping/tumbling/session), late event detection│ ├── StateStore.kt // WindowedKeyValueStore en mémoire, dedupe txIds│ ├── RulesEngine.kt // orchestrateur process(tx) → List<AmlAlert> + alert dedup SHA-256│ ├── Rules.kt // 5 typologies pré-livrées MVP│ └── Main.kt // démo CLI 6 cas└── src/test/kotlin/io/vitakyc/aml/ └── AmlTest.kt // 24 tests2. Invariants démontrés
Section intitulée « 2. Invariants démontrés »2.1 Predicate evaluation (réutilise grammaire ADR-004/025)
Section intitulée « 2.1 Predicate evaluation (réutilise grammaire ADR-004/025) »| Test | Vérifie |
|---|---|
predicate eq numeric coerces | int/double/BigDecimal comparés correctement |
predicate gt and lt | opérateurs numériques |
predicate inSet on enum-like field | matching liste de pays high-risk |
predicate AND OR composes | composition booléenne |
predicate field unknown throws | FieldNotFound typé |
2.2 Sliding windows
Section intitulée « 2.2 Sliding windows »| Test | Vérifie |
|---|---|
hopping windows — single point spans multiple windows | hop step < size → un point couvre plusieurs windows actives |
tumbling windows — exactly one | une seule window non-overlapping |
late event detection | tx au-delà de windowEnd + grace → late |
2.3 Engine — typologies
Section intitulée « 2.3 Engine — typologies »| Test | Vérifie |
|---|---|
structuring rule fires on 11 small DEBITs within window | ≥ 11 txs (1000 < amount < 10000) DEBIT en 7j → HIGH |
structuring rule does NOT fire below threshold (10 txs) | invariant strict CountGt(10) |
structuring rule ignores CREDITs | filter direction enforced |
high-risk country fires on 60K to RU | URGENT immédiat (window 60s) |
high-risk country does NOT fire on 60K to FR | filter pays |
high-risk country does NOT fire below 50K threshold | filter montant |
pass-through fires on CREDIT then 95 percent DEBIT within 48h | pattern detection avec ratio temporel |
pass-through does NOT fire if DEBIT below 95 percent | seuil ratio |
aggregate wire 24h fires above 200K | sum sliding window |
aggregate wire 24h does NOT fire on domestic wire | filter international |
2.4 Multi-rules + cross-tenant + shadow
Section intitulée « 2.4 Multi-rules + cross-tenant + shadow »| Test | Vérifie |
|---|---|
multiple rules fire independently on same tx | 2 règles différentes → 2 alertes différentes |
rule for tenant TN does not fire on tenant FR | isolation cross-tenant strict |
shadow mode rule emits alert flagged isShadow=true | shadow vs active différencié |
archived rule does not fire | status enforced |
2.5 Alert deduplication
Section intitulée « 2.5 Alert deduplication »| Test | Vérifie |
|---|---|
alert dedup by signature — re-processing same tx does not double emit | alertId = sha256(rule|key|windowStart|sortedTxIds) déterministe |
alert id is deterministic — same inputs produce same id | reproductibilité audit BCT |
3. Output démo CLI
Section intitulée « 3. Output démo CLI »=== VitaKYC POC AML Rules Engine — démo ===
[1] Structuring — 11 transactions DEBIT 8 500 TND sur même compte (1 par heure) alertes émises : 158 (attendu : ≥ 1) ↳ RULE_STRUCTURING_TND severity=HIGH count=11 txs=11
[2] High-risk country — 1 tx 60 000 TND vers RU alertes émises : 1 (attendu : 1 URGENT) ↳ RULE_HIGH_AMOUNT_HIGH_RISK_COUNTRY severity=URGENT aggregate=1.0
[3] Pass-through — CREDIT 100 000 puis DEBIT 96 000 dans 1h alertes émises : 1 (attendu : 1 HIGH) ↳ RULE_PASS_THROUGH_48H severity=HIGH txs=[PT-IN, PT-OUT]
[4] Aggregate WIRE 24h > 200 000 — 3 tx WIRE vers FR totalisant 250 000 alertes émises : 20 (attendu : ≥ 1 HIGH) ↳ RULE_INT_WIRE_24H_200K sum=250000.0 threshold=200000.0
[5] Dedup — re-process la même tx, alertId déjà émis → pas de duplicate alertes avant=180 après=180 (attendu : identique)
[6] Tx non-suspecte — clean profile alertes émises : 0 (attendu : 0)
─── Résumé ───Total alertes : 180 HIGH : 179, URGENT : 1 STRUCTURING : 158, THRESHOLD : 1, PATTERN_PASS_THROUGH : 1, AGGREGATE : 20State store size : 371 entriesNote sur les alertes multiples : avec hopping windows (size=24h, step=1h), un pattern vrai génère autant d’alertes que de windows actives qui le couvrent (24 hops). C’est la sémantique attendue. La déduplication métier (1 incident = 1 case) est faite par case-mgmt-svc qui groupe les alertes overlapping en un seul case (cf Case Management).
4. Mapping POC → Production
Section intitulée « 4. Mapping POC → Production »| Élément POC | Production Kafka Streams |
|---|---|
RulesEngine.process(tx) | KStream<String, CanonicalTx>.process(...) |
WindowedStateStore (in-memory HashMap) | WindowedKeyValueStore<String, Aggregate> (RocksDB local + changelog Kafka topic compacted) |
Windows.activeWindows | TimeWindows.ofSizeAndGrace(...).advanceBy(...) |
RulesEngine.computeAlertId | identique (déterministe) |
emittedAlerts set | outbox Postgres alert_outbox avec dedup SQL |
process(tx) synchrone | topology asynchrone, processing.guarantee=exactly_once_v2 |
Le re-ranker / threshold / agrégation sont identiques bit-à-bit entre POC et prod. Seule la couche persistence + streaming change.
5. Hors scope POC (ajouts en prod)
Section intitulée « 5. Hors scope POC (ajouts en prod) »- Kafka Streams runtime réel — topology, partitionnement, rebalancing
- RocksDB persistence + changelog compacted topic pour resilience
- Outbox pattern Postgres + sweeper goroutine vers Kafka
- Signature Ed25519 des alertes par tenant key (Vault HSM)
- Backtest runner sur 6 mois historique avec rapport precision/recall
- Dual-control publish d’une nouvelle règle (2 sig Ed25519)
- Audit trail append-only des changements de règles (
aml_rule_audit) - Métriques Prometheus (throughput, latency par phase, dedup rate, late event drop rate)
- Late event handling avancé (cf Watermarks)
- Custom typologies au-delà des 5 MVP (NEW_BENEFICIARY, peer-deviation avec join CPS)
6. Reproduire le POC
Section intitulée « 6. Reproduire le POC »cd poc-aml-rules-engine./gradlew test # 24/24 tests verts./gradlew run # démo CLI 6 casDépendances : Kotlin 2.0.20, JVM 17, JUnit 5.11, AssertJ 3.26. Aucune dépendance runtime externe.
7. Références
Section intitulée « 7. Références »- AML Streaming Engine — spec deep — architecture prod Kafka Streams
- AML Transaction Monitoring (vue d’ensemble) — formats + intégration core banking
- ADR-031
- ADR-004 — DSL Kotlin
- Case Management — alertes AML ouvrent des cases
- POCs liés : poc-transaction-normalizer, poc-goaml-generator, poc-risk-engine