Couvre les corrections PII batch A/A-2, le NIR multi-ligne en flux reel, le gazetteer FINESS Corse derive depuis la base locale, et les tests de regression associes. Aucun build ni diffusion.
Condition du GO-CONDITIONNEL Qwen sur le lot engine capabilities
(cb3b767/890edb3/5e5f0bd) : un profil YAML forçant enable_eds/enable_gliner
ne doit pas déclencher un chargement voué à l'échec silencieux.
NerManagers.ensure_loaded() applique désormais un garde-fou via la sonde
engine_capabilities.capabilities_map() (injectable) AVANT toute tentative
de load EDS/GLiNER : si le moteur optionnel demandé est indisponible dans
le build courant → warning + désactivation forcée dans les réglages runtime.
Best-effort (sonde en échec ⇒ réglages inchangés, les try/except de load
protègent déjà). Sonde légère (find_spec), aucun import lourd.
CamemBERT (requis) inchangé. Diff limité au garde-fou + tests cibles.
TDD : 4 tests (test_gui_v6_engine_bridge.py) — eds/gliner indispo désactivés
et jamais chargés, moteur dispo conservé, fail-safe sonde. 282 unit passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Axe application GUI (utilisateur final) : cohérence UI/moteurs propre au build
GUI, sans présumer du build CLI. EDS-Pseudo / GLiNER désactivés (switch disabled
+ « non embarqué dans cette version ») et `enable_eds/gliner` forcés à False quand
indisponibles ; CamemBERT-bio reste le moteur standard actif. Note Moteurs des
Profils rendue honnête. `_mini_toggle` gère `disabled`/`disabled_hint` + `.switch`.
2 tests GUI (toggles désactivés si indispo + état forcé False ; actifs si dispo).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Axe CLI (intégration dans d'autres programmes) : contrat stable, codes retour
fiables. `--engines` liste les moteurs réellement disponibles dans CET
exécutable CLI (`[OUI]/[NON] Label (requis/optionnel) — raison`) et sort 0, sans
traiter. `input` devient optionnel pour ce mode (sinon code 2).
Le fail-closed CamemBERT (code 3) et le best-effort EDS/GLiNER (jamais déclarés
actifs si le chargement échoue) restent inchangés. Ne présume pas du build GUI.
2 tests (--engines → code 0 + moteurs listés ; absence d'input → code 2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Utilitaire neutre (ni CLI ni GUI) qui dit la vérité sur les moteurs réellement
disponibles dans le build COURANT (la sonde reflète l'exécutable qui tourne, sans
présumer d'un autre build). Consommé séparément par chaque axe produit.
- `EngineCapability(key, label, available, required, reason)`.
- Sondes légères `importlib.util.find_spec` (pas d'import lourd au démarrage) +
présence du modèle ONNX pour CamemBERT (gère _MEIPASS en frozen).
- camembert=requis ; eds (edsnlp+spacy) / gliner=optionnels. Sondes injectables,
fail-closed. `capabilities_map()` / `available_engines()`.
6 tests (sondes injectables dispo/indispo, required, reasons, sondes réelles).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retour Dom : « les règles du profil doivent être dans le menu profil, pas à
part ! ». Même logique que le Masquage — les règles qui influencent
l'anonymisation appartiennent au profil ; un sous-onglet séparé crée la
même confusion.
- Retrait du sous-onglet « Administration > Règles » (_SUBTABS, builder,
méthode _build_regles supprimée). Sous-onglets restants : Réglages /
Profils / Partage.
- Section « Profils > Règles du profil » enrichie : wording clair (règles
d'anonymisation portées par le profil), aperçu illustratif de la table
des règles (réutilise _rule_row + _HELP_REGLES), édition fine annoncée
« à venir ».
- Abandon du « Testeur de règle » (écran outil global) pour ne pas
réintroduire un second réglage métier.
Cible UX : Réglages / Profils (Général・Masquage・Mots・Moteurs・Règles du
profil) / Partage. Test obsolète test_rules_subtab_has_no_unexplained_2
remplacé par test_no_separate_rules_subtab.
262 tests unit OK (0 régression), self-test OK, nav 3 sous-onglets + section
Règles dans Profils + thème OK. Préserve d8bc0cd + GO Qwen. Aucun build/push
sans GO Dom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retour Dom : le sous-onglet Masquage séparé créait de la confusion. Le
masquage fait partie de la manière d'anonymiser associée au profil.
- Retrait du sous-onglet « Administration > Masquage » (_SUBTABS, builder,
méthode _build_masquage).
- Section « Profils > Masquage » enrichie : masque manuel requis, template
de masque (lié au profil édité), bouton « Ouvrir l'éditeur de masque »
(fenêtre dédiée) + dossier des templates, et apparence du masque
(couleur, style des marqueurs + aperçu, marges H/V, coins arrondis).
- Le template enregistré depuis l'éditeur remplit désormais le champ
Template du profil (preferred_manual_mask_template via _pro_template_var).
- Profils devient le centre des réglages métier (général/masquage/mots/
moteurs/règles). Réglages inchangé (pas de pastilles, pas de grosse
refonte). Nettoyage du code mort (_REPLACEMENT_CODES, _HELP_MASQUAGE).
261 tests unit OK (0 régression), self-test OK, nav 4 sous-onglets + éditeur
de masque depuis Profils + thème OK. Préserve 72841ed/GO Qwen. Aucun build/
push sans GO Dom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retour Dom : remplacer la page vitrine par un vrai éditeur de profils.
- gui_v6/profile_editor.py : couche logique (build_profile_spec,
profile_is_editable runtime vs defaut, list_profile_choices, slug_for_copy,
save/set_default/delete) au-dessus de profile_defaults — persistance dans
config/profiles.yml.
- gui_v6/editable_list.py : EditableTermList (tableau scrollable de termes,
ajout/suppression, pas de pastilles) — reste lisible à 50+ termes.
- tab_config : sous-onglet « 👤 Profils » réintroduit comme éditeur — menu
déroulant « Profil à modifier », boutons Nouveau / Dupliquer / Enregistrer /
Annuler / Définir par défaut, sections Identité, Masquage (require_manual_mask,
template), Moteurs (force_disable_vlm), Mots (à masquer/conserver/ignorer
éditables), Règles « à venir ». Profils défaut = lecture seule (dupliquer
pour modifier). Confirmation non bloquante (pas de modale).
- Réglages : bouton « ✏️ Modifier le profil… » → ouvre Profils sur le profil
actif. Pas de pastilles inline.
Persiste : label, description, require_manual_mask, force_disable_vlm,
preferred_manual_mask_template, param_lists (3 listes). 260 tests unit OK
(0 régression), self-test OK, nav 5 sous-onglets + thème OK. Préserve
1bbe70a/d30f7b7. Aucun build/push sans GO Dom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le module usage_telemetry est maintenant réellement branché : la GUI V6
envoie les statistiques au portail après chaque run (les stats web
restaient vides sans cela).
- processing_runner : RunSummary porte une liste DocResult (ordinal,
page_count via page_count_for, status, duration_ms, extension) — peuplée
dans la boucle. Aucun nom/chemin de fichier.
- usage_telemetry : report_run_summary(summary, base_url, license_ref,
machine_id, session, ...) construit le payload depuis le RunSummary et
l'envoie (non bloquant). N'envoie RIEN sans license_ref. Spool JSONL si
échec réseau.
- tab_usage : _finish() déclenche l'envoi en thread daemon (jamais bloquant
pour l'UI ni le run).
- app : fournit le reporter à UsageTab avec le contexte licence (base_url du
LicenseClient, license_ref via local_status, machine_id, app_version).
Tests : RunSummary.documents peuplé (0 chemin) ; report_run_summary (payload
correct, réseau KO → spool sans crash, pas d'envoi sans licence) ; _finish
appelle le reporter. 252 tests unit OK (0 régression), self-test OK.
V5/moteur/app_aivanov intacts, 0 dépendance. Aucun build/push sans GO Dom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retour Dom après validation visuelle : simplifier.
- Réglages > Listes locales : suppression des pastilles de termes et des
éditeurs inline (_compact_tag_editor). Remplacés par un texte court +
compteurs (À conserver/À masquer/À ignorer du profil actif) + bouton
« Ouvrir le tableau des termes » qui ouvre DIRECTEMENT TermsTableWindow.
- Retrait du bouton « Voir le profil » (son rôle = accéder au tableau).
- Retrait du sous-onglet « Profils » (doublon non câblé) : _SUBTABS,
builders, _build_profils/_rebuild_profils. Les helpers profil
(_active_profile_summary/_open_terms_table) sont conservés pour Réglages.
- Nettoyage du code mort associé : _compact_tag_editor, constantes
_PRESERVE_TERMS/_MASK_TERMS/_STOPWORDS, textes d'aide qui référençaient
l'onglet Profils.
Chemin utilisateur : Administration > Réglages > Ouvrir le tableau des
termes. 247 tests unit OK (0 régression), self-test OK. Préserve a9e8b2c
(thème, bêta, aide ?, fenêtre tableau). Aucun build/push sans GO Dom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase A de la mission télémétrie d'usage par client.
- gui_v6/usage_telemetry.py :
- page_count_for(path) : PDF→fitz, image→1, autres→None ; best-effort, ne
lève jamais, ne lit que l'extension (jamais le nom).
- build_usage_payload(...) : compteurs (document/succeeded/failed/total_pages)
+ documents filtrés aux seules clés autorisées (ordinal/page_count/status/
duration_ms/extension) → aucun nom/chemin de fichier ne peut fuir.
- UsageTelemetryClient(session injectée) : report() non bloquant (capture
tout, False en cas d'échec réseau) vers POST /api/v1/usage/report.
- spool JSONL local (spool_payload/flush_spool) pour rejouer les échecs.
Module isolé, non câblé au runner pour l'instant (le branchement fin-de-run
viendra après le backend, hors validation visuelle GUI en cours). Aucun
build/push sans GO Dom. 10 tests unitaires (payload sans nom de fichier,
réseau indispo ne crashe pas, compteurs, page_count PDF mockable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cinq retours utilisateur sur l'exécutable Windows GUI V6.
- Thème : `_render()` vidait les widgets mais conservait le cache
`_tab_frames`/`_visible_tab` → l'onglet Utilisation se vidait (TclError
sur widget détruit) au changement de thème. Reset du cache dans
`_render()` → onglet actif recréé proprement.
- Onglet principal « Configuration » → « Administration » (clé interne
inchangée).
- Sous-onglet « Règles 2 » → « Règles » (le « 2 » était un badge non
câblé).
- Actions de maquette non câblées (Partage Export/Import, Règles Nouvelle
règle/Recharger/Tester/Fermer) désactivées + suffixe « (à venir) » via
`_mockup_button` : plus aucune action morte qui semble fonctionner.
- Aide « ? » restaurée (façon V5) : `ui_kit.HelpButton`/`help_button`
réutilisable ouvrant une fenêtre d'aide en français simple, posée sur
Utilisation, Administration (Réglages/Masquage/Partage/Règles) et
À propos. Partage : phrase visible + aide expliquant qu'on partage les
réglages, jamais les documents patients.
`tests/unit/test_gui_v6_app_shell.py` : régression thème, libellés,
présence d'aide, navigation. 228 tests unit OK (0 régression), self-test
GUI V6 OK. V5/moteur/app_aivanov non touchés, aucune dépendance ajoutée.
Verdict Qwen requis avant push/build/diffusion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remplace l'éditeur de masquage encastré dans l'onglet Configuration —
jugé inutilisable par Dom (document trop à l'étroit, non défilable) —
par une fenêtre dédiée où le document est majoritaire et réellement
navigable.
- gui_v6/mask_editor_model.py : couche logique pure (rectangles par
page, conversions écran↔PDF, hit-test, sérialisation template)
testable sans display ; réutilise MaskRect/Template de
pdf_mask_designer → format de template inchangé (compat moteur).
- gui_v6/mask_editor_window.py : MaskEditorWindow (CTkToplevel)
redimensionnable — canvas + scrollbars H+V câblées + molette (le
manque qui rendait l'éditeur inutilisable), zoom + ajuster
largeur/page, navigation pages, rectangles au glisser-déposer,
sélection (clic) + suppression (Suppr / clic-droit), templates
JSON/YAML, mode aperçu d'exemple sans PDF.
- tab_config.py : l'onglet Masquage lance la fenêtre dédiée ; retrait
du canvas encastré et de ~290 lignes de code mort associé.
- tests/unit/test_gui_v6_mask_editor.py : 13 tests logique + 3 smoke
headless (scrollbars, ajout/sélection/suppression, save/load
roundtrip, câblage onglet→fenêtre).
Sans nouvelle dépendance. V5, moteur et app_aivanov non touchés.
221 tests unit OK (0 régression), self-test GUI V6 OK.
Verdict Qwen requis avant push/build/diffusion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trois détecteurs simples « layout/context-aware » (chantier v11.5 P0),
validés par 2 revues Codex + 10 tests adversariaux Qwen, 0 régression :
- RE_ADRESSE réécrit en grammaire de tokens (_RE_VOIE_TYPE + _RE_VOIE_TOKEN) :
capture initiales (« J. Loeb »), voies commémoratives à chiffres
(« 8 Mai 1945 »), apostrophes ' et ’, bornage à la ligne courante,
arrêt sur point post-mot (anti-débordement clinique).
- _mask_ville_gazetteers : retourne toujours un tuple (texte, liste) même
sans Aho-Corasick ; masque les communes Saint/St/Sainte/Ste multi-mots à
espaces (« St Martin de Hinx ») entièrement, sans exiger de contexte géo.
- DATE_NAISSANCE retiré de la propagation globale + DATE_NAISSANCE_GLOBAL
ajouté aux skip vector/raster : on ne masque plus une date nue sur tout le
document. La DDN reste masquée en contexte fort, page par page. Les dates
cliniques identiques à la DDN hors contexte sont préservées.
tests/unit/test_p0_layout_detectors.py : 38 tests dédiés (matrice adresse
générique, anti-FP, communes Saint, propagation DDN, 10 tests adversariaux
Qwen). Suite tests/unit complète : 147 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Configure numerical library and torch threading for H1, keep raster threading/timing instrumentation, remove CONCERTATION from forced masks after real PDF FP testing, and record coordination archive state.
Application du stash@{0} resté en WIP depuis le 27/04 :
"On main: wip-gui-profils-masque-manuel-build-windows-2026-04-27"
## Apport
- Pseudonymisation_Gui_V5.py (+1208 lignes) : profils, panneau paramètres
avancés, éditeur de masques intégré, gestion whitelist/blacklist
- launcher.py (+315) : splash natif PyInstaller, single-instance,
téléchargement modèles
- anonymisation_onefile.spec : config PyInstaller mise à jour
- pdf_mask_designer.py (+114) : éditeur de masques amélioré
- config_defaults.py (+23) : constantes nouvelles
- tests/unit/test_config_externalization.py (+12) : tests config
- .gitignore (+5)
## Pourquoi
La version courante de la GUI sur la branche feature manquait :
- L'éditeur de masques
- Les profils
- Le panneau paramètres avancés
- Le splash natif au démarrage
Aucun conflit avec mes 10 commits Q-1 (pas de chevauchement de fichiers).
## Validation
75 passed, 10 xfailed sur pytest tests/unit/.
## Note
Le stash reste disponible dans `git stash list` jusqu'à drop explicite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Étend `RE_RPPS` pour tolérer 0 à 3 mots qualificateurs entre `RPPS`
et le séparateur `:` ou `-`. Couvre les variantes observées :
- RPPS prescripteur :
- RPPS du médecin signataire :
- RPPS de garde -
- N° RPPS :
Si un qualificateur est présent, le séparateur (`:` ou `-`) devient
obligatoire pour éviter d'aspirer du narratif (faux positif type
"Le RPPS est consulté pour vérifier 12345678901 dans la base").
La lambda `_repl_rpps` reconstruit `RPPS : [RPPS]` en sortie : le
qualificateur est consommé mais perdu (pas de fuite, choix cosmétique).
Cas 005_bacterio_complete passe désormais (retiré de KNOWN_FAILURES).
La fuite `10101010101` derrière `RPPS prescripteur :` est masquée.
Cohérent avec le cadrage section 10.1 (règle cœur générique
applicable à tout établissement de santé français — pas de
spécificité locale).
Tests : 72 passed, 1 xfailed (avant : 71 passed, 2 xfailed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Couche 2 (revue humaine sur documents complets) : ajout de 6 cas
synthétiques pour atteindre la cible cadrage produit (10 cas).
Cas ajoutés :
- 005_bacterio_complete : layout BACTERIO N° venue rejeté avant IPP
+ RPPS prescripteur (pattern qualifié non détecté).
- 006_trackare_soignants : export Trackare avec activités HH:MM NOM,
Note IDE/médicale, Signé — médicament greedy.
- 007_lettre_sortie_complete : courrier médecin→médecin, multi-villes,
email institutionnel @chcb.fr (cassé par le force_term CHCB).
- 008_anesthesie_complete : protocole anesthésique avec molécules
BDPM, prénoms basques rares (Maddi, Pantxoa).
- 009_multi_etablissements : 3 établissements distincts (CHCB, CHU
Bordeaux, Clinique Aguilera), prénoms basques avec ñ (Beñat).
- 010_fiche_admission_minimale : fiche administrative dense, labels
variés (Nom de jeune fille :, Prénom :, Ville :, Mutuelle :).
Gate pytest (tests/unit/test_synthetic_review.py) :
- vérifie l'inventaire (10 cas) et fait passer chaque cas via run_case.
- 3 cas marqués xfail(strict=True) pour révéler 9 fuites de PII et
2 patterns partiels que le moteur ne couvre pas aujourd'hui :
* 005 — RPPS avec qualificateur (RPPS prescripteur :)
* 009 — Bordeaux résiduel après [ETAB], CHCB en fin de phrase,
Biarritz sur ligne Ville :, ñ qui casse Beñat → [NOM]ñat
* 010 — Nom de jeune fille / Prénom / Ville sans label "Patient :",
NIR au format espacé partiellement consommé en TEL,
numéro de mutuelle MGEN non couvert
- xfail strict force pytest à signaler un xpass quand un fix passe :
rappel automatique de retirer l'entrée de KNOWN_FAILURES.
Le runner tools/run_synthetic_review_corpus.py reste utilisable en
direct (sortie diff/audit/summary) pour la revue humaine. Les sorties
actual/ sont gitignorées (régénérées à chaque exécution).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Script demo_evaluation.py montrant tous les outils
- Correction test flottant dans test_quality_evaluator.py
- Installation pytest/pytest-cov
- Tous les tests passent (16/16)