# Audit prêt-pour-bêta — GUI V6 (HEAD `4b7c8db`, 2026-06-25)

Périmètre : GUI V6 (`gui_v6/`) + chaîne prod (licence, télémétrie, diffusion) pour
une mise en bêta testeur. Cible : un testeur installe l'EXE Windows → active sa
licence → traite des PDF/images médicaux → ses stats remontent au portail.

Audit en 3 axes (parcours/UX/PII, moteurs/OnnxTR/frozen, licence/télémétrie/diffusion).
Findings vérifiés dans le code (lignes citées) ; les 2 plus lourds recontrôlés par Claude.

**À FAIRE par Dom : valider / élaguer cette liste avant tout code (cocher / barrer).**

---

## P0 — Bloqueurs bêta (à corriger avant toute diffusion)

### P0-1 — Fuite PII silencieuse : la GUI ne fait pas fail-close si le NER obligatoire échoue
- `gui_v6/engine_bridge.py:222-231` appelle `ensure_loaded()` sans vérifier l'état ;
  si CamemBERT-bio ne charge pas (`:173-175` → `UNAVAILABLE`, `camembert=None`), le
  traitement continue en **regex + gazetteers seuls** et le document sort compté
  « Réussi ». Le rescan résiduel ne couvre PAS les noms de personnes.
- Le CLI de prod fait l'inverse : `scripts/anonymize_cli.py:184-198` fail-close (code 3,
  aucune sortie) si le modèle obligatoire manque.
- **Impact** : modèle ONNX corrompu/absent (antivirus, ressource manquante) → PDF qui
  paraît traité, statut OK, mais noms patients potentiellement en clair. **Risque #1.**
- **Fix** : dans `process_fn`, si `use_local_ner` et état `UNAVAILABLE` → lever une
  exception explicite (refus de traitement), le runner la remonte en échec. Aligne la
  GUI sur la garantie code-3 du CLI.

### P0-2 — Chaîne prod cassée : la GUI pointe `http://localhost`, jamais le portail réel
- `Pseudonymisation_Gui_V6.py:57` → `AnonymisationApp()` sans client → `gui_v6/app.py:41`
  `LicenseClient("http://localhost")`. La télémétrie réutilise cette URL (`app.py:199`).
  Aucune env var / config / injection build ne fixe l'URL portail (grep exhaustif = 0).
- **Impact** : sur un poste testeur, activation licence ET télémétrie POSTent sur
  `localhost` → rien n'écoute → **activation impossible, stats jamais remontées**. Toute
  la « chaîne prod » demandée est non fonctionnelle. Le portail tourne sur
  `app.aivanov.eu` (vérifié live, HTTPS 200, artefact servi).
- **Fix** : injecter l'URL réelle (`DEFAULT_PORTAL_URL="https://app.aivanov.eu"` ou env
  var), `localhost` devient le secours dev. **Impose un rebuild** (l'EXE déjà publié est
  inutilisable pour l'activation).

### P0-3 — Gain −2 Go non réalisé : `optimum` réintroduit torch en dur
- `requirements.txt:3` = `optimum[onnxruntime]>=2.0.0` ; optimum déclare `torch>=1.11`
  en dépendance **cœur** (vérifié). Le build installe requirements → torch dans le venv →
  PyInstaller le bundle malgré le retrait des hiddenimports (`4b7c8db`). EXE ~+2 Go.
- `optimum` n'est importé que par `ner_manager_onnx.py` (legacy ONNX **non câblé** à la
  GUI), `Pseudonymisation_Gui_V5.py` (V5 legacy) et un script de finetune — **jamais par
  `gui_v6/`** (le NER GUI = `camembert_ner_manager` en onnxruntime brut).
- **Fix** : retirer `optimum` (et hiddenimports `optimum*`) du build frozen GUI ;
  valider « torch-free » par la **taille EXE réelle** + grep de l'arbo PyInstaller, pas
  par le diff du spec.

### P0-4 — Build non reproductible : pas de précache des poids OnnxTR
- `anonymisation_gui_v6_onefile.spec:50-67` **raise** `FileNotFoundError` si les poids
  `db_resnet50` + `crnn_vgg16_bn` absents du cache ; `scripts/build_windows_oneclick.ps1`
  ne télécharge jamais ces poids. Le build « marche » seulement grâce au cache résiduel
  de la machine 192.168.1.11.
- **Fix** : étape pré-PyInstaller : `python -c "from onnxtr.models import ocr_predictor;
  ocr_predictor(det_arch='db_resnet50', reco_arch='crnn_vgg16_bn')"` + doc.

### P0-5 — Crash frozen ONNX probable : le hotfix CLI `6c6f653` non reporté au launcher GUI
- Le launcher V6 ne pose pas `ANON_SKIP_LEGACY_ONNX_MANAGER=1` (lu à l'import du core,
  `anonymizer_core_refactored_onnx.py:211`) et ne pré-charge pas CamemBERT — alors que le
  hotfix CLI (`scripts/anonymize_cli.py:87`) le fait pour éviter « cannot load module more
  than once per process » en frozen Windows.
- **Fix** : poser le flag tout en haut de `Pseudonymisation_Gui_V6.py` + pré-charger
  CamemBERT avant le 1er import du core. **À confirmer sur l'EXE** (non reproductible en dev).

### P0-6 — Contrôle de licence purement décoratif (décision Dom requise)
- Enforcement nul : `tab_usage.py:205` active « Lancer » selon le nombre de documents
  seulement ; aucune gate sur le statut licence (lu pour affichage uniquement).
- Pas de vérif locale : `license_client.py:208-213` lit `license.json` **sans vérifier
  signature ni `machine_id`** → fichier copiable sur N postes = N licences « actives ».
- `machine_id` = MAC seule 12 chars (`machine_id.py:12-14`) = clonable, change en VM.
- D-20.1 (fingerprint multi-composants) et D-20.4 (vérif locale machine_id) **non
  implémentés**. La bêta est une fenêtre idéale (aucune activation prod existante à migrer).
- **Décision Dom** : niveau d'enforcement bêta (souple / dur / différé) — voir question.

### P0-7 — Pas de protection multi-instance (lock perdu vs V5)
- `Pseudonymisation_Gui_V6.py:50-59` lance `mainloop()` sans file-lock `msvcrt.locking`
  (présent en V5). Double-clic testeur → 2 instances, 2 chargements ONNX, 2 écritures dans
  le même dossier sortie.
- **Fix** : réintroduire le lock V5 + message « application déjà ouverte ».

---

## P1 — Forte friction testeur / écarts (à arbitrer pour la bêta)

- **P1-1 — Dropzone décorative** (`tab_usage.py:103-115`) : zone « déposez vos fichiers »
  sans glisser-déposer réel. Confusion dès la 1re étape. → câbler DnD (tkinterdnd2) ou
  retirer la métaphore.
- **P1-2 — 7 toggles « Données à détecter » non câblés** (`tab_config.py:351-357`) : les
  interrupteurs (Noms, DDN, Établissements…) n'ont ni variable ni callback → aucun effet.
  Promesse UI non tenue. → câbler au moteur ou rendre informatifs (lecture seule).
- **P1-3 — Import/Export config désactivés « à venir »** (`tab_config.py:817-840`) alors
  que le Partage est mis en avant. Régression vs workflow email V5. → câbler ou retirer.
- **P1-4 — `config_path` jamais transmis** (`app.py:155-162`, `tab_usage.py:53,176`) : le
  `dictionnaires.yml` externe éditable n'est pas garanti chargé en frozen (pas de copie au
  1er lancement). Personnalisations établissement potentiellement ignorées.
- **P1-5 — Erreurs/quarantaine sans pointeur** (`tab_usage.py:268,282`) : « échec » /
  « quarantaine » sans indiquer où est le doc non livré (`_*_failed/`). → afficher le
  chemin sortie + bouton « ouvrir le dossier ».
- **P1-6 — Pas de validation d'inscriptibilité du dossier de sortie** (`processing_runner.py:214`) :
  dossier en lecture seule → tout le lot échoue par doc avec message cryptique. → test
  d'écriture en amont.
- **P1-7 — Version app incohérente** : `gui_v6/__init__.py:11` = `6.0.0-g1` vs artefact/note
  bêta `2026.06.18.1203` (remonte aussi en télémétrie). → aligner sur le schéma de release.
- **P1-8 — Runbook non exécutable tel quel** : republier impose un rebuild après P0-2 (le
  SHA de `note-beta-client.md` deviendra caduc). → ajouter étape « vérifier URL portail
  embarquée » au runbook.
- **P1-9 — Clé de signature licence = clé dev auto-générée** (`app_aivanov/app/signing.py`) :
  KEY_ID « dev », paire RSA régénérée si volume recréé → licences invalidées. → provisionner
  une clé prod stable avant d'activer la vérif de signature côté client.
- **P1-10 — Politiques VM (D-20.2) / dossiers partagés (D-20.3) non implémentées** (décision
  Dom, marquées « DECISION REQUIS » dans D-20). Acceptable de différer en bêta interne — à acter.
- **P1-11 — Audit JSONL `original="docTR"`** (`anonymizer_core_refactored_onnx.py:5356`) :
  trace de provenance périmée (OCR = OnnxTR) visible dans le livrable d'audit. → `"OnnxTR"`.

---

## P2 — Polish (best-effort)

- **P2-1 — Étapes de progression statiques** (`tab_usage.py:144-147`) : pastilles
  Extraction/Détection/Masquage/PDF décoratives ; pas de feedback intra-document.
- **P2-2 — Cartes format de sortie décoratives** (`tab_usage.py:118-127`) : suggèrent un
  choix alors que la sortie est toujours PDF+TXT. → présenter comme « sorties produites ».
- **P2-3 — Commentaires/docstrings docTR périmés** (`anonymizer_core_refactored_onnx.py:55,102`) :
  cosmétique, comportement OK.
- **P2-4 — `os_info` non envoyé à l'activation** (`license_client.py:161-164`) : perte d'info
  diagnostique mineure.
- **P2-5 — Contact / canal de retour vide** dans `docs/beta/note-beta-client.md:36`.
- **P2-6 — `/api/v1/version` ne déclenche pas de download** (`tab_about.py:164-178`) : le
  testeur va manuellement sur le portail. Acceptable bêta.

---

## Verdicts positifs (ne pas re-traiter)

- **Télémétrie RGPD : irréprochable.** Double filtrage liste-blanche (`usage_telemetry.py:72`,
  `_ALLOWED_DOC_KEYS`) + recalcul serveur Pydantic strict. Aucun nom/chemin/texte ne peut
  fuiter. Non bloquante (thread daemon, timeout 4 s, spool best-effort). App utilisable
  hors-ligne.
- **Pipeline cœur fail-closed correct** : quarantaine moteur → échec, `SEUIL_RESCAN_RESIDUEL=0`,
  `redact_pdf_raster` pas dans un `except: pass`. La fuite P0-1 vient du **wiring GUI**, pas du cœur.
- **engine_bridge / affichage moteurs** : logique « n'afficher que les moteurs embarqués »
  saine et cohérente avec OnnxTR (OCR) + CamemBERT-bio (NER onnxruntime). 0 référence docTR
  dans les chaînes affichées à l'utilisateur.
- **Câblage `ONNXTR_CACHE_DIR` frozen** : chemins concordants, fail-closed si OnnxTR absent.
- **Portail déployé et fonctionnel** : `app.aivanov.eu` HTTPS, artefact servi, download
  authentifié (licence active requise). Le serveur n'est pas le problème — le client n'y est
  pas connecté.

---

## Décompte : P0 = 7 · P1 = 11 · P2 = 6

## Synthèse pour décision
Le « complet pour prod bêta » est **plus que rebuild + ship** : la chaîne prod est
aujourd'hui **non fonctionnelle bout-en-bout** (P0-2 localhost, P0-6 licence décorative),
il y a un **risque PII #1** (P0-1 fail-open), et le **−2 Go n'est pas acquis** (P0-3 optimum).
Rien de rédhibitoire : findings nets, fixes ciblés. C'est un vrai chantier de finition +
câblage, pas une réécriture.
