Add human review protocol and admin rules contract
This commit is contained in:
163
config/admin_rules.default.yml
Normal file
163
config/admin_rules.default.yml
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Template versionne des regles administrables.
|
||||||
|
# Ce fichier decrit un contrat cible pour le futur moteur de regles d'administration.
|
||||||
|
# Il n'est pas encore branche automatiquement dans le pipeline.
|
||||||
|
version: 1
|
||||||
|
defaults:
|
||||||
|
review_required_for_activation: true
|
||||||
|
environments:
|
||||||
|
- test
|
||||||
|
- prod
|
||||||
|
sections:
|
||||||
|
- narrative
|
||||||
|
- structured
|
||||||
|
- table
|
||||||
|
rules:
|
||||||
|
- id: rule_chcb_exact_mask
|
||||||
|
label: Masquer le sigle CHCB
|
||||||
|
description: Sigle local a masquer dans tous les contextes documentaires.
|
||||||
|
type: exact_term
|
||||||
|
action: mask
|
||||||
|
placeholder: "[MASK]"
|
||||||
|
status: active
|
||||||
|
match:
|
||||||
|
exact_value: CHCB
|
||||||
|
normalization:
|
||||||
|
case_insensitive: true
|
||||||
|
whole_word: true
|
||||||
|
multiline: false
|
||||||
|
scope:
|
||||||
|
document_families:
|
||||||
|
- all
|
||||||
|
environments:
|
||||||
|
- test
|
||||||
|
- prod
|
||||||
|
sections:
|
||||||
|
- narrative
|
||||||
|
- structured
|
||||||
|
- table
|
||||||
|
governance:
|
||||||
|
owner: qualite
|
||||||
|
justification: Sigle local considere comme identifiant d'etablissement a masquer.
|
||||||
|
created_at: "2026-04-21"
|
||||||
|
review_required_for_activation: true
|
||||||
|
approved_by: responsable_qualite
|
||||||
|
tests:
|
||||||
|
required_case_ids:
|
||||||
|
- 005_force_mask_default_term
|
||||||
|
- 001_crh_hospitalisation_complete
|
||||||
|
|
||||||
|
- id: rule_identifier_1234567
|
||||||
|
label: Identifier normalise 1234567
|
||||||
|
description: Exemple de regle couvrant les variantes N°, No et Numero.
|
||||||
|
type: normalized_identifier
|
||||||
|
action: mask
|
||||||
|
placeholder: "[NDA]"
|
||||||
|
status: candidate
|
||||||
|
match:
|
||||||
|
canonical_value: "1234567"
|
||||||
|
normalization:
|
||||||
|
case_insensitive: true
|
||||||
|
whole_word: true
|
||||||
|
multiline: true
|
||||||
|
allow_bare_value: true
|
||||||
|
accepted_prefixes:
|
||||||
|
- "N°"
|
||||||
|
- "No"
|
||||||
|
- "Numero"
|
||||||
|
prefix_value_separators:
|
||||||
|
- ""
|
||||||
|
- " "
|
||||||
|
- ":"
|
||||||
|
- " : "
|
||||||
|
scope:
|
||||||
|
document_families:
|
||||||
|
- compte_rendu
|
||||||
|
- imagerie
|
||||||
|
environments:
|
||||||
|
- test
|
||||||
|
sections:
|
||||||
|
- narrative
|
||||||
|
- structured
|
||||||
|
- table
|
||||||
|
governance:
|
||||||
|
owner: qualite
|
||||||
|
justification: Cas type demande pour les numeros administratifs variables.
|
||||||
|
created_at: "2026-04-21"
|
||||||
|
review_required_for_activation: true
|
||||||
|
approved_by: null
|
||||||
|
tests:
|
||||||
|
required_case_ids:
|
||||||
|
- 003_multiline_venue_number
|
||||||
|
- 001_crh_hospitalisation_complete
|
||||||
|
|
||||||
|
- id: rule_ipp_context_abc12345
|
||||||
|
label: IPP contextuel ABC12345
|
||||||
|
description: Exemple de valeur a masquer seulement en contexte de label IPP.
|
||||||
|
type: contextual_identifier
|
||||||
|
action: mask
|
||||||
|
placeholder: "[IPP]"
|
||||||
|
status: draft
|
||||||
|
match:
|
||||||
|
canonical_value: ABC12345
|
||||||
|
context_prefixes:
|
||||||
|
- IPP
|
||||||
|
- I.P.P.
|
||||||
|
- "N° Ipp"
|
||||||
|
context_separators:
|
||||||
|
- ":"
|
||||||
|
- " : "
|
||||||
|
- "\n"
|
||||||
|
normalization:
|
||||||
|
case_insensitive: true
|
||||||
|
whole_word: true
|
||||||
|
multiline: true
|
||||||
|
scope:
|
||||||
|
document_families:
|
||||||
|
- all
|
||||||
|
environments:
|
||||||
|
- test
|
||||||
|
sections:
|
||||||
|
- structured
|
||||||
|
- table
|
||||||
|
governance:
|
||||||
|
owner: qualite
|
||||||
|
justification: Prototype de regle contextuelle pour identifiants structures.
|
||||||
|
created_at: "2026-04-21"
|
||||||
|
review_required_for_activation: true
|
||||||
|
approved_by: null
|
||||||
|
tests:
|
||||||
|
required_case_ids:
|
||||||
|
- 004_structured_admin_complete
|
||||||
|
|
||||||
|
- id: rule_preserve_classification_internationale
|
||||||
|
label: Preserver classification internationale
|
||||||
|
description: Protection explicite d'une formulation metier.
|
||||||
|
type: preserve_phrase
|
||||||
|
action: preserve
|
||||||
|
status: active
|
||||||
|
match:
|
||||||
|
exact_value: classification internationale
|
||||||
|
normalization:
|
||||||
|
case_insensitive: true
|
||||||
|
whole_word: false
|
||||||
|
multiline: false
|
||||||
|
scope:
|
||||||
|
document_families:
|
||||||
|
- all
|
||||||
|
environments:
|
||||||
|
- test
|
||||||
|
- prod
|
||||||
|
sections:
|
||||||
|
- narrative
|
||||||
|
- structured
|
||||||
|
governance:
|
||||||
|
owner: metier
|
||||||
|
justification: La formulation doit rester visible pour l'usage controle.
|
||||||
|
created_at: "2026-04-21"
|
||||||
|
review_required_for_activation: true
|
||||||
|
approved_by: responsable_qualite
|
||||||
|
tests:
|
||||||
|
required_case_ids:
|
||||||
|
- 006_whitelist_phrases_preserved
|
||||||
|
- 001_crh_hospitalisation_complete
|
||||||
|
- 002_imagerie_complete
|
||||||
71
docs/fiche-validation-humaine-modele.md
Normal file
71
docs/fiche-validation-humaine-modele.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Fiche de validation humaine - modele
|
||||||
|
|
||||||
|
## 1. Identification
|
||||||
|
|
||||||
|
- Version / commit :
|
||||||
|
- Date :
|
||||||
|
- Relecteur metier :
|
||||||
|
- Operateur :
|
||||||
|
- Responsable qualite :
|
||||||
|
- Portee :
|
||||||
|
|
||||||
|
## 2. Perimetre relu
|
||||||
|
|
||||||
|
- Familles documentaires :
|
||||||
|
- Cas synthetiques relus :
|
||||||
|
- Documents reels relus :
|
||||||
|
- Regles ajoutees / modifiees :
|
||||||
|
|
||||||
|
## 3. Resultats automatiques
|
||||||
|
|
||||||
|
- Tests unitaires : OK / NOK
|
||||||
|
- Suite synthetique rapide : OK / NOK
|
||||||
|
- Corpus synthetique complet : OK / NOK
|
||||||
|
- Corpus reel annote : OK / NOK / NA
|
||||||
|
- Scanner de fuite : OK / NOK
|
||||||
|
|
||||||
|
## 4. Checklist dossier par dossier
|
||||||
|
|
||||||
|
### Cas / document :
|
||||||
|
|
||||||
|
- Source :
|
||||||
|
- Sortie relue :
|
||||||
|
- Diff relu :
|
||||||
|
|
||||||
|
#### Fuites
|
||||||
|
|
||||||
|
- Nom / prenom patient : OK / NOK
|
||||||
|
- Date de naissance : OK / NOK
|
||||||
|
- Adresse / code postal / ville : OK / NOK
|
||||||
|
- Telephone / email : OK / NOK
|
||||||
|
- Identifiants administratifs : OK / NOK
|
||||||
|
- Tableaux / en-tetes / pieds de page : OK / NOK
|
||||||
|
|
||||||
|
#### Preservation
|
||||||
|
|
||||||
|
- Services / actes / structures utiles conserves : OK / NOK
|
||||||
|
- Formulations metier preservees : OK / NOK
|
||||||
|
- Document exploitable pour le controle : OK / NOK
|
||||||
|
|
||||||
|
#### Observations
|
||||||
|
|
||||||
|
- Commentaires :
|
||||||
|
- Type d'anomalie : BLOQUANT / MAJEUR / MINEUR / AUCUNE
|
||||||
|
|
||||||
|
## 5. Decision globale
|
||||||
|
|
||||||
|
- Decision :
|
||||||
|
- ACCEPTE
|
||||||
|
- ACCEPTE_AVEC_RESERVE
|
||||||
|
- REFUSE
|
||||||
|
- A_CORRIGER_PUIS_REVOIR
|
||||||
|
|
||||||
|
- Motif :
|
||||||
|
- Actions demandees :
|
||||||
|
- Delai :
|
||||||
|
|
||||||
|
## 6. Trace de validation
|
||||||
|
|
||||||
|
- Nom validateur final :
|
||||||
|
- Date :
|
||||||
|
- Signature / attribution :
|
||||||
263
docs/protocole-validation-humaine.md
Normal file
263
docs/protocole-validation-humaine.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
# Protocole de validation humaine
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Ce protocole sert a valider la fonction d'anonymisation avant diffusion ou activation
|
||||||
|
d'une nouvelle version du moteur, d'une nouvelle regle ou d'une nouvelle famille de
|
||||||
|
documents.
|
||||||
|
|
||||||
|
Il ne remplace pas les tests automatiques. Il les complete sur le point qui compte
|
||||||
|
le plus pour le projet : **le document anonymise est-il conforme et exploitable ?**
|
||||||
|
|
||||||
|
## 2. Quand la validation humaine est obligatoire
|
||||||
|
|
||||||
|
La validation humaine est obligatoire si l'un des points suivants est vrai :
|
||||||
|
|
||||||
|
- modification du coeur d'anonymisation ;
|
||||||
|
- ajout ou modification d'une regle de masquage globale ;
|
||||||
|
- ajout ou modification d'une regle de preservation ;
|
||||||
|
- changement sur les noms patients, dates de naissance ou identifiants structurants ;
|
||||||
|
- nouvelle famille documentaire ;
|
||||||
|
- ecart constate sur le corpus synthetique complet ou le corpus reel annote ;
|
||||||
|
- demande explicite du responsable qualite ou du metier.
|
||||||
|
|
||||||
|
## 3. Entrees minimales requises
|
||||||
|
|
||||||
|
Avant la revue humaine, il faut disposer de :
|
||||||
|
|
||||||
|
- l'identifiant de version ou de commit ;
|
||||||
|
- la liste des changements ;
|
||||||
|
- les resultats des tests unitaires ;
|
||||||
|
- les resultats de la suite `tests/synthetic_regression/` ;
|
||||||
|
- les resultats de la suite `tests/synthetic_review/` ;
|
||||||
|
- les resultats du corpus reel annote si la modification est large ;
|
||||||
|
- pour chaque cas relu, le triplet :
|
||||||
|
- source ;
|
||||||
|
- attendu ;
|
||||||
|
- produit reel.
|
||||||
|
|
||||||
|
## 4. Roles
|
||||||
|
|
||||||
|
### 4.1 Operateur de revue
|
||||||
|
|
||||||
|
Prepare les elements de preuve :
|
||||||
|
|
||||||
|
- sorties texte ;
|
||||||
|
- diffs ;
|
||||||
|
- rapport d'audit ;
|
||||||
|
- resume des ecarts ;
|
||||||
|
- liste des regles impactees.
|
||||||
|
|
||||||
|
### 4.2 Relecteur metier
|
||||||
|
|
||||||
|
Verifie que :
|
||||||
|
|
||||||
|
- les donnees sensibles ne fuient pas ;
|
||||||
|
- le document reste lisible ;
|
||||||
|
- l'information utile au controle est preservee.
|
||||||
|
|
||||||
|
### 4.3 Responsable qualite / referent
|
||||||
|
|
||||||
|
Prend la decision finale :
|
||||||
|
|
||||||
|
- accepte ;
|
||||||
|
- accepte avec reserve ;
|
||||||
|
- refuse ;
|
||||||
|
- renvoie en correction.
|
||||||
|
|
||||||
|
## 5. Corpus a relire
|
||||||
|
|
||||||
|
### 5.1 Revue minimale
|
||||||
|
|
||||||
|
Pour une correction locale :
|
||||||
|
|
||||||
|
- les cas synthetiques impactes ;
|
||||||
|
- les cas complets de `tests/synthetic_review/` impactes ;
|
||||||
|
- au moins 2 documents reels proches du probleme corrige.
|
||||||
|
|
||||||
|
### 5.2 Revue standard
|
||||||
|
|
||||||
|
Pour une evolution de regles :
|
||||||
|
|
||||||
|
- tous les cas complets de `tests/synthetic_review/` ;
|
||||||
|
- un sous-ensemble reel annote representatif ;
|
||||||
|
- un echantillon de documents non annotes mais proches de la production.
|
||||||
|
|
||||||
|
### 5.3 Revue renforcee
|
||||||
|
|
||||||
|
Pour une release importante :
|
||||||
|
|
||||||
|
- toutes les familles documentaires critiques ;
|
||||||
|
- un echantillon reel relu manuellement pour chaque famille ;
|
||||||
|
- une analyse des nouveaux faux positifs et faux negatifs.
|
||||||
|
|
||||||
|
## 6. Support de revue
|
||||||
|
|
||||||
|
### 6.1 Pour les cas synthetiques complets
|
||||||
|
|
||||||
|
Utiliser :
|
||||||
|
|
||||||
|
- `tests/synthetic_review/cases/<cas>/test.txt`
|
||||||
|
- `tests/synthetic_review/cases/<cas>/expected.txt`
|
||||||
|
- `tests/synthetic_review/actual/<cas>/actual.txt`
|
||||||
|
- `tests/synthetic_review/actual/<cas>/diff.txt`
|
||||||
|
|
||||||
|
### 6.2 Pour les cas reels
|
||||||
|
|
||||||
|
Utiliser :
|
||||||
|
|
||||||
|
- le document source autorise ;
|
||||||
|
- le texte pseudonymise ;
|
||||||
|
- le PDF de sortie si disponible ;
|
||||||
|
- l'audit JSON/JSONL ;
|
||||||
|
- le rapport de fuite ;
|
||||||
|
- le rapport d'evaluation si le document est annote.
|
||||||
|
|
||||||
|
## 7. Checklist de revue dossier par dossier
|
||||||
|
|
||||||
|
### 7.1 Fuites interdites
|
||||||
|
|
||||||
|
Verifier l'absence de :
|
||||||
|
|
||||||
|
- nom et prenom du patient ;
|
||||||
|
- date de naissance ;
|
||||||
|
- adresse, code postal, ville de residence ;
|
||||||
|
- telephone et email ;
|
||||||
|
- IPP, NDA, RPPS, FINESS, OGC, numero d'examen, numero interne ;
|
||||||
|
- identifiants visibles en en-tete, pied de page, tableau ou bloc structure ;
|
||||||
|
- identifiants coupes sur plusieurs lignes ;
|
||||||
|
- variantes compactes ou prefixees type `N°1234567`.
|
||||||
|
|
||||||
|
### 7.2 Preservation fonctionnelle
|
||||||
|
|
||||||
|
Verifier que restent lisibles :
|
||||||
|
|
||||||
|
- la structure du document ;
|
||||||
|
- les services, actes et formulations metier legitimes ;
|
||||||
|
- les dates non sensibles si elles doivent etre conservees ;
|
||||||
|
- les libelles utiles au controle ;
|
||||||
|
- les phrases metier explicitement preservees.
|
||||||
|
|
||||||
|
### 7.3 Exploitabilite
|
||||||
|
|
||||||
|
Verifier que le document reste :
|
||||||
|
|
||||||
|
- comprehensible ;
|
||||||
|
- utilisable par le controle ;
|
||||||
|
- pas sur-caviarde au point de perdre sa valeur.
|
||||||
|
|
||||||
|
## 8. Classification des anomalies
|
||||||
|
|
||||||
|
### 8.1 Bloquant
|
||||||
|
|
||||||
|
- fuite de PII patient ;
|
||||||
|
- fuite d'un identifiant administratif critique ;
|
||||||
|
- regression majeure sur plusieurs familles ;
|
||||||
|
- regle active sans preuve de validation.
|
||||||
|
|
||||||
|
Decision :
|
||||||
|
|
||||||
|
- pas de release ;
|
||||||
|
- correction obligatoire.
|
||||||
|
|
||||||
|
### 8.2 Majeur
|
||||||
|
|
||||||
|
- faux positif important qui rend le document difficilement exploitable ;
|
||||||
|
- preservation non respectee sur un bloc metier cle ;
|
||||||
|
- comportement instable ou non explicable.
|
||||||
|
|
||||||
|
Decision :
|
||||||
|
|
||||||
|
- correction avant activation large ;
|
||||||
|
- ou activation limitee a un perimetre test.
|
||||||
|
|
||||||
|
### 8.3 Mineur
|
||||||
|
|
||||||
|
- ecart de forme sans perte de conformite ;
|
||||||
|
- difference de rendu mineure ;
|
||||||
|
- bruit d'audit sans impact document.
|
||||||
|
|
||||||
|
Decision :
|
||||||
|
|
||||||
|
- peut etre accepte si documente.
|
||||||
|
|
||||||
|
## 9. Decision de validation
|
||||||
|
|
||||||
|
Chaque revue doit se conclure par une decision explicite :
|
||||||
|
|
||||||
|
- `ACCEPTE`
|
||||||
|
- `ACCEPTE_AVEC_RESERVE`
|
||||||
|
- `REFUSE`
|
||||||
|
- `A_CORRIGER_PUIS_REVOIR`
|
||||||
|
|
||||||
|
La decision doit citer :
|
||||||
|
|
||||||
|
- la version revue ;
|
||||||
|
- le nom du relecteur ;
|
||||||
|
- la date ;
|
||||||
|
- les cas relus ;
|
||||||
|
- les anomalies ouvertes ;
|
||||||
|
- la portee de la decision.
|
||||||
|
|
||||||
|
## 10. Preuves a conserver
|
||||||
|
|
||||||
|
Conserver a minima :
|
||||||
|
|
||||||
|
- identifiant de commit ;
|
||||||
|
- resultat des tests automatiques ;
|
||||||
|
- liste des cas relus ;
|
||||||
|
- fiche de validation humaine ;
|
||||||
|
- rapport de diff ;
|
||||||
|
- decision signee ou attribuee nominativement.
|
||||||
|
|
||||||
|
## 11. Workflow operationnel recommande
|
||||||
|
|
||||||
|
### Etape 1
|
||||||
|
|
||||||
|
Executer les tests automatiques :
|
||||||
|
|
||||||
|
- `pytest ...`
|
||||||
|
- `python3 tools/run_synthetic_review_corpus.py`
|
||||||
|
|
||||||
|
### Etape 2
|
||||||
|
|
||||||
|
Preparer le lot de revue :
|
||||||
|
|
||||||
|
- cas impactes ;
|
||||||
|
- textes attendus ;
|
||||||
|
- textes reels ;
|
||||||
|
- synthese des ecarts.
|
||||||
|
|
||||||
|
### Etape 3
|
||||||
|
|
||||||
|
Faire la revue humaine avec la fiche standard.
|
||||||
|
|
||||||
|
### Etape 4
|
||||||
|
|
||||||
|
Classer chaque anomalie :
|
||||||
|
|
||||||
|
- bloquant ;
|
||||||
|
- majeur ;
|
||||||
|
- mineur.
|
||||||
|
|
||||||
|
### Etape 5
|
||||||
|
|
||||||
|
Prendre une decision de version :
|
||||||
|
|
||||||
|
- go ;
|
||||||
|
- go limite ;
|
||||||
|
- no go.
|
||||||
|
|
||||||
|
## 12. Regle simple de gouvernance
|
||||||
|
|
||||||
|
Une regle nouvelle n'est **jamais** activee directement en production si :
|
||||||
|
|
||||||
|
- elle n'a pas de cas de test associe ;
|
||||||
|
- elle n'a pas ete simulee ;
|
||||||
|
- personne n'a valide ses effets visibles.
|
||||||
|
|
||||||
|
## 13. Fichier associe
|
||||||
|
|
||||||
|
Le modele de fiche a remplir est dans :
|
||||||
|
|
||||||
|
- [fiche-validation-humaine-modele.md](/home/dom/ai/anonymisation/docs/fiche-validation-humaine-modele.md:1)
|
||||||
307
docs/spec-regles-administration.md
Normal file
307
docs/spec-regles-administration.md
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
# Specification MVP - regles d'administration
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Cette specification decrit un MVP pour administrer des regles d'anonymisation
|
||||||
|
sans modifier directement le code du moteur.
|
||||||
|
|
||||||
|
Le but n'est pas de donner un acces libre a des regex dangereuses.
|
||||||
|
Le but est de fournir un **moteur de regles gouverne**, testable et reversible.
|
||||||
|
|
||||||
|
## 2. Objectifs fonctionnels
|
||||||
|
|
||||||
|
Le MVP doit permettre de :
|
||||||
|
|
||||||
|
- creer une regle ;
|
||||||
|
- visualiser sa forme normalisee ;
|
||||||
|
- simuler son effet sur un corpus cible ;
|
||||||
|
- l'activer ou la desactiver ;
|
||||||
|
- garder une trace complete des changements ;
|
||||||
|
- lier chaque regle a des cas de test.
|
||||||
|
|
||||||
|
## 3. Hors perimetre MVP
|
||||||
|
|
||||||
|
Le MVP ne cherche pas encore a :
|
||||||
|
|
||||||
|
- couvrir tous les cas regex complexes ;
|
||||||
|
- gerer des workflows d'approbation multi-niveaux ;
|
||||||
|
- remplacer toute la configuration coeur existante ;
|
||||||
|
- editer directement les heuristiques internes du pipeline.
|
||||||
|
|
||||||
|
## 4. Types de regles supportes
|
||||||
|
|
||||||
|
### 4.1 `exact_term`
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
- masquer un terme exact.
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
|
||||||
|
- `CHCB`
|
||||||
|
- `LOCAL_SIGLE`
|
||||||
|
|
||||||
|
Parametres principaux :
|
||||||
|
|
||||||
|
- `match.exact_value`
|
||||||
|
- `normalization.case_insensitive`
|
||||||
|
- `normalization.whole_word`
|
||||||
|
|
||||||
|
### 4.2 `normalized_identifier`
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
- masquer une valeur canonique et ses variantes normalisees.
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
- valeur canonique : `1234567`
|
||||||
|
|
||||||
|
Variantes attendues selon la configuration :
|
||||||
|
|
||||||
|
- `1234567`
|
||||||
|
- `N°1234567`
|
||||||
|
- `N° 1234567`
|
||||||
|
- `No1234567`
|
||||||
|
- `Numero 1234567`
|
||||||
|
|
||||||
|
Parametres principaux :
|
||||||
|
|
||||||
|
- `match.canonical_value`
|
||||||
|
- `normalization.accepted_prefixes`
|
||||||
|
- `normalization.prefix_value_separators`
|
||||||
|
- `normalization.allow_bare_value`
|
||||||
|
- `normalization.multiline`
|
||||||
|
|
||||||
|
### 4.3 `contextual_identifier`
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
- masquer un identifiant seulement dans un contexte structure donne.
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
|
||||||
|
- `IPP : ABC12345`
|
||||||
|
- `N° venue : 1234567`
|
||||||
|
- `N° examen : 23L35781`
|
||||||
|
|
||||||
|
Parametres principaux :
|
||||||
|
|
||||||
|
- `match.canonical_value`
|
||||||
|
- `match.context_prefixes`
|
||||||
|
- `match.context_separators`
|
||||||
|
- `normalization.multiline`
|
||||||
|
|
||||||
|
### 4.4 `preserve_phrase`
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
- garantir qu'une phrase ou formulation metier ne soit jamais masquee.
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
|
||||||
|
- `classification internationale`
|
||||||
|
- `prise en charge`
|
||||||
|
|
||||||
|
Parametres principaux :
|
||||||
|
|
||||||
|
- `match.exact_value`
|
||||||
|
- `normalization.case_insensitive`
|
||||||
|
|
||||||
|
## 5. Structure des regles
|
||||||
|
|
||||||
|
Le schema de reference est fourni ici :
|
||||||
|
|
||||||
|
- [admin_rules.schema.json](/home/dom/ai/anonymisation/schemas/admin_rules.schema.json:1)
|
||||||
|
|
||||||
|
Le template versionne est ici :
|
||||||
|
|
||||||
|
- [admin_rules.default.yml](/home/dom/ai/anonymisation/config/admin_rules.default.yml:1)
|
||||||
|
|
||||||
|
## 6. Champs minimaux par regle
|
||||||
|
|
||||||
|
Chaque regle doit porter :
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `label`
|
||||||
|
- `type`
|
||||||
|
- `action`
|
||||||
|
- `status`
|
||||||
|
- `match`
|
||||||
|
- `scope`
|
||||||
|
- `governance`
|
||||||
|
|
||||||
|
Et, si `action = mask` :
|
||||||
|
|
||||||
|
- `placeholder`
|
||||||
|
|
||||||
|
## 7. Etats de cycle de vie
|
||||||
|
|
||||||
|
### `draft`
|
||||||
|
|
||||||
|
- regle redigee ;
|
||||||
|
- jamais activee ;
|
||||||
|
- pas encore validee.
|
||||||
|
|
||||||
|
### `candidate`
|
||||||
|
|
||||||
|
- regle complete ;
|
||||||
|
- simulation en cours ;
|
||||||
|
- revue humaine a faire.
|
||||||
|
|
||||||
|
### `approved`
|
||||||
|
|
||||||
|
- regle validee fonctionnellement ;
|
||||||
|
- prete a etre activee.
|
||||||
|
|
||||||
|
### `active`
|
||||||
|
|
||||||
|
- regle exploitable en environnement autorise.
|
||||||
|
|
||||||
|
### `disabled`
|
||||||
|
|
||||||
|
- regle desactivee temporairement.
|
||||||
|
|
||||||
|
### `retired`
|
||||||
|
|
||||||
|
- regle retiree, conservee pour audit.
|
||||||
|
|
||||||
|
## 8. Portee d'une regle
|
||||||
|
|
||||||
|
Une regle ne doit pas etre globalisee par defaut.
|
||||||
|
|
||||||
|
Le MVP doit permettre de preciser :
|
||||||
|
|
||||||
|
- familles documentaires ;
|
||||||
|
- environnements ;
|
||||||
|
- zones documentaires :
|
||||||
|
- `narrative`
|
||||||
|
- `structured`
|
||||||
|
- `table`
|
||||||
|
- `header`
|
||||||
|
- `footer`
|
||||||
|
|
||||||
|
## 9. Gouvernance minimale
|
||||||
|
|
||||||
|
Chaque regle doit indiquer :
|
||||||
|
|
||||||
|
- `owner`
|
||||||
|
- `justification`
|
||||||
|
- `created_at`
|
||||||
|
- `review_required_for_activation`
|
||||||
|
- `tests.required_case_ids`
|
||||||
|
|
||||||
|
Pour une regle active, l'absence de cas de test associe doit etre interdite.
|
||||||
|
|
||||||
|
## 10. Ecrans MVP recommandes
|
||||||
|
|
||||||
|
### 10.1 Liste des regles
|
||||||
|
|
||||||
|
Colonnes minimales :
|
||||||
|
|
||||||
|
- id ;
|
||||||
|
- label ;
|
||||||
|
- type ;
|
||||||
|
- status ;
|
||||||
|
- scope ;
|
||||||
|
- auteur ;
|
||||||
|
- date ;
|
||||||
|
- dernier test ;
|
||||||
|
- dernier verdict.
|
||||||
|
|
||||||
|
Actions :
|
||||||
|
|
||||||
|
- filtrer ;
|
||||||
|
- dupliquer ;
|
||||||
|
- desactiver ;
|
||||||
|
- ouvrir ;
|
||||||
|
- comparer deux versions.
|
||||||
|
|
||||||
|
### 10.2 Creation / edition d'une regle
|
||||||
|
|
||||||
|
Blocs de formulaire :
|
||||||
|
|
||||||
|
- identification ;
|
||||||
|
- type ;
|
||||||
|
- valeur source ;
|
||||||
|
- normalisation ;
|
||||||
|
- portee ;
|
||||||
|
- gouvernance ;
|
||||||
|
- rattachement des cas de test.
|
||||||
|
|
||||||
|
### 10.3 Simulation
|
||||||
|
|
||||||
|
Avant activation, afficher :
|
||||||
|
|
||||||
|
- variantes generees ;
|
||||||
|
- cas touches ;
|
||||||
|
- extrait avant / apres ;
|
||||||
|
- nouveaux masquages ;
|
||||||
|
- pertes de preservation ;
|
||||||
|
- verdict automatique.
|
||||||
|
|
||||||
|
### 10.4 Validation / activation
|
||||||
|
|
||||||
|
Le panneau d'activation doit afficher :
|
||||||
|
|
||||||
|
- resultat du validateur de schema ;
|
||||||
|
- resultat des tests lies ;
|
||||||
|
- resultat du corpus de simulation ;
|
||||||
|
- presence ou non d'une revue humaine ;
|
||||||
|
- bouton `Activer` ou blocage.
|
||||||
|
|
||||||
|
### 10.5 Historique
|
||||||
|
|
||||||
|
Chaque changement doit laisser une trace :
|
||||||
|
|
||||||
|
- qui ;
|
||||||
|
- quand ;
|
||||||
|
- quoi ;
|
||||||
|
- pourquoi ;
|
||||||
|
- sur quel perimetre ;
|
||||||
|
- avec quel resultat.
|
||||||
|
|
||||||
|
## 11. Garde-fous obligatoires
|
||||||
|
|
||||||
|
Le MVP doit bloquer :
|
||||||
|
|
||||||
|
- une regle active sans cas de test ;
|
||||||
|
- une regle `preserve_phrase` avec action `mask` ;
|
||||||
|
- une regle trop large sans justification ;
|
||||||
|
- une activation sans simulation ;
|
||||||
|
- une regle syntaxiquement invalide.
|
||||||
|
|
||||||
|
## 12. Commande de validation hors interface
|
||||||
|
|
||||||
|
Le depot fournit un validateur CLI :
|
||||||
|
|
||||||
|
- [validate_admin_rules.py](/home/dom/ai/anonymisation/tools/validate_admin_rules.py:1)
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/validate_admin_rules.py --config config/admin_rules.default.yml --show-variants
|
||||||
|
```
|
||||||
|
|
||||||
|
## 13. Recommandation d'implementation
|
||||||
|
|
||||||
|
Ordre recommande :
|
||||||
|
|
||||||
|
1. schema et validateur ;
|
||||||
|
2. stockage YAML versionne ;
|
||||||
|
3. simulation hors interface ;
|
||||||
|
4. ecran d'edition ;
|
||||||
|
5. ecran de simulation ;
|
||||||
|
6. activation gouvernee.
|
||||||
|
|
||||||
|
## 14. Point important
|
||||||
|
|
||||||
|
Ces artefacts definissent le **contrat de regles** et le **MVP d'administration**.
|
||||||
|
Ils ne branchent pas encore ces regles dans le moteur de production.
|
||||||
|
|
||||||
|
La prochaine etape technique sera :
|
||||||
|
|
||||||
|
- chargement de `admin_rules` ;
|
||||||
|
- compilation ;
|
||||||
|
- simulation ;
|
||||||
|
- puis application controlee dans le pipeline.
|
||||||
390
schemas/admin_rules.schema.json
Normal file
390
schemas/admin_rules.schema.json
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://local.anonymisation/admin-rules.schema.json",
|
||||||
|
"title": "Admin rules configuration",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"rules"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"review_required_for_activation": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"environments": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"test",
|
||||||
|
"staging",
|
||||||
|
"prod"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"narrative",
|
||||||
|
"structured",
|
||||||
|
"table",
|
||||||
|
"header",
|
||||||
|
"footer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/rule"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"scope": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"document_families",
|
||||||
|
"environments",
|
||||||
|
"sections"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"document_families": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"environments": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"test",
|
||||||
|
"staging",
|
||||||
|
"prod"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"narrative",
|
||||||
|
"structured",
|
||||||
|
"table",
|
||||||
|
"header",
|
||||||
|
"footer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tests": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"required_case_ids"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"required_case_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"governance": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"owner",
|
||||||
|
"justification",
|
||||||
|
"created_at",
|
||||||
|
"review_required_for_activation",
|
||||||
|
"tests"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"owner": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 2
|
||||||
|
},
|
||||||
|
"justification": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 8
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"review_required_for_activation": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"approved_by": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ticket": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tests": {
|
||||||
|
"$ref": "#/$defs/tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"normalization": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"case_insensitive": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"whole_word": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"multiline": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"allow_bare_value": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"accepted_prefixes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"prefix_value_separators": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"match": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"exact_value": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"canonical_value": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"context_prefixes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"context_separators": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rule": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"label",
|
||||||
|
"type",
|
||||||
|
"action",
|
||||||
|
"status",
|
||||||
|
"match",
|
||||||
|
"scope",
|
||||||
|
"governance"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9_][a-z0-9_-]{2,63}$"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"exact_term",
|
||||||
|
"normalized_identifier",
|
||||||
|
"contextual_identifier",
|
||||||
|
"preserve_phrase"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"mask",
|
||||||
|
"preserve"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"placeholder": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\[[A-Z_]+\\]$"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"draft",
|
||||||
|
"candidate",
|
||||||
|
"approved",
|
||||||
|
"active",
|
||||||
|
"disabled",
|
||||||
|
"retired"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"match": {
|
||||||
|
"$ref": "#/$defs/match"
|
||||||
|
},
|
||||||
|
"normalization": {
|
||||||
|
"$ref": "#/$defs/normalization"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"$ref": "#/$defs/scope"
|
||||||
|
},
|
||||||
|
"governance": {
|
||||||
|
"$ref": "#/$defs/governance"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"const": "mask"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"placeholder"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "exact_term"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"match": {
|
||||||
|
"required": [
|
||||||
|
"exact_value"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "preserve_phrase"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"const": "preserve"
|
||||||
|
},
|
||||||
|
"match": {
|
||||||
|
"required": [
|
||||||
|
"exact_value"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "normalized_identifier"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"match": {
|
||||||
|
"required": [
|
||||||
|
"canonical_value"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "contextual_identifier"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"match": {
|
||||||
|
"required": [
|
||||||
|
"canonical_value",
|
||||||
|
"context_prefixes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
tests/unit/test_admin_rules_validator.py
Normal file
80
tests/unit/test_admin_rules_validator.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Tests de non-regression pour le contrat des regles d'administration.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tools.validate_admin_rules import (
|
||||||
|
generate_rule_variants,
|
||||||
|
load_rules_config,
|
||||||
|
validate_rules_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_admin_rules_template_is_valid():
|
||||||
|
path = Path("config/admin_rules.default.yml")
|
||||||
|
|
||||||
|
data = load_rules_config(path)
|
||||||
|
errors = validate_rules_config(data)
|
||||||
|
|
||||||
|
assert errors == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalized_identifier_variants_cover_requested_forms():
|
||||||
|
rule = {
|
||||||
|
"type": "normalized_identifier",
|
||||||
|
"match": {
|
||||||
|
"canonical_value": "1234567",
|
||||||
|
},
|
||||||
|
"normalization": {
|
||||||
|
"allow_bare_value": True,
|
||||||
|
"multiline": True,
|
||||||
|
"accepted_prefixes": ["N°", "No"],
|
||||||
|
"prefix_value_separators": ["", " "],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
variants = generate_rule_variants(rule, limit=20)
|
||||||
|
|
||||||
|
assert "1234567" in variants
|
||||||
|
assert "N°1234567" in variants
|
||||||
|
assert "N° 1234567" in variants
|
||||||
|
assert "No1234567" in variants
|
||||||
|
assert "No 1234567" in variants
|
||||||
|
|
||||||
|
|
||||||
|
def test_preserve_phrase_must_use_preserve_action():
|
||||||
|
data = {
|
||||||
|
"version": 1,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "rule_bad_preserve",
|
||||||
|
"label": "Bad preserve",
|
||||||
|
"type": "preserve_phrase",
|
||||||
|
"action": "mask",
|
||||||
|
"status": "draft",
|
||||||
|
"match": {
|
||||||
|
"exact_value": "classification internationale",
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"document_families": ["all"],
|
||||||
|
"environments": ["test"],
|
||||||
|
"sections": ["narrative"],
|
||||||
|
},
|
||||||
|
"governance": {
|
||||||
|
"owner": "qualite",
|
||||||
|
"justification": "test de contrat",
|
||||||
|
"created_at": "2026-04-21",
|
||||||
|
"review_required_for_activation": True,
|
||||||
|
"approved_by": None,
|
||||||
|
"tests": {
|
||||||
|
"required_case_ids": ["006_whitelist_phrases_preserved"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = validate_rules_config(data)
|
||||||
|
|
||||||
|
assert any("preserve_phrase" in error for error in errors)
|
||||||
260
tools/validate_admin_rules.py
Normal file
260
tools/validate_admin_rules.py
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Validation semantique des regles d'administration.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
VALID_TYPES = {
|
||||||
|
"exact_term",
|
||||||
|
"normalized_identifier",
|
||||||
|
"contextual_identifier",
|
||||||
|
"preserve_phrase",
|
||||||
|
}
|
||||||
|
VALID_ACTIONS = {"mask", "preserve"}
|
||||||
|
VALID_STATUSES = {"draft", "candidate", "approved", "active", "disabled", "retired"}
|
||||||
|
VALID_ENVIRONMENTS = {"test", "staging", "prod"}
|
||||||
|
VALID_SECTIONS = {"narrative", "structured", "table", "header", "footer"}
|
||||||
|
|
||||||
|
|
||||||
|
def load_rules_config(path: Path) -> dict[str, Any]:
|
||||||
|
with path.open("r", encoding="utf-8") as handle:
|
||||||
|
data = yaml.safe_load(handle) or {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("Le fichier doit contenir un mapping YAML en racine.")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _is_non_empty_string(value: Any) -> bool:
|
||||||
|
return isinstance(value, str) and bool(value.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def generate_rule_variants(rule: dict[str, Any], limit: int = 12) -> list[str]:
|
||||||
|
rule_type = rule.get("type")
|
||||||
|
match = rule.get("match") or {}
|
||||||
|
normalization = rule.get("normalization") or {}
|
||||||
|
variants: list[str] = []
|
||||||
|
|
||||||
|
if rule_type in {"exact_term", "preserve_phrase"}:
|
||||||
|
exact_value = str(match.get("exact_value", "")).strip()
|
||||||
|
return [exact_value] if exact_value else []
|
||||||
|
|
||||||
|
if rule_type == "normalized_identifier":
|
||||||
|
canonical = str(match.get("canonical_value", "")).strip()
|
||||||
|
prefixes = normalization.get("accepted_prefixes") or []
|
||||||
|
separators = normalization.get("prefix_value_separators") or [" "]
|
||||||
|
if normalization.get("allow_bare_value", False) and canonical:
|
||||||
|
variants.append(canonical)
|
||||||
|
for prefix in prefixes:
|
||||||
|
for separator in separators:
|
||||||
|
variants.append(f"{prefix}{separator}{canonical}")
|
||||||
|
if normalization.get("multiline", False):
|
||||||
|
variants.append(f"{prefix}\n{canonical}")
|
||||||
|
return _dedupe_keep_order(variants)[:limit]
|
||||||
|
|
||||||
|
if rule_type == "contextual_identifier":
|
||||||
|
canonical = str(match.get("canonical_value", "")).strip()
|
||||||
|
prefixes = match.get("context_prefixes") or []
|
||||||
|
separators = match.get("context_separators") or [": ", ":"]
|
||||||
|
for prefix in prefixes:
|
||||||
|
for separator in separators:
|
||||||
|
variants.append(f"{prefix}{separator}{canonical}")
|
||||||
|
if (rule.get("normalization") or {}).get("multiline", False):
|
||||||
|
variants.append(f"{prefix}\n{canonical}")
|
||||||
|
variants.append(f"{prefix} :\n{canonical}")
|
||||||
|
return _dedupe_keep_order(variants)[:limit]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_keep_order(values: list[str]) -> list[str]:
|
||||||
|
seen: set[str] = set()
|
||||||
|
output: list[str] = []
|
||||||
|
for value in values:
|
||||||
|
if value in seen:
|
||||||
|
continue
|
||||||
|
seen.add(value)
|
||||||
|
output.append(value)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rules_config(data: dict[str, Any]) -> list[str]:
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
version = data.get("version")
|
||||||
|
if not isinstance(version, int) or version < 1:
|
||||||
|
errors.append("`version` doit etre un entier >= 1.")
|
||||||
|
|
||||||
|
rules = data.get("rules")
|
||||||
|
if not isinstance(rules, list):
|
||||||
|
errors.append("`rules` doit etre une liste.")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
seen_ids: set[str] = set()
|
||||||
|
for index, rule in enumerate(rules):
|
||||||
|
prefix = f"rules[{index}]"
|
||||||
|
if not isinstance(rule, dict):
|
||||||
|
errors.append(f"{prefix}: chaque regle doit etre un mapping.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule_id = rule.get("id")
|
||||||
|
if not _is_non_empty_string(rule_id):
|
||||||
|
errors.append(f"{prefix}: `id` est obligatoire.")
|
||||||
|
elif rule_id in seen_ids:
|
||||||
|
errors.append(f"{prefix}: `id` duplique `{rule_id}`.")
|
||||||
|
else:
|
||||||
|
seen_ids.add(rule_id)
|
||||||
|
|
||||||
|
if not _is_non_empty_string(rule.get("label")):
|
||||||
|
errors.append(f"{prefix}: `label` est obligatoire.")
|
||||||
|
|
||||||
|
rule_type = rule.get("type")
|
||||||
|
if rule_type not in VALID_TYPES:
|
||||||
|
errors.append(f"{prefix}: `type` invalide.")
|
||||||
|
|
||||||
|
action = rule.get("action")
|
||||||
|
if action not in VALID_ACTIONS:
|
||||||
|
errors.append(f"{prefix}: `action` invalide.")
|
||||||
|
|
||||||
|
status = rule.get("status")
|
||||||
|
if status not in VALID_STATUSES:
|
||||||
|
errors.append(f"{prefix}: `status` invalide.")
|
||||||
|
|
||||||
|
if action == "mask" and not _is_non_empty_string(rule.get("placeholder")):
|
||||||
|
errors.append(f"{prefix}: `placeholder` est obligatoire pour une regle de masquage.")
|
||||||
|
|
||||||
|
match = rule.get("match")
|
||||||
|
if not isinstance(match, dict):
|
||||||
|
errors.append(f"{prefix}: `match` doit etre un mapping.")
|
||||||
|
match = {}
|
||||||
|
|
||||||
|
normalization = rule.get("normalization") or {}
|
||||||
|
if normalization and not isinstance(normalization, dict):
|
||||||
|
errors.append(f"{prefix}: `normalization` doit etre un mapping.")
|
||||||
|
normalization = {}
|
||||||
|
|
||||||
|
scope = rule.get("scope")
|
||||||
|
if not isinstance(scope, dict):
|
||||||
|
errors.append(f"{prefix}: `scope` doit etre un mapping.")
|
||||||
|
scope = {}
|
||||||
|
|
||||||
|
governance = rule.get("governance")
|
||||||
|
if not isinstance(governance, dict):
|
||||||
|
errors.append(f"{prefix}: `governance` doit etre un mapping.")
|
||||||
|
governance = {}
|
||||||
|
|
||||||
|
document_families = scope.get("document_families")
|
||||||
|
if not isinstance(document_families, list) or not document_families:
|
||||||
|
errors.append(f"{prefix}: `scope.document_families` doit etre une liste non vide.")
|
||||||
|
|
||||||
|
environments = scope.get("environments")
|
||||||
|
if not isinstance(environments, list) or not environments:
|
||||||
|
errors.append(f"{prefix}: `scope.environments` doit etre une liste non vide.")
|
||||||
|
else:
|
||||||
|
invalid_envs = [value for value in environments if value not in VALID_ENVIRONMENTS]
|
||||||
|
if invalid_envs:
|
||||||
|
errors.append(f"{prefix}: environnements invalides: {', '.join(invalid_envs)}.")
|
||||||
|
|
||||||
|
sections = scope.get("sections")
|
||||||
|
if not isinstance(sections, list) or not sections:
|
||||||
|
errors.append(f"{prefix}: `scope.sections` doit etre une liste non vide.")
|
||||||
|
else:
|
||||||
|
invalid_sections = [value for value in sections if value not in VALID_SECTIONS]
|
||||||
|
if invalid_sections:
|
||||||
|
errors.append(f"{prefix}: sections invalides: {', '.join(invalid_sections)}.")
|
||||||
|
|
||||||
|
if not _is_non_empty_string(governance.get("owner")):
|
||||||
|
errors.append(f"{prefix}: `governance.owner` est obligatoire.")
|
||||||
|
if not _is_non_empty_string(governance.get("justification")):
|
||||||
|
errors.append(f"{prefix}: `governance.justification` est obligatoire.")
|
||||||
|
if not _is_non_empty_string(governance.get("created_at")):
|
||||||
|
errors.append(f"{prefix}: `governance.created_at` est obligatoire.")
|
||||||
|
|
||||||
|
tests = governance.get("tests")
|
||||||
|
if not isinstance(tests, dict):
|
||||||
|
errors.append(f"{prefix}: `governance.tests` doit etre un mapping.")
|
||||||
|
tests = {}
|
||||||
|
required_case_ids = tests.get("required_case_ids")
|
||||||
|
if not isinstance(required_case_ids, list) or not required_case_ids:
|
||||||
|
errors.append(f"{prefix}: `governance.tests.required_case_ids` doit etre une liste non vide.")
|
||||||
|
|
||||||
|
if rule_type == "exact_term":
|
||||||
|
if not _is_non_empty_string(match.get("exact_value")):
|
||||||
|
errors.append(f"{prefix}: `match.exact_value` est obligatoire pour `exact_term`.")
|
||||||
|
|
||||||
|
if rule_type == "preserve_phrase":
|
||||||
|
if action != "preserve":
|
||||||
|
errors.append(f"{prefix}: `preserve_phrase` doit utiliser `action: preserve`.")
|
||||||
|
if not _is_non_empty_string(match.get("exact_value")):
|
||||||
|
errors.append(f"{prefix}: `match.exact_value` est obligatoire pour `preserve_phrase`.")
|
||||||
|
|
||||||
|
if rule_type == "normalized_identifier":
|
||||||
|
if not _is_non_empty_string(match.get("canonical_value")):
|
||||||
|
errors.append(f"{prefix}: `match.canonical_value` est obligatoire pour `normalized_identifier`.")
|
||||||
|
prefixes = normalization.get("accepted_prefixes", [])
|
||||||
|
if prefixes and not isinstance(prefixes, list):
|
||||||
|
errors.append(f"{prefix}: `normalization.accepted_prefixes` doit etre une liste.")
|
||||||
|
separators = normalization.get("prefix_value_separators", [])
|
||||||
|
if separators and not isinstance(separators, list):
|
||||||
|
errors.append(f"{prefix}: `normalization.prefix_value_separators` doit etre une liste.")
|
||||||
|
|
||||||
|
if rule_type == "contextual_identifier":
|
||||||
|
if not _is_non_empty_string(match.get("canonical_value")):
|
||||||
|
errors.append(f"{prefix}: `match.canonical_value` est obligatoire pour `contextual_identifier`.")
|
||||||
|
context_prefixes = match.get("context_prefixes")
|
||||||
|
if not isinstance(context_prefixes, list) or not context_prefixes:
|
||||||
|
errors.append(f"{prefix}: `match.context_prefixes` doit etre une liste non vide.")
|
||||||
|
|
||||||
|
if status == "active" and governance.get("review_required_for_activation", False):
|
||||||
|
if not _is_non_empty_string(governance.get("approved_by")):
|
||||||
|
errors.append(f"{prefix}: `governance.approved_by` est obligatoire pour une regle active.")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Valider les regles d'administration")
|
||||||
|
parser.add_argument(
|
||||||
|
"--config",
|
||||||
|
default="config/admin_rules.default.yml",
|
||||||
|
help="Chemin vers le fichier YAML a valider.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--show-variants",
|
||||||
|
action="store_true",
|
||||||
|
help="Afficher un apercu des variantes generees pour les regles de type identifiant.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
config_path = Path(args.config)
|
||||||
|
data = load_rules_config(config_path)
|
||||||
|
errors = validate_rules_config(data)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("Configuration invalide:")
|
||||||
|
for error in errors:
|
||||||
|
print(f"- {error}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
rules = data.get("rules", [])
|
||||||
|
print(f"Configuration valide: {config_path} ({len(rules)} regle(s))")
|
||||||
|
for rule in rules:
|
||||||
|
print(f"- {rule['id']} [{rule['status']}] {rule['type']}")
|
||||||
|
if args.show_variants:
|
||||||
|
variants = generate_rule_variants(rule)
|
||||||
|
if variants:
|
||||||
|
print(" Variantes:")
|
||||||
|
for value in variants:
|
||||||
|
print(f" - {value}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user