Audit prêt-pour-bêta de GUI V6 (3 axes : parcours/UX/PII, moteurs/OnnxTR/frozen, licence/télémétrie/diffusion) → punch-list P0=7 / P1=11 / P2=6, findings vérifiés. Design validé Dom : périmètre « A + chaîne prod », P0 + P1 honnêteté UI, toggles « Données à détecter » câblés au moteur (gating par type + couplage rescan, pas de plancher dur), licence souple + binding poste (D-20.4), −2 Go via excludes PyInstaller, Linux-first TDD + un seul build Windows. Plus 3 exigences prod intégrées : diagnostics logs auto-upload scrubbé (liste-blanche), mise à jour propre (installeur), modèles embarqués hors-ligne. 6 chantiers (A-F), 2 repos. Diffusion = gate Dom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
162 lines
10 KiB
Markdown
162 lines
10 KiB
Markdown
# 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.
|