# RAPPORT D'EXÉCUTION — v3.33.1

**Hotfix.** L'audit ne doit plus faire exploser l'ingestion de la moissonneuse.
Aucune migration, aucune dépendance, aucun changement d'écran. v3.33.0 intacte.

---

## Le bug, tel qu'il s'est produit

Production, 30/08/2026 à 15:42 (`storage/logs/laravel.log`). Sur la grande passe de rattrapage,
**47 livraisons sur 55** répondaient `500 — erreur interne` :

```
production.ERROR: Ingestion de veille échouée {"ref_consultation":"1030252",
Illuminate\Database\QueryException(code: 22001): SQLSTATE[22001]: String data,
right truncated: 1406 Data too long for column 'new_values' at row 1
SQL: insert into `audit_logs` (`old_values`, `new_values`, ...)
#19 OwenIt\Auditing\Auditor->execute(Object(App\Models\Tender))
#28 VeilleImportService.php(749): Model->save()
#29 VeilleImportService.php(669): rangerBlocDansLesNotes(Tender, '===== DOSSIER =...')
```

La chaîne : la moissonneuse livre un DOSSIER océrisé (jusqu'à ~60 Ko) → il est rangé dans les
`notes` de la fiche → `save()` → l'observer d'audit recopie l'ancienne ET la nouvelle valeur des
notes, échappées pour le JSON (les accents et l'arabe gonflent le texte de trois à six fois), dans
`audit_logs.new_values`, une colonne `TEXT` de 65 535 octets → débordement → l'exception remonte et
**annule le `save()` tout entier**. Rien n'était stocké : ni le dossier, ni la fiche IA, ni les
concurrents. Les 8 livraisons qui passaient étaient les dossiers courts.

Et les livraisons qui « réussissaient » gavaient `audit_logs` de blobs de plusieurs dizaines de
kilo-octets, pour un « qui a fait ça ? » qui n'a pas de réponse : c'est un robot.

---

## §A — Le correctif

**A1 — Le chemin d'ingestion n'écrit plus aucun audit.**
`VeilleImportService::sansAudit()` (nouveau) désarme l'audit du paquet sur les trois modèles que
l'ingestion écrit — `Tender`, `TenderResult`, `Competitor` — le temps d'un rappel, puis le
rétablit (le `finally` du paquet s'en charge, même sur exception). Il enveloppe :

- `ingererConsultation()` **en entier** — donc la fiche, ses notes, ses clés IA, ses concurrents,
  et la propagation aux jumelles (`propagerAuxJumelles`) : le corps est passé dans un
  `ingerer()` privé, appelé sous la garde et seulement par là ;
- `rangerBlocDansLesNotes()` — le `save()` exact qui explosait, gardé **au plus près de
  l'écriture** parce que la fusion des squelettes y passe aussi ;
- `rapatrierChampsIa()` — la seconde écriture machine de `veille:fusionner-squelettes`.

L'audit des actions **humaines** est intact : ce chemin-ci est le seul désarmé. Et ce qui se
déclenche **en aval** d'un changement de statut (caution, prime, écriture de trésorerie) reste
audité — le désarmement est ciblé sur trois modèles, jamais global.

*Note* : `veille:assainir-clotures` n'emprunte pas ces `save()` (elle passe par
`CycleDeVieAoService`), et l'audit console est éteint sur le socle (`audit.console = false`) :
aucune commande ne peut reproduire le débordement.

**A2 — La ceinture : `Tender::$auditExclude`.**
Cinq champs massifs sortent du journal : `notes`, `ai_analysis`, `ai_projection`, `decision_aid`,
`alertes_ao`. Les quatre derniers sont des documents JSON entiers, le premier porte l'avis et le
dossier océrisés. Même une **édition humaine** des notes ne peut donc plus reproduire le 1406.
Rien de ce qui engage l'entreprise n'y perd : statut, montants, attributaire, participation,
cautions — tout cela reste audité, et l'écran de la fiche montre déjà le contenu des cinq exclus.

**A3 — Aucune migration.** La colonne n'est pas agrandie : on arrête de gaver, on n'élargit pas
l'entonnoir. Une base mutualisée n'a pas à héberger des blobs sans valeur.

---

## §B — Le ménage de ce qui a déjà été écrit

`php artisan veille:purger-audits-ingest`

- **Critère** : `url LIKE '%/api/veille/ingest%'`. Le paquet d'audit résout l'URL de la requête à
  chaque écriture ; `POST /api/veille/ingest` est **une** porte, et une seule — celle de la
  moissonneuse. Aucune action humaine n'y passe : l'API est sans session, sans cookie, sans écran.
  Le critère « `user_id` vide + agent Node » a été écarté : il désignerait aussi bien une autre
  machine, une file ou une commande — moins sûr, pour rien de plus. Le chemin est cherché **à
  l'intérieur** de l'URL absolue : rien n'est supposé du domaine de l'installation.
- **Par paquets de 500** : sur une base mutualisée, un `DELETE` de plusieurs centaines de milliers
  de lignes tiendrait la table trop longtemps.
- **Bilan** : « N ligne(s) purgée(s). » **Idempotente** : la seconde passe affiche « 0 ».
- `--simuler` compte sans rien supprimer.
- Non planifiée : elle se lance à la main, une fois.

---

## §C — Recette

6 tests de bout en bout — `tests/Feature/Veille/AuditIngestV3331Test.php` :

1. un `POST ingest` avec un dossier de **~60 Ko** + `fiche_ia` sur une fiche existante → `200`,
   dossier rangé sous son séparateur, fiche IA posée, **0 ligne d'audit** ;
2. une seconde livraison identique n'audite toujours rien ;
3. une modification **humaine** d'un statut et d'un montant **EST** auditée — le journal vit — et
   sa ligne ne porte ni `notes` ni `ai_analysis` ;
4. une édition humaine des seules notes laisse une ligne **vide de valeurs** : hors d'atteinte du
   1406 ;
5. la purge : 3 lignes d'ingestion supprimées, la ligne humaine intacte, seconde passe « 0 » ;
6. `--simuler` compte sans supprimer.

⚠️ La suite tourne en console, où l'audit est éteint par défaut. Ces tests rallument
`audit.console` : sans cela, ils passeraient pour la mauvaise raison — l'audit n'aurait jamais été
armé, et la garde ne serait pas prouvée.

`./vendor/bin/pint` : passed. Suite complète : verte.

---

## À faire après déploiement

1. Déployer à l'identique sur les deux installations (c'est un socle).
2. Sur chacune, une fois : `php artisan veille:purger-audits-ingest`
   (au besoin `--simuler` d'abord pour voir le volume).
3. Relancer la passe de rattrapage de la moissonneuse : les 47 livraisons perdues repassent — les
   fiches n'avaient rien d'anormal, c'est le journal qui refusait l'écriture.
