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