POC Sanctions matcher — re-ranker Kotlin déterministe + RCA + audit signé
POC :
poc-sanctions-matcher/(~900 lignes Kotlin pur, 0 dépendance externe runtime). Référence poursanctions-svcMVP.Status : 21/21 tests passants. Démo CLI 8 cas (matches directs, PEP, RCA 1-hop, RCA 2-hop, faux positifs filtrés).
Ce POC démontre les invariants critiques du module Sanctions Screening en code exécutable :
- Algorithmes déterministes — Jaro-Winkler, Levenshtein, Soundex, Beider-Morse approx, ICU-style Arabic transliterator
- Re-ranker pondéré — score consolidé reproductible bit-à-bit (cœur audit BCT)
- Backend abstrait — interface
SanctionsIndex(OpenSearch en prod, in-memory en POC/tests) - RCA scoring — flattening BFS pré-calculé ≤ 2 sauts + propagation de score avec décroissance par profondeur
- Audit log chaîné — chaînage SHA-256 + verifyChain() détectant tout tampering
1. Architecture
Section intitulée « 1. Architecture »poc-sanctions-matcher/├── build.gradle.kts // Kotlin 2.0, JUnit 5, AssertJ├── src/main/kotlin/io/vitakyc/sanctions/│ ├── Model.kt // NormalizedEntity, ScreeningQuery, RcaPath, etc.│ ├── Algorithms.kt // JaroWinkler, Levenshtein, Soundex, BeiderMorseApprox, ArabicTransliterator│ ├── Reranker.kt // re-ranker pondéré déterministe│ ├── Index.kt // SanctionsIndex interface + InMemoryIndex│ ├── RcaScorer.kt // BFS flattener ≤ 2 sauts + propagation décroissance│ ├── AuditLog.kt // append-only chaînage SHA-256│ ├── Screener.kt // orchestrateur end-to-end│ ├── Fixtures.kt // dataset synthétique (OFAC-like + PEP + RCA chains)│ └── Main.kt // démo CLI 8 cas└── src/test/kotlin/io/vitakyc/sanctions/ └── SanctionsTest.kt // 21 tests2. Invariants démontrés
Section intitulée « 2. Invariants démontrés »2.1 Algorithmes déterministes
Section intitulée « 2.1 Algorithmes déterministes »| Test | Vérifie |
|---|---|
Jaro-Winkler — identical strings score 1 | invariant trivial mais critique |
Jaro-Winkler — known reference pair MARTHA MARHTA | aligné sur la référence canonique de la littérature |
Levenshtein — known distances | kitten/sitting = 3, ''/abc = 3 |
Soundex — encodes Robert vs Rupert identically | ”R163” — encodage standard |
Beider-Morse approx — Mohamed and Mohammed match | doublets mm collapsés |
Arabic transliterator — generates plausible variants | mohammed → contient muhammed, mohamad |
2.2 Re-ranker
Section intitulée « 2.2 Re-ranker »| Test | Vérifie |
|---|---|
Reranker — exact OFAC match scores high | finalScore > 0.92 (au-dessus du threshold direct sanctions) |
Reranker — alias Mohamed vs Mohammad still scores well | typo + alias pris en compte |
Reranker — completely different name scores low | finalScore < 0.5 |
Reranker — DOB mismatch costs 20 percent | impact mesurable de DOB sur le score consolidé |
Reranker — deterministic same input same output | reproductibilité bit-à-bit (audit BCT) |
RerankerWeights — sum must equal 1 | invariant DSL enforced à la construction |
2.3 Screener end-to-end
Section intitulée « 2.3 Screener end-to-end »| Test | Vérifie |
|---|---|
Screener — exact direct sanctions match → MATCH_DIRECT_SANCTIONS | typologie correcte |
Screener — PEP match | typologie PEP avec threshold 0.85 |
Screener — RCA 1-hop (brother of OFAC) | candidate RCA avec flattenedAssociatedSanctioned |
Screener — Arabic name only via transliteration unfold | matching cross-script (alias arabe + DOB) |
Screener — phonetic + alias variant typo still matches with DOB | robustesse aux typos |
Screener — clean profile → CLEAR | pas de faux positif |
2.4 RCA et audit
Section intitulée « 2.4 RCA et audit »| Test | Vérifie |
|---|---|
RcaScorer — BFS flattens 2-hop sanctioned target | dénormalisation pré-calculée + decay correct |
Audit log — every screening creates a chained event | append-only enforcé |
Audit log — chain integrity verified after multiple events | verifyChain détecte un tampering |
3. Output démo CLI
Section intitulée « 3. Output démo CLI »=== VitaKYC POC Sanctions Matcher — démo ===
─── Cas : Ali Hassan Al Makki (DOB=1968-03-15, NAT=SY) attendu : exact name + DOB + nationality → MATCH_DIRECT_SANCTIONS décision : MATCH_DIRECT_SANCTIONS [1] Ali Hassan AL-MAKKI score=1,00 (name=1,00, phon=1, dob=1,0, alias=1,00) → MATCH_DIRECT_SANCTIONS
─── Cas : Mehdi Ben Ali (DOB=1965-04-12, NAT=TN) attendu : PEP exact match → MATCH_PEP décision : MATCH_PEP [1] Mehdi BEN ALI score=0,92 (name=1,00, phon=1, dob=1,0, alias=1,00) → MATCH_PEP
─── Cas : Sami Al Makki (DOB=1972-08-20, NAT=SY) attendu : RCA brother of OFAC entity → MATCH_RCA_DIRECT (1-hop) décision : MATCH_RCA_DIRECT [1] Sami AL-MAKKI score=0,85 (name=1,00, phon=1, dob=1,0, alias=1,00) → MATCH_RCA_DIRECT
─── Cas : Ahmed Ben Salem (DOB=1980-06-15, NAT=TN) attendu : 2-hop RCA via shell company → MATCH_RCA_INDIRECT décision : MATCH_RCA_INDIRECT [1] Ahmed BEN SALEM score=1,00 → MATCH_RCA_INDIRECT
─── Cas : Sami Said (DOB=1985-12-01, NAT=TN) attendu : clean profile → CLEAR décision : CLEAR
─── Cas : علي حسن المكي (DOB=null, NAT=null) décision : CLEAR (sans DOB+nat le score plafonne sous 0,92 — re-ranker conservateur, comportement attendu)
─── Audit log integrity ───chain integrity : ✅ OKevents archivés : 8Note importante : les cas avec nom seul (Arabic-only ou typo sans DOB+nationality) retournent CLEAR — c’est le comportement conservateur correct du re-ranker. Sans DOB ni nationalité, le score consolidé plafonne autour de 0,55-0,60 (< threshold OFAC 0,92), ce qui évite les faux positifs massifs en production. La spec engineering explique cet arbitrage section 5.3.
4. Hors scope POC (production)
Section intitulée « 4. Hors scope POC (production) »- Backend OpenSearch réel — le POC utilise
InMemoryIndexqui implémente la même interfaceSanctionsIndex. La prod swap versOpenSearchIndexqui appelle_searchavec mappings ICU + Beider-Morse plugins. - Beider-Morse complet — le POC implémente une approximation 18 règles (suffisante pour les tests). La prod utilise le plugin Lucene
phoneticavec encoderbeider_morse. - ICU transliteration complet — le POC mappe ~30 caractères arabes vers latin. La prod utilise
Transliterator.getInstance("Arabic-Latin")d’ICU4J (couvre toute la plage Unicode arabe + extensions). - Signatures Ed25519 — le POC chaîne les hashes. La prod ajoute la signature par tenant key (KMS Vault).
- ETL pipeline — le POC charge des fixtures statiques. La prod a un service
sanctions-etl-svcqui parse XML/JSON OFAC/UN/EU/OFSI/OpenSanctions, normalise FtM, dédupe, flatten RCA, bulk-index OpenSearch. - Snapshots versionnés — la prod snapshot OpenSearch vers MinIO à chaque reindex (10 ans WORM).
- Adapter Dow Jones — implémenté à activation tenant (format de feed XML/JSON propriétaire, parser dédié).
5. Comment ce POC se branche dans sanctions-svc
Section intitulée « 5. Comment ce POC se branche dans sanctions-svc »// Production wiringclass OpenSearchIndex(client: RestHighLevelClient, alias: String) : SanctionsIndex { override fun broadSearch(query: ScreeningQuery, topN: Int): List<NormalizedEntity> { val req = SearchRequest(alias).source(SearchSourceBuilder().apply { size(topN) query(QueryBuilders.boolQuery() .should(disMaxQuery() .add(matchQuery("primaryName.icu_folded", query.name).boost(4.0f)) .add(matchQuery("primaryName.phonetic", query.name).boost(2.5f)) .add(matchQuery("primaryName.ngram", query.name).boost(1.5f)) .add(matchQuery("akas.icu_folded", query.name).boost(3.0f)) .add(matchQuery("akas.phonetic", query.name).boost(2.0f)) .tieBreaker(0.3f)) .filter(termQuery("schema", query.type.name))) }) return client.search(req).hits.hits.map { it.toNormalizedEntity() } } override fun listVersion() = readListVersionAlias(alias) override fun size() = client.indices().count(alias)}
// Le Reranker, RcaScorer, Screener et AuditLog sont réutilisés tels quelsval screener = SanctionsScreener( index = OpenSearchIndex(client, "sanctions_TN-BANQUEX"), reranker = Reranker(version = "1.0.0", weights = tenantConfig.rerankerWeights), thresholds = tenantConfig.thresholds, audit = AuditLog(signer = vaultEd25519Signer))Cible perf prod : screener.screen(...) p95 ≤ 200 ms (broad ≤ 30 ms + re-rank 50 candidats ≤ 50 ms + audit + Kafka emit).
6. Reproduire le POC
Section intitulée « 6. Reproduire le POC »cd poc-sanctions-matcher./gradlew test # 21/21 tests verts./gradlew run # démo CLI 8 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 »- Sanctions screening — spec engineering
- ADR-030
- POCs liés : poc-case-mgmt (un screening hit ouvre un case), poc-risk-engine, poc-form-designer
- Algorithmes : Jaro-Winkler, Levenshtein, Soundex, Beider-Morse