Files
anonymisation/tests/unit/test_synthetic_review.py
Domi31tls e0b526b2c7 fix(detect): établissements multi-ligne, CHCB en fin de phrase, ville après [ETAB] (#3 #4 #5)
Trois fixes qui font passer 009_multi_etablissements en vert et
ferment la liste des fuites identifiées par la couche 2.

#3 — `Centre Hospitalier Universitaire de Bordeaux` coupé sur deux lignes
Nouveau pattern `RE_ETAB_LINEBREAK` (strict) en pré-passe sur la page
entière, juste avant le découpage en lignes. Match `<TYPE>\n<suite>`
avec :
- TYPE limité (Centre Hospitalier, Hôpital, Clinique, Polyclinique,
  CHU, CHRU, CHS) ;
- un seul `\n` autorisé entre TYPE et suite ;
- la suite démarre obligatoirement par un connecteur typique
  (Universitaire, de, d', du, des, la, le, les) puis UN nom propre.
Évite le FP `CENTRE HOSPITALIER COTE BASQUE\nService d'anesthésie`
(le `\n` n'est pas immédiat après le type, donc pas de match).

#4 — `CHCB` en fin de phrase suivi de ` ;`
`_kv_value_only_mask` splittait `transféré au CHCB pour la rééducation ;`
sur le `;` du `SPLITTER` (`\s*[:|;\t]\s*`), produisant une value vide.
La key contenait CHCB mais n'était passée qu'à `_mask_critical_in_key`
qui ne couvre pas les force_terms admin_rules.
Fix : fallback sur `_mask_line_by_regex(line)` (qui appelle
`_apply_overrides` → force_terms) si la value est vide ou la key
dépasse 5 mots (heuristique narrative).

#5 — `Biarritz` non masqué après `[ETABLISSEMENT] à Biarritz`
`_mask_ville_gazetteers` skippait par sécurité toute ville détectée
juste après un placeholder établissement précédé de `de/du/d'/à`. Le
`à` était inclus pour éviter les FP, mais c'est la préposition de
LOCALISATION par excellence : `Clinique Aguilera à Biarritz` perd
Biarritz à tort. Restreint le skip à `de/du/d'` (qui sont des parties
de nom d'établissement type `CHU de Bordeaux`). `à` reste actif.

Couche 2 entièrement verte : 73 passed, 0 xfailed (avant : 72 + 1
xfailed). KNOWN_FAILURES vidé. La gate pytest est désormais le
contrat de non-régression sur 10 documents complets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 11:32:45 +02:00

71 lines
2.1 KiB
Python

#!/usr/bin/env python3
"""
Gate pytest sur le corpus synthétique de revue humaine (couche 2).
Chaque cas dans tests/synthetic_review/cases/ doit produire un texte
identique à expected.txt, satisfaire ses expectations.json et ne révéler
aucune fuite via le LeakScanner.
Les cas listés dans KNOWN_FAILURES sont marqués xfail(strict=True) :
ils sont attendus en échec aujourd'hui car ils révèlent des bugs réels
du moteur. Quand un bug est fixé, le cas correspondant passe → pytest
signale le xpass strict, ce qui force à retirer son entrée de
KNOWN_FAILURES.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tools.run_synthetic_review_corpus import ( # noqa: E402
CASES_DIR,
run_case,
)
KNOWN_FAILURES: dict[str, str] = {}
def _case_dirs() -> list[Path]:
if not CASES_DIR.exists():
return []
return sorted(path for path in CASES_DIR.iterdir() if path.is_dir())
def _make_param(case_dir: Path) -> "pytest.ParameterSet":
if case_dir.name in KNOWN_FAILURES:
return pytest.param(
case_dir,
id=case_dir.name,
marks=pytest.mark.xfail(
strict=True,
reason=KNOWN_FAILURES[case_dir.name],
),
)
return pytest.param(case_dir, id=case_dir.name)
def test_synthetic_review_inventory():
"""Le corpus doit contenir 10 cas (cible de cadrage produit)."""
assert CASES_DIR.exists(), f"Dossier corpus introuvable : {CASES_DIR}"
case_dirs = _case_dirs()
assert len(case_dirs) == 10, (
f"Attendu 10 cas dans synthetic_review/cases, trouvé {len(case_dirs)} : "
f"{[c.name for c in case_dirs]}"
)
@pytest.mark.parametrize("case_dir", [_make_param(c) for c in _case_dirs()])
def test_synthetic_review_case(case_dir: Path):
result = run_case(case_dir)
assert not result["failures"], (
f"{case_dir.name} : {', '.join(result['failures'])}\n"
f"Diff disponible dans {result['output_dir']}/diff.txt"
)