Aller au contenu

POC · CPS Registry (Kotlin)

Valide : ADR-026 (intégration Form Designer ↔ Risk Matrix via CPS), ADR-007 (CPS mapping obligatoire), ADR-025 (validation symboles côté Risk Matrix). Statut : 12/12 tests passent, démo CLI fonctionnelle, prêt à être industrialisé dans profile-schema-svc.

Ce POC démontre le cœur du Client Profile Schema registry : la logique métier qui garantit la référentielle entre Form Designer et Risk Matrix, refuse les opérations dangereuses (suppression de champ consommé, validation d’un symbole inexistant, rename direct), et produit un hash canonique reproductible pour l’audit.

Source code : /poc-cps-registry/ dans le repo VitaKYC.


Fenêtre de terminal
cd poc-cps-registry
./gradlew test # 12/12 tests JUnit 5
./gradlew run # démo CLI : flow Form Designer → CPS → Risk Matrix
enum class VariableType { STRING, INTEGER, DECIMAL, BOOLEAN, ENUM, ISO3166_ALPHA2, ISO4217, ... }
enum class Sensitivity { PUBLIC, INTERNAL, PII, SPI }
enum class VariableStatus { ACTIVE, DEPRECATED, RETIRED }
enum class ConsumerKind { RISK_RULE, RISK_OVERRIDE, AML_RULE, REPORT_FIELD }
data class Variable(
val path: String, // "client.profession"
val type: VariableType,
val enumCatalog: String? = null,
val source: String, // "form:FORM_KYC_INDIVIDUAL@v2.7"
val required: Boolean = false,
val sensitivity: Sensitivity,
val status: VariableStatus = VariableStatus.ACTIVE,
val deprecatedAt: String? = null,
val deprecatedReason: String? = null,
val deprecationWindowDays: Int = 90,
val usedBy: List<UsedByRef> = emptyList(),
val modelCard: String? = null
)
InvariantErreur typéeQuand
Path unique par tenantDuplicateVariabledeclare() sur un path existant
Pas de symbole fantômeSymbolUnknown (+ suggestions Levenshtein)validateSymbols() Risk Matrix
Pas de suppression tant qu’il y a des consommateursDeleteInUseretire() d’une variable avec usedBy.isNotEmpty()
Rename direct interditRenameNotAllowedrename() — pattern correct : declare new + deprecate old
Fenêtre de dépréciation ≥ 90 joursInvalidDeprecationWindowdeprecate(windowDays = X < 90)

Quand un compliance officer tape un symbole mal orthographié, le CPS propose les variables existantes proches :

// Compliance officer tape "client.profesion" (manque un s)
cps.validateSymbols(listOf("client.profesion"))
→ CpsError.SymbolUnknown: 'client.profesion' not found.
Suggestions: [client.profession, client.country_pros_bracket]
val cps1 = CpsRegistry("TN-BANQUEX")
cps1.declare(profession)
cps1.declare(country)
val hash1 = cps1.snapshot().hash
val cps2 = CpsRegistry("TN-BANQUEX")
cps2.declare(country) // ordre inverse
cps2.declare(profession)
val hash2 = cps2.snapshot().hash
assert(hash1 == hash2) // hash stable indépendamment de l'ordre d'insertion

Important pour l’audit : permet de vérifier que le contrat n’a pas été altéré.


======================================================================
CPS Registry — démo flow Form Designer → CPS → Risk Matrix
======================================================================
▶ 1. Form Designer publie v2.7 : déclare client.profession, client.country, client.pep
▶ 2. Module screening activé : déclare screening.ofacMatch, screening.pepConfirmed
▶ 3. Risk Matrix publie policy TN-BANQUEX@2.1 → référence les variables
▶ 4. Validation policy — tous symboles OK ✓
▶ 5. Tentative de policy référant un symbole inexistant : client.homeowner_status
✓ Rejeté : CPS_SYMBOL_UNKNOWN
▶ 6. Form Designer tente de supprimer client.pep (utilisé par 1 règle)
✓ Rejeté : utilisé par 1 consommateur(s)
▶ 7. Form Designer publie v2.8 : ajoute client.income_bracket
▶ 8. Deprecate client.homeowner_status_legacy → fenêtre 90j ✓
Snapshot final : 6 variables actives, 1 dépréciée
hash = sha256:2e96741276aaf...

Exemple de variable avec consommateur enregistré :

