# Journal de bord — SIOP V2 Trace chronologique des sessions (la plus récente en premier). Le **playbook** (`docs/0X-*`) est le livre ; ici, c'est le quotidien. --- ## 2026-07-17 — Pr. Daaif (+ Claude) — Arbitrages de recette R5 rendus : le corpus passe en tableau **Actions** - **Arbitrage du référent** sur la revue pixel : « Ça serait mieux d'utiliser un tableau car la plupart des fichiers vont être des PDF » — le reste validé (« Appliquer » à écriture directe, calibrage au runbook). - **Bibliothèque en tableau**, conforme à l'écran 5 de maquette-r5 : colonnes Document (nom + type · taille · date · auteur), Rattaché à, Indexation, Corpus (interrupteur, visible pour ASSETS.edit), actions Ouvrir/Télécharger/Supprimer. Les vignettes restent aux cartes « Documents » des fiches (photos d'OT). - Détail aligné sur la maquette : une image non indexable affiche son interrupteur **éteint** quel que soit l'état stocké — l'interrupteur montre la réalité du corpus, pas une colonne de base. - e2e adapté (lignes de tableau), **16/16 Playwright rejoués** ; artefact de revue pixel mis à jour (même URL), arbitrages marqués rendus. **Décisions** - Revue pixel R5 **validée par le référent** (1 correction appliquée). Le tag `release/r5` peut être posé. **Prochaine étape** : tag `release/r5` sur le mot du référent. Restes : recette R4 sur téléphone (Expo Go), redéploiement Dokploy (`release/r3` puis r5 avec `AI_SERVICE_TOKEN`), calibrage `AI_SEUIL_*` sur corpus SPELEV. --- ## 2026-07-17 — Pr. Daaif (+ Claude) — Recette R5 (sans clé API) + durcissement production `siop2-ai` **Actions** - **Recette rejouée au vrai modèle, mode extractif (aucune clé API)** sur un corpus mis en scène (notice Otis Gen2 générée, 2 pages). Elle a **invalidé le modèle d'embeddings choisi en R5.1** : sur la question type du plan (« quel couple de serrage pour les guides du Gen2 ? »), MiniLM-384 classait la page-réponse DERRIÈRE des passages sans rapport (0,24 contre 0,41). Banc comparatif mesuré : `mpnet-base-v2` multilingue (768 d) rétablit le classement et une marge signal/bruit exploitable (pertinent ≥ 0,46, hors-corpus ≤ 0,42) ; `multilingual-e5-large` (1 024 d, 2,2 Go) classait bien aussi mais scores compressés (0,73-0,90) et poids rédhibitoires. **Bascule vers mpnet** (ADR-004 amendé, migration `r5_embeddings_mpnet` : pgvector 384 → 768, index vidé — re-dérivable par « Réindexer tout ») + **découpage affiné** (~350 caractères : la phrase-réponse ne se noie plus, l'extrait cité est lisible) + seuils par défaut recalés (0,45 / 0,40 / 0,55). - **Recette validée après bascule** : réponse sourcée p. 2 en tête ✓, refus honnête chiffré sur question hors corpus ✓ (« routeur wifi » → refus ; charabia → refus), suggestions étagées ✓, D1-D5 tenues, le tout SANS clé. 16/16 Playwright (l'embeddeur déterministe suit les 768 dims sans recalibrage), 78 tests API, 23 pytest. - **Revue pixel** : captures réelles des 6 écrans (web ×4, liseré « suggéré », mobile) face aux écrans de maquette-r5 — artefact publié pour le référent avec 3 points d'arbitrage (« Appliquer » à écriture directe, vignettes vs tableau du corpus, refus « voisin de domaine » dépendant du calibrage client). - **Durcissement production** : `apps/ai/Dockerfile` (uv, venv non éditable, **modèle ONNX téléchargé au build** — ADR-004 §1, non-root, healthcheck) ; compose Dokploy : service `siop2-ai` interne (jamais sur `dokploy-network`, secret `AI_SERVICE_TOKEN` requis, génération opt-in par variables, seuils calibrables), `siop2-api` branché (`AI_SERVICE_URL`) ; runbook enrichi (§2 variables, §5 service IA, §6 calibrage des seuils sur corpus client, réindexation post-déploiement). **Décisions** - Le refus « voisin de domaine » (question ascenseur absente du corpus) reste dépendant du calibrage : jamais d'invention (extraits réels cités), mais pas toujours un refus. Les seuils sont des variables d'environnement pour être calibrés sur le corpus SPELEV réel — procédure au runbook §6. - Le tag `release/r5` attend la validation de la revue pixel par le référent. **Prochaine étape** : validation du référent (revue pixel + arbitrages) → tag `release/r5`. Restes : recette R4 sur téléphone (Expo Go), redéploiement Dokploy (`release/r3` puis r5), secret `DOKPLOY_WEBHOOK_URL`. --- ## 2026-07-17 — Pr. Daaif (+ Claude) — R5.3 : les écrans de l'IA (web + mobile) et le corpus administrable **Actions** - **Contrat** (76 opérations) : `Document` expose son état de corpus (`inCorpus`, `indexedAt`, `chunkCount`), `PATCH /documents/{id}/corpus` (bascule réversible, ASSETS.edit), `POST /assistant/reindex` (bilan chiffré traduit du dialecte interne). Clients web **et mobile** régénérés — le job ci-contract vérifie désormais les deux. - **Web — page Assistant** (sidebar Pilotage, WORK_ORDERS.view) : chat fidèle aux écrans 1-2 de maquette-r5 — réponse avec **extraits exacts cités** (« Ouvrir » → PDF en onglet authentifié, OT → fiche), avertissement « l'IA propose, vous validez » permanent ; **refus honnête chiffré** (« j'ai cherché dans N documents et M bilans ») avec l'action utile (téléverser la notice). - **Web — Bibliothèque = corpus** (écran 5) : bandeau 09-08 (l'anonymisation est DITE), statut par document (indexé · date · extraits / à indexer / exclu / image non indexable), interrupteur d'exclusion (PDF seulement), « Réindexer tout » avec bilan. **Fiche OT** (écran 3) : « Décrire pour suggérer » dans la carte Bilan — suggestions justifiées (« N bilans similaires », confiance), « Appliquer » = le geste humain qui écrit (cohérent avec la carte à enregistrement direct), **liseré « suggéré »** retiré dès qu'un choix manuel reprend la main. - **Mobile — clôture** (écran 4) : décrire au pouce → **chips suggérées** (un appui = un champ pré-rempli, ✓), note D1 visible ; hors-ligne le bouton dit « réseau requis » (l'IA est un service serveur — la clôture en file R4 n'en dépend pas). - **`apps/ai`** : seuils par env (`AI_SEUIL_*`) — nécessaires à la CI et au calibrage de recette. - **CI** : le job e2e démarre `siop2-ai` (embeddeur déterministe) et le parcours R5 se recette en vrai : upload d'un **PDF généré avec xref valide** → réindexation → statut → réponse sourcée (extrait « 25 Nm », p. 1) → refus sur charabia → exclusion → suggestion appliquée sur un OT créé. **16/16 Playwright** (recettes R0→R5). - **Vérifications réelles** : seuils CI **mesurés** (vrai match 0,66 vs bruit de collisions 0,11 → pertinence 0,20 ; codes pertinents 0,14-0,16 vs parasite 0,08 → suggestion 0,10) ; chaîne complète au **vrai modèle ONNX** (réindexation, ask sourcé p. 8/p. 10, suggestions étagées avec vrais comptes de bilans) ; parcours **mobile Expo web 6/6** et **web 7/7** au vrai modèle, 0 erreur console. 78 tests API (2 nouveaux : bascule corpus gardée, reindex traduit + 403). **Décisions** - Sur le web, « Appliquer » écrit immédiatement (comme tout le reste de la carte Bilan R2) : le clic EST la validation humaine — l'esprit de la maquette (« rien sans vous ») est porté par le geste, pas par un bouton « Enregistrer » séparé. À montrer en revue pixel. - Les seuils par défaut du vrai modèle (0,30/0,35/0,55) restent **à calibrer en recette sur le corpus client réel** : mesuré ce jour sur un corpus non-métier, le multilingual-MiniLM donne des scores plats (hors-sujet 0,35-0,43 vs pertinent 0,24-0,28) — c'est exactement pourquoi ils sont configurables. **Prochaine étape** : recette R5 complète (doit passer SANS clé API), durcissement production (Dockerfile `siop2-ai` + compose Dokploy), puis tag `release/r5`. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`. --- ## 2026-07-17 — Pr. Daaif (+ Claude) — R5.2 : l'assistant au contrat + suggestion de codes de bilan **Actions** - **`apps/ai`** : `/internal/ask` — recherche → **seuil de pertinence** → extraits sourcés, ou **refus honnête** portant la taille du corpus cherché (« 1 document, 3 bilans » — l'écran 2 des maquettes aura ses chiffres) ; la rédaction passe par le `Generateur` (opt-in — mode extractif par défaut). `/internal/suggest` — similarité sémantique entre la description libre et les libellés **actifs** des référentiels (contextualisés « anomalie constatée : … »), un seul code par champ, au-dessus du seuil, confiance FORTE/MOYENNE + « N bilans similaires sur ce parc » (comptés dans les chunks d'historique). Sans LLM : déterministe, explicable. 23 pytest. - **Contrat** (74 opérations) : `POST /assistant/ask` → `AssistantAnswer` (mode EXTRACTIVE/GENERATED/REFUSAL, extraits cités document/page ou bilan daté, corpus cherché) ; `POST /assistant/suggest-bilan` → codes existants seulement. Clients web/mobile régénérés. - **API NestJS** : module `assistant` — proxy vers `siop2-ai` (`AI_SERVICE_URL`/`AI_SERVICE_TOKEN` à l'env, ADR-004 §4), permissions par la matrice (`ask` = WORK_ORDERS.view ; `suggest` = WORK_ORDERS.edit — qui remplit des bilans), traduction du dialecte interne vers le contrat, **503 propre** si le service IA est éteint (jamais un 500). 6 tests e2e sur un **stub HTTP** (76 tests API, 14 suites). - **Vérifié sur la vraie chaîne** (NestJS → ai → pgvector, vrai modèle) — et elle a débusqué un bug : fastembed ne renvoie pas des vecteurs normés, la similarité des suggestions dépassait 1 (pgvector normalisait dans son opérateur, ce qui masquait l'écart). **Normalisation à l'encodage** + réindexation : bilans du parc trouvés en tête (0.41), refus honnête hors corpus, suggestions cosinus ≤ 1 à confiances étagées. **Décisions** - Les seuils (pertinence 0.30, suggestion 0.35, confiance forte 0.55) sont des constantes de départ — à calibrer en recette sur le corpus réel du client. **Prochaine étape** : R5.3 — les écrans validés (Assistant sidebar, corpus dans la Bibliothèque, suggestions dans les fiches OT web et clôture mobile). Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`. --- ## 2026-07-17 — Pr. Daaif (+ Claude) — R5.1+ : clé API de génération configurable (demande du référent) **Actions** - **`generation.py`** : l'interface `Generateur` d'ADR-004 §3 prend corps — `GenerateurExtractif` (contrat de base, aucun LLM) et `GenerateurAPI` (SDK officiel `anthropic`, dépendance **optionnelle** `--extra generation`, absente des tests/CI). Consigne système : citations [n] obligatoires depuis les extraits fournis, « n'invente RIEN », rappel de validation humaine. Tout échec (refus du modèle, quota, réseau) **retombe sur l'extractif** — jamais d'erreur utilisateur imputable au LLM ; `stop_reason == "refusal"` traité explicitement. - **Configuration complète et validée au boot** : `AI_GENERATION=off|api`, `AI_API_KEY` (exigée en mode api — le démarrage refuse sinon, message clair), `AI_MODEL` (défaut `claude-opus-4-8`). `.env.example` posé ; `/healthz` expose le **mode** (off/api), jamais la clé ; le générateur rejoint l'état de l'app (R5.2 le consommera). - 5 tests ajoutés (19 pytest au total) : défaut extractif, refus de boot api-sans-clé, mode inconnu refusé, clé/modèle lus de l'env, invite pure numérotant les extraits. Vérifié en réel : boot refusé sans clé, `GenerateurAPI` construit avec le SDK, healthz sans secret. **Décisions** - La clé vit en variable d'environnement (Dokploy secrets), pas en base ni dans l'UI — cohérent avec `JWT_SECRET` et ADR-002. **Prochaine étape** : R5.2 — assistant au contrat (proxy NestJS), le mode extractif d'abord, la rédaction branchée sur ce `Generateur`. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`. --- ## 2026-07-17 — Pr. Daaif (+ Claude) — R5.1 : socle `apps/ai` — ingestion anonymisée + recherche sémantique **Actions** - **ADR-004 actée** : embeddings **locaux sur CPU** (fastembed ONNX, `paraphrase-multilingual-MiniLM-L12-v2`, 384 dims — les textes du client ne quittent jamais le serveur), pgvector dans le Postgres existant (table `RagChunk` **possédée par Prisma**, migration `r5_ia` + état de corpus sur Document), génération **opt-in** (`AI_GENERATION=off` par défaut : mode extractif honnête, la recette doit passer sans clé API), suggestion de bilan sans LLM (similarité sémantique, explicable). Topologie : `siop2-ai` jamais exposé, joint par l'API NestJS seule (`X-Service-Token`). - **`apps/ai` posé** (FastAPI + uv, python 3.11) : config validée au démarrage, `/healthz`, `/internal/reindex` (idempotent) et `/internal/search` protégés par le jeton de service. **Pipeline** : PDF MinIO → texte par page (pypdf) → **anonymisation D4** (e-mails, téléphones marocains, noms connus de la base — insensible casse/accents, fonction pure) → découpage (~900 car., chevauchement, testé) → embeddings → `RagChunk` avec localisateur (« p. 42 », « bilan du 17/07 »). Bilans codés clôturés ingérés aussi (« sur votre parc… »). L'exclusion de corpus (D3) s'applique à l'ingestion ET à la lecture. - **14 pytest verts + ruff** (embeddeur **déterministe** en test/CI — aucun téléchargement, même interface 384 dims) ; **job CI `ai`** (uv), deploy en dépend. - **Vérifié en réel avec le vrai modèle ONNX** : réindexation du corpus seedé en 7 s (1 PDF réel → 31 extraits paginés, 3 bilans), recherche sémantique concluante (PDF trouvé par le sens, bilans du parc par « frottement des guides »), e-mails du PDF remplacés par ⟨contact⟩, **0 identité dans les chunks** (contrôle SQL sur les 9 noms seedés). **Décisions** - Convention de test R5 : pytest unitaires purs en CI (sans DB ni réseau) ; l'intégration réelle (DB + MinIO + modèle) se vérifie en local et en recette. **Prochaine étape** : R5.2 — l'assistant au contrat (proxy NestJS authentifié → `siop2-ai`, mode extractif sourcé « sourcé ou silencieux ») + suggestion de codes de bilan. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`. --- ## 2026-07-17 — Pr. Daaif (+ Claude) — R5 ouverte : maquettes IA (design d'abord) **Actions** - **`maquette-r5.html`** (docs/02-design/maquettes) : 6 écrans — **Assistant sourcé** (recette type du cadrage : « couple de serrage des guides Gen2 ? » → réponse à citations numérotées, sources document/page/**extrait exact** ouvrables), **Sans source = refus** (l'anti-hallucination visible : « je préfère ne pas inventer » + ce qui a été cherché + action utile), **Suggestion de bilan** web et mobile (description libre → codes EXISTANTS des référentiels, justification + confiance, « Appliquer » pré-remplit avec liseré « suggéré », rien d'enregistré sans le geste humain), **Corpus & ingestion** (bibliothèque R3 + bilans codés, statut par document, exclusion réversible, bandeau 09-08), **Voix** (option « si budget » : dictée opt-in, transcription relue, audio purgé immédiatement). Bi-thème, tokens répliqués, vérifiée en navigateur (6 onglets + sombre, zéro erreur console). - Artefact de validation publié (6 écrans + preuve bi-thème + **5 décisions à acter** : D1 aucune écriture automatique, D2 sourcé ou silencieux, D3 corpus fermé et visible, D4 anonymisation à l'ingestion 09-08, D5 voix opt-in avec purge). **Décisions** - **→ Levé le 17/07/2026 : maquettes R5 et les 5 décisions (D1-D5) VALIDÉES par le référent.** Lancement R5.1 (socle `apps/ai` + ADR-004). **Prochaine étape** : R5.1 — ADR-004 (embeddings/modèles), migration `r5_ia` (chunks pgvector, état de corpus), pipeline d'ingestion anonymisé et testé. Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`. --- ## 2026-07-17 — Pr. Daaif (+ Claude) — R4 CLOSE : tag `release/r4` (recette téléphone reportée) **Actions** - Tag annoté **`release/r4`** posé et poussé (CI verte sur `c8b3c17` : 74 tests API, 17 jest-expo, 14 Playwright). **Décisions** - **Décision du référent : la recette sur téléphone (Expo Go — vrai mode avion, scan caméra) est REPORTÉE**, il y accédera plus tard avec un appareil. Le tag est posé sur la foi de la recette « mode avion » rejouée 13/13 en Expo web piloté (toute la mécanique D1/D2 vérifiée, capteurs exclus). **La validation terrain reste due avant toute mise en production client de l'app mobile** — notée comme reste de release. **Prochaine étape** : ouverture **R5 — IA** (design d'abord : maquettes assistant RAG sourcé + suggestion de codes de bilan, décisions à acter). Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`. --- ## 2026-07-17 — Pr. Daaif (+ Claude) — R4.3 : file d'écriture, verrou optimiste, Synchro & conflits **Actions** - **Verrou optimiste côté serveur (D2)** : `updatedAt` exposé au détail OT ; `baseUpdatedAt` optionnel sur transition/coche/bilan → **409 « Conflit de version » contextualisé** (qui, quand — depuis le dernier événement). Toute écriture « secondaire » (coche, bilan, commentaire, conso, MO) fait désormais avancer la version, sinon le verrou serait aveugle. Sans version fournie, comportement web inchangé. 4 tests e2e dédiés (74 au total). - **File d'écriture mobile (D1)** : store persisté AsyncStorage (survit au redémarrage, un ENVOI interrompu redevient rejouable), rejeu **dans l'ordre** ; succès → sortie de file **et propagation de la version fraîche** aux saisies restantes du même OT (nos propres écritures ne se conflictent pas entre elles — un écart étranger reste détecté) ; coupure → tout reste en attente ; refus → CONFLIT et la file s'arrête là. Transitions, coches, bilan et **photos (D5** : compressées ~1 600 px, expo-image-picker/manipulator**)** passent par la file, avec patch optimiste du cache (l'OT affiche « Terminé » localement, chips « en file » partout). - **Écran Synchro & conflits** (écran 7) : file visible et horodatée, badge tabbar (ambre → rouge si conflit), carte de conflit avec le message de l'API et les trois choix — voir l'OT, **rejouer sur la version à jour**, abandonner. Préchargement du parc et des référentiels à l'entrée (trou D1 débusqué par la vérif : le bilan hors-ligne n'avait pas ses vocabulaires). - **Sécurité & routage (demande du référent) : ADR-003** — qui vit où sur l'appareil et ce qui le protège (jeton en trousseau, cache/file en sandbox, purge complète à la déconnexion), l'API seule autorité, deep links `siop://` jamais suivis aveuglément (le scan extrait et résout localement). **Correctif réel au passage : la file et le cache persisté n'étaient pas purgés au logout** — un autre compte sur le même téléphone aurait pu rejouer les saisies du précédent. - **Recette « mode avion » rejouée en Expo web piloté : 13/13** — OT ouvert, avion, Démarrer + bilan + clôture hors-ligne (état local immédiat, 3 en file), Salma modifie l'OT pendant ce temps, retour réseau → rejeu auto → **conflit tranché par l'humain** → file vide → serveur : Terminé avec bilan. 17 tests jest-expo (5 sur la file), zéro erreur console inattendue. **Décisions** - La recette officielle « mode avion en sous-sol » reste à rejouer **sur téléphone** (Expo Go) avec le référent — le web a validé toute la mécanique, pas les capteurs ni le vrai mode avion. - Durcissements notés à l'ADR-003 : chiffrement applicatif de la file et épinglage TLS si exigence client. **Prochaine étape** : recette R4 sur appareil avec le référent → tag `release/r4` → R5 IA (design d'abord). Redéploiement Dokploy de `release/r3` toujours en attente côté serveur. --- ## 2026-07-17 — Pr. Daaif (+ Claude) — R4.2 : scan QR, fiches terrain, grille cochable **Actions** - **Scanner réel** (expo-camera, QR seulement) : analyse du code testée (`analyseScan` — URL portail `…/q/REF` de l'étiquette A6 quel que soit le domaine, ou référence tapée ; les QR étrangers sont refusés proprement, jamais d'écran blanc). **Résolution D4 : le parc en cache d'abord** (fonctionne en sous-sol), rafraîchissement seulement si le réseau est là ; référence inconnue → message honnête. Repli saisie manuelle (seul chemin sur web, assumé). - **Fiche ascenseur** (consultation, D3) : identité, organes, interventions **visibles par le rôle** (invariant « voir autre »), raccourci vers l'OT en cours. **Fiche OT** : un bouton principal selon la machine à états, pièces & main-d'œuvre aux coûts figés, garde de clôture alimentée par les `closureBlockers` de l'API (une seule source de vérité). **Clôture terrain** : bilan codé 6 champs (sélecteur plein écran au pouce — RN n'a pas de `