fix(detect): F5 — masque la continuation orpheline d'un nom composé (EJNAINI)
Dernière fuite de l'audit_30. Cas Trackare : un nom composé "NOCENT-EJNAINI"
éclaté en colonnes devient "[NOM]-\nEJNAINI" — le 1er composant est masqué
par le NER mais le 2e reste en clair (ni span NER intact ni candidat regex ne
le couvre ; être dans paranames ne suffit pas sans candidat).
Fix : post-passe dans process_pdf (étape 3a-bis), après selective_rescan, qui
masque le token majuscule orphelin suivant immédiatement un "[NOM]-". Couvre
le texte ET le raster (NOM_GLOBAL). Réfute la conclusion de Qwen ("paranames
résoudra EJNAINI").
Validation audit_30 (29 docs) : score 98.3 → 98.5/100, LEAK SCORE 100/100
(0 fuite), 0 régression FP. tests/unit 85 passed. BA127127 : EJNAINI 7→0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4502,6 +4502,21 @@ def process_pdf(
|
||||
# 3) Rescan selectif
|
||||
final_text = selective_rescan(final_text, cfg=cfg)
|
||||
|
||||
# 3a-bis) Nettoyage post-masquage : continuation orpheline d'un nom composé
|
||||
# coupé par saut de ligne. Cas Trackare en colonnes : "NOCENT-EJNAINI"
|
||||
# est éclaté en "[NOM]-\nEJNAINI" → le 1er composant est masqué par le
|
||||
# NER mais le 2e (token majuscule orphelin juste après "[NOM]-") reste
|
||||
# en clair, car ni span NER intact ni candidat regex ne le couvre.
|
||||
# On masque ce token dans le texte ET via NOM_GLOBAL (raster).
|
||||
_re_nom_orphan = re.compile(r"(\[NOM\]-\s*\n?\s*)([A-ZÀ-Ÿ][A-ZÀ-Ÿ'\-]{3,})\b")
|
||||
def _clean_nom_orphan(m):
|
||||
tok = m.group(2)
|
||||
if tok.lower() in _MEDICAL_STOP_WORDS_SET:
|
||||
return m.group(0)
|
||||
anon.audit.append(PiiHit(-1, "NOM_GLOBAL", tok, PLACEHOLDERS["NOM"]))
|
||||
return m.group(1) + PLACEHOLDERS["NOM"]
|
||||
final_text = _re_nom_orphan.sub(_clean_nom_orphan, final_text)
|
||||
|
||||
# 3b) Nettoyage post-masquage : codes postaux orphelins (5 chiffres collés à un placeholder)
|
||||
# et téléphones fragmentés sur plusieurs lignes
|
||||
_re_cp_orphan = re.compile(r"(\[(?:ADRESSE|NOM|VILLE)\])\s*(\d{5})\b")
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
from: claude
|
||||
to: dom
|
||||
date: 2026-06-03
|
||||
topic: ejnaini-root-cause (T-H)
|
||||
status: RÉSOLU — fix F5 implémenté + validé audit_30
|
||||
references:
|
||||
- file: docs/coordination/inbox/for-dom/2026-06-02_qwen_ejnaini-investigation.md
|
||||
- doc: trackare-BA127127-23135726
|
||||
---
|
||||
|
||||
# EJNAINI — cause racine confirmée (et conclusion de Qwen corrigée)
|
||||
|
||||
## Test empirique (moteur actuel, paranames + stopwords nettoyés)
|
||||
|
||||
Re-traitement du PDF source `433_23135726/trackare-BA127127...pdf` :
|
||||
|
||||
| Token | Avant | Maintenant |
|
||||
|---|---|---|
|
||||
| `NOCENT` | 7 fuites | **0** ✅ |
|
||||
| `EJNAINI` | 7 fuites | **7** ❌ |
|
||||
|
||||
Pattern résiduel **déterministe** (7×) :
|
||||
```
|
||||
[NOM]-
|
||||
EJNAINI
|
||||
```
|
||||
|
||||
## Conclusion de Qwen RÉFUTÉE
|
||||
|
||||
Qwen concluait : « paranames résoudra EJNAINI ». **Faux.**
|
||||
- `EJNAINI` **est** dans paranames (`noms_famille_world.txt.gz`) et chargé dans le core (vérifié).
|
||||
- Il reste pourtant non masqué.
|
||||
|
||||
## Vraie cause racine
|
||||
|
||||
Deux chemins de masquage parallèles dans le moteur :
|
||||
1. **Spans NER** → remplacement direct du span détecté. Capte `Cécilia NOCENT-EJNAINI` **là où il est intact** (166 hits NOM).
|
||||
2. **NameCandidates** (regex) → cross-validation (NER/INSEE/paranames) → `safe_names` → remplacement global **+ redaction raster** (`NOM_GLOBAL`).
|
||||
|
||||
En zone tableau Trackare, le nom est éclaté sur deux lignes : `NOCENT-` en fin de ligne, `EJNAINI` orphelin plus bas.
|
||||
- Le span intact `Cécilia NOCENT-EJNAINI` n'existe pas → chemin 1 ne le voit pas.
|
||||
- Aucun candidat regex ne propose `EJNAINI` seul → chemin 2 ne le voit pas (donc paranames jamais consulté pour lui).
|
||||
- F1 ne décompose que les tokens **uniques** à trait d'union déjà dans `names`, pas les **spans multi-mots** NER.
|
||||
|
||||
`NOCENT` finit masqué (`[NOM]-`) par un autre artefact de remplacement, mais `EJNAINI`, n'étant ni dans un span intact ni dans `safe_names`, fuit.
|
||||
|
||||
## Fix recommandé (robuste, corrige texte ET PDF)
|
||||
|
||||
Quand le NER détecte un nom **multi-mots ou à trait d'union** (PER), **décomposer** le span et injecter ses tokens composants (≥4 chars, non stop-word, confirmés gazetteer/NER) comme **NameCandidates haute confiance** → ils entrent dans `safe_names` → remplacement global + `NOM_GLOBAL` (raster).
|
||||
|
||||
C'est une généralisation de F1 aux spans NER multi-mots. Avantage : masque `EJNAINI` partout (texte + raster), pas un patch cosmétique sur le `.txt`.
|
||||
|
||||
## Fix implémenté (F5) — post-passe orpheline
|
||||
|
||||
Au lieu de la décomposition des spans NER (le span n'existe pas en zone tableau),
|
||||
la solution retenue cible directement le pattern résiduel : après `selective_rescan`,
|
||||
on masque le token majuscule orphelin qui suit immédiatement un `[NOM]-` (cas
|
||||
`[NOM]-\nEJNAINI`). Implémenté dans `process_pdf` (étape 3a-bis), même style que le
|
||||
nettoyage des codes postaux orphelins. Masque le texte **et** le raster (via `NOM_GLOBAL`).
|
||||
|
||||
## Validation (2026-06-03)
|
||||
|
||||
- BA127127 re-traité : `NOCENT=0, EJNAINI=0` ✅
|
||||
- `tests/unit` : 85 passed
|
||||
- **audit_30 complet** (29 docs, 1 quarantiné) :
|
||||
- **Score global 98.5/100 (A+)** (baseline 97.0, +1.5)
|
||||
- **Leak score 100/100** — 0 fuite audit, 0 regex, 0 INSEE contexte fort
|
||||
- FP score 95 (+5 vs baseline), 0 terme médical masqué
|
||||
- **Aucune régression**
|
||||
|
||||
EJNAINI était la dernière fuite de l'audit_30. **Leak score = 100%.**
|
||||
|
||||
— Claude
|
||||
|
||||
Reference in New Issue
Block a user