{
"path": "client.pep",
"type": "ENUM",
"enum_catalog": "PEP_STATUS",
"source": "form:FORM_KYC_INDIVIDUAL@v2.7",
"sensitivity": "PII",
"status": "ACTIVE",
"used_by": [
{ "kind": "RISK_RULE", "ref": "policy:TN-BANQUEX@2.1#dimension=client#rule=pep" }
]
}

CPS Registry — invariants Form Designer ↔ Risk Matrix
✓ Déclarer une variable nouvelle → OK et retrievable
✓ Déclarer une variable déjà existante → DuplicateVariable
✓ Valider un symbole connu → OK sans exception
✓ Valider un symbole inconnu → SymbolUnknown avec suggestions
✓ Retirer une variable utilisée par Risk Matrix → DeleteInUse
✓ Retirer une variable libre (aucun consommateur) → OK, passe RETIRED
✓ Déprécier une variable → statut DEPRECATED + timestamp
✓ Fenêtre de dépréciation < 90j → InvalidDeprecationWindow
✓ Rename strict interdit → RenameNotAllowed
✓ Register + unregister consommateur → usedBy évolue correctement
✓ findDeprecatedAmong retourne les variables dépréciées
✓ Snapshot produit un hash stable et reproductible
BUILD SUCCESSFUL — 12 tests passed

poc-cps-registry/
├── build.gradle.kts — Kotlin 2.0 + kotlinx.serialization + JUnit 5
├── settings.gradle.kts
├── README.md
└── src/
├── main/kotlin/io/vitakyc/cps/
│ ├── Model.kt — data classes (Variable, ClientProfileSchema, enums, erreurs typées)
│ ├── Registry.kt — CpsRegistry in-memory + invariants + Levenshtein suggestions
│ └── Main.kt — CLI démo cross-module
└── test/kotlin/io/vitakyc/cps/
└── RegistryTest.kt — 12 tests JUnit 5 + AssertJ
  • Model.kt : data classes immutables, enums, erreurs typées (sealed class CpsError). Pas de logique métier.
  • Registry.kt : logique du registry, invariants, lifecycle. Pas de persistance (c’est un POC).
  • Main.kt : démo CLI, pas utilisée en prod.
ChoixJustification
Kotlin 2.0 + JVM 17Cohérent avec les autres POCs + stack VitaKYC
In-memory LinkedHashMapPOC autonome. Industrialisation dans profile-schema-svc avec PostgreSQL JSONB append-only.
sealed class CpsErrorErreurs typées Kotlin-idiomatic, exhaustives dans le when
kotlinx.serializationJSON natif Kotlin, utilisé pour le dump de snapshots
Levenshtein customSuggestions minimales (< 20 LOC). Suffit pour la UX autocomplete.

6. Ce que le POC ne fait PAS (scope profile-schema-svc)

Section intitulée « 6. Ce que le POC ne fait PAS (scope profile-schema-svc) »

Volontairement hors-scope :

  • Persistance PostgreSQL JSONB append-only
  • Kafka consumer (form.published, screening.enabled, etc.) + producer (cps.updated)
  • API REST / gRPC exposée
  • Cache Caffeine côté consommateurs
  • mTLS + RBAC granulaire
  • Audit WORM 10 ans avec hash chain
  • UI bindings (les workflows 3 et 10 des mockups montrent l’UI)
  • Validation JSONSchema 2020-12 complète (utilisée en interne)

Tout ça est documenté sur la page engineering CPS.


POCRelation
poc-risk-engineConsommateur principal du CPS. La prochaine itération du POC Risk Engine intégrera CpsRegistry.validateSymbols() à la construction de policy pour refuser les règles faisant référence à des symboles inconnus.
poc-fatca-generatorConsommera le CPS pour client.tinUs, client.residentUsStatus, etc.
poc-goaml-generatorConsommera le CPS pour les variables de contexte STR.
poc-rne-connectorSource : déclarera entity.ubo.* au CPS quand le connecteur RNE est activé pour un tenant.

  1. Sprint S03 — monter profile-schema-svc autour de ce POC (Spring Boot + PostgreSQL + Kafka)
  2. S04 — wire le Form Designer : émission form.published events + UI badges “Used by X rules”
  3. S05 — wire le Risk Matrix editor : autocomplete depuis CPS + validation serveur stricte
  4. S06 — audit WORM + export JSON signé pour audit BCT
  5. S08 — migration des tenants pilotes : discovery + validation manuelle + go-live strict