# Rapport — v3.32.0 (la fiche AO refaite de zéro, l'analyse locale pour TOUS, l'API payante réduite au bouton prix)

**Se livre aux DEUX installations, à l'identique. 1 migration additive & idempotente
(`2026_09_04_100000_amorcer_famille_travaux_installation` — un `insertOrIgnore`, rejouable, base
vide RUWAD comprise). Aucune dépendance composer, aucun asset compilé, AUCUNE lib PDF.**

> ⚠️ **Bascule de doctrine décidée par le gérant.** L'information des fiches AO ne vient plus
> d'appels au modèle payant : elle vient d'une extraction **locale** sur le VPS de la moissonneuse
> (Ollama, coût zéro) qui lit le dossier complet (RC + CPS + avis) de **tous** les AO et livre un
> JSON structuré (`fiche_ia`). L'API payante (Claude) n'a plus qu'**UN** usage : le bouton
> « **Prédire le prix (IA)** », à la demande. Rien d'autre — et plus jamais automatiquement.

---

## Ce que ce lot change, en une phrase par volet

1. **§A** — toutes les fiches AO portent le MÊME niveau d'information, dans toutes les phases
   (veille, étoilé, à préparer, soumissionné) ; les clôturés ont EN PLUS le résultat détaillé ;
2. **§B** — l'ingestion accepte le contrat `fiche_ia` (photo du dossier, familles multiples avec
   parts, mode de passation), borné champ par champ, jamais bloquant ;
3. **§C** — `GET /api/veille/a-approfondir` sert TOUTES les fiches sans `fiche_ia` (étoilés et
   parcours d'abord) ; l'étoile devient un simple marqueur de suivi ; la Veille se trie par
   pertinence ;
4. **§D** — plus AUCUN appel API automatique ; le seul chemin vers l'API est le bouton
   « Prédire le prix (IA) », offert sur toutes les phases.

## §A — La fiche AO refaite (maquette `docs/maquettes/fiche-ao-v3.32.html`)

Une **seule fiche**, trois classes partagées, posées par les deux écrans
(`VeilleInfolist` pour Veille/Étoilés/Clôturés, `TenderInfolist` pour À préparer/Soumissionnés) :

- **`FicheAoService`** — la résolution de CHAQUE champ, écrite une fois : `fiche_ia` d'abord, les
  colonnes de la fiche ensuite (portail, avis, saisies), les anciennes clés d'`ai_analysis`
  (résumé, bordereau, délai) en repli, « — » sinon — **jamais une valeur inventée** ;
- **`FicheAoHtml`** — le balisage de la maquette (classes `fao-*`), le même à l'écran (stylé dans
  `public/css/socle-dense.css`, clair ET sombre) et dans la vue imprimable ;
- **`FicheAo`** — les sections Filament, dans l'ordre de la maquette :
  1. **En-tête** : référence (mono) · maître d'ouvrage · badge de phase (En veille / Étoilé / À
     préparer / Soumissionné / Clôturé) — l'objet ENTIER en titre — les familles avec parts
     (« Audiovisuel 50 % · Travaux & installation 33 % », dominante en accent) — le compte à
     rebours (« Dépôt dans N jours ») avec les échéances secondaires (« Avant : visite des lieux
     05/09 · échantillon 14/09 », lues de `fiche_ia`) ;
  2. **Grille des 12 champs clés** (4 colonnes desktop, 2 en mobile) : Mode de passation ·
     Jugement · Estimation (badge « ≠ portail » au-delà de 20 % d'écart) · Ouverture des plis ·
     Caution provisoire · Caution définitive · Délai d'exécution · Délai de garantie · Lieu ·
     Lots · Prix · Réservé PME — « — » pour l'absent, la grille garde ses 12 cases ;
  3. **« Le travail à faire »** : le paragraphe `resume`, bord accent ;
  4. **« Bordereau des prix »** : le tableau ENTIER (N° · Désignation · Unité · Qté · Montant si
     présent), la famille de chaque ligne en sous-texte, note de pied sur la ventilation,
     défilement horizontal interne ;
  5. **Deux colonnes** : « Ce qu'il faut présenter » (références, agrément, offre technique,
     échantillon) / « Conditions du marché » (visite, variantes, forme, pénalités, moyens,
     dépôt) — les items vides se masquent **ligne à ligne** ;
  6. **« Résultat & concurrence »** (clôturés) : synthèse (Attributaire · Montant · « N reçues ·
     n écartées ») puis le tableau détaillé de TOUS les concurrents comme au portail — badge
     Attributaire / « Classée 2ᵉ » / « Écartée — motif », chaque ligne liée à sa fiche
     Concurrent ; en note : date de publication et écart à l'estimation ;
  7. **Pied d'actions** : « Prédire le prix (IA) » (primaire) · « Télécharger en PDF » · « Voir
     sur le portail » · « Dossier Drive ».

**Supprimé de l'écran** (le code des services reste) : GO/NO-GO partout, « Dossier de
préparation », « Dossier IA en attente », l'en-tête v3.31 (absorbé — pas de doublon). La
**Projection de prix** n'apparaît QUE si une prédiction a été lancée. Restent, repliées, les
pièces justificatives : Méthode du règlement, Fiche portail, Notes, Liens, Texte intégral.

**PDF sans dépendance** : `GET /veille/{id}/imprimable` + `GET /ao/{id}/imprimable` (même
contrôleur `FicheAoImprimableController`, policy `view`, auth + throttle) → une page Blade
autonome (CSS print inline, A4, pas de menu) qui déclenche `window.print()` — l'utilisateur
choisit « Enregistrer en PDF ». **Aucune lib.**

**Mobile** : la grille passe en 2 colonnes (media query `fao-*`), le bordereau défile dans son
cadre, les sections secondaires gardent le repli v3.30.

## §B — Le contrat `fiche_ia` (`FicheIaService`)

`POST /api/veille/ingest`, jeton inchangé, trois champs nouveaux **tous optionnels** (la
moissonneuse v3.34 les enverra — chantier VPS mené en parallèle, même contrat) :

- **`fiche_ia`** — chaque champ borné à sa longueur du contrat (resume ≤ 600, mode_passation ≤ 80,
  … moyens_materiels ≤ 300, references_demandees ≤ 400), `offre_technique` ≤ 12 éléments ≤ 160,
  `bordereau` ≤ 60 lignes (`n`, `designation` ≤ 200, `unite`/`quantite` ≤ 20, `montant_dh`
  numérique ou null, `famille` slug validé ou null). **Validation souple, jamais bloquante** :
  un champ inconnu ou malformé est ignoré CHAMP PAR CHAMP en silence — un `fiche_ia` qui n'est
  pas un objet est ignoré en bloc, l'ingestion réussit. Stockage :
  `ai_analysis['fiche_ia']` **remplacé entier** à chaque livraison (c'est une photo du dossier)
  + `ai_analysis['fiche_ia_at']` ;
- **`familles_ia`** — ≤ 4 éléments `{famille: slug, part: entier}`, slugs validés contre les
  familles ACTIVES (slug inconnu ignoré), dédoublonnés, dominante en tête →
  `ai_analysis['familles_ia']`. **Résolution** : `familles_ia[0]` → `famille_ia` (v3.31) →
  mots-clés → Autre. Le badge liste la dominante en plein + les autres avec parts ; le **filtre
  famille** (Veille, Clôturés) matche **TOUTE famille de la liste** — cas témoin : un AO 50 %
  Audiovisuel / 33 % Travaux sort dans les DEUX filtres ;
- **`mode_passation_ia`** — ≤ 80, alimente la résolution v3.31 en PREMIER
  (`fiche_ia.mode_passation` ou `mode_passation_ia` → `mode_passation_texte` → colonne précise →
  fiche portail → « Marché »).

La réponse d'ingestion écho `fiche_ia` / `familles_ia` / `mode_passation_ia` (true/false) — la
moissonneuse voit sa lecture prendre, champ de contrat par champ de contrat.

**Famille « Travaux & installation »** : amorcée par la migration (`insertOrIgnore`, slug
`travaux-installation`, **aucun mot-clé inclus** — elle n'est posée QUE par le VPS qui lit le
bordereau ligne à ligne, jamais devinée d'un objet ; « travaux » reste un marqueur hors cible du
classement par mots-clés). Le cache du référentiel est vidé par la migration.

**Vérification croisée** (§B4) : `fiche_ia.estimation_dh` vs estimation portail — au-delà de
20 %, badge « ≠ portail » sur le champ Estimation, tooltip avec les deux valeurs. Signal d'erreur
OCR ou de rectificatif, jamais bloquant.

## §C — L'analyse locale pour TOUS

- **`GET /api/veille/a-approfondir`** (même jeton, même throttle, fermée par défaut, aucune
  écriture) : les fiches **sans `fiche_ia`** portant un `ref_consultation` (sans lui, le VPS ne
  retrouverait pas le dossier), triées **priorité puis urgence** — étoilés et parcours
  (`participation = soumission`) en priorite 1, les autres en 2, chacun à date limite croissante,
  sans date en dernier. `?limite=` défaut 30, plafond 60. Champs : tender_id, ref_consultation,
  org_acronyme, pmmp_url, reference, objet, date_limite, **priorite**.
  **L'ancien `etoiles-a-approfondir` répond en ALIAS** (même contrôleur, mêmes champs — compat
  moissonneuse v3.33 pendant la transition).
- **L'étoile démotorisée** (§C2) : elle ne déclenche PLUS aucune analyse — c'est un marqueur de
  suivi (notifications, priorité 1 ci-dessus). Retiré : la mise en file du dossier étoilé à
  l'ingestion, la projection automatique après lecture de dossier
  (`DossierEtoileService::appliquer()` ne projette plus), le rebond « analyse → projection » de
  `RunAiAnalysis`.
- **Tri de la Veille par pertinence** (§C3) : tri PAR DÉFAUT = part cumulée dans les familles
  actives × proximité d'échéance (facteur `60 − jours`, plafonné) — un AO dont la famille
  dominante est connue ET qui ferme bientôt remonte ; score nul (sans famille ou sans date) →
  l'ordre historique (échéance croissante, sans date en dernier). Calcul en mémoire (comme le
  filtre famille), implémentation simple et stable, le tri manuel par colonne reste.

## §D — L'API payante réduite au bouton « Prédire le prix (IA) »

- **Débranché** (§D1) : l'ingestion ne met plus JAMAIS un job d'analyse API en file — ni le
  `contenu` (l'extraction v3.18), ni le `dossier` d'un étoilé (la chaîne « dossier →
  RunAiAnalysis(extraction) → seed → projection » est morte). Les clés `extraction` /
  `dossier_extraction` de la réponse répondent toujours `null` (conservées pour les journaux de
  la moissonneuse v3.33). Le contenu et le dossier restent DÉCOUPÉS sans IA (objet, acheteur,
  dates, enrichissements, notes) — rien de déterministe n'a bougé.
- **Le seul chemin** (§D2) : « Prédire le prix (IA) » — l'action de projection renommée, visible
  sur TOUTES les phases, posée dans le pied de la fiche (et le menu « Analyse IA » des offres).
  La chaîne existante : `TenderProjectionService` (contexte 100 % base, désormais enrichi du
  `resume` de `fiche_ia` et de son estimation en repli) + gardes v3.16 (`recaler()`) +
  appariement par famille v3.31 + prix arrondis. **Aucune pièce envoyée** — texte seulement.
  Résultat dans « Projection de prix (IA) », horodaté « Prédit le … », relançable — et affiché
  seulement si une prédiction a été lancée (une prédiction lancée reste visible même l'AO
  tranché : elle dit ce qu'on savait au moment de prédire).
- **Disparues de l'écran** (§D3, le code des services reste) : « Analyser le dossier (IA) »
  (upload de pièces compris), « Extraire d'un avis (IA) », le menu « Interprétation (IA) »
  (listes Veille et Clôturés, fiches), « Analyser la concurrence (IA) », le pré-remplissage et
  l'édition du GO/NO-GO. Un AO ajouté à la main se complète à la main.
- **La cloche** (§D4) : plus aucune notification d'analyse automatique — il n'y a plus d'analyses
  automatiques. Les notifications du bouton prix (lancement, fin, échec) restent.

## Recette (au vert)

`php artisan test` : **2 538 tests au vert (1 sauté), 9 797 assertions** — dont `FicheAoV332Test`
(12 tests) : le
contrat (stockage, bornes, malformés, slugs inconnus), le filtre multi-familles, le badge
« ≠ portail », la fiche pleine (toutes les sections de la maquette) et la fiche pauvre (pas de
section fantôme), le clôturé détaillé (Attributaire / Classée 2ᵉ / Écartée — motif),
l'imprimable (authentifié, fiche seule, `window.print()`), l'étoile démotorisée, le tri par
pertinence, le bouton prix. Les suites v3.18 → v3.31 réécrites sur la nouvelle doctrine — dont
l'assertion de la recette : **l'ingestion d'un dossier ne pousse AUCUN job API**.

Non-régression tenue : familles v3.30 (mots-clés en repli), presets v3.28, assainissement +
`nonMasques()` v3.31, notifications AO suivis, mobile v3.30, dark mode (variables `fao-*`
doublées sous `.dark`), base vide RUWAD (aucune donnée société dans la migration ni les tests).

## Fichiers touchés (hors tests)

- **Nouveaux** : `app/Services/Tenders/FicheIaService.php`, `app/Services/Tenders/FicheAoService.php`,
  `app/Support/FicheAoHtml.php`, `app/Filament/Admin/Resources/Tenders/Schemas/FicheAo.php`,
  `app/Http/Controllers/FicheAoImprimableController.php`,
  `app/Http/Controllers/Api/VeilleAApprofondirController.php`,
  `resources/views/fiche-ao/imprimable.blade.php`,
  `database/migrations/2026_09_04_100000_amorcer_famille_travaux_installation.php`.
- **Réécrits** : `VeilleInfolist`, `TenderInfolist`.
- **Modifiés** : `VeilleIngestController`, `VeilleImportService` (débranchement API + contrat),
  `TenderProjectionService` (familles multiples, pertinence, estimation/résumé fiche_ia),
  `FiltresAo` (filtre multi-familles), `SectionsAo` (mode de passation), `SectionsDossierIa`
  (projection toutes phases), `TenderActions` (bouton prix, menus purgés), `VeilleTable` (tri
  pertinence, estimation fiche_ia), `ViewVeille`, `ListVeille`, `ListAoClotures`,
  `AoCloturesTable`, `RunAiAnalysis`, `DossierEtoileService`, `routes/api.php`, `routes/web.php`,
  `lang/fr/ai.php`, `public/css/socle-dense.css`.
- **Supprimé** : `VeilleEtoilesAApprofondirController` (remplacé par l'alias).

## À faire côté exploitation (le gérant)

1. Construire et publier l'archive delta **v3.30.0 → v3.32.0** (les deux installations sont en
   v3.30.0) — les migrations v3.30→v3.32 tournent dans la fenêtre de mise à jour ;
2. Lancer UNE fois `php artisan veille:assainir-clotures` (v3.31, livrée dans ce delta) ;
3. Côté VPS (chantier parallèle) : brancher la moissonneuse v3.34 sur
   `GET /api/veille/a-approfondir` et livrer `fiche_ia`/`familles_ia`/`mode_passation_ia` à
   l'ingestion — en attendant, les fiches affichent leurs replis (« — ») proprement.
