# Rapport d'exécution — v3.33.0 (le bordereau brut affiché, l'historique consultable, la veille purgée)

**Trois volets indépendants, courts, sans effet l'un sur l'autre. Aucune migration, aucune
dépendance composer, aucun changement de l'affichage validé en v3.32.1 hors la section
« Bordereau des prix ». Se livre aux deux installations, à l'identique — tient sur une base vide.**

Le volet A **conditionne la grande passe de rattrapage de la moissonneuse** (~500 dossiers relus) :
sans lui, tout `bordereau_texte` livré par cette passe serait ingéré, puis perdu à l'affichage.
Il doit être en production **avant** que la passe démarre.

---

## Volet A — `bordereau_texte` : la moissonneuse l'envoyait déjà, l'app l'ignorait

### Le constat

Depuis la moissonneuse v3.34, un dossier dont le bordereau n'est pas découpable en lignes
structurées (colonnes exotiques, missions numérotées, prix unitaire « par projet » — le cas réel
049-SMIT) arrive avec `fiche_ia.bordereau_texte` : la **zone brute** du tableau, du titre jusqu'à
« Arrêté le présent bordereau », ≤ 3 000 caractères. Le contrat `fiche_ia` ne connaissant pas ce
champ, `FicheIaService` l'ignorait en silence — exactement comme il doit ignorer un champ inconnu.
Résultat : la donnée arrivait et disparaissait.

### A1 — Le contrat d'ingestion

`bordereau_texte` rejoint la table `FicheIaService::TEXTES` avec une borne de **3 000 caractères**.
Il suit donc à la lettre les trois règles du contrat, sans exception ni traitement particulier :

- **borné** — au-delà de 3 000 caractères, le surplus est **coupé**, jamais refusé (c'est la règle
  de tout le contrat depuis la v3.32.0 : mieux vaut un champ tronqué qu'une moisson en erreur) ;
- **ignoré champ par champ** — une valeur qui n'est pas une chaîne (un tableau, un objet) fait
  tomber ce champ **et lui seul** ; le reste de la photo est retenu et l'ingestion répond 201 ;
- **assaini** en UTF-8, `trim` des bords uniquement : les **retours à la ligne internes survivent**,
  et c'est toute la valeur du champ.

Aucune règle de validation n'a été ajoutée à `POST /api/veille/ingest` : le corps `fiche_ia` y part
brut, comme depuis la v3.32.0 — c'est le service qui borne.

### A2 — L'affichage

- `FicheAoService::bordereau()` rend une clé de plus, `texte` : le `bordereau_texte` de la photo,
  **et seulement quand `lignes` est vide**. Le tableau structuré garde la priorité — une seule
  ligne découpée suffit à faire taire le brut, il ne s'affiche jamais à côté.
- `FicheAoHtml::bordereau()` rend alors un bloc `<pre class="fao-brut">` — monospace, retours à la
  ligne préservés (`white-space: pre-wrap`), cadre qui défile (`max-height: 28rem`, jamais la page)
  — suivi de la mention discrète **« Tableau brut du dossier (non structuré) »**, dans le style de
  note déjà en place (`.fao-tbl-note`).
- La section « Bordereau des prix » s'ouvre désormais aussi sur le seul texte brut, à l'écran
  (`FicheAo::bordereau()`) comme dans la vue imprimable — les deux lisent la même source.
- CSS ajouté aux deux feuilles, `public/css/socle-dense.css` (écran) et la feuille embarquée de
  `resources/views/fiche-ao/imprimable.blade.php` (impression).

**Rien d'autre ne change dans la fiche** : la v3.32.1 validée au pixel n'est pas touchée — un seul
sélecteur CSS neuf, une seule branche neuve dans le rendu du bordereau, aucune autre section.

## Volet B — « Voir l'historique complet » sur les Clôturés

### Le constat

La page « Clôturés du marché » est vide alors que la base porte tout l'historique moissonné —
attributaires, montants, concurrence. Coupable : l'**archivage automatique à +30 jours**
(v3.22 §3, `nettoyerVeille()`), qui masque les fiches de veille périmées pour que la liste reste
une liste. Ce défaut est bon et **ne change pas** ; ce qui manquait, c'est le moyen de consulter la
matière.

### Ce qui a été fait

Un filtre-bascule `FiltresAo::historiqueComplet()` — **« Voir l'historique complet », décoché par
défaut** — posé sur la seule page Clôturés. Comme « Voir les écartés » (v3.31 §D1), ce n'est qu'un
interrupteur : c'est la **requête de la page** qui le lit (`FiltresAo::historiqueRappele()`), donc
un état de session Filament perdu ne peut que le décocher — jamais faire réapparaître des fiches
en silence. Le défaut ne dépend d'aucun état.

Actif, il lève l'exclusion `nonMasques()` **pour les fiches qui portent de la matière de marché** :
un attributaire, un montant retenu, ou au moins un pli relevé — nouveau scope
`Tender::scopeAvecMatiereDeMarche()`.

> ⚠️ **Point d'interprétation à valider par le gérant.** Le prompt demande que le filtre « inclue
> aussi les fiches clôturées masquées », **et** que « les écartées de l'assainissement restent
> derrière leur propre filtre existant ». Les deux ne peuvent tenir que si l'historique ne ramène
> pas *tout* le masqué. Le partage retenu est celui des données, pas d'une étiquette : les deux
> commandes d'écartement (assainissement v3.31 §D2, irrécupérables §C ci-dessous) ont **la même
> garde** — elles ne touchent jamais une fiche portant un attributaire ou un résultat. Donc
> « masquée **avec** matière » = archivée par l'âge, c'est l'histoire qu'on vient lire ;
> « masquée **sans** matière » = écartée parce qu'elle n'apprenait rien, et elle reste derrière
> « Voir les écartés », inchangé. Si le gérant veut au contraire tout voir d'un seul geste, il
> suffit de retirer la condition de matière dans `AoCloturesTable` — une ligne.

Le reste est inchangé : le masquage lui-même n'est **jamais** réécrit (vérifié par test), le tri
reste la date de clôture décroissante (`defaultSort('deposit_date', 'desc')`), et les colonnes
existantes (résultat, attributaire, plis, montant retenu, écart) suffisent.

L'état vide de la page nomme désormais le geste : « Passé un mois, il est archivé automatiquement —
le filtre « Voir l'historique complet » le ramène. »

## Volet C — `veille:ecarter-irrecuperables`

Une fiche de veille **sans `ref_consultation`** est invisible pour tout ce qui alimente la veille :
l'ingestion cible par ref (v3.32.2 §A1), et les deux files du VPS (`a-approfondir`,
`a-classifier`) refusent d'office une fiche sans ref (v3.32.2 §B2) — elles ne sauraient pas où
renvoyer la réponse. Ce sont les saisies manuelles d'avant la moissonneuse : elles resteront
« acheteur à compléter » pour toujours. Décision gérant : on les écarte.

`php artisan veille:ecarter-irrecuperables` — préfixe `veille:` (convention des modules),
déterministe, **aucun identifiant, montant ni société en dur**, tourne sur n'importe quelle
installation, **non planifiée** (elle se lance à la main). La règle vit dans
`CycleDeVieAoService::ecarterIrrecuperables()`, à côté de l'assainissement dont elle reprend
l'esprit et le mécanisme réversible.

**Trois gardes absolues :**

1. **le parcours** — seule la veille est regardée (`->veille()`) : une fiche soumissionnée ou à
   préparer est une de NOS offres, elle ne se juge pas sur un identifiant de portail ;
2. **l'étoile** — ce que le gérant suit de près ne disparaît jamais de sa liste ;
3. **la matière** — un attributaire, un montant retenu ou le moindre pli relevé fait de la fiche
   une connaissance de marché : elle reste.

**Rien n'est supprimé** : `masque = true`, une note datée qui dit pourquoi, et « Voir les écartés »
la rappelle. Une `ref_consultation` vide (`''`) vaut absente. **Idempotente** : les fiches déjà
masquées sont hors de la requête, une seconde passe affiche « 0 écartée(s) ».

Bilan affiché : `N écartée(s) / M gardée(s) (étoilées ou à résultat).`

## Ce qui n'a pas bougé

- Aucune migration — `bordereau_texte` vit dans le JSON `ai_analysis`, et le volet B ne lit que
  des colonnes existantes ;
- aucune dépendance composer ;
- le masquage automatique (v3.22 §3), l'assainissement (v3.31 §D2) et le filtre « Voir les
  écartés » : tous inchangés ;
- l'affichage v3.32.1 hors la section Bordereau ;
- la validation d'entrée de `POST /api/veille/ingest`.

## Tests

`tests/Feature/Veille/BordereauBrutEtHistoriqueV333Test.php` — **13 tests, 55 assertions**, de bout
en bout par la route API, les pages Livewire et la commande artisan :

**§A1 — le contrat (3)** : `bordereau_texte` accepté et rangé dans la photo, retours à la ligne
intacts ; un texte de 3 500 caractères borné à 3 000 pile, le reste de la photo retenu ; un
tableau au lieu d'une chaîne ignoré **champ seul**, ingestion 201.

**§A2 — l'affichage (3)** : `lignes` vide + `bordereau_texte` → section visible, `fao-brut` rendu,
mention « Tableau brut du dossier (non structuré) », contenu du dossier présent ; le tableau
structuré fait taire le brut (`texte` à null, mention absente) ; sans bordereau du tout, la
section ne s'affiche pas.

**§B — l'historique (4)** : archive avec attributaire invisible par défaut, visible sous
`historique_complet` ; une archive dont **seuls les plis** sont relevés revient aussi ; une
archive **vide** ne revient pas sous `historique_complet` mais bien sous `voir_ecartes` ; le
filtre ne **démasque rien en base**.

**§C — la commande (3)** : 1 écartée / 2 gardées sur un jeu couvrant les trois gardes (parcours à
préparer, étoilée, à résultat) + une fiche avec ref intacte ; note posée, fiche non supprimée ;
rejouable « 0 écartée(s) » ; `ref_consultation = ''` traitée comme absente.

Suite complète verte, Pint impeccable.

## Recette locale faite

- Fiche avec `bordereau_texte` seul → tableau brut préformaté dans la section « Bordereau des
  prix », mention discrète en dessous (test de rendu Livewire) ;
- page Clôturés : la clôture récente seule par défaut, l'archive attribuée en plus quand
  « Voir l'historique complet » est actif ;
- `veille:ecarter-irrecuperables` sur base de test : bilan exact, étoilées / parcours / à résultat
  intacts, relance → « 0 écartée(s) ».

## Après déploiement (prod)

1. **Avant de lancer la passe de rattrapage de la moissonneuse**, vérifier que cette version est
   bien celle qui tourne (pied de page) — sinon les `bordereau_texte` de la passe seront ingérés
   et invisibles.
2. Ouvrir une fiche AO dont le bordereau n'est pas structuré (049-SMIT après la passe) : la
   section « Bordereau des prix » doit montrer le tableau brut.
3. Page « Clôturés du marché » → activer « Voir l'historique complet » : les attributaires,
   montants et plis moissonnés doivent apparaître. Si une fiche attendue manque, elle est
   probablement masquée **et vide** → « Voir les écartés ».
4. `php artisan veille:ecarter-irrecuperables` — lire le bilan avant de conclure. Rien n'est
   supprimé : en cas de doute, « Voir les écartés » puis « Réafficher » rend n'importe quelle
   fiche.
