# Plan de chantier — Unification Léa + VWB (préparé le 2026-06-17 au soir) > Préparé par analyse multi-agents (3 agents read-only) + graphify, après le check UI post-reboot DGX. > **Rien n'a été modifié.** Document de cadrage pour la reprise. > Méthode imposée : chirurgie itérative supervisée (1 modif = 1 test = validation Dom). Pas de batch. ## 0. Diagnostic corrigé (important — deux hypothèses du soir invalidées) Le check UI a fait remonter 3 symptômes : **T3** (Léa « ne trouve pas » le bloc-notes), **T5/anchors** (images d'ancres absentes au VWB), et le sujet de fond **fonds commun**. L'analyse de code rectifie le diagnostic « à chaud » : | Hypothèse du soir | Verdict après analyse code | |---|---| | T3 = silo machine_id (Léa-VM ne voit pas le savoir du .11) | **FAUX au niveau sélection.** Le `SemanticMatcher` ne filtre par aucune machine ; Léa tourne sur le DGX qui héberge les dossiers des deux postes → elle *voit* déjà tous les workflows. | | T3 = filtre `is_production_ready` | **FAUX.** Aucun composant runtime (matcher, exécuteur, chat) ne lit `is_production_ready`/`learning_state`. | | **Vraie cause T3** | **Fragmentation de l'apprentissage.** 100 répétitions → ~20 workflows distincts nommés d'après les apps vues (« Bloc-notes, Explorateur et Terminal (2)(3)… »), aucun nommé franchement « ouvrir le bloc-notes » → le matching sémantique se dilue sur 20 quasi-doublons. (Le silo existe, mais au niveau *renforcement cross-machine*, pas sélection.) | --- ## Chantier A — Consolidation de l'apprentissage (débloque T3) — PRIORITÉ HAUTE ### Cause racine Chaque session de streaming **crée** un workflow neuf, **sans jamais chercher ni fusionner** un existant. - Nommage + suffixe `(2)(3)` : `agent_v0/server_v1/stream_processor.py:4335-4344` (`_generate_workflow_name`) — collision de nom → variante numérotée au lieu de renforcement. - Persistance directe sans dédup : `stream_processor.py:4417-4445` (`_persist_workflow`), build `:2966-3112` (`_build_workflow_from_session`). - 1 observation / node : build **toujours séquentiel** (`graph_builder.py:345` `clusters={i:[i]}`), donc `observation_count = 1` (`graph_builder.py:909`). DBSCAN d'agrégation volontairement désactivé. - **Aucune fonction de merge/dédup de workflows** dans tout `core/`. Le `VariantManager` (`core/variants/variant_manager.py:266`, seul code qui fait `observation_count += 1`) **n'est jamais appelé**. - `is_production_ready` calculé dans `core/training/quality_validator.py:114,238-246` (seuil `min_observations_per_node=3`) — toujours False car 1 obs/node, **mais sans effet runtime** (label informatif uniquement). ### Leviers (effort / risque / impact) - **A. Découpler « exécutable » de « production_ready »** — faible / faible / moyen — quick-win sémantique. - **B. Fusion/dédup create-or-update à la persistance** — élevé / moyen / **fort** — cause racine. - **C. Rebrancher l'agrégation des observations** (`VariantManager` ou `_run_cross_session_learning` qui réécrit `observation_count`) — moyen / moyen / fort. - **D. Seuil `min_observations` configurable/contextuel** — faible / moyen / faible — cosmétique seul. - **E. `workflow_id` = signature stable de la trajectoire** (hash de la séquence d'actions) au lieu des apps vues — moyen / moyen / fort — supprime le `(2)(3)` à la racine, rend B trivial. **Reco** : **E + B** (signature stable + create-or-update avec agrégation d'observations), D en complément, A en quick-win si on veut juste « rendre exécutable » vite. ### Composants `stream_processor.py` (L2966-3112, L4335-4344, L4417-4445, cross-session L3149-3268, list L4518) · `graph_builder.py` (L345, L909, L384-456) · `quality_validator.py` (L114, L238) · `semantic_matcher.py` (sélection) · `variant_manager.py` (L266, à rebrancher) · `replay_learner.py:358` (consolidate = hints seulement, leurre). --- ## Chantier B — Affichage anchors VWB (débloque T5) — FIX PRÉCIS, FAIBLE RISQUE ### Cause racine Le commit `b8b963059` n'a corrigé **que la moitié** : l'import lit `target.context_hints.anchor_image_base64` **uniquement pour les actions simples**. - `services/learned_workflow_bridge.py` : branche action simple `else` L226-233 (lit le base64, ajouté par le commit) ; **les actions *compound*** (majoritaires dans les workflows Léa) passent par `_convert_compound_substep` L279 qui **ne lit jamais le base64** → substeps `anchor_id=NULL` → frontend affiche « Ancre requise » sans image (`frontend_v4/.../StepNode.tsx:113-119`). - Aggravant état DGX : les workflows en base datent d'avril, **100% des steps `anchor_id=NULL`** → ré-import nécessaire après fix. ### Chaîne (pour mémoire) import `api_v3/learned_workflows.py:249` → convert `learned_workflow_bridge.py:72` → `save_anchor_image` → table `visual_anchors` (`db/models.py:163`) → API `GET /api/v3/anchor//thumbnail` (`api_v3/capture.py:356`) → React `StepNode.tsx` (`api.ts:136`). Pas de mismatch URL/chemin. ### Fix Propager `anchor_image_base64` aux substeps compound (passer `target` dans la boucle compound L169-187 / `_convert_compound_substep`, poser l'ancre sur le 1er substep cliquable — éviter de dupliquer sur N substeps). Risque faible/additif. **Puis ré-importer** les workflows cibles. --- ## Chantier C — Fonds commun / mutualisation cross-poste — STRATÉGIQUE, DÉCISION PRODUIT D'ABORD ### État - Fédération `core/federation/` **entièrement débranchée au runtime** : `GlobalFAISSIndex.search()` (`faiss_global.py:199`) **jamais appelé** ; endpoints export/import (`api_stream.py:6277-6372`) sans aucun déclencheur (ni cron, ni frontend). - **Anonymisation = le maillon le plus mûr et prêt** : `learning_pack.py` exclut machine_id/hostname/patient/nip/ipp (`_clean_metadata` L410, `_SENSITIVE_METADATA_KEYS` L61-66), hash SHA-256 (L388), export = embeddings 512d + signatures (pas de pixels/OCR brut). - Silo réel = `stream_processor._run_cross_session_learning` L3197/L3284 (`workflow_machine != machine_id → continue`) : bloque le renforcement cross-machine. - Identité : `machine_id` workflows = `hostname_os` (ex `DESKTOP-58D5CAC_windows`, `agent_v1/config.py:34-37`) ≠ token enrôlement `cbd8f9f0…` (`agent_registry.py:107-111`, sécurité parc). À ne pas confondre. ### Options - **Intra-clinique brut** (lever filtre cross-session L3197/L3284, charger toutes machines) — simple, **non anonymisé** → acceptable seulement intra-site. - **Cross-clinique anonymisé** (brancher `search()` global + déclencheur export/import + index global peuplé) — la vraie « fédération », effort moyen-élevé, **seul canal sûr PII**. --- ## Décisions produit à trancher AVANT de coder (pour Dom) 1. **Quel point d'entrée Léa au runtime ?** Deux providers concurrents : `agent_chat/app.py:678,906` (port 5004, SemanticMatcher sur `data/training/workflows/`) vs chat serveur `api_stream.py:6623` (`_list_available_workflows` qui liste des **sessions live**, pas les workflows entraînés). Les logs DGX du soir montrent le 5004 + SemanticMatcher → **probablement 5004**, à confirmer formellement avant de coder A. 2. **Critère de « même parcours »** pour la fusion (levier E/B) : signature de trajectoire ? nom de base ? (workflows de 7 à 89 nodes pour « le même » parcours → alignement non trivial). 3. **Source de vérité workflows** (DETTE-015) : DB VWB SQLite (5002) vs JSON `data/training/workflows/` — la route `/api/workflows` de Léa fusionne les deux avec une dédup fragile. Trancher la source canonique. 4. **Niveau de mutualisation** : intra-clinique brut (rapide, non anonymisé) vs fonds commun cross-clinique anonymisé (fédération). Implications réglementaires opposées. 5. **Re-exécutabilité des packs fédérés** : l'export n'emporte que des squelettes anonymisés (pas les templates/coordonnées) → un chemin de re-résolution visuelle est nécessaire (cohérent avec le contrat 100% vision). --- ## Ordre recommandé pour la reprise 1. **Confirmer le provider runtime de Léa** (Q1) — 10 min, read-only, débloque tout le reste. 2. **Chantier B (anchors)** — fix ciblé compound + ré-import. Faible risque, gain visuel immédiat (T5). 3. **Chantier A (consolidation)** — E + B, chirurgie itérative. Débloque T3 (le bloc-notes « connu »). 4. **Chantier C (fonds commun)** — décision produit (Q4) puis implémentation anonymisée. Le plus stratégique pour la proposition de valeur, mais le moins urgent pour une démo. ## Garde-fous - Tout changement au build/persist (Chantier A) touche le chemin qui alimente la démo Urgence → chirurgie itérative, 1 test par modif. - Champ de mines `core/` : vérifier le wiring runtime réel avant de rebrancher (`VariantManager`), pas seulement la présence du code. - Mutualisation cross-site = uniquement via `LearningPack` anonymisé, jamais recopie de JSON bruts (PII : OCR, titres fenêtres patients).