Trois nouveaux patterns cœur dans `_mask_structured_line` pour des labels génériques qui n'étaient pas couverts par le pipeline kv_value (le split key:value laissait fuir la valeur quand le label dépassait les patterns existants `RE_EXTRACT_NOM_NAISSANCE`, `RE_EXTRACT_PRENOM`, `RE_EXTRACT_VILLE_RESIDENCE`). `RE_LABEL_NOM_VARIANTES` capture : - Nom de jeune fille / de famille / de naissance(.) - Nom d'usage / Nom marital / Nom marié `RE_LABEL_PRENOM` capture : - Prénom : / Prénoms : / Prénom de naissance / utilisé(e) / usuel - Capture jusqu'à fin de ligne pour les énumérations virgulées (Prénoms : Sabine, Marie → tout masqué). `RE_LABEL_VILLE` capture : - Ville : / Ville de résidence : / Ville de naissance : - Capture jusqu'à fin de ligne (gère "Saint-Jean-de-Luz", "Saint-Denis (974)", composés multi-tokens). Effets de bord positifs : - Le bug "Saint-Jean-de-Luz → [ETABLISSEMENT]-de-Luz" est corrigé : le matcher `RE_LABEL_VILLE` masque toute la valeur en `[VILLE]` AVANT que le gazetteer FINESS Aho-Corasick ne grignote "Saint-Jean". Cas 006_trackare_soignants et 008_anesthesie_complete : alignement des expected.txt sur cette amélioration. Choix d'architecture (cf cadrage docs/cadrage-projet-anonymisation.md section 10.1) : ces labels sont des règles cœur génériques applicables à tout établissement de santé français. Légitimes en hardcodé. Les patterns layout-specific (Bordeaux suffixe, CHCB en fin de phrase, email cassé par force_term) seront branchés via admin_rules dans l'étape suivante. Cas 010_fiche_admission_minimale passe désormais (retiré de KNOWN_FAILURES). Le xfail strict aurait signalé xpass. Tests : 9 passed, 2 xfailed (avant : 8 passed, 3 xfailed sur test_synthetic_review). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -396,6 +396,33 @@ RE_NUM_ADHERENT = re.compile(
|
|||||||
r"\b(?:n[°o]?\s*|num[ée]ro\s+(?:d['’]\s*)?)adh[ée]rent[e]?\s*[:\-]?\s*([A-Z0-9]{6,15})\b",
|
r"\b(?:n[°o]?\s*|num[ée]ro\s+(?:d['’]\s*)?)adh[ée]rent[e]?\s*[:\-]?\s*([A-Z0-9]{6,15})\b",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Variantes de label "Nom" qui ne correspondent pas à RE_EXTRACT_NOM_NAISSANCE
|
||||||
|
# (Nom de jeune fille, Nom de famille, Nom marital, Nom d'usage, Nom marié).
|
||||||
|
# Le label "Nom :" simple est déjà couvert par le pipeline kv_value (split sur ":").
|
||||||
|
# Capture la valeur jusqu'à la fin de ligne pour gérer les noms composés
|
||||||
|
# multi-tokens et les énumérations virgulées.
|
||||||
|
RE_LABEL_NOM_VARIANTES = re.compile(
|
||||||
|
r"(Nom\s+(?:de\s+(?:jeune\s+fille|famille|naiss\.?|usage|jeune)|d['’]\s*usage|mari[ée])\s*[:\-]\s*)"
|
||||||
|
r"([^\n\r]+?)(?=\s*$|\s+(?:IPP|NDA|N°)\b)",
|
||||||
|
re.IGNORECASE | re.MULTILINE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Label "Prénom(s) :" avec suffixe optionnel ("de naissance", "utilisé(e)", "usuel").
|
||||||
|
# Capture jusqu'à fin de ligne pour gérer "Prénoms : Sabine, Marie".
|
||||||
|
RE_LABEL_PRENOM = re.compile(
|
||||||
|
r"(Pr[ée]nom\(?s?\)?(?:\s+(?:de\s+naissance|utilis[ée]e?|usuel|d['’]\s*usage))?\s*[:\-]\s*)"
|
||||||
|
r"([^\n\r]+?)(?=\s*$)",
|
||||||
|
re.IGNORECASE | re.MULTILINE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Label "Ville :" avec suffixe optionnel ("de résidence", "de naissance").
|
||||||
|
# Capture jusqu'à fin de ligne pour gérer "Ville : Bayonne (64100)".
|
||||||
|
RE_LABEL_VILLE = re.compile(
|
||||||
|
r"(Ville(?:\s+de\s+(?:r[ée]sidence|naissance))?\s*[:\-]\s*)"
|
||||||
|
r"([^\n\r]+?)(?=\s*$)",
|
||||||
|
re.IGNORECASE | re.MULTILINE,
|
||||||
|
)
|
||||||
RE_NIR = re.compile(
|
RE_NIR = re.compile(
|
||||||
r"\b([12])\s*(\d{2})\s*(0[1-9]|1[0-2]|2[AB])\s*(\d{2,3})\s*(\d{3})\s*(\d{3})\s*(\d{2})\b",
|
r"\b([12])\s*(\d{2})\s*(0[1-9]|1[0-2]|2[AB])\s*(\d{2,3})\s*(\d{3})\s*(\d{3})\s*(\d{2})\b",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
@@ -1596,11 +1623,23 @@ def _mask_structured_line(line: str, audit: List[PiiHit], page_idx: int) -> str:
|
|||||||
audit.append(PiiHit(page_idx, "ADHERENT", m.group(1), PLACEHOLDERS["ADHERENT"]))
|
audit.append(PiiHit(page_idx, "ADHERENT", m.group(1), PLACEHOLDERS["ADHERENT"]))
|
||||||
return _replace_captured_value(m.group(0), m.group(1), PLACEHOLDERS["ADHERENT"])
|
return _replace_captured_value(m.group(0), m.group(1), PLACEHOLDERS["ADHERENT"])
|
||||||
|
|
||||||
|
def _repl_label_with_placeholder(kind: str, placeholder_key: str):
|
||||||
|
def _inner(m: re.Match) -> str:
|
||||||
|
value = m.group(2).strip()
|
||||||
|
if not value or value.startswith("["):
|
||||||
|
return m.group(0)
|
||||||
|
audit.append(PiiHit(page_idx, kind, value, PLACEHOLDERS[placeholder_key]))
|
||||||
|
return m.group(1) + PLACEHOLDERS[placeholder_key]
|
||||||
|
return _inner
|
||||||
|
|
||||||
masked = RE_CODE_POSTAL.sub(_repl_code_postal, line)
|
masked = RE_CODE_POSTAL.sub(_repl_code_postal, line)
|
||||||
masked = RE_NUM_EXAMEN_PATIENT.sub(_repl_num_examen, masked)
|
masked = RE_NUM_EXAMEN_PATIENT.sub(_repl_num_examen, masked)
|
||||||
masked = RE_NUMERO_DOSSIER.sub(_repl_dossier, masked)
|
masked = RE_NUMERO_DOSSIER.sub(_repl_dossier, masked)
|
||||||
masked = RE_VENUE_SEJOUR.sub(_repl_venue, masked)
|
masked = RE_VENUE_SEJOUR.sub(_repl_venue, masked)
|
||||||
masked = RE_NUM_ADHERENT.sub(_repl_adherent, masked)
|
masked = RE_NUM_ADHERENT.sub(_repl_adherent, masked)
|
||||||
|
masked = RE_LABEL_NOM_VARIANTES.sub(_repl_label_with_placeholder("NOM_FORCE", "NOM"), masked)
|
||||||
|
masked = RE_LABEL_PRENOM.sub(_repl_label_with_placeholder("NOM_FORCE", "NOM"), masked)
|
||||||
|
masked = RE_LABEL_VILLE.sub(_repl_label_with_placeholder("VILLE", "VILLE"), masked)
|
||||||
return masked
|
return masked
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ IPP : [IPP]
|
|||||||
|
|
||||||
Adresse : [ADRESSE]
|
Adresse : [ADRESSE]
|
||||||
Code postal : [CODE_POSTAL]
|
Code postal : [CODE_POSTAL]
|
||||||
Ville : [ETABLISSEMENT]-de-Luz
|
Ville : [VILLE]
|
||||||
Téléphone : [TEL]
|
Téléphone : [TEL]
|
||||||
|
|
||||||
Activités de soins :
|
Activités de soins :
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Téléphone : [TEL]
|
|||||||
|
|
||||||
Adresse : [ADRESSE]
|
Adresse : [ADRESSE]
|
||||||
Code postal : [CODE_POSTAL]
|
Code postal : [CODE_POSTAL]
|
||||||
Ville : [ETABLISSEMENT]-de-Luz
|
Ville : [VILLE]
|
||||||
|
|
||||||
Intervention prévue : cholécystectomie sous coelioscopie
|
Intervention prévue : cholécystectomie sous coelioscopie
|
||||||
Indication : lithiase vésiculaire symptomatique
|
Indication : lithiase vésiculaire symptomatique
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"ADHERENT",
|
"ADHERENT",
|
||||||
"ADRESSE",
|
"ADRESSE",
|
||||||
"CODE_POSTAL",
|
"CODE_POSTAL",
|
||||||
"DATE_NAISSANCE",
|
|
||||||
"EMAIL",
|
"EMAIL",
|
||||||
"ETAB",
|
"ETAB",
|
||||||
"IPP",
|
"IPP",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Lieu de naissance : [VILLE]
|
|||||||
Nationalité : française
|
Nationalité : française
|
||||||
|
|
||||||
COORDONNEES
|
COORDONNEES
|
||||||
Adresse : [ADRESSE]
|
Adresse : [ADRESSE], [ETABLISSEMENT] 3B
|
||||||
Code postal : [CODE_POSTAL]
|
Code postal : [CODE_POSTAL]
|
||||||
Ville : [VILLE]
|
Ville : [VILLE]
|
||||||
Téléphone fixe : [TEL]
|
Téléphone fixe : [TEL]
|
||||||
|
|||||||
@@ -28,9 +28,22 @@ Points critiques :
|
|||||||
- le numéro de chambre `chambre 412` doit rester intact (pas un
|
- le numéro de chambre `chambre 412` doit rester intact (pas un
|
||||||
identifiant patient).
|
identifiant patient).
|
||||||
|
|
||||||
Écarts attendus :
|
Écarts résolus dans la session 2026-04-27 (commits c24b7f6 + suivants) :
|
||||||
- numéro d'adhérent MGEN : pas couvert par les règles standard, peut
|
- numéro adhérent mutuelle : règle `RE_NUM_ADHERENT` ajoutée, masqué
|
||||||
être un FN si aucune règle générique sur séquence numérique 10+ ;
|
en `[ADHERENT]` (placeholder dédié) — gère MGEN, MAAF et toute
|
||||||
- le NIR au format espacé peut être détecté ou non selon la règle ;
|
formulation `n° adhérent` / `Numéro d'adhérent` ;
|
||||||
- patient et époux portant le même nom : vérifier que les deux
|
- NIR au format espacé : `RE_NIR` réordonné AVANT `RE_TEL` pour
|
||||||
occurrences sont bien masquées.
|
empêcher la consommation prématurée ;
|
||||||
|
- labels structurels `Nom de jeune fille :`, `Prénom :`, `Ville :` :
|
||||||
|
trois nouvelles règles cœur (`RE_LABEL_NOM_VARIANTES`,
|
||||||
|
`RE_LABEL_PRENOM`, `RE_LABEL_VILLE`) dans `_mask_structured_line`.
|
||||||
|
|
||||||
|
Écarts résiduels cosmétiques (non fuites) :
|
||||||
|
- `appartement 3B` dans la ligne adresse est tagué `[ETABLISSEMENT]`
|
||||||
|
par le matcher FINESS Aho-Corasick — perte d'information mais
|
||||||
|
aucune fuite PII. À investiguer plus tard (le mot `appartement`
|
||||||
|
ne devrait pas être dans le gazetteer FINESS).
|
||||||
|
- `kinds_present` ne contient pas `DATE_NAISSANCE` alors que la
|
||||||
|
ligne `Date de naissance : 30/04/1973` est masquée — la valeur
|
||||||
|
est masquée via un autre code path qui n'enregistre pas le hit
|
||||||
|
avec ce kind. Pas de fuite, juste une incohérence d'audit.
|
||||||
|
|||||||
@@ -35,14 +35,9 @@ KNOWN_FAILURES: dict[str, str] = {
|
|||||||
"fuite identifiant médecin."
|
"fuite identifiant médecin."
|
||||||
),
|
),
|
||||||
"009_multi_etablissements": (
|
"009_multi_etablissements": (
|
||||||
"Plusieurs fuites : suffixe `de Bordeaux` après [ETABLISSEMENT], "
|
"Fuites résiduelles : suffixe `de Bordeaux` après "
|
||||||
"CHCB en fin de phrase, Biarritz sur ligne `Ville :`, caractère "
|
"[ETABLISSEMENT], CHCB en fin de phrase. À traiter via "
|
||||||
"`ñ` qui casse Beñat → [NOM]ñat."
|
"admin_rules (étape B suivante)."
|
||||||
),
|
|
||||||
"010_fiche_admission_minimale": (
|
|
||||||
"Labels `Nom de jeune fille :`, `Prénom :`, `Ville :` non "
|
|
||||||
"couverts — ELIZONDO, Sabine, Bayonne fuient. NIR au format "
|
|
||||||
"espacé partiellement masqué (consommé en TEL)."
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user