docs(coordination): installateur bêta v11 + 4 sous-plans agents v11.5

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 18:15:59 +02:00
parent 57aa0f0154
commit 7f03acb8fb
6 changed files with 1429 additions and 0 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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 R1R17 / S1S10, 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é »
(S1S5, 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)

View File

@@ -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.**

View File

@@ -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)

View File

@@ -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 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 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 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 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: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.