diff --git a/docs/coordination/inbox/for-dom/2026-06-05_claude_package-beta-installateur-maj.md b/docs/coordination/inbox/for-dom/2026-06-05_claude_package-beta-installateur-maj.md new file mode 100644 index 0000000..d029694 --- /dev/null +++ b/docs/coordination/inbox/for-dom/2026-06-05_claude_package-beta-installateur-maj.md @@ -0,0 +1,46 @@ +--- +from: claude +to: dom +date: 2026-06-05T18:20:00+02:00 +topic: package-beta-installateur-maj +status: done +references: + - file: docs/coordination/inbox/for-dom/2026-06-05_claude_pack-beta-build-report.md + - decision: docs/coordination/decisions/2026-06-05_dom_d16-test-windows-avant-diffusion.md +--- + +# Rapport package bêta — MISE À JOUR (installateur ajouté) + +Suite à ton GO, l'installateur Inno Setup a été produit (étapes D-16 §1-4). + +## Installateur produit ✅ +- Inno Setup **déjà présent** (chemin user `%LOCALAPPDATA%\Programs\Inno Setup 6\`) — + pas besoin de l'installer. (Mon check initial cherchait dans `Program Files`, d'où + le « absent » précédent.) +- Build via `build_windows_installer_only.ps1` (depuis l'EXE existant, **pas de + rebuild**) — `INSTALLER_EXIT_CODE=0`. +- **`release\Anonymisation-Setup.exe`** — **729 576 700 octets (695.8 Mo)**, langue FR. +- Non signé (conforme D-3). + +## Artefacts finaux (`192.168.1.11:…\release\`) +| Fichier | Taille | SHA-256 | +|---|---|---| +| `Anonymisation.exe` | 696.7 Mo | `80A740DA…F629DF9` | +| `Anonymisation-Windows.zip` | 728 Mo | `E22F8264…A8A2` | +| `Anonymisation-Setup.exe` | 695.8 Mo | `12D9D6A8…663DC` | + +(Manifeste complet : `release\SHA256SUMS.txt`.) + +Tout est issu de `feature/q1-quarantine-mvp @ 15f73f8` (leak 100/100, validé GO). + +## ⏸️ Reste : diffusion OwnCloud — j'ai besoin du canal +Conformément à D-16, je n'ai **rien uploadé**. Tu m'as donné le GO mais pas le +**dossier/URL OwnCloud** de dépôt. Dis-moi où déposer (ZIP seul ? ou ZIP + Setup + +SHA256SUMS ?) et je m'en charge. Sinon le pack reste prêt en local. + +## Recommandation diffusion +Pour le bêta-testeur, je suggère de livrer **soit le ZIP soit le Setup**, pas les deux +(évite la confusion), + `SHA256SUMS.txt`. Le ZIP (EXE portable + docs) est le plus +simple ; le Setup ajoute un raccourci menu Démarrer mais demande un « installer ». + +— Claude diff --git a/docs/coordination/inbox/for-dom/2026-06-05_claude_planA_gui-v6-archi.md b/docs/coordination/inbox/for-dom/2026-06-05_claude_planA_gui-v6-archi.md new file mode 100644 index 0000000..6bd79a5 --- /dev/null +++ b/docs/coordination/inbox/for-dom/2026-06-05_claude_planA_gui-v6-archi.md @@ -0,0 +1,320 @@ +--- +from: claude (Agent A) +to: dom +date: 2026-06-05T19:30:00+02:00 +topic: planA-gui-v6-architecture +status: open +priority: high +nature: PLANIFICATION (lecture seule — aucun code modifié, aucun commit) +references: + - plan: docs/coordination/inbox/for-dom/2026-06-05_claude_plan-v11-5-parallele.md (57aa0f0) + - mockup: docs/ui_mockup_v6.html (validé 2026-05-06) + - wip: backup/windows-wip-2026-06-05 (b8c9c41) + - base_wip: 0124457 +gardefou: "98 tests unit doivent rester verts — le moteur ne bouge pas" +--- + +# Plan A — Architecture GUI v6 + +Sous-plan détaillé de la transposition GUI v6. **Document de conception +uniquement.** Aucun fichier de code n'a été touché. + +## 0. Constat majeur (corrige une hypothèse du plan v11.5) + +Le plan v11.5 décrit le WIP `backup/windows-wip-2026-06-05` comme « +1250 lignes +de GUI v6 ». **Vérification faite, ce n'est pas une GUI v6 :** + +- Le diff `0124457..b8c9c41` (+1148/-102) ajoute **profils métier, masques PDF + réutilisables, paramètres avancés** — features qui sont **déjà toutes dans le + `Pseudonymisation_Gui_V5.py` actuel** (v5.5). +- Le WIP est en **tkinter pur** : aucune trace de `customtkinter`/`ctk`/`CTk`. +- Le fichier de travail actuel est en fait **en avance** sur le WIP : `git diff + backup/windows-wip-2026-06-05 -- Pseudonymisation_Gui_V5.py` = seulement + 24 insertions / 5 suppressions, et ce delta = les fixes D-11 (VLM masqué hors + admin), D-13 (tag « MODE ADMIN » dans le titre) et RGPD (`CHCB`→`CHUXX`, + `chcb_strict`→`chuxx_strict`) que le WIP **n'a pas encore**. + +**Conséquences pour l'Agent A :** +1. Le WIP n'est **pas** une base de départ v6 — c'est l'ancêtre de la v5.5 actuelle. + La « matière première » réelle de la GUI v6 = le **mockup HTML v6** + la **v5.5 + actuelle** (logique métier déjà écrite et fonctionnelle). +2. La GUI v6 = **réécriture de la couche présentation** (tkinter → customtkinter, + 2 onglets → 3 onglets + sous-onglets) **en réutilisant telle quelle toute la + logique métier** (worker, profils, masques, params, contrat moteur). +3. La sauvegarde Gitea (section 0 du plan v11.5) est **déjà faite** : la branche + existe sur `remotes/gitea/backup/windows-wip-2026-06-05`. Risque « disque + unique » levé. ⚠️ reste à vérifier : que la v5.5 *actuelle* (en avance sur le + WIP) soit elle aussi sauvegardée hors disque avant de démarrer le codage v6. + +`customtkinter` **n'est ni installé dans `.venv` ni listé dans les requirements** +→ à ajouter comme dépendance v11.5 (impact PyInstaller à anticiper avec Agent D). + +--- + +## 1. Inventaire de l'existant + +### 1.1 GUI v5.5 (`Pseudonymisation_Gui_V5.py`, 2894 lignes) + +**Stack :** tkinter + ttk, thème `sv_ttk` optionnel (fallback `clam`), PIL pour +logo/icônes (dégradation si absent). Palette magenta/pêche dérivée du logo +(`CLR_PRIMARY=#E91E63`, etc.). Onglets *custom* faits main (pas `ttk.Notebook`). + +**Structure actuelle — 3 onglets plats :** + +| Onglet | Contenu | +|---|---| +| **Anonymisation** | Étape 1 (choisir dossier OU fichier) → Étape 2 (info formats : raster PDF + .txt) → checkbox VLM (si admin) → bouton Lancer/Arrêter → progress → résultats (3 cartes stats + badge fuites + perf + ouvrir dossier + journal repliable) | +| **Paramètres** | Whitelist / Blacklist / Stop-words (3 listes éditables) ; masques PDF réutilisables (ouvrir éditeur, combo modèle, dossier modèles) ; export/import JSON ; sauvegarder | +| **Profils** | Profil actif (combo + actualiser), description éditable, flags (masque obligatoire, désactiver VLM), masque mémorisé, actions (nouveau/enregistrer/renommer/défaut/supprimer), panneau résumé | + +**Briques techniques déjà en place (à conserver intégralement) :** +- `App` (classe monolithique), `UiMessage`/`MsgType` (file worker→UI), `ToolTip`. +- Worker threadé (`_run` → `threading.Thread(_worker)`), pompe `_pump_logs` + (`root.after(60)`). +- Détection police/dark-mode, résolution assets/config compatible PyInstaller + (`_asset`, `_app_dir`, `_exe_dir`, `_resolve_config`, `_resolve_profiles_config`). +- 4 managers NER chargés en interne (ONNX, EDS-Pseudo, CamemBERT, VLM optionnel). +- Mode admin (`admin_mode.is_admin`) : masque le VLM + annote le titre. + +### 1.2 Mockup v6 (`docs/ui_mockup_v6.html`, 898 lignes) — cible UX validée + +**3 onglets principaux :** +1. **📄 Utilisation** — dropzone glisser-déposer + liste fichiers, bouton Go, + barre progression « Fichier 1/3 », 4 cartes résultats (Documents, PII masqués, + Durée, Qualité), bandeau « Aucune fuite détectée », journal. +2. **⚙️ Configuration** — **4 sous-onglets** : + - **⚙️ Réglages** : catégories PII activables (Noms, Dates naissance, + Établissements, Adresses/CP, N° sécu, Tél/email, N° mutuelle) + choix moteur + (CamemBERT-bio RAPIDE / EDS-Pseudo PRÉCIS / GLiNER OPTIONNEL). + - **🎭 Masquage** : couleur rectangles, libellés placeholders par type + (NOM, Date naissance, Établissement…), marges/coins arrondis, **éditeur de + masques PDF intégré** (canvas, zoom, DPI, compteur masques, template). + - **🔄 Partage** : export/import config (whitelist/blacklist). + - **🛡️ Règles** : table de règles personnalisées (Label, Type, Cible→Résultat, + Statut) + simulateur (texte test → sortie). +3. **ℹ️ À propos** — version, thème, build. + +**Thèmes :** sélecteur (cf. roadmap mémoire : 4 thèmes). + +### 1.3 WIP backup (`b8c9c41`) +Ancêtre de la v5.5 (cf. §0). Sert de **référence de lecture** pour les libellés +français et l'organisation des écrans Profils/Masques, **pas de base à merger**. + +--- + +## 2. Architecture cible GUI v6 (customtkinter) + +### 2.1 Principe directeur +**Séparer présentation et logique.** La v5.5 mélange les deux dans une classe +`App` de 2894 lignes. La v6 extrait la logique métier (déjà testée, déjà +fonctionnelle) dans un *contrôleur* réutilisable, et réécrit uniquement la +couche vue en customtkinter selon le mockup. + +### 2.2 Arborescence proposée + +``` +Pseudonymisation_Gui_V6.py # point d'entrée : main(), bootstrap ctk, App +gui_v6/ +├── __init__.py +├── app.py # AppV6(ctk.CTk) : shell, header, nav 3 onglets +├── theme.py # palette + 4 thèmes ctk + tokens (couleurs/polices) +├── widgets/ # composants réutilisables +│ ├── dropzone.py # zone glisser-déposer + liste fichiers +│ ├── stat_card.py # carte statistique résultat +│ ├── phrase_list.py # liste éditable (whitelist/blacklist/stopwords) +│ ├── tooltip.py # ToolTip (porté depuis v5) +│ └── tabview.py # onglets/sous-onglets stylés +├── tabs/ +│ ├── tab_use.py # onglet Utilisation +│ ├── tab_config.py # onglet Configuration (host des 4 sous-onglets) +│ ├── config_reglages.py # sous-onglet Réglages (PII + moteur) +│ ├── config_masquage.py # sous-onglet Masquage + éditeur masques intégré +│ ├── config_partage.py # sous-onglet Partage (export/import) +│ ├── config_regles.py # sous-onglet Règles + simulateur [zone B] +│ └── tab_about.py # onglet À propos + état licence [zone C] +├── controller.py # AnonymController : SEULE porte vers le moteur +├── worker.py # worker threadé + file UiMessage (porté de v5) +└── assets_v6/ # logo, icônes (réutilise assets/ existant) +``` + +**Note de packaging :** `gui_v6/` est un package neuf (frontière propre Agent A, +cf. plan v11.5 §3). Aucun fichier du périmètre bêta n'est modifié. Ajouter +`gui_v6/` et `customtkinter` au `.spec` PyInstaller = tâche post-GO bêta (Agent D). + +### 2.3 Mapping mockup → modules + +| Onglet mockup | Module v6 | Réutilise (v5.5) | +|---|---|---| +| Utilisation | `tabs/tab_use.py` | `_run`/`_worker`, stats, badge fuites, journal | +| Config → Réglages | `tabs/config_reglages.py` | sélection moteur NER, seuils (nouveau : cases PII) | +| Config → Masquage | `tabs/config_masquage.py` | combo masques + `pdf_mask_designer` | +| Config → Partage | `tabs/config_partage.py` | `_export_params`/`_import_params` | +| Config → Règles | `tabs/config_regles.py` | nouveau (zone B) | +| À propos | `tabs/tab_about.py` | `_version_long`, build_info (+ licence zone C) | +| (Profils v5) | intégré dans Config ou onglet dédié | tout l'appareil `profile_defaults` | + +**Décision à trancher avec Dom :** le mockup v6 n'a **pas** d'onglet « Profils » +distinct (v5 en a un). Deux options : (a) garder un 4ᵉ onglet principal +« Profils », ou (b) intégrer la sélection de profil en bandeau dans Utilisation + +gestion dans Config. Recommandation : **option (b)** pour coller au mockup validé, +avec un sélecteur de profil en haut de l'onglet Utilisation. + +--- + +## 3. Liste des écrans / workflows + +**Workflow principal (Utilisation) :** +1. Glisser-déposer OU parcourir (dossier/fichier) → liste fichiers. +2. (option) choisir profil métier + masque manuel. +3. Lancer → progress par fichier → cartes résultats + badge fuites → ouvrir dossier. +4. Arrêter en cours possible ; journal détaillé repliable. + +**Workflows Configuration :** +- Réglages : activer/désactiver catégories PII, choisir moteur NER. +- Masquage : couleur/placeholders/marges + dessiner et enregistrer un masque PDF. +- Partage : exporter config (JSON pour email) / importer config reçue. +- Règles : créer une règle perso (cible→résultat), tester via simulateur. + +**Workflows transverses :** profils (CRUD + défaut), thème (4 thèmes), +état licence (bandeau). + +--- + +## 4. Contrat minimal avec le moteur (GARDE-FOU — le moteur ne bouge pas) + +La GUI v6 consomme **exactement les mêmes API que la v5.5**. Aucune signature +moteur ne change ⇒ les 98 tests unit restent verts. Tout passe par +`gui_v6/controller.py` (point d'entrée unique vers le backend). + +### 4.1 Fonction moteur centrale (à appeler à l'identique) + +```python +# anonymizer_core_refactored_onnx.py +process_document(doc_path, out_dir, **kwargs) -> Dict[str, str] # multi-formats +process_pdf(pdf_path, out_dir, ...) -> Dict[str, str] # fallback PDF +``` +kwargs effectivement passés par le worker v5 (à reproduire tels quels) : +`make_vector_redaction=False`, `also_make_raster_burn=True`, `config_path`, +`use_hf`, `ner_manager`, `ner_thresholds`, `ogc_label`, `vlm_manager`, +`camembert_manager`. Sélection via `getattr(core, 'process_document', None) or +core.process_pdf` + clé `doc_path`/`pdf_path`. **Retour = dict chemins de sortie** +(clés `audit`, etc.) — la v6 lit ces clés à l'identique (comptage audit, badge fuites). + +### 4.2 Managers NER (instanciés et chargés comme en v5) +- `ner_manager_onnx.NerModelManager(cache_dir)` + `NerThresholds` — `.is_loaded()`, + `.load(model_id)`, `.models_catalog()`. +- `eds_pseudo_manager.EdsPseudoManager(cache_dir)` — idem. +- `camembert_ner_manager.CamembertNerManager()` — `.is_loaded()`, `.load()`. +- `vlm_manager.VlmManager` / `VlmConfig` — **masqué hors admin** (D-11), + `.is_loaded()`. + +### 4.3 Modules support (réutilisés sans modification) +- `config_defaults` : `load_effective_dictionaries_dict`, `load_effective_param_lists`, + `deep_merge_dict`, `read_*_text`, `ensure_runtime_dictionaries_config`. +- `gui_batch_paths` : `list_supported_documents`, `build_batch_output_dir`, + `iter_pseudonymized_texts`. +- `manual_masking` : `ensure_mask_templates_dir`, `list_mask_templates`, + `mask_template_label`, `resolve_manual_mask_pdf`, `append_jsonl_file`. +- `profile_defaults` : `list_effective_profiles`, `save_runtime_profile`, + `delete_runtime_profile`, `set_runtime_default_profile`, `get_default_profile_key`, + `ensure_runtime_profiles_config`. +- `pdf_mask_designer` : `Template`, `load_template_yaml`, `apply_template_vector`, + `MaskDesignerApp` (intégrer dans le sous-onglet Masquage plutôt que Toplevel). +- `format_converter.SUPPORTED_EXTENSIONS`. +- `admin_mode.is_admin` / `admin_required`. +- `build_info` (BUILD_DATE/COMMIT/BRANCH). + +### 4.4 Construction de la config par profil (logique worker à porter telle quelle) +Le worker v5 fabrique un **YAML temporaire** fusionnant config effective + +`param_lists` du profil + overlay, puis le passe en `config_path`. Cette mécanique +(`deep_merge_dict` + `tempfile.mkstemp` à côté de la config) **est reportée à +l'identique** dans `gui_v6/worker.py`. Le moteur reçoit donc le même intrant +qu'aujourd'hui → sortie inchangée → audit qualité ≥ baseline. + +**Règle d'or :** `controller.py`/`worker.py` ne contiennent **aucune** logique de +détection. Ils orchestrent. Toute tentation de « pré-traiter » le texte côté GUI += violation du garde-fou. + +--- + +## 5. Stratégie de migration progressive (v5 → v6 sans casser) + +1. **Cohabitation.** v6 = fichier neuf `Pseudonymisation_Gui_V6.py` + package + `gui_v6/`. La v5.5 reste l'entrée par défaut tant que la v6 n'a pas passé le + smoke test et l'audit qualité. Bascule par défaut = dernière étape (Agent D). +2. **Extraction d'abord, vue ensuite.** Étape 1 : extraire worker + contrôleur + depuis la v5.5 **sans changer de toolkit** (refactor pur, testable). Étape 2 : + réécrire la vue en customtkinter par-dessus ce contrôleur. Ça découple le risque + « moteur » du risque « UI ». +3. **Parité fonctionnelle par onglet.** Migrer Utilisation → Configuration → + Profils dans cet ordre ; à chaque onglet, vérifier que le workflow produit les + **mêmes sorties** que la v5 sur un même lot (diff des dossiers `anonymise/`). +4. **Tests conservés.** `gui_batch_paths` / `manual_masking` ont déjà leurs tests : + ne pas y toucher. Ajouter un **smoke test de lancement** v6 + un test de + **non-régression du contrat** (mocked managers, vérifier que le worker appelle + `process_document` avec exactement les kwargs attendus). +5. **Garde-fou n°1 permanent.** `pytest tests/unit` (98) doit rester vert à chaque + commit v6. Si un test moteur casse ⇒ la v6 a franchi sa frontière, rollback. +6. **Rétro-port RGPD/admin.** La v6 doit naître au niveau de la v5.5 **actuelle** + (CHUXX, admin tag, VLM masqué), pas du WIP `b8c9c41` qui est en retard. + +--- + +## 6. Zones de contact + +### 6.1 Avec Agent B (D-13 — Paramètres avancés / Profils techniques) +- **Fichiers partagés :** sous-onglets « avancés » de Config (`config_reglages.py`, + `config_regles.py`) + onglet/bandeau Profils. +- **Contrat attendu de B (avant que A code ces écrans) :** + - liste des réglages **protégés admin** (cachés/désactivés en non-admin) ; + - API `admin_mode.admin_required(feature)` pour verrouiller une action ; + - règle de sauvegarde : config sensible **bloquée** en non-admin. +- **A fournit :** des conteneurs/onglets prêts où B injecte ses contrôles + + un helper `is_admin()` déjà câblé dans le shell (titre annoté, sections + masquées). A réserve le sous-onglet « Règles » comme zone B. +- **À écrire :** contrat A↔B avant tout code (plan v11.5 §3). + +### 6.2 Avec Agent C (Licence — affichage état) +- **Emplacement UI réservé par A :** bandeau d'état en haut du shell (sous le + header) + bloc dédié dans l'onglet **À propos** (statut, expiration, grace). +- **API attendue de C (`license.py`, à créer) :** une fonction de statut du type + `get_license_status() -> {valid, expires_at, grace_days, machine_id, message}` + que A appelle au démarrage et affiche (vert/orange/rouge). A **n'implémente + aucune crypto** ; A consomme le statut. +- **Dégradation :** si `license.py` absent (dev), le bandeau s'efface + silencieusement (même pattern que `admin_mode`/`vlm_manager` en try/except). + +--- + +## 7. Risques spécifiques GUI v6 + mitigations + +| Risque | Mitigation | +|---|---| +| customtkinter absent du venv/spec | Ajouter dépendance + tester build EXE tôt avec Agent D | +| Éditeur de masques (`MaskDesignerApp`) conçu pour Toplevel tk | L'intégrer en frame dans le sous-onglet Masquage, ou le garder en fenêtre détachée v1 | +| Glisser-déposer natif (mockup) absent de tkinter pur | `tkinterdnd2` ou fallback « Parcourir » ; à valider avec Dom | +| Régression silencieuse moteur via worker | Test contrat (kwargs `process_document`) + 98 tests verts | +| v6 part du WIP en retard (CHCB/admin) | Naître de la v5.5 actuelle (§5.6) | +| Dérive de portée (refonte logique) | controller/worker = orchestration pure, zéro détection | + +--- + +## Résumé (5-8 lignes) + +Le « WIP +1250 lignes » n'est **pas** une GUI v6 : c'est l'ancêtre tkinter de la +v5.5 actuelle (profils/masques/params déjà livrés), et la v5.5 est même **en +avance** dessus (fixes CHUXX/admin/VLM). La vraie matière première v6 = le +**mockup HTML validé** + la **logique métier v5.5** à réutiliser telle quelle. +Architecture cible : `Pseudonymisation_Gui_V6.py` + package `gui_v6/` +(customtkinter, **à ajouter au venv/spec**), 3 onglets (Utilisation / Configuration +à 4 sous-onglets / À propos), éditeur de masques intégré, 4 thèmes. **Garde-fou +n°1 :** tout passe par `gui_v6/controller.py`+`worker.py` qui appellent +`process_document(...)` avec les **mêmes kwargs** qu'aujourd'hui → le moteur ne +bouge pas, les 98 tests unit restent verts. Migration progressive : extraire +contrôleur d'abord, réécrire la vue ensuite, cohabitation v5/v6, bascule par +défaut en dernier (Agent D). Zones de contact : **B** sur les sous-onglets +Réglages/Règles + Profils (contrat admin écrit avant code), **C** sur le bandeau +d'état + bloc À propos (A consomme `get_license_status()`, A n'implémente aucune +crypto). + +— Claude (Agent A) diff --git a/docs/coordination/inbox/for-dom/2026-06-05_claude_planB_d13-complet.md b/docs/coordination/inbox/for-dom/2026-06-05_claude_planB_d13-complet.md new file mode 100644 index 0000000..bc0e3cd --- /dev/null +++ b/docs/coordination/inbox/for-dom/2026-06-05_claude_planB_d13-complet.md @@ -0,0 +1,335 @@ +--- +from: claude (Agent B) +to: dom +date: 2026-06-05T19:30:00+02:00 +topic: planB-d13-complet +status: open +priority: high +scope: PLANIFICATION uniquement — lecture seule, aucun code modifié +references: + - plan: docs/coordination/inbox/for-dom/2026-06-05_claude_plan-v11-5-parallele.md (commit 57aa0f0) + - decision: docs/coordination/decisions/2026-06-02_dom_d13-partial-scope.md + - module: admin_mode.py + - gui: Pseudonymisation_Gui_V5.py (2893 lignes, v5.4) +--- + +# Plan B — D-13 complet : protection des réglages avancés en mode non-admin (GUI v6) + +> **Périmètre.** Sous-plan du chantier v11.5 (cap D-17). Définit les **règles** +> admin / non-admin à appliquer dans la GUI v6 (customtkinter). Ne contient aucun +> code ; l'implémentation attend le GO bêta (D-16) et se fait dans `gui_v6/` + +> extension `admin_mode.py`. Les **écrans** des sections « avancé » sont co-conçus +> avec l'Agent A (voir § Zone de contact A↔B). + +## 0. Rappel du modèle de menace D-13 + +Le mode admin n'est **pas** un contrôle d'accès cryptographique : c'est un +« verrou anti-distrait » (cf. docstring `admin_mode.py`). Activation par +`ANON_ADMIN=1` ou fichier `.admin`. Deux objectifs distincts, à ne pas confondre : + +1. **Anti-leak RGPD** (critique) — empêcher l'envoi de données hors poste. + Déjà couvert par D-11 : VLM/Ollama **caché** en non-admin, et de toute façon + `VlmManager=None` quand le module est neutralisé. +2. **Anti-dégradation qualité** (important, périmètre v11.5) — empêcher le + bêta-testeur / utilisateur final de **casser la détection** en éditant des + stopwords, profils techniques, regex, ou d'**écraser des fichiers de config + de référence**. C'est l'objet de ce plan. + +Conséquence : pour les réglages qui ne provoquent **pas** de fuite externe mais +peuvent **dégrader le masquage**, la bonne politique par défaut est +**griser/désactiver (visible mais verrouillé)** plutôt que **cacher**, pour rester +pédagogique. Exception : ce qui touche au leak externe se **cache**. + +--- + +## 1. Inventaire exhaustif des réglages exposés + +Source : balayage de `Pseudonymisation_Gui_V5.py`, `config/`, `config_defaults.py`, +`profile_defaults.py`. + +### 1.A — Réglages UI (widgets actuels v5) + +| # | Réglage | Widget v5 | Variable / méthode | Écrit dans | +|---|---|---|---|---| +| R1 | **Analyse visuelle VLM (Ollama)** | `Checkbutton` (l.769) | `self.use_vlm` / `_on_vlm_toggle` | aucun (runtime) | +| R2 | **Profil : « Désactiver le VLM »** | `Checkbutton` (l.1088) | `profile_force_disable_vlm_var` | `profiles.yml` | +| R3 | **Whitelist — phrases à NE PAS anonymiser** | `Listbox` + ajout/suppr (l.919) | `_wl_listbox` | `dictionnaires.yml` → `whitelist_phrases` | +| R4 | **Blacklist — mots à TOUJOURS masquer** | `Listbox` (l.928) | `_bl_listbox` | `dictionnaires.yml` → `blacklist.force_mask_terms` | +| R5 | **Stop-words additionnels** (ne jamais traiter comme nom) | `Listbox` (l.939) | `_sw_listbox` | `dictionnaires.yml` → `additional_stopwords` | +| R6 | **Profil actif** (sélection) | `Combobox` (l.1032) | `processing_profile_label_var` | lecture seule | +| R7 | **Profil : description** | `Entry` (l.1064) | `profile_description_var` | `profiles.yml` | +| R8 | **Profil : « Masque manuel obligatoire »** | `Checkbutton` (l.1077) | `profile_require_manual_mask_var` | `profiles.yml` | +| R9 | **Profil : masque PDF mémorisé** | `Combobox` (l.1109) | `manual_mask_template_var` | `profiles.yml` | +| R10 | **Créer / Renommer / Supprimer / Définir par défaut un profil** | Boutons (l.2040-2147) | `_create/_rename/_delete/_set_default_…profile` | `profiles.yml` | +| R11 | **Sauvegarder le profil courant** | Bouton (l.2147) | `_save_selected_processing_profile` | `profiles.yml` | +| R12 | **Masque manuel : template actif** (anonymisation) | `Combobox` (l.881) | `manual_mask_template_var` | runtime | +| R13 | **Éditeur de masques PDF** (designer) | Bouton (l.2306) | `_open_manual_mask_designer` | `config/mask_templates/` | +| R14 | **Sauvegarder les paramètres** (WL/BL/SW → YAML) | Bouton (l.2629) | `_save_params` → `_save_param_listboxes` | **`dictionnaires.yml` (écriture)** | +| R15 | **Exporter les paramètres** (→ JSON) | Bouton (l.2539) | `_export_params` | JSON sur disque (Bureau) | +| R16 | **Importer des paramètres** (JSON → listes) | Bouton (l.2596) | `_import_params` | listes en mémoire | +| R17 | **Dossier / fichier source** | sélecteur | `dir_var` / `_single_file` | runtime | + +### 1.B — Réglages présents dans le **schéma de config** mais PAS exposés en UI v5 + +Importants à connaître car la GUI v6 pourrait vouloir les exposer (profils +techniques). Aujourd'hui chargés silencieusement (en-tête v5 : « Pas d'onglet +Avancé (NER + YAML chargés silencieusement) »). + +| # | Réglage | Fichier / clé | Statut v5 | Sensibilité | +|---|---|---|---|---| +| S1 | **`regex_overrides`** (patterns + placeholders custom) | `dictionnaires.yml` → `regex_overrides[]` | non exposé UI | **technique sensible** (une mauvaise regex casse la détection ou plante) | +| S2 | **`blacklist.force_mask_regex`** | `dictionnaires.yml` | non exposé UI | technique sensible | +| S3 | **`whitelist.org_gpe_keep` / `sections_titres` / `noms_maj_excepts`** | `dictionnaires.yml` → `whitelist.*` | non exposé UI | technique sensible (peut désactiver le masquage d'établissements) | +| S4 | **`kv_labels_preserve`** | `dictionnaires.yml` | non exposé UI | technique sensible | +| S5 | **`flags.regex_engine` / `case_insensitive` / `unicode_word_boundaries`** | `dictionnaires.yml` → `flags` | non exposé UI | technique sensible | +| S6 | **`additional_villes_blacklist` / `additional_dpi_labels` / `additional_companion_blacklist`** | `dictionnaires.yml` | non exposé UI | modérée (qualité) | +| S7 | **`dictionaries_overlay`** par profil (surcharge YAML embarquée) | `profiles.yml` → `dictionaries_overlay` | partiellement (via BL profil) | **technique sensible** | +| S8 | **Choix du moteur NER** (GLiNER / CamemBERT-bio / EDS-Pseudo / ONNX) | aucun fichier UI ; chargé via `_auto_load_ner()` (l.473), managers l.437-439 | **non exposé** (silencieux) | **technique sensible** (désactiver un moteur dégrade le recall F1=0.963) | +| S9 | **Seuils NER** (`NerThresholds`) | `ner_manager_onnx.py` | non exposé UI | technique sensible | +| S10 | **Chemins config** (`cfg_path`, `profiles_path`, `MODELS_DIR`) | `DEFAULT_CFG`, `DEFAULT_PROFILES_CFG` | non exposé UI (pas de file picker) | sensible (réécriture d'un autre fichier) | + +> Note : le **choix du moteur NER** (reporté à v11.5 selon D-13) n'a **aucune UI +> aujourd'hui**. L'exposer en v6 est une **création** d'écran, donc à protéger +> dès l'origine. Recommandation forte : **réservé admin**, et même en admin, +> exposer en lecture/diagnostic plutôt qu'en désactivation libre, pour ne pas +> permettre de couper un moteur et faire chuter le recall sans le vouloir. + +### 1.C — Fichiers de config sensibles (cibles d'écriture) + +| Fichier | Rôle | Écriture en v5 par | Politique non-admin | +|---|---|---|---| +| `config/dictionnaires.yml` | surcharge locale active (WL/BL/SW/regex) | R14 `_save_param_listboxes` | **bloquer l'écriture** | +| `config/dictionnaires.default.yml` | **source de vérité** | jamais (ne doit jamais l'être) | **bloquer (admin compris)** | +| `config/profiles.yml` | profils locaux | R10/R11 | **bloquer écriture** (lecture/sélection OK) | +| `config/profiles.default.yml` | source de vérité profils | jamais | **bloquer (admin compris)** | +| `config/admin_rules.yml` | règles d'admin candidates | (gouvernance) | **bloquer** | +| `config/mask_templates/`, `config/mask_templates` GUI | masques PDF | R13 designer | autorisé (non sensible PII) | +| Export JSON (Bureau) | échange par email | R15 | **autorisé** (sortie, pas d'écrasement config) | + +--- + +## 2. Matrice admin / non-admin + +Légende : **V** = visible, **É** = éditable, **S** = sauvegardable (peut écrire un fichier). +`—` = non applicable. `(cacher)` = absent de l'UI. `(grisé)` = visible mais désactivé. + +| # | Réglage | non-admin V | non-admin É | non-admin S | admin V | admin É | admin S | Mode UI non-admin | +|---|---|:--:|:--:|:--:|:--:|:--:|:--:|---| +| R1 | VLM Ollama (case) | ❌ | ❌ | — | ✅ | ✅ | — | **cacher** (leak RGPD) | +| R2 | Profil « Désactiver VLM » | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** (lié VLM) | +| R3 | Whitelist phrases | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** (lecture) | +| R4 | Blacklist force-mask | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** (lecture) | +| R5 | Stop-words additionnels | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** (lecture) | +| R6 | Profil actif (sélection) | ✅ | ✅ | — | ✅ | ✅ | — | **actif** (choisir un profil pré-validé est sûr) | +| R7 | Profil : description | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** | +| R8 | Profil : masque manuel obligatoire | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** | +| R9 | Profil : masque PDF mémorisé | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** | +| R10 | Créer/Renommer/Suppr/Défaut profil | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** (écrit profiles.yml) | +| R11 | Sauvegarder le profil | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** | +| R12 | Masque manuel actif (anonymisation) | ✅ | ✅ | — | ✅ | ✅ | — | **actif** (sécurité, ajoute du masquage) | +| R13 | Éditeur masques PDF | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | **actif** (n'augmente jamais le leak) | +| R14 | Sauvegarder paramètres → YAML | ❌ | — | ❌ | ✅ | — | ✅ | **cacher** (écrit dictionnaires.yml) | +| R15 | Exporter paramètres (JSON) | ✅ | — | ✅ | ✅ | — | ✅ | **actif** (sortie d'échange, pas d'écrasement config) | +| R16 | Importer paramètres (JSON) | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | **actif en mémoire**, mais **R14 bloqué** → l'import reste sans effet persistant en non-admin (voir § 3.3) | +| R17 | Dossier / fichier source | ✅ | ✅ | — | ✅ | ✅ | — | **actif** (cœur métier) | +| S1 | `regex_overrides` (si exposé v6) | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** (profil technique) | +| S2 | `force_mask_regex` | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** | +| S3 | `whitelist.*` techniques | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** | +| S4 | `kv_labels_preserve` | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** | +| S5 | `flags.*` (regex_engine…) | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** | +| S6 | `additional_villes/dpi/companion` | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | **grisé** (qualité, lecture) | +| S7 | `dictionaries_overlay` profil | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** | +| S8 | Choix moteur NER | ✅ (diag) | ❌ | ❌ | ✅ | ✅* | ❌* | **grisé/diagnostic** ; *même admin : lecture conseillée (voir § 3.4) | +| S9 | Seuils NER | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** | +| S10 | Chemins config (file pickers) | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | **cacher** (ne pas exposer en v6 hors admin) | + +**Principe de lecture de la matrice** : +- **Cacher** = réglage *leak-sensible* (VLM) ou *technique avancé* (regex/profil + technique/moteur/seuils/chemins). Inutile et risqué de le montrer au bêta. +- **Griser** = réglage *qualité* visible à titre pédagogique (le bêta voit ce que + l'admin a configuré) mais non modifiable / non sauvegardable. +- **Actif** = réglage qui *ne peut qu'augmenter la sécurité* (ajouter du masquage : + masque manuel R12/R13) ou *cœur métier* (R6 sélection de profil validé, R17 + source), ou *sortie sans écrasement* (R15 export). + +--- + +## 3. Règles UI et règles de sauvegarde + +### 3.1 — Cacher vs désactiver/griser (décision par catégorie) + +| Catégorie | Politique non-admin | Justification | +|---|---|---| +| **Leak externe** (VLM, force_disable_vlm) | **Cacher** | Ne doit pas exister dans l'UI bêta (D-11). | +| **Technique avancé** (regex_overrides, force_mask_regex, whitelist.*, flags, kv_labels, dictionaries_overlay, seuils NER, chemins config) | **Cacher** | Bruit pour le bêta + casse silencieuse de la détection. Regrouper dans un onglet/section « Profils techniques » entièrement masquée hors admin. | +| **Qualité éditable** (WL/BL/SW, descriptions, flags profil, villes/dpi/companion) | **Griser (read-only)** | Pédagogique : le bêta voit la config sans pouvoir la dégrader. | +| **Gestion de profils** (CRUD, sauvegarde profil) | **Cacher** | Écrit `profiles.yml`. | +| **Sécurité additive** (masque manuel actif, designer, sélection profil, source) | **Actif** | Ne réduit jamais le masquage. | +| **Échange** (export JSON) | **Actif** ; **import** actif en mémoire mais sans persistance (R14 bloqué) | Sortie pour email, pas d'écrasement de config. | + +### 3.2 — Implémentation UI (customtkinter v6) + +1. **Cacher** : ne pas instancier le widget/onglet quand `not is_admin()`. + L'onglet/section « Profils techniques » et la section VLM ne sont **pas + créés** hors admin (pas seulement `grid_remove`, pour éviter toute + réactivation accidentelle). +2. **Griser** : créer le widget puis `configure(state="disabled")`. Pour les + `Listbox`/listes éditables : désactiver les boutons +Ajouter / Supprimer et + passer la liste en lecture seule ; afficher un bandeau discret + « Réglages avancés en lecture seule — mode admin requis pour modifier ». +3. **Helper centralisé** (extension `admin_mode.py`) : + - `admin_only_visible(widget)` → ne crée/affiche que si admin. + - `admin_only_editable(widget)` → `state="normal"` si admin sinon `"disabled"`. + - `guard_save(feature) ` → wrappe l'écriture (voir 3.3). + Cela centralise la logique au lieu de la disperser dans la GUI (frontière + Agent B : `admin_mode.py` + sections « avancé » de `gui_v6/`). +4. **Titre fenêtre** : conserver le tag `[⚙ MODE ADMIN]` (déjà en v5, l.383-384). + En v6, ajouter une **bannière** persistante en mode admin (rouge/orange). + +### 3.3 — Règles de sauvegarde (blocage de l'écriture des fichiers sensibles) + +Le point dur de D-13 complet : **l'UI masquée ne suffit pas**. Il faut un garde +au niveau de l'**écriture** pour qu'aucun chemin (raccourci clavier, import + +save, futur bouton) ne puisse écraser un fichier sensible hors admin. + +1. **Garde à la source** : toute méthode qui écrit un fichier de config sensible + doit appeler `admin_required(...)` (déjà fourni par `admin_mode.py`) **avant** + l'écriture. Cibles : `_save_param_listboxes` (R14 → `dictionnaires.yml`), + `_save_selected_processing_profile`, `_create/_rename/_delete/_set_default…` + (R10/R11 → `profiles.yml`), et toute future écriture de `regex_overrides`, + `dictionaries_overlay`, `flags`, seuils. +2. **Liste blanche d'écriture** : définir dans `admin_mode.py` un ensemble + `SENSITIVE_CONFIG_FILES = {dictionnaires.yml, dictionnaires.default.yml, + profiles.yml, profiles.default.yml, admin_rules.yml, hospital_stopwords.yml, + medical_terms_whitelist.yml}` + une fonction `assert_writable(path)` qui lève + si `path` est sensible et `not is_admin()`. Appelée par tous les `write_text` + de config. Filet de sécurité indépendant de l'UI. +3. **`*.default.yml` jamais réécrits** — même en admin. `assert_writable` refuse + l'écriture des `*.default.yml` quel que soit le mode (sources de vérité). +4. **Import JSON (R16)** : autorisé à charger en mémoire (pas de fuite), mais le + bouton **Sauvegarder (R14) étant caché/bloqué en non-admin**, l'import reste + sans effet persistant. À documenter dans l'UI : « Import chargé. Sauvegarde + réservée au mode admin. » Évite de laisser croire que la config est modifiée. +5. **Export JSON (R15)** : autorisé en non-admin — c'est une **sortie** vers le + Bureau pour échange par email, pas un écrasement de config. (Cohérent avec le + workflow « export → merge → renvoi YAML » des préférences projet.) + +### 3.4 — Cas particulier : choix du moteur NER (S8) + +Reporté à v11.5 par D-13 mais **sans UI existante**. Recommandation : +- Hors admin : **non exposé** (ou bandeau diagnostic en lecture seule listant les + moteurs chargés : EDS-Pseudo / GLiNER / CamemBERT-bio / ONNX + état). +- En admin : exposer en **diagnostic** (voir/recharger) ; **déconseiller** une + case « désactiver moteur X » librement, car couper un moteur fait chuter le + recall (multi-signal F1=0.963). Si Dom veut le toggle, l'assortir d'un + avertissement explicite « peut réduire la détection ». À trancher par Dom. + +--- + +## 4. Tests attendus (matrice admin / non-admin) + +Tests pilotables sans GUI réelle en testant les **helpers** + les **gardes +d'écriture** ; tests GUI en smoke (`gui_v6`). Cible : `tests/unit/test_d13_admin_*`. + +### 4.A — `admin_mode` (logique) +- `is_admin()` : `ANON_ADMIN ∈ {1,true,yes,on}` → True ; vide/0 → False ; + fichier `.admin` présent → True ; `force_refresh` re-évalue le cache. +- `admin_required("x")` : lève `RuntimeError` hors admin, ne lève pas en admin. +- `assert_writable(path)` (nouveau) : + - fichier sensible + non-admin → lève ; + - fichier sensible + admin → OK **sauf** `*.default.yml` → lève (toujours) ; + - fichier non sensible (export JSON, mask_templates) → OK dans les deux modes. + +### 4.B — Matrice par réglage (paramétrée admin ∈ {False, True}) +Pour chaque réglage R1–R17 / S1–S10, asserter la cible de la matrice § 2 : + +| Assertion | non-admin attendu | admin attendu | +|---|---|---| +| widget créé (visible) | selon col. « non-admin V » | « admin V » | +| widget `state` | `disabled` si grisé, absent si caché | `normal` | +| la sauvegarde écrit le fichier | **non** (lève / no-op) pour R10/R11/R14/S* | **oui** | +| `dictionnaires.yml` / `profiles.yml` non modifiés après tentative non-admin | hash fichier inchangé | modifié après save admin | + +### 4.C — Non-régression (garde-fou n°1 du plan maître) +- `tests/unit` (98 passed) **restent verts** — D-13 ne touche pas le moteur. +- Audit `evaluate_quality.py` ≥ 98.5 ; leak score 100/100 inchangé. +- Smoke v6 : lancement non-admin → aucune section technique/VLM présente ; + lancement admin (`ANON_ADMIN=1`) → sections présentes + bannière admin. + +### 4.D — Test « anti-contournement » +- Simuler import JSON (R16) puis tentative de save (R14) en non-admin → + `dictionnaires.yml` **inchangé**. +- Vérifier qu'aucun `write_text` sur un fichier sensible n'est atteignable hors + `assert_writable` (revue : grep des `write_text` sur `config/` dans `gui_v6/`). + +--- + +## 5. Impacts GUI v5 vs GUI v6 + +### GUI v5 (`Pseudonymisation_Gui_V5.py`) — **laisser tel quel** +- D-13 **partiel** est déjà livré et acté (VLM caché, titre admin). Conforme au + gel bêta (D-16) : on ne re-patche pas 2893 lignes tkinter. +- **Aucune modification v5** dans ce chantier. (Si un hotfix MVP devenait + nécessaire, il resterait hors périmètre v11.5.) + +### GUI v6 (`Pseudonymisation_Gui_V6.py` / `gui_v6/`) — **lieu d'implémentation** +- D-13 **complet** s'implémente nativement à la construction de chaque écran v6, + via les helpers `admin_mode` (§ 3.2). Pas de rétro-fit : la visibilité/édition + est décidée **au moment de créer le widget**. +- Frontières (plan maître § 3) : Agent B possède `admin_mode.py` (extension : + `assert_writable`, `SENSITIVE_CONFIG_FILES`, helpers UI) et les **règles** des + sections « avancé » ; Agent A possède les écrans `gui_v6/`. +- Structure cible v6 (proposition) : un onglet **« Profils techniques »** + une + section **VLM** entièrement **conditionnés à `is_admin()`** (non instanciés + hors admin) ; la section **« Paramètres avancés »** (WL/BL/SW) **toujours + visible** mais **read-only** hors admin. + +--- + +## 6. Zone de contact Agent A ↔ Agent B (contrat à figer avant code) + +Les écrans « Paramètres avancés » et « Profils techniques » de la GUI v6 sont +**co-conçus** : **B fournit les règles, A fournit les écrans**. Contrat proposé : + +**Ce que B (ce plan) fournit à A :** +1. La **matrice § 2** (visible/éditable/sauvegardable par réglage et par mode). +2. Les **helpers** `admin_mode` (signatures) que A appellera : + - `is_admin() -> bool` + - `admin_only_visible(parent, build_fn)` — n'appelle `build_fn` que si admin. + - `admin_only_editable(widget)` — applique `state`. + - `assert_writable(path)` — à appeler avant toute écriture config. +3. La **liste des écritures à garder** (R10/R11/R14, futurs S1/S7/S5…). +4. La **convention de regroupement** : tout réglage « technique avancé » + (S1–S5, S7, S9, S10, R2) dans **un seul** conteneur masquable d'un bloc. + +**Ce que A fournit à B :** +1. Les conteneurs/onglets v6 nommés (où s'accrochent les sections « avancé »). +2. L'emplacement de la **bannière mode admin** (cohérence avec la bannière + licence réservée à l'Agent C). +3. Le point d'appel unique des écritures de config (pour y placer + `assert_writable`) afin d'éviter des `write_text` dispersés. + +**À trancher par Dom :** +- S8 (toggle moteur NER) en admin : **diagnostic seul** (recommandé) ou toggle + avec avertissement ? +- Import JSON (R16) en non-admin : garder l'import-en-mémoire (proposé) ou le + cacher aussi ? + +--- + +## 7. Synthèse des recommandations + +1. **Deux politiques** : *cacher* le leak-sensible (VLM) et le technique avancé + (regex/profils techniques/moteur/seuils/chemins) ; *griser* le qualité + (WL/BL/SW) ; *laisser actif* l'additif-sécurité (masques) et l'export. +2. **Garde d'écriture indépendante de l'UI** (`assert_writable` + + `SENSITIVE_CONFIG_FILES`) : filet de sécurité contre tout contournement. + `*.default.yml` jamais réécrits, même en admin. +3. **GUI v5 inchangée** ; tout dans `gui_v6/` + extension `admin_mode.py`. +4. **Tests** : matrice paramétrée admin/non-admin + anti-contournement + 98 tests + moteur verts (garde-fou non négociable). +5. **Contrat A↔B** figé avant tout code (helpers + matrice + points d'écriture). +6. **Attente GO bêta (D-16)** avant tout codage — ce document est de la + planification pure. + +— Claude (Agent B) diff --git a/docs/coordination/inbox/for-dom/2026-06-05_claude_planC_licence.md b/docs/coordination/inbox/for-dom/2026-06-05_claude_planC_licence.md new file mode 100644 index 0000000..563239c --- /dev/null +++ b/docs/coordination/inbox/for-dom/2026-06-05_claude_planC_licence.md @@ -0,0 +1,523 @@ +--- +from: claude (Agent C — chantier v11.5) +to: dom +date: 2026-06-05T19:30:00+02:00 +topic: planC-licence-d14 +status: open +priority: high +references: + - decision: docs/coordination/decisions/2026-06-02_dom_d14-plateforme-licence-architecture.md + - plan: docs/coordination/inbox/for-dom/2026-06-05_claude_plan-v11-5-parallele.md +scope: CONCEPTION uniquement — aucun code de prod, aucun déploiement +--- + +# Plan C — Plateforme licence (D-14) : conception détaillée + +> Sous-plan de l'Agent C. **Lecture seule** sur le code existant. Aucune ligne de +> `license.py` ni de `platform/` n'est écrite ici : ce document est la spec qui +> sera codée APRÈS le GO bêta (D-16), Phase 1.1 puis 1.2. +> +> Cadre D-14 **respecté à la lettre** (FastAPI + PostgreSQL + HTMX/Jinja2, OVH HDS, +> `app.aivanov.fr`, fastapi-users, Brevo, RSA-PSS 2048 + SHA256, `license.dat` DPAPI, +> phone home ≤ 30 j, 1 licence = 1 poste, grace 15 j, offline 30 j, révocation au check). + +## 0. Ancrage dans l'existant (vérifié, read-only) + +- Pas de `license.py` ni de `platform/` aujourd'hui → **fichiers/dossiers 100 % neufs**, + zéro conflit avec le moteur (`anonymizer_core_refactored_onnx.py`) ou la GUI. +- `cryptography==41.0.7` **déjà installée** → pas de nouvelle dépendance lourde côté client + (RSA-PSS/SHA256 fournis par `cryptography.hazmat`). Aucun ajout au risque + `numpy<2.0` / `gliner==0.2.18`. +- Le `.spec` PyInstaller bundle déjà `config/` → la **clé publique** s'embarque + naturellement comme `config/license_pubkey.pem` (ajout d'une seule ligne `datas` + au moment du codage, pas maintenant). +- `admin_mode.py` fournit le patron `is_admin()` / `admin_required()` et + `_project_root()` (résolution `sys._MEIPASS` en frozen) → `license.py` réutilise la + même logique de résolution de chemins en mode EXE. + +--- + +## 1. Architecture serveur (`platform/`, Phase 1.2 ~50h) + +### 1.1 Arborescence du nouveau dossier (repo séparé ou sous-dossier `platform/`) + +``` +platform/ +├── app/ +│ ├── main.py # FastAPI app + routers +│ ├── config.py # settings (env: DB URL, Brevo key, clé privée path) +│ ├── db.py # SQLAlchemy async engine + session +│ ├── models.py # tables ORM (clients, licences, postes, activations, revocations) +│ ├── auth.py # fastapi-users (UserManager, JWT/cookie) +│ ├── crypto/ +│ │ ├── signer.py # signature RSA-PSS d'une licence (clé PRIVÉE, serveur only) +│ │ └── private_key.pem # JAMAIS commité (.gitignore + secret CI) — monté via volume OVH +│ ├── routers/ +│ │ ├── pages.py # pages HTMX/Jinja2 (login, mes licences, activation, DL) +│ │ ├── api_client.py # endpoints appelés par l'EXE (/activate, /check, /download) +│ │ └── api_admin.py # endpoints admin Dom (create licence, revoke, parc) +│ ├── services/ +│ │ ├── licensing.py # logique métier : 1 licence=1 poste, expiration, grace +│ │ ├── activation.py # bind machine_id ↔ licence, anti-réactivation +│ │ └── email.py # Brevo (activation, renouvellement, expiration J-30/J-7) +│ ├── templates/ # Jinja2 + fragments HTMX +│ └── static/ +├── migrations/ # Alembic +├── tests/ +├── Caddyfile # reverse proxy + Let's Encrypt (app.aivanov.fr) +├── docker-compose.yml # api + postgres + caddy +└── .github/workflows/deploy.yml +``` + +### 1.2 Schéma DB PostgreSQL + +Cinq tables. fastapi-users gère `users` (= comptes de connexion). Le **client métier** +(`clients`) est distinct du `user` pour autoriser plusieurs comptes par organisation +plus tard, mais en MVP `user ↔ client` est 1:1. + +``` +users (géré par fastapi-users) + id UUID PK + email TEXT UNIQUE + hashed_password TEXT + is_active BOOL + is_superuser BOOL -- Dom = superuser (back-office) + is_verified BOOL + +clients -- l'organisation cliente (hôpital, cabinet) + id UUID PK + user_id UUID FK→users.id (1:1 en MVP) + raison_sociale TEXT + finess TEXT NULL -- optionnel, cohérent métier santé + contact_email TEXT + created_at TIMESTAMPTZ + +licences -- 1 abonnement annuel = N postes achetés + id UUID PK + client_id UUID FK→clients.id + ref TEXT UNIQUE -- ex. LIC-2026-000123 (humain) + postes_max INT -- nb de postes autorisés (souvent 1) + version_max TEXT NULL -- version max couverte par l'abo (ex "11.x") + issued_at TIMESTAMPTZ + expires_at TIMESTAMPTZ -- date de fin d'abonnement annuel + status ENUM(active, suspended, expired, cancelled) + created_at TIMESTAMPTZ + +postes -- 1 ligne = 1 machine_id activée sous une licence + id UUID PK + licence_id UUID FK→licences.id + machine_id TEXT -- empreinte poste (voir §3.2) + label TEXT NULL -- nom donné par le client ("Poste accueil") + os_info TEXT NULL -- diag (Windows build), non PII + activated_at TIMESTAMPTZ + last_seen_at TIMESTAMPTZ -- dernier phone home réussi + status ENUM(active, revoked) + UNIQUE(licence_id, machine_id) -- 1 machine ne s'active qu'une fois/licence + -- contrainte applicative : COUNT(active) ≤ licences.postes_max + +activations -- journal d'audit (immuable, append-only) + id UUID PK + poste_id UUID FK→postes.id NULL + licence_id UUID FK→licences.id + machine_id TEXT + event ENUM(activate, check, refuse_quota, refuse_revoked, revoke, renew) + ip INET NULL + user_agent TEXT NULL + detail JSONB NULL + created_at TIMESTAMPTZ +``` + +**Règle "1 licence = 1 poste" (D-14)** : implémentée par `postes_max` (défaut 1) + +contrainte applicative dans `services/licensing.py` : refus d'activation si +`COUNT(postes WHERE status=active) >= postes_max`. La table reste générique (permet +un futur multi-postes) sans casser le modèle MVP. + +**Révocation au prochain check (D-14)** : `postes.status = revoked` → le `/check` +suivant renvoie `revoked`, l'EXE supprime son cache et repasse non-licencié. Pas de +push, pas de connexion permanente requise. + +### 1.3 Endpoints FastAPI + +**API client (appelés par l'EXE) — `routers/api_client.py`** + +| Méthode | Route | Auth | Rôle | +|---|---|---|---| +| POST | `/api/v1/activate` | token client (clé licence + email/mdp ou jeton d'activation) | Lie `machine_id` à la licence, renvoie la **licence signée** (§4) | +| POST | `/api/v1/check` | machine_id + ref licence | Phone home : renvoie statut (active/expired/grace/revoked) + éventuelle licence re-signée (renouvellement) | +| GET | `/api/v1/download/{version}` | session client | Téléchargement de l'EXE (remplace OwnCloud) | +| GET | `/api/v1/version` | public | Dernière version dispo (pour notif maj) | + +**Pages HTMX (humain) — `routers/pages.py`** + +| Route | Page | +|---|---| +| `GET /` `GET /login` | Connexion (fastapi-users, cookie) | +| `GET /licences` | « Mes licences » : liste, expiration, postes consommés/max | +| `POST /licences/{id}/activate-token` (HTMX) | Génère un **jeton d'activation à usage unique** à coller dans l'EXE | +| `GET /licences/{id}/postes` (HTMX fragment) | Liste des postes activés, bouton « révoquer » | +| `POST /postes/{id}/revoke` (HTMX) | Passe le poste en `revoked` (effectif au prochain check) | +| `GET /download` | Page de téléchargement + checksum | + +**API admin (Dom, superuser) — `routers/api_admin.py`** + +| Route | Rôle | +|---|---| +| `POST /admin/clients` | Créer un client + compte | +| `POST /admin/licences` | Émettre une licence (postes_max, expires_at) | +| `POST /admin/licences/{id}/renew` | Prolonger d'un an | +| `POST /admin/licences/{id}/cancel` | Suspendre/annuler | +| `GET /admin/parc` | Vue parc : clients, licences, postes, last_seen | + +### 1.4 Pages HTMX (UX MVP, Phase 1.2) + +- **Login** (fastapi-users, cookie session) → redirige vers `/licences`. +- **Mes licences** : carte par licence (réf, statut, expiration, jauge postes + `2/3`), bouton « Activer un poste » qui ouvre un fragment HTMX affichant le + **jeton d'activation** (copier-coller dans l'EXE). +- **Postes** : tableau (label, machine_id tronqué, last_seen, statut) + révoquer. +- **Téléchargement** : dernier EXE + checksum SHA256. +- Back-office Dom (superuser) : parc global + actions admin. + +> HTMX = fragments HTML renvoyés par FastAPI, zéro SPA, déploiement simple (D-14). + +--- + +## 2. Module client `license.py` (Phase 1.1 ~12h, fichier neuf) + +### 2.1 Principe + +`license.py` est **autonome** : il ne dépend que de `cryptography` (déjà présente) et +de la lib standard. Il n'importe NI le moteur NI la GUI → testable seul, zéro conflit. +La GUI (Agent A) ne fait qu'appeler son **API publique de statut** (§7). + +### 2.2 Interface publique (contrat figé exposé à la GUI) + +```python +# --- Types --- +class LicenseState(Enum): + ACTIVE # licence valide, dans la période + GRACE # expirée mais < 15 j → mode dégradé autorisé + EXPIRED # > 15 j après expiration → bloquant (sauf bêta) + OFFLINE_STALE # pas de phone home depuis > 30 j → exige reconnexion + REVOKED # révoquée côté serveur + UNLICENSED # aucune licence (ex. bêta, ou avant activation) + INVALID # signature falsifiée / fichier corrompu / machine_id divergent + +@dataclass(frozen=True) +class LicenseStatus: + state: LicenseState + client_id: str | None + expires_at: datetime | None + days_remaining: int | None # négatif si en grace + last_check_at: datetime | None + machine_id: str + message_fr: str # texte prêt pour la bannière GUI + can_anonymize: bool # ACTIVE et GRACE → True ; sinon False (hors bêta) + +# --- API que la GUI appelle --- +def get_status(force_refresh: bool = False) -> LicenseStatus: ... + # Lit license.dat (cache), valide signature + machine_id + dates SANS réseau. + # Si dernier check > 30 j → tente un phone home ; sinon reste offline. + +def activate(license_token: str) -> LicenseStatus: ... + # Appelle POST /activate, reçoit la licence signée, la chiffre dans license.dat. + +def check_now() -> LicenseStatus: ... + # Force un phone home (POST /check) ; met à jour last_check_at + re-signature. + +def deactivate() -> None: ... + # Supprime license.dat local (libère le poste après révocation côté serveur). + +def is_beta_build() -> bool: ... + # True si BETA (pas de licence) → court-circuite tout (Phase 0 Réunion). +``` + +> **Phase 0 / bêta** : `is_beta_build()` renvoie True (flag de build), `get_status()` +> renvoie `UNLICENSED` avec `can_anonymize=True`. Aucun appel réseau, aucun blocage. +> C'est le mode livré au testeur Réunion (D-14, Phase 0). + +### 2.3 Algo de vérification RSA-PSS (offline, cœur de la sécu) + +``` +verify(license_json, signature, pubkey): + 1. Recomposer le payload canonique = json.dumps(license_obj, sort_keys=True, + separators=(',',':')) encodé UTF-8 # canonicalisation déterministe obligatoire + 2. public_key.verify( + signature, # bytes (base64-décodés) + canonical_payload, + padding.PSS(mgf=MGF1(SHA256()), salt_length=PSS.MAX_LENGTH), + SHA256()) + 3. Si InvalidSignature → state = INVALID (refus) + 4. Vérifier machine_id(payload) == machine_id(local) # anti-recopie sur autre PC + 5. Vérifier version couverte (payload.version_max ≥ version courante si présent) + 6. Calcul d'état temporel (now vs expires_at, last_check) → ACTIVE/GRACE/EXPIRED/... +``` + +Clé publique embarquée : `config/license_pubkey.pem` (PEM SubjectPublicKeyInfo, +RSA 2048). Clé privée : **jamais** dans le repo client, uniquement sur OVH +(`platform/app/crypto/private_key.pem`, monté en volume/secret CI). + +### 2.4 `machine_id` (empreinte poste, §3.2 pour le flow) + +``` +machine_id = SHA256( os_uuid || cpu_id || mac_primaire )[:32] # hex tronqué +``` + +- **Windows** : `MachineGuid` (registre `HKLM\SOFTWARE\Microsoft\Cryptography`) + + `wmic csproduct uuid` + 1ʳᵉ MAC non virtuelle. +- **Linux/Mac** (dev/tests) : `/etc/machine-id` + MAC. +- Hashé (SHA256 tronqué) → **non réversible**, pas un identifiant PII brut. +- Tolérance : on hashe des composants stables ; pas le n° de disque (changé au + reformatage). Si dérive (carte réseau changée) → `INVALID` → ré-activation + nécessaire (cas rare, géré par le support). + +### 2.5 Cache local chiffré `license.dat` + +- Contenu : la licence signée (JSON+signature) **+** métadonnées locales + (`last_check_at`, `machine_id`). +- **Windows** : chiffré via **DPAPI** (`win32crypt.CryptProtectData`, scope + `CRYPTPROTECT_LOCAL_MACHINE`) → déchiffrable seulement sur ce poste/compte. +- **Linux/Mac** : chiffrement symétrique simple (Fernet) avec clé dérivée du + `machine_id` (suffisant hors prod Windows ; D-14 dit « chiffré simple »). +- Emplacement : à côté de l'EXE en frozen (réutilise `_project_root()` du patron + `admin_mode.py`), `%LOCALAPPDATA%\Aivanov\Anonymisation\license.dat` recommandé + pour survivre aux mises à jour. +- **Anti-rollback horloge** : on stocke `last_check_at` ET on refuse un `now` < + `last_check_at` (recul d'horloge) → bascule `OFFLINE_STALE` plutôt que prolonger + frauduleusement la grace. + +### 2.6 Logique grace / offline / révocation (machine à états) + +``` +À get_status(): + charger+vérifier license.dat + si INVALID/REVOKED/absent → état correspondant (can_anonymize=False, sauf bêta) + sinon: + age_check = now - last_check_at + si age_check > 30 j → tenter check_now() + succès → repartir avec licence fraîche + échec réseau → état OFFLINE_STALE (can_anonymize=False : exige reconnexion) + calc temporel: + now <= expires_at → ACTIVE (can_anonymize=True) + expires_at < now <= expires_at+15 j → GRACE (can_anonymize=True, bannière) + now > expires_at+15 j → EXPIRED (can_anonymize=False) +``` + +- **Grace 15 j** : `can_anonymize=True` + `message_fr` = « Licence expirée — pensez à + renouveler (J-X) ». Mode dégradé = juste la bannière (D-14), le moteur ne change pas. +- **Offline 30 j** : tant que `age_check ≤ 30 j`, **aucun réseau requis** (full offline). + Au-delà, un phone home est exigé ; s'il échoue → `OFFLINE_STALE` bloquant jusqu'à + reconnexion (évite usage illimité hors-ligne). +- **Révocation** : détectée au `/check` (serveur renvoie `revoked`) → `deactivate()` + local → `REVOKED`. Pas instantané par design (D-14), effectif au prochain check. + +--- + +## 3. Format exact de la licence signée + +### 3.1 Objet JSON (payload signé) + +```json +{ + "v": 1, + "license_ref": "LIC-2026-000123", + "client_id": "5f3a...uuid", + "machine_id": "9b1c2d...32hex", + "issued_at": "2026-06-10T09:00:00Z", + "expires_at": "2027-06-10T09:00:00Z", + "version_max": "11.x", + "grace_days": 15, + "offline_max_days": 30 +} +``` + +> Canonicalisation **obligatoire** avant signature ET vérification : +> `json.dumps(payload, sort_keys=True, separators=(',',':'))` → bytes UTF-8. +> Tout écart de sérialisation invalide la signature. + +### 3.2 Enveloppe stockée / transmise + +```json +{ + "payload": { ... l'objet ci-dessus ... }, + "signature": "base64( RSA-PSS-SHA256( canonical(payload) ) )", + "alg": "RSASSA-PSS-SHA256", + "key_id": "aivanov-license-2026" +} +``` + +`key_id` permet une **rotation de clé** future (le client embarque plusieurs pubkeys +indexées par `key_id`). MVP : une seule clé. + +--- + +## 4. Flows + +### 4.1 Activation d'un poste +``` +Client se connecte sur app.aivanov.fr → /licences → « Activer un poste » + → serveur génère jeton d'activation usage unique (lié licence_id) +Client lance l'EXE → saisit le jeton → license.py.activate(token) + → POST /activate { token, machine_id, os_info } + → serveur : vérifie quota (COUNT active < postes_max), crée poste, journalise + → serveur signe la licence (clé privée) et renvoie l'enveloppe + → license.py chiffre l'enveloppe dans license.dat (DPAPI), state=ACTIVE +Refus si quota atteint → 409 refuse_quota → message GUI « postes max atteints ». +``` + +### 4.2 Expiration + grace period +``` +Au lancement, get_status() (offline) : + now <= expires_at → ACTIVE + J0..J15 après expires_at → GRACE : anonymisation OK + bannière jaune « J-X » + > J15 → EXPIRED : anonymisation bloquée, CTA « renouveler » +Renouvellement : Dom renew côté serveur → au prochain /check, licence re-signée + avec nouveau expires_at → state repasse ACTIVE automatiquement. +``` + +### 4.3 Offline 30 jours +``` +Poste sans réseau : + age_check <= 30 j → fonctionne 100 % offline (vérif locale signature+dates) + age_check > 30 j → tente /check ; si échec → OFFLINE_STALE (bloquant) + message « Connexion requise pour valider la licence ». +``` + +### 4.4 Révocation +``` +Dom (ou client) clique « révoquer » → postes.status=revoked (audit logged). +Effet : rien d'immédiat sur le poste (offline). +Au prochain /check du poste (≤ 30 j) → serveur renvoie revoked + → license.py.deactivate() supprime license.dat → state=REVOKED (bloquant). +``` + +--- + +## 5. Plan de branches et livrables + +> **Tout démarre APRÈS le GO bêta (D-16).** Branches créées depuis la branche de +> livraison figée, conformément au plan maître §6. + +### Phase 1.1 — client `license.py` (~12h) — EN PREMIER (le plus isolé) +- **Branche** : `feature/v11-5-license-client` +- **Livrables** : + - `license.py` (module neuf, API §2.2) + - `config/license_pubkey.pem` (clé publique de test d'abord, prod ensuite) + - 1 ligne ajoutée au `.spec` (`("config/license_pubkey.pem", "config")`) — ajout + isolé, ne touche aucune entrée existante + - flag de build `BETA` (dans `build_info.py`) pour `is_beta_build()` + - `tests/unit/test_license.py` (§6) +- **Isolation** : `license.py` n'importe ni le moteur ni la GUI → **zéro conflit**. + Mergeable indépendamment (plan maître §6.2). + +### Phase 1.2 — plateforme `platform/` (~50h) — APRÈS, en parallèle de A/B +- **Branche** : `feature/v11-5-platform` (ou repo `platform/` dédié) +- **Livrables** : arborescence §1.1, migrations Alembic, docker-compose, + Caddyfile (`app.aivanov.fr`), workflow GitHub Actions, `tests/` serveur. +- **Isolation** : dossier `platform/` entièrement neuf → **zéro fichier applicatif + partagé** avec moteur/GUI/admin. + +### Ce qui est isolé (résumé anti-collision pour Agent D) +| Zone Agent C | Conflit possible ? | +|---|---| +| `license.py` (neuf) | Non | +| `platform/` (neuf) | Non | +| `config/license_pubkey.pem` (neuf) | Non | +| `.spec` (+1 entrée datas) | Quasi nul (ajout en fin de liste) | +| `build_info.py` (+1 flag BETA) | Faible (1 constante) — à coordonner avec D | +| Point d'appel GUI | **Contrat §7** — A réserve l'emplacement, C fournit l'API | + +--- + +## 6. Tests attendus + +### Client `license.py` (`tests/unit/test_license.py`) — pas de mock réseau pour la crypto +| Test | Attendu | +|---|---| +| Signature valide | licence signée avec la clé privée de test → `ACTIVE` | +| Signature **falsifiée** (1 octet modifié dans payload OU signature) | `INVALID`, `can_anonymize=False` | +| `machine_id` divergent (licence d'un autre poste) | `INVALID` | +| Expiration : now < expires | `ACTIVE` | +| Grace : expires < now ≤ +15 j | `GRACE`, `can_anonymize=True`, `days_remaining` négatif | +| Au-delà grace : now > +15 j | `EXPIRED`, `can_anonymize=False` | +| Offline ≤ 30 j (pas de réseau) | reste `ACTIVE`/`GRACE` sans appel réseau | +| Offline > 30 j + check échoue | `OFFLINE_STALE`, bloquant | +| Révocation (check renvoie revoked) | `REVOKED`, `license.dat` supprimé | +| Recul d'horloge (now < last_check) | pas de prolongation frauduleuse → `OFFLINE_STALE` | +| Cache corrompu | `INVALID` sans crash | +| Mode bêta (`is_beta_build()`) | `UNLICENSED` + `can_anonymize=True`, zéro réseau | + +> Fixtures : on génère une **paire RSA de test** dans la fixture (jamais la clé prod), +> on signe des payloads à la volée → tests déterministes, hermétiques, sans serveur. + +### Serveur `platform/tests/` +| Test | Attendu | +|---|---| +| Activation poste | crée `postes`, renvoie licence signée vérifiable par la pubkey | +| Quota 1 licence = 1 poste | 2ᵉ activation sur `postes_max=1` → `409 refuse_quota` | +| Réactivation même machine_id | idempotent (pas de doublon) | +| `/check` poste révoqué | renvoie `revoked` | +| Renew | `expires_at` prolongé → licence re-signée | +| Auth | endpoints admin refusés aux non-superusers | +| Audit | chaque événement écrit une ligne `activations` | + +--- + +## 7. Zone de contact avec Agent A (GUI v6) + +L'Agent A réserve un emplacement UI (bannière d'état licence). **C fournit l'API**, +A ne fait que l'afficher. Contrat figé : + +```python +from license import get_status, LicenseState + +st = get_status() # jamais bloquant, pas de réseau sauf si >30 j +banner_text = st.message_fr # ex. « Licence active — expire le 10/06/2027 » +banner_level = { # pour la couleur de bannière + LicenseState.ACTIVE: "ok", # vert/neutre + LicenseState.GRACE: "warning", # jaune « renouveler J-X » + LicenseState.EXPIRED: "error", # rouge bloquant + LicenseState.OFFLINE_STALE: "warning", # « connexion requise » + LicenseState.REVOKED: "error", + LicenseState.UNLICENSED: "info", # bêta : info discrète ou rien + LicenseState.INVALID: "error", +}[st.state] +allow_run = st.can_anonymize # la GUI grise « Anonymiser » si False +``` + +- La GUI **n'appelle jamais** `/activate` ou `/check` directement : tout passe par + `license.py` (`activate(token)`, `check_now()`). +- L'écran d'activation (saisie du jeton) appelle `license.activate(token)` et affiche + le `LicenseStatus` retourné. +- En bêta, `get_status()` renvoie `UNLICENSED` + `can_anonymize=True` → A peut masquer + totalement la bannière (rien à afficher). + +--- + +## 8. Risques & points à valider par Dom + +| Point | Reco | +|---|---| +| Stabilité `machine_id` (carte réseau changée → ré-activation) | Hasher des composants stables (MachineGuid + UUID carte mère), pas le disque. Acceptable : support gère les rares dérives. | +| DPAPI `LOCAL_MACHINE` vs `CURRENT_USER` | `LOCAL_MACHINE` = tous les comptes du poste partagent la licence (cohérent « 1 poste »). À confirmer côté hôpital (sessions partagées). | +| `pywin32` (DPAPI) pas encore listé côté EXE | Ajout dépendance Windows uniquement, en Phase 1.1. Hors périmètre `numpy<2.0`. | +| Rotation de clé future | `key_id` prévu dans l'enveloppe (§3.2) → non bloquant. | +| Bêta sans licence | `is_beta_build()` court-circuite tout (D-14 Phase 0 respecté). | + +--- + +## Résumé exécutif + +Conception complète de la brique licence D-14, **respectant le cadre validé** sans le +réinventer. Côté serveur (`platform/`, Phase 1.2) : FastAPI + PostgreSQL (5 tables +clients/licences/postes/activations + users fastapi-users), endpoints client +(`/activate`, `/check`, `/download`) + pages HTMX (« Mes licences », activation, +révocation) + back-office Dom. Côté client (`license.py`, Phase 1.1) : module **neuf et +isolé** (zéro import moteur/GUI), API publique figée (`get_status`, `activate`, +`check_now`, `deactivate`), vérif **RSA-PSS 2048/SHA256 offline**, `machine_id` hashé, +cache `license.dat` **DPAPI**, machine à états grace 15 j / offline 30 j / révocation au +check. Format licence = JSON canonique signé + enveloppe base64 avec `key_id`. `cryptography` +**déjà installée** (aucune dépendance lourde nouvelle). Plan de branches : 1.1 client (mergeable +seul, le plus isolé) puis 1.2 plateforme. Tests crypto hermétiques (paire RSA de test) + +tests serveur (quota, révocation, renew, audit). Contrat GUI fourni à l'Agent A +(bannière via `message_fr` + `can_anonymize`). **Aucun code de prod écrit, aucun déploiement.** diff --git a/docs/coordination/inbox/for-dom/2026-06-05_claude_planD_integration.md b/docs/coordination/inbox/for-dom/2026-06-05_claude_planD_integration.md new file mode 100644 index 0000000..b625adb --- /dev/null +++ b/docs/coordination/inbox/for-dom/2026-06-05_claude_planD_integration.md @@ -0,0 +1,202 @@ +--- +from: claude (Agent D — intégration) +to: dom +date: 2026-06-05T18:40:00+02:00 +topic: planD-integration-v11-5 +status: open +priority: high +references: + - plan: docs/coordination/inbox/for-dom/2026-06-05_claude_plan-v11-5-parallele.md + - decision: docs/coordination/decisions/2026-06-05_dom_d17-v11-5-chantiers-paralleles.md + - decision: docs/coordination/decisions/2026-06-05_dom_d16-test-windows-avant-diffusion.md + - decision: docs/coordination/decisions/2026-06-02_dom_d14-plateforme-licence-architecture.md + - decision: docs/coordination/decisions/2026-06-02_dom_d13-partial-scope.md +--- + +# Plan D — Intégration / merge v11.5 (anti-collision 3 agents) + +**Planification uniquement — lecture seule.** Aucun code modifié, aucun commit. +Ce document définit comment les chantiers A (GUI v6), B (D-13 complet) et +C (licence) s'intègrent sans se marcher dessus. + +État vérifié au moment de la rédaction (HEAD `57aa0f0`, branche `feature/q1-quarantine-mvp`) : +- Suite `tests/unit` : **98 tests collectés** (baseline confirmée). +- `admin_mode.py` (2.4 ko) et `config_defaults.py` (5.8 ko) **existent déjà** → B étend, ne crée pas. +- `license.py`, `gui_v6/`, `platform/` : **n'existent pas encore** → créations propres (A, C). +- `Pseudonymisation_Gui_V5.py` (119 ko) : fichier de livraison bêta → **gelé**, sert de référence à A. +- `backup/windows-wip-2026-06-05` : **déjà poussé sur Gitea** (section 0 du plan maître close). + +--- + +## 1. Frontières de fichiers (qui crée / modifie quoi) + +### Agent A — GUI v6 (zone PROPRE) +| Fichier / dossier | Action | Note | +|---|---|---| +| `Pseudonymisation_Gui_V6.py` | **CRÉE** (neuf) | réécriture propre, pas de merge brut du WIP | +| `gui_v6/` (nouveau package) | **CRÉE** | onglets, widgets, thèmes, assets | +| `gui_v6/assets/`, thèmes | **CRÉE** | maquette v6 validée 2026-05-06 | +| `Pseudonymisation_Gui_V5.py` | **NE TOUCHE PAS** | reste le point d'entrée bêta jusqu'à bascule finale | + +A consomme le moteur via l'API interne stable (mêmes signatures qu'en v5). +A **réserve deux emplacements UI** : (a) section « Paramètres avancés / Profils +techniques » pour B, (b) bannière état licence (statut/expiration) pour C. + +### Agent B — D-13 complet (zone PROPRE + zone partagée avec A) +| Fichier / dossier | Action | Note | +|---|---|---| +| `admin_mode.py` | **ÉTEND** (existe déjà) | ajoute la matrice de réglages protégés, garde `is_admin()`/`admin_required()` | +| `config_defaults.py` | **ÉTEND** (existe déjà) | flags admin/non-admin par réglage | +| `gui_v6/` sections « avancé » | **CO-ÉCRIT avec A** | B = règles d'accès, A = écrans | +| moteur de détection (`anonymizer_core_*`) | **NE TOUCHE PAS** | garde-fou n°1 | + +### Agent C — Licence (zone PROPRE, la plus isolée) +| Fichier / dossier | Action | Note | +|---|---|---| +| `license.py` | **CRÉE** (neuf) | client : vérif RSA-PSS, expiration, grace 15 j, offline 30 j, révocation | +| `platform/` (serveur) | **CRÉE** (neuf) | activation poste, 1 licence = 1 machine_id (D-14 phase 1.2) | +| clé **publique** embarquée | **CRÉE** | clé privée RSA **jamais** dans le repo client (serveur OVH uniquement) | +| GUI, core | **NE TOUCHE PAS** | C n'expose qu'une API statut/expiration consommée par A | + +### Agent D — Intégration (ce document) +| Zone | Action | +|---|---| +| `docs/coordination/` | docs de merge, ce plan | +| `tests/` (structure, CI) | organisation des nouveaux dossiers de tests par chantier | +| code applicatif | **NE TOUCHE PAS** | + +### Fichiers PARTAGÉS à risque (à surveiller en priorité) +1. **`gui_v6/` sections « avancé »** — seule vraie co-édition (A↔B). Mitigation : + contrat écrit A↔B avant tout code ; B livre une **interface de règles** + (`admin_mode.get_field_policy(field) -> visible|disabled|hidden`) que A appelle, + plutôt que B éditant les écrans directement. +2. **`Pseudonymisation_Gui_V6.py`** — propriété exclusive A. B et C n'y écrivent pas ; + ils exposent des fonctions, A les branche. +3. **`requirements.txt` / `.spec` PyInstaller** — touchés par C (dépendances RSA : + `cryptography`) et par le build final. Mitigation : un **seul** agent (D, au merge) + consolide `requirements.txt` et le `.spec` ; A/B/C déposent leurs deltas de deps en doc. +4. **`config_defaults.py`** — B l'étend. A le lit seulement. Pas d'écriture concurrente. + +--- + +## 2. Dépendances entre agents + +``` +C (licence) ──API statut/expiration──► A (GUI v6, bannière licence) +B (D-13) ──API get_field_policy────► A (GUI v6, écrans avancés) +A (GUI v6) ──écrans + emplacements───► B se greffe dessus +``` + +- **B dépend de A** pour les écrans : B ne peut finaliser ses sections « avancé » + qu'une fois la structure d'onglets v6 posée par A. B peut **démarrer en parallèle** + sur la logique pure (matrice de règles dans `admin_mode.py` + tests headless), + puis brancher l'UI quand A a livré. +- **A dépend de C** pour l'API licence (statut/expiration). A peut avancer avec un + **stub** d'interface licence (contrat figé) tant que C n'a pas fini, puis brancher + le vrai `license.py`. +- **C ne dépend de personne** → chantier le plus isolé, mergeable en premier. +- **D dépend de tous** (validation finale). + +Règle d'or : chaque dépendance passe par une **interface contractualisée** (signature +de fonction figée tôt), pas par un partage de fichier. Cela permet le parallélisme. + +--- + +## 3. Ordre de merge recommandé (et justification) + +Confirme la proposition §6 du plan maître : + +1. **Base** : après **GO bêta** (D-16), figer la branche de livraison + (`feature/q1-quarantine-mvp` à `15f73f8` ou le hotfix éventuel), puis créer + `feature/v11-5` **depuis cette base figée**. +2. **C (licence) en premier** — *le plus isolé* : `license.py` + `platform/` neufs, + zéro conflit moteur/GUI. Mergeable seul, testable seul. Réduit le risque tôt. +3. **A (GUI v6) ensuite** — gros morceau, fichier neuf `Pseudonymisation_Gui_V6.py`. + Branche la bannière licence sur l'API de C (déjà mergée). Pas de conflit avec C + (surfaces disjointes). +4. **B (D-13) en dernier** — se *greffe sur A* (sections avancées de la GUI v6). + Merge après A pour que les écrans existent. La logique `admin_mode.py` étant déjà + prête et testée headless, le merge B = branchement UI + tests matrice. +5. **Validation D** — qualité + tests + build, puis bascule de v6 par défaut + (changement du point d'entrée v5→v6) en **dernier commit**, isolé et réversible. + +Justification : on merge du moins couplé au plus couplé. C isolé d'abord retire le +risque cryptographique tôt ; A pose le squelette UI dont B a besoin ; B greffé en +dernier minimise la fenêtre de co-édition. La bascule v5→v6 est le tout dernier pas, +trivialement réversible (revert d'un seul commit). + +--- + +## 4. Critères d'acceptation v11.5 (gate de merge) + +Aucun merge dans `feature/v11-5` n'est accepté sans : + +| Critère | Cible | Vérification | +|---|---|---| +| Non-régression moteur | `tests/unit` **98 passed** (inchangé) | `pytest tests/unit -q` | +| Leak score | **100/100** inchangé | `tests/unit/test_leak_scanner.py` + audit_30 | +| Audit qualité | `evaluate_quality.py` **≥ 98.5** (baseline) | `scripts/evaluate_quality.py` | +| Build EXE | **reproductible** (PyInstaller --onefile, config externe) | build Windows 192.168.1.11 | +| GUI v6 (A) | smoke lancement + workflow principal OK ; contrat moteur identique v5 | tests `gui_batch_paths`, `manual_masking` conservés | +| D-13 (B) | chaque réglage protégé caché/désactivé en non-admin ; `admin_required` lève ; sauvegarde config sensible bloquée non-admin | nouveaux tests matrice admin | +| Licence (C) | signature RSA-PSS (valide/falsifiée), expiration, grace 15 j, offline 30 j, révocation ; serveur : 1 licence = 1 machine_id | nouveaux tests `license.py` + serveur | + +**Garde-fou non négociable** : les 98 tests verts + leak 100/100 sont le filet. +Le moteur de détection ne bouge pas → tout chantier qui ferait baisser ces deux +chiffres est rejeté, point. + +--- + +## 5. Stratégie de branches + +``` +feature/q1-quarantine-mvp (livraison bêta — GELÉE jusqu'au GO Dom) + │ ◄── hotfix MVP éventuel possible ICI uniquement (D-16) + │ + [GO BÊTA] → tag de livraison figé (ex: beta-v11) + │ + └──► feature/v11-5 (créée APRÈS GO, depuis la base figée) + ├── feat/v11-5-licence (C) → merge 1 + ├── feat/v11-5-gui-v6 (A) → merge 2 + └── feat/v11-5-d13 (B) → merge 3 (greffé sur A) +``` + +Règles : +- **Aucune branche v11.5 créée avant le GO bêta** (gel D-16/D-17). +- `feature/v11-5` part de la **base figée** (tag de livraison), pas de `main` ni + d'une branche en mouvement. +- Sous-branches par chantier, merge dans `feature/v11-5` dans l'ordre §3. +- Hotfix MVP, s'il survient pendant la bêta, reste sur `feature/q1-quarantine-mvp` + et sera **rebasé/cherry-pické** dans la base figée avant création de `feature/v11-5` + (ne jamais mélanger hotfix et refonte). +- Tag de sécurité conservé sur `backup/windows-wip-2026-06-05` (anti-gc). + +--- + +## 6. Risques principaux + mitigations + +| Risque | Impact | Mitigation | +|---|---|---| +| GUI v6 casse le moteur | leak/qualité régressent | Contrat moteur strict (mêmes I/O que v5) + 98 tests verts obligatoires au merge | +| Co-édition A/B sur écrans avancés | conflits Git, double logique | Contrat écrit A↔B AVANT code ; B expose `get_field_policy()`, A consomme — pas de co-édition de fichier | +| Mélange hotfix MVP / v11.5 | divergence, régression bêta | Gel respecté ; v11.5 sur branche dédiée créée APRÈS GO ; hotfix cherry-pické proprement | +| Clé privée RSA fuit | licences forgeables | Clé privée **serveur OVH uniquement** (D-14) ; client n'embarque que la clé publique | +| `requirements.txt` / `.spec` édités par 3 agents | build cassé, conflits | Consolidation par **un seul** agent (D) au merge ; deltas de deps livrés en doc | +| WIP GUI v6 sur disque unique | perte de la base A | Déjà mitigé : backup poussé sur Gitea + tag anti-gc | +| Plateforme licence = ~50h | dérapage planning | Phasage D-14 : 1.1 client (~12h) avant 1.2 serveur (~50h) ; C livrable client d'abord | +| Bascule v5→v6 par défaut | régression point d'entrée | Bascule = dernier commit isolé, revert trivial | + +--- + +## 7. Rappel garde-fous + +- **GEL BÊTA** : rien ne démarre en *code* avant le GO de Dom (D-16/D-17). Seuls + plans, inventaires, contrats d'interface et docs sont produits maintenant. +- **Le moteur de détection ne bouge pas.** v11.5 = refonte UI + ajouts périphériques + (licence, admin). Le leak score 100/100 et les 98 tests sont intouchables. +- **Interfaces contractualisées** entre A/B/C : tout passe par des signatures de + fonctions figées tôt, jamais par du partage de fichier — c'est ce qui rend le + parallélisme sûr. +- **Un seul agent (D) consolide** `requirements.txt` et le `.spec` au merge. + +— Claude (Agent D) diff --git a/docs/coordination/log.md b/docs/coordination/log.md index 03b8618..93d18a1 100644 --- a/docs/coordination/log.md +++ b/docs/coordination/log.md @@ -61,5 +61,8 @@ Format : `YYYY-MM-DD HH:MM | AUTEUR | ACTION | RÉSUMÉ` 2026-06-05 12:35 | claude | EN ATTENTE Dom | Pack prêt sur 192.168.1.11:release\. Pas d'upload OwnCloud sans accord (action sortante). WIP Windows sauvegardé backup/windows-wip-2026-06-05. 2026-06-05 14:45 | dom | DECISION D-16 | Aucun upload OwnCloud maintenant. Dom teste l'application sous Windows. Inno Setup à installer puis repackager avec installateur seulement après tests et GO explicite Dom. 2026-06-05 17:55 | dom | DECISION D-17 | Post-bêta v11.5 à préparer en parallèle : GUI v6, D-13 complet, plateforme licence. Claude doit orchestrer des agents séparés, sans perturber le pack bêta v11 avant GO Windows. +2026-06-05 18:05 | dom | ECRIT for-qwen | Qwen reçoit rôle revue transverse v11.5 en lecture seule : T-P découpage/conflits, T-Q matrice d'acceptation, T-R registre risques. Aucun code ni packaging avant GO bêta Windows. 2026-06-05 17:55 | dom | DECISION D-17 | Cap v11.5 : préparer en parallèle GUI v6 + D-13 complet + plateforme licence APRÈS GO bêta. Gel bêta : plans/inventaires/découpage seulement tant que tests Windows Dom pas finis. Demande plan parallèle à Claude. 2026-06-05 18:10 | claude | DEPOSE plan v11.5 | for-dom/2026-06-05_claude_plan-v11-5-parallele.md : 4 agents (A GUI v6 / B D-13 / C licence / D intégration), frontières fichiers, ordre merge, tests. ALERTE : WIP backup GUI v6 (b8c9c41) uniquement sur disque Windows → recommande push Gitea urgent. Gel bêta respecté (planif pure). +2026-06-05 18:14 | claude | INSTALLATEUR v11 OK | Inno Setup déjà présent (user). build_windows_installer_only.ps1 → release\Anonymisation-Setup.exe 695.8 Mo, EXIT_CODE=0, non signé. SHA256=12D9D6A8...663DC. Manifeste SHA256SUMS.txt MAJ (EXE+ZIP+Setup). Rapport → for-dom/2026-06-05_claude_package-beta-installateur-maj.md. OwnCloud toujours en attente du canal (D-16). +2026-06-05 18:18 | claude | 4 AGENTS v11.5 LIVRÉS | Sous-plans A(GUI v6)/B(D-13)/C(licence)/D(intégration) déposés for-dom. CORRECTION Agent A : le WIP backup b8c9c41 n'est PAS une GUI v6 mais l'ancêtre tkinter de v5.5 (v5.5 en avance de 24 lignes). Vraie base v6 = mockup HTML + logique v5.5. customtkinter à ajouter aux requirements. Qwen a livré en // T-P/T-Q/T-R (revue transverse, via Codex) — recoupe Agent D.