# Rapport — v3.31.0 (famille par IA locale, projection honnête par famille, fiche hiérarchisée, clôturés assainis)

**Se livre aux DEUX installations, à l'identique. AUCUNE migration : les stockages visés existent
déjà (`ai_analysis` json, `masque`). Aucune dépendance composer, aucun asset compilé.**

> ⚠️ **L'IA payante reste réservée aux AO étoilés** — inchangé. La classification par famille de ce
> lot tourne sur le VPS de la moissonneuse (modèle open-source local, Ollama) : **zéro dirham
> d'API**, l'app ne fait qu'en recevoir le résultat.

---

## Le problème

Quatre défauts **constatés à l'écran sur la prod** (audit navigateur du 29/08, cas témoins cités) :

1. **§A** — les mots-clés ont atteint leur plafond sémantique : « Étude technique et suivi des
   travaux de la **rénovation du réseau** d'assainissement » et « l'**évaluation** des valeurs
   limites des **rejets** » classés *Développement & informatique* (collisions : réseau,
   plateforme, évaluation) ;
2. **§B** — fiche témoin 73 (acheteur nouveau, communication, estimation 100 200 DH) : les
   « soumissionnaires attendus » étaient des sociétés de **formation**, avec des prix mécaniques
   affichés **au centime** (98 496,60 DH) — alors que la base contient des concurrents en
   communication : c'est **l'appariement** qui ne les trouvait pas (repli « les plus fréquents
   tous domaines ») ;
3. **§C** — « je ne vois pas clairement le mode de passation » (il EST dans les données, enterré
   dans des sections repliées) et « trop d'infos, mal organisées » ;
4. **§D** — la page Clôturés remontrait 14 vieilles fiches manuelles (« Acheteur à compléter »,
   références vides, « MDSFSDS / MDSFSDS », « Résultat non publié » de 2024-2025) : l'état de
   session Filament avait perdu le filtre coché par défaut.

## §A — La famille vient d'une IA locale sur le VPS

- **Ingestion** (`POST /api/veille/ingest`) : nouveau champ `famille_ia` (slug d'une famille de
  `familles_objet`). Rangé dans `ai_analysis['famille_ia']` + `ai_analysis['famille_ia_at']`
  (`VeilleImportService::appliquerFamilleIa()`), **validé contre les familles ACTIVES** ; un slug
  inconnu est **ignoré en silence** (le VPS et l'app peuvent dériver) et l'ingestion réussit quand
  même. La réponse écho `famille_ia: true/false`.
- **Résolution** (`TenderProjectionService::familleDeFiche()`) : **famille IA** (si présente et
  valide) → **mots-clés configurables** (v3.30) → null/Autre. Les natures Fournitures/Travaux
  affichées (v3.22.1) gardent leur primauté (`familleAfficheeDeFiche()`). Aucune saisie manuelle
  de famille n'existe — elle primerait sur tout si elle naissait un jour, elle n'est pas créée.
- **`GET /api/veille/a-classifier`** (même jeton, même throttle, fermée par défaut — 503 sans
  jeton, 401 sinon) : les fiches de veille **sans** `famille_ia` (id, ref_consultation,
  org_acronyme, objet), plafond 200/appel — c'est par lui que le VPS reclassifie le stock (~500)
  puis les retardataires.
- **Badge Famille** : indicateur discret quand la famille vient de l'IA (point médian + tooltip
  « Classée par IA locale ») — colonne des listes ET en-tête des fiches. Tous les lecteurs de
  famille (badge, filtre, reclassement, interprétation du marché, projection) passent par la même
  résolution.

## §B — La projection s'apparie par la FAMILLE, et reste honnête

- **Nouvel ordre d'appariement** (`TenderProjectionService::sources()`) : concurrents du **même
  acheteur** d'abord, puis concurrents des AO de la **même famille** (via `familleDeFiche()` des
  deux côtés — le correctif clé de la fiche 73). Le vivier « les plus fréquents tous domaines »
  est **supprimé** (`concurrentsLesPlusFrequents()` n'existe plus).
- **Honnêteté** (décision gérant) : **moins de 2 concurrents pertinents** ⇒
  `historique_suffisant = false`, **aucun candidat listé**, aucun appel au modèle
  (`TenderProjectionAiService` s'abstenait déjà), et la fiche affiche « **Base historique
  insuffisante pour ce domaine — elle s'enrichit chaque nuit par la moisson des clôturés** »
  (clé `ai.projection.insufficient`). Pas de scénarios inventés.
- **Fin de la fausse précision** : les prix attendus sont **arrondis à la centaine de DH**
  (98 500 DH, pas 98 496,60) dès leur calcul (`candidats()`), et chaque ligne indique sa **base**
  réelle (« 5 AO Communication en commun », « 3 AO chez cet acheteur » — `base_nombre` compté par
  origine). `recaler()` impose désormais TOUJOURS la base du socle (le modèle ne réécrit plus le
  `basis`). Le niveau de confiance global reste.
- Gardes v3.16 inchangées : rien d'inventé, plancher respecté, décision au gérant.

## §C — La fiche AO : hiérarchie claire, mode de passation VISIBLE

- **En-tête enrichi** (fiche Veille ET fiche Tender) : **Référence · Acheteur · Date limite
  (compte à rebours) · Mode de passation (badge) · Estimation · Famille (badge)** — puis l'objet
  propre en pleine largeur (et, sur nos offres, l'étape et le statut).
  `SectionsAo::modePassationTexte()` lit la première valeur non vide :
  `ai_analysis['mode_passation_texte']` → la colonne quand elle dit quelque chose de **précis**
  (« Bon de commande » ; « marche » est le défaut de colonne et n'enterre pas le portail) → la
  fiche portail (ligne « Mode de passation / Procédure ») → le libellé de la colonne.
- **Ordre des sections** (mêmes composants sur les deux fiches) : 1. Analyse IA (résumé +
  GO/NO-GO) ouverte · 2. Projection IA ouverte · 3. Dossier de préparation **replié** ·
  4. Résultat observé & concurrence (clôturés seulement, règle v3.29) + Attribution + Écart +
  Méthode du RC (repliée) · 5. Fiche portail repliée · 6. Notes / Liens · 7. Texte intégral
  replié.
- **Dédoublonnage** : une info de l'en-tête ne se répète plus en tête d'une section ouverte —
  « Interprétation & prix (IA) » perd Référence/Date de dépôt/Objet, « Écart » perd le rappel
  « Estimation publiée », « Montants » (fiche Tender) perd le montant estimatif. La Fiche portail
  repliée garde son contenu intégral : c'est la copie du portail.

## §D — Clôturés du marché : assainis et stables

- **`nonMasques()` au niveau REQUÊTE** (`modifyQueryUsing`) sur « Clôturés du marché » **et** sur
  la Veille (qui reposait sur le même filtre par défaut) : l'état de session ne peut plus faire
  réapparaître les écartés. Le filtre « Masquer les AO écartés » (coché par défaut) disparaît au
  profit d'un **« Voir les écartés »** explicite, décoché par défaut ; le marqueur « Écarté »
  (v3.28) lève aussi l'exclusion (`FiltresAo::ecartesRappeles()`).
- **`veille:assainir-clotures`** (one-shot, non planifiée, idempotente) : écarte (`masque = true`
  + note de traçabilité datée — **rien n'est supprimé**) les fiches de veille qui cumulent **date
  limite ≤ 31/12/2025** ET (acheteur à compléter OU référence vide/factice OU aucun résultat
  publié). **Garde absolue** : une fiche avec un vrai attributaire reste visible — elle porte la
  connaissance concurrence. Bilan affiché : N écartées / N gardées.
- **« MDSFSDS / MDSFSDS »** : `VeilleBlocSource::reduireSigleDuplique()` réduit le motif
  « SIGLE / SIGLE - … » (dans `nettoyerAcheteur()`, donc à l'ingestion), et la commande corrige
  les fiches existantes — la fiche témoin (Min. Solidarité) comprise. « MEF / DAAG » (deux sigles
  différents) ne bouge pas.

## Recette (E)

- **A** : `tests/Feature/Veille/FamilleIaTest.php` — slug actif rangé + horodaté, slug inconnu
  ignoré en silence, primauté IA sur mots-clés, natures conservées, `a-classifier` (contenu,
  périmètre veille, jeton 401/503) ; cas témoin §B : un AO Communication chez un acheteur inconnu
  propose les concurrents Communication, PAS l'habitué formation.
- **B** : `tests/Feature/Tenders/TenderProjectionTest.php` — plus jamais d'origine « fréquence »,
  < 2 pertinents ⇒ message « base insuffisante » et zéro candidat/scénario, prix arrondis à la
  centaine, recalage inchangé.
- **C/D** : `tests/Feature/Veille/FicheEtCloturesV331Test.php` — résolution du mode de passation
  (4 sources), badge visible dans l'en-tête, clôturés stables sans état de filtre, « Voir les
  écartés », assainissement (écarte/garde/idempotent), réduction « X / X ».
- **Non-régression** : suite complète au vert — familles v3.30 (mots-clés en repli), presets,
  analyse complète étoilé, notifications, mobile, base vide RUWAD.

## À vérifier après déploiement

1. Page **Clôturés du marché** : les vieilles fiches écartées ne réapparaissent plus, même après
   déconnexion/reconnexion ; « Voir les écartés » les rappelle.
2. Lancer **une fois** `php artisan veille:assainir-clotures` (le gérant décide du moment) et
   vérifier le bilan affiché.
3. Fiche d'un AO : le **mode de passation** en badge dans l'en-tête ; l'ordre des sections.
4. Côté VPS (livré en parallèle, moissonneuse v3.31) : `famille_ia` sur chaque nouvel AO +
   reclassification du stock via `GET /api/veille/a-classifier` ; le badge Famille montre le point
   médian + tooltip « Classée par IA locale ».
