diff --git a/CLAUDE.md b/CLAUDE.md index ffd1f07..6c57d7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,5 +64,6 @@ pnpm + Turborepo. `apps/api` : NestJS, Prisma, PostgreSQL (pgvector + PostGIS), - ✅ **R5.1+ génération opt-in** : `Generateur` ADR-004 §3 — extractif par défaut, `AI_GENERATION=api` + `AI_API_KEY` (exigée au boot, jamais loguée ; Dokploy secrets) + `AI_MODEL` (défaut claude-opus-4-8), SDK anthropic en extra optionnel, citations [n] obligatoires, repli extractif sur tout échec (dont `stop_reason=refusal`). **R5.2 assistant au contrat** : `POST /assistant/ask` (WORK_ORDERS.view) + `POST /assistant/suggest-bilan` (WORK_ORDERS.edit) proxifiés par NestJS (traduction dialecte interne → contrat, 503 propre), suggestion = codes EXISTANTS seulement (confiance + « N bilans similaires »), normalisation fastembed corrigée (débusquée en chaîne réelle), 23 pytest, tests API sur stub HTTP. - ✅ **R5.3 — écrans IA** : page web Assistant (chat sourcé, refus honnête chiffré, « Ouvrir » vers PDF/OT), Bibliothèque = corpus administrable (statut d'indexation par document, interrupteur d'exclusion, « Réindexer tout », bandeau 09-08), suggestions fiche OT (« Appliquer » = geste humain, liseré « suggéré » retiré au choix manuel) et clôture mobile (chips, « réseau requis » hors-ligne) ; contrat 76 opérations (corpus sur Document, `PATCH /documents/{id}/corpus`, `POST /assistant/reindex` — ci-contract vérifie aussi le client mobile) ; seuils `AI_SEUIL_*` par env ; job e2e CI avec `siop2-ai` (embeddeur déterministe, seuils calibrés sur mesures réelles), 16/16 Playwright, 78 tests API ; chaîne vérifiée au vrai modèle ONNX (web 7/7, mobile Expo web 6/6, 0 erreur console). - ✅ **Recette R5 sans clé API + durcissement** (17/07) : la recette a invalidé MiniLM-384 (page-réponse classée derrière des passages sans rapport, 0,24 vs 0,41) → **bascule mesurée vers `paraphrase-multilingual-mpnet-base-v2` 768 d** (ADR-004 amendé, migration `r5_embeddings_mpnet`, découpage ~350 car., seuils 0,45/0,40/0,55) ; recette type ✓ (réponse sourcée p. 2, refus honnête, D1-D5, tout en extractif) ; revue pixel publiée (artefact, 3 arbitrages) ; Dockerfile `siop2-ai` (modèle au build, non-root), compose Dokploy (service interne, `AI_SERVICE_TOKEN` requis, génération opt-in), runbook §5-6 (service IA, calibrage seuils client, réindexation post-déploiement). -- 🔄 **Reprise ici** : validation du référent (revue pixel R5 + 3 arbitrages) → tag `release/r5`. Restes : recette R4 sur téléphone (Expo Go), redéploiement Dokploy (`release/r3` puis r5), secret `DOKPLOY_WEBHOOK_URL`, calibrage `AI_SEUIL_*` sur corpus SPELEV réel. +- ✅ **Revue pixel R5 validée par le référent (17/07)** — 1 correction appliquée sur arbitrage : la Bibliothèque-corpus passe en **tableau** (Document/Rattaché à/Indexation/Corpus + actions ; interrupteur éteint pour les non-indexables), vignettes conservées sur les cartes Documents des fiches ; « Appliquer » direct et calibrage au runbook validés tels quels. 16/16 Playwright rejoués. +- 🔄 **Reprise ici** : 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`), secret `DOKPLOY_WEBHOOK_URL`, calibrage `AI_SEUIL_*` sur corpus SPELEV réel. - Détail quotidien : `docs/journal/journal.md`. Dépôt : `siop-spelev/siop2` (privé), jalons R0→R5. diff --git a/apps/web/e2e/parcours-r5.spec.ts b/apps/web/e2e/parcours-r5.spec.ts index 77d460d..0313ea4 100644 --- a/apps/web/e2e/parcours-r5.spec.ts +++ b/apps/web/e2e/parcours-r5.spec.ts @@ -65,7 +65,7 @@ test('recette R5 : corpus → réindexation → assistant sourcé → refus honn ), }); await page.getByRole('button', { name: 'Téléverser', exact: true }).last().click(); - const vignette = page.locator('.doc', { hasText: fichier }); + const vignette = page.locator('tr', { hasText: fichier }); // tableau du corpus (arbitrage référent) await expect(vignette).toBeVisible(); await expect(vignette.locator('.st')).toHaveText('à indexer'); // jamais indexé à la naissance diff --git a/apps/web/src/components/carte-documents.tsx b/apps/web/src/components/carte-documents.tsx index 86a628a..bbeb208 100644 --- a/apps/web/src/components/carte-documents.tsx +++ b/apps/web/src/components/carte-documents.tsx @@ -65,7 +65,7 @@ function ApercuVignette({ doc }: { doc: DocumentDto }) { } /** Statut corpus (R5, D3) : lisible d'un coup d'œil, jamais ambigu. */ -function StatutCorpus({ doc }: { doc: DocumentDto }) { +export function StatutCorpus({ doc }: { doc: DocumentDto }) { if (doc.contentType !== 'application/pdf') { return image — non indexable; } @@ -84,42 +84,42 @@ function StatutCorpus({ doc }: { doc: DocumentDto }) { ); } +/** Interrupteur d'inclusion au corpus (R5, D3) — PDF seulement, réversible. + * Une image non indexable s'affiche ÉTEINTE quel que soit l'état stocké : + * l'interrupteur montre la réalité du corpus, pas une colonne de base. */ +export function InterrupteurCorpus({ doc }: { doc: DocumentDto }) { + const bascule = useSetDocumentCorpus(); + return ( + ) : null} {documents?.length ? ( -
- {documents.map((d) => ( - suppression.mutate(id) : undefined} - /> - ))} +
+
+ + + + + + + {administreCorpus ? : null} + + + + {documents.map((d) => ( + + + + + {administreCorpus ? ( + + ) : null} + + + ))} + +
DocumentRattaché àIndexationCorpus +
+ {d.fileName} + + {DOCUMENT_KIND_LABELS[d.kind]} · {tailleLisible(d.size)} ·{' '} + {new Intl.DateTimeFormat('fr-FR', { dateStyle: 'medium' }).format( + new Date(d.createdAt), + )} + {d.uploadedByName ? ` · ${d.uploadedByName}` : ''} + + + {d.assetReference ? `Asc. ${d.assetReference}` : null} + {d.assetReference && d.workOrderReference ? ' · ' : null} + {d.workOrderReference ?? null} + {!d.assetReference && !d.workOrderReference ? '—' : null} + + + + + + + + + {peutEditer ? ( + + ) : null} + +
+
) : (
Aucun document.
diff --git a/docs/journal/journal.md b/docs/journal/journal.md index bf51797..bd9bfa4 100644 --- a/docs/journal/journal.md +++ b/docs/journal/journal.md @@ -4,6 +4,29 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook** --- +## 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**