# Rapport — v3.17.2 (remplir un AO de veille depuis un lien **+ le contenu collé**)

**Se livre aux DEUX installations, à l'identique. Aucune migration, aucune dépendance nouvelle,
aucune suppression de fichier.**

## Le constat

La méthode « Ajouter depuis un lien portail » captait bien les identifiants (`refConsultation`,
`orgAcronyme`) — le lien fonctionnait. Mais le pré-remplissage échouait : `VeillePortailService`
faisait un `Http::get()` sur l'URL, et marchespublics.gov.ma ne sert ses consultations qu'à un
navigateur connecté. Côté serveur on ne recevait que la **coquille générique** du portail.

Résultat en production : l'objet d'une fiche s'est rempli avec « **Marchés publics électroniques** »
— le simple `<title>` de cette coquille, que le filtre anti-titre-générique ne connaissait pas.

## §1 — La lecture serveur est retirée, pas rafistolée

Le défaut n'était pas ce filtre. C'était l'idée de tirer un objet de marché d'une page qu'on ne
reçoit pas : **aucune liste de titres à écarter ne rend vraie une lecture qui n'a pas eu lieu**.
Ajouter « marchés publics électroniques » à la liste aurait laissé passer la variante suivante.

`VeillePortailService` ne fait donc plus aucun appel réseau. C'était aussi cinq secondes d'attente
à chaque ajout, dans la requête web, pour un résultat au mieux inutile et au pire faux.

Ce qui reste est ce qui n'a jamais dépendu du réseau : les deux identifiants lus dans l'URL, d'où
l'on reconstruit le lien et la clé de dédoublonnage. `object` reste **vide** (« à compléter »),
`recupere` toujours `false` — les deux clés sont conservées pour ne pas bouger le contrat de
l'appelant.

## §2 — La vraie voie : le contenu collé, lu par l'IA

L'exploitant, lui, **voit** la page. Il sélectionne, copie, colle.

**Le service** (`VeilleExtractionService`) gagne `extraireDuTexte()` à côté de `extraire()`. Les
deux passent par le même `demander()` : même *system prompt*, même consigne stricte, même
`nettoyer()` qui ne laisse survivre que les champs importables, même sortie — un **brouillon**.
Un collage de moins de 80 caractères est refusé **avant** l'appel : payer un appel pour se faire
répondre du vide n'aide personne. Le texte est borné au plafond des pièces
(`services.ai.max_document_chars`), et on dit au modèle que le collage contient aussi la navigation
et les pieds de page du site.

**La plomberie.** Un texte collé est une source au même titre qu'une pièce :

- `AiAnalysisRun::CLE_TEXTE` porte le texte dans les `options` du suivi ;
- `AiAnalysisRun::exigeDesPieces()` raffine l'enum **par exécution** — une extraction de veille
  n'exige des pièces que lorsqu'aucun texte ne l'accompagne. `AiAnalysisKind::exigeDesPieces()`
  reste ce qu'il était : ce dont une *nature* d'analyse a besoin par défaut ;
- le Job lit les pièces **dès qu'il y en a**, et l'absence de document n'est un échec que pour les
  exécutions sans autre source.

**Le rattachement.** Les identifiants du portail voyagent avec le brouillon (`rattacherAuPortail()`
dans le Job, puis transmis à l'import à la validation). Sans cela, valider un brouillon né d'un lien
créerait une **seconde fiche du même AO** — c'est exactement ce que le test anti-doublon vérifie, et
il échoue si l'on retire ce report.

**L'action.** « Ajouter depuis un lien portail » gagne un `Textarea` facultatif. Le déroulé :

1. **toujours** — la fiche est créée/complétée depuis le lien, immédiatement : le lien est utile
   sans attendre l'IA ;
2. **si un contenu est collé** — une extraction part en file, rattachée au `ref_consultation` ;
3. **sans clé ni fonction IA** — le champ n'est même pas affiché, et si un contenu arrive quand
   même il est ignoré avec un message clair. Jamais de 500.

**La référence provisoire.** Une fiche née d'un lien porte `PMMP-<identifiant>` faute de mieux.
Cette valeur n'a été saisie par personne : la validation d'un brouillon a le droit de la remplacer
par la vraie référence de l'avis. La règle « on complète, on n'écrase pas » protège le travail d'un
humain, pas un pis-aller du logiciel — d'où `estRemplacable()`, volontairement limité à ce seul cas.

## §3 — L'aide de la méthode 4

« Extraire d'un avis (IA) » précise qu'un avis, un DCE ou un PV **téléchargé depuis le portail**
fait aussi bien l'affaire : c'est la même mécanique, côté fichier.

## Recette

Suite complète verte, `pint`, `npm run build`. Tests ajoutés :

- **§1** — `apercu()` sur un HTML de coquille (`<title>` = « Marchés publics électroniques ») rend
  `object === null`, `recupere === false`, et **aucune requête n'est envoyée** ; une fiche créée
  depuis un lien a bien un objet vide et son `pmmpLink()`.
- **§2** — `extraireDuTexte()` produit un brouillon aux seuls champs importables ; un texte trop
  court est refusé **sans appel au modèle** ; un lien **avec** contenu crée la fiche *et* met
  l'extraction en file (aucun appel synchrone) ; un lien **sans** contenu ne lance rien ; le
  brouillon **complète la même fiche** (une seule fiche en base) et **remplace la référence
  provisoire** ; sans fonction IA, la fiche est créée et rien n'est mis en file.

Deux points d'appui notés en chemin :

- un `Http::fake()` attrape-tout posé **avant** `fakeClaude()` gagne sur lui — les stubs sont
  évalués dans l'ordre d'enregistrement — et le modèle répond du vide. Le test concerné ne pose plus
  de fake global (l'ajout depuis un lien ne fait de toute façon plus d'appel).
- le test anti-doublon a été vérifié dans les deux sens : en retirant le report des identifiants, il
  compte 2 fiches au lieu d'1.

Non-régression : les quatre méthodes d'alimentation (dont l'upload de fichier), l'interprétation de
marché, la projection du dépôt, les liens de la v3.17.1.

## Livraison

```bash
git diff --name-only --diff-filter=ACMR v3.17.1 v3.17.2
```

**Aucune migration.** Sauvegarde phpMyAdmin, extraction du paquet, `php artisan optimize:clear`.

## Fichiers

**Modifiés** — `app/Services/Tenders/VeillePortailService.php`,
`app/Services/Tenders/VeilleImportService.php`, `app/Services/AI/VeilleExtractionService.php`,
`app/Services/AI/AiAnalysisDispatcher.php`, `app/Jobs/RunAiAnalysis.php`,
`app/Models/AiAnalysisRun.php`, `app/Filament/Admin/Resources/Veille/Actions/VeilleActions.php`,
`lang/fr/ai.php`, `tests/Feature/Veille/VeilleImportTest.php`,
`tests/Feature/Veille/VeilleIaTest.php`, `CHANGELOG.md`.

**Ajouté** — `docs/rapports/RAPPORT-V3.17.2.md`.
