09 — Plan d'implémentation du POC Serpe¶
Traduit le doc 08 (vision consolidée) en plan d'exécution : ce qu'on code, dans quel ordre, avec quels fichiers, et ce qu'il faut débloquer avant. Ce document ne remet pas en cause le doc 08 — il le détaille côté implémentation.
0. État des lieux au 21/07/2026 (point de départ réel)¶
Trois chantiers existent en parallèle, sur trois branches, et ne sont pas encore fusionnés :
| Branche | Contenu | État |
|---|---|---|
feat/vertical-slice-memoire |
scripts/vertical-slice-memoire.ts : cadre → 13 blocs « Moyens humains » → fusion Gemini/Vertex → .docx |
Chaîne complète et prouvée sauf l'appel Vertex : 403 billing not enabled sur kayro-502614 |
feat/refonte-mockup (courante) |
app/pages/ao/[id]/rediger.vue (éditeur cadre-piloté) + app/pages/observabilite.vue (Module D), via useRedactionMemoire.ts / useObservabilite.ts |
UI posée, données 100 % mockées, aucun backend |
feat/scaffold-poc (base) |
Schéma Supabase (0001-0004), auth, kanban, provider Gemini (server/utils/generation/providers/gemini.ts) |
Socle fonctionnel, jamais branché aux deux autres |
Le travail principal du plan est donc de convergence : débloquer Vertex, généraliser le vertical slice, puis rebrancher le mockup UI sur le pipeline réel — pas de partir de zéro.
1. Vue d'ensemble¶
1.1 Schéma cible (doc 08 §3)¶
DCE reçu
├─► CADRE de mémoire ──► parseRubriques() ──► [{label, points, theme}]
│ (paragraphes, (src/cadre.js) │ 37% des cadres ont un barème
│ pas styles Word) │ 43% des rubriques : theme=null
└─► RC / CCTP / DQE ──► contexte (acheteur, lieu, lots, contraintes)
│
blocs.jsonl ──► sélection par THÈME ──► budget de mots ∝ POINTS
(1 776 blocs, 13 thèmes, KV/bundle) │
▼
POST /api/generation/rubrique (Nitro, gemini.ts)
réécriture LLM : fondre les blocs en prose,
adaptée au client/chantier, sourceLien conservé
│
▼
assemblage .docx (lib `docx`, mise en forme Serpe)
1.2 Deux jalons, pas un seul¶
Le POC répond à deux questions distinctes, qu'il ne faut jamais mélanger dans la communication à Serpe :
- Jalon « conforme » (atteignable maintenant) : produire un mémoire
complet qui couvre toutes les rubriques du cadre client, avec un texte
rédigé et traçable — la matière vient du corpus RAG (
blocs.jsonl) et de la bibliothèque canonique. C'est la promesse du plan ci-dessous, étapes 1 à 6. - Jalon « compétitif » (bloqué par une donnée externe) : apprendre des affaires gagnées pour orienter la rédaction vers ce qui fait gagner, pas seulement ce qui a été écrit. Bloqué tant que l'export Zoho win/loss (code affaire → issue) n'est pas obtenu — étape 7, angle mort #1 (§7).
Ne jamais présenter le jalon 1 comme si l'app « apprenait à gagner » : sur les ~1 500 mémoires du corpus, l'issue n'est connue que pour 8 gagnés identifiables contre 1 382 perdus/inconnus.
2. Prérequis Vertex AI — à débloquer en premier¶
C'est le bloqueur unique qui empêche de valider ou d'invalider le vertical slice. Rien d'autre dans le plan n'a de sens tant que ce point n'est pas réglé.
2.1 Actions console GCP (projet kayro-502614)¶
- Activer la facturation sur le projet
kayro-502614(Console GCP → Facturation → Associer un compte de facturation). C'est la cause du 403 actuel (billing not enabled) — confirmé par le message d'erreur du vertical slice. - Activer l'API Vertex AI (
aiplatform.googleapis.com) si ce n'est pas déjà fait suite à l'activation de la facturation : - Vérifier le rôle du service account utilisé par l'app (celui référencé
par
GOOGLE_APPLICATION_CREDENTIALS=./service-account.jsonen local, et parGOOGLE_SERVICE_ACCOUNT_JSONsur Cloudflare) : il doit porter le rôleroles/aiplatform.user(« Vertex AI User ») sur le projet, a minima. - Vérifier l'accès au modèle
gemini-2.5-flasheneurope-west4— tous les modèles Gemini ne sont pas garantis dans toutes les régions. Test direct après activation :Un 403 persistant après facturation + API activées indique un problème de modèle/région (essayerset -a; source .env; set +a npx tsx scripts/vertical-slice-memoire.ts # branche feat/vertical-slice-memoireus-central1en repli, ou vérifier l'allowlist du modèle sur le projet via Model Garden). - Quotas : le tiers gratuit/standard Vertex a des quotas par minute
(requêtes et tokens) qui suffisent largement pour un POC (quelques mémoires
par jour) mais peuvent limiter un test de charge sur 12 thèmes en boucle
serrée — vérifier
IAM & Admin → Quotasfiltré suraiplatform.googleapis.comsi des 429 apparaissent pendant la généralisation (étape 1 ci-dessous).
2.2 Ordre de grandeur de coût¶
Un mémoire complet couvre ~10 à 15 rubriques (cadre typique). Pour chaque
rubrique, l'appel generateContent reçoit :
- la matière (blocs sélectionnés, plafonnée à INPUT_WORD_CAP ≈ 2 600 mots
côté vertical slice, soit ~3 500 à 4 500 tokens en entrée selon la
rubrique) ;
- le prompt système/consignes (~200-400 tokens) ;
- une sortie bornée par le budget de mots alloué (budgetMots ∝ points),
typiquement ~700-1 500 tokens en sortie (maxOutputTokens: 4096 en
plafond dur dans le code actuel).
Donc, par mémoire : 10-15 appels × (~4-5k tokens entrée + ~1k tokens sortie) ≈ 50-75k tokens entrée + 10-15k tokens sortie. Avec la tarification Gemini 2.5 Flash (tokens texte, à vérifier au tarif courant sur la page pricing Vertex AI au moment de l'implémentation — l'ordre de grandeur est de quelques centimes à ~0,10-0,20 € par mémoire complet, très loin d'être un poste de coût significatif face au temps chargé d'études économisé). Pas d'optimisation de coût nécessaire au stade POC ; à surveiller si le volume de mémoires générés par mois devient important (facturation dashboard GCP, budget alerte à poser dès l'activation de la facturation).
2.3 Local (fichier) vs Workers (JSON inline) — déjà géré¶
server/utils/generation/providers/gemini.ts gère déjà les deux chemins :
- Local/Node : GoogleAuth sans credentials explicite → détection
automatique via GOOGLE_APPLICATION_CREDENTIALS (fichier service-account.json,
gitignoré) ;
- Cloudflare Workers : GOOGLE_SERVICE_ACCOUNT_JSON (le contenu JSON du
service account, inline, car pas de filesystem sur Workers) — variable à
définir dans le dashboard Cloudflare (mode « variables plaintext par
environnement », cf. commits récents sur wrangler.toml).
- google-auth-library est importée dynamiquement dans generateSection
(commentaire en tête de fichier) car son import statique casse le boot du
Worker (util.inherits, streams incompatibles). Ne pas repasser cet import
en statique lors de la généralisation.
Aucune action de code n'est nécessaire ici — seulement renseigner
GOOGLE_SERVICE_ACCOUNT_JSON dans le dashboard Cloudflare Pages une fois que
le service account a le bon rôle IAM (§2.1).
3. Étapes d'implémentation¶
Étape 1 — Généraliser le vertical slice aux 12 thèmes¶
Objectif : passer de « ça marche sur un thème choisi à la main » à « ça
marche sur n'importe quel cadre, tous thèmes confondus ».
Livrable : scripts/vertical-slice-memoire.ts (ou son successeur)
capable de traiter un cadre entier (toutes ses rubriques) et de produire un
.docx multi-sections.
Dépend de : §2 débloqué (sinon on ne peut rien valider).
Tâches concrètes :
1. Merger/rebaser feat/vertical-slice-memoire une fois Vertex débloqué ;
relancer npx tsx scripts/vertical-slice-memoire.ts et confirmer une
vraie génération (pas le repli concaténation).
2. Généraliser loadCadreRubrique() → loadCadreRubriques() : au lieu de
chercher une seule rubrique par regex (RUBRIQUE_MATCH), reprendre
src/cadre.js (audit) pour parser toutes les rubriques du cadre avec
leur libellé + points, puis mapper chaque rubrique à un thème parmi les 13
(THEMES déjà listés dans app/composables/useRedactionMemoire.ts).
3. Généraliser selectBlocs(theme) : paramétrer par thème au lieu de la
constante THEME = 'Moyens humains / personnel'.
4. Le budget de mots par rubrique doit être calculé (points / totalPoints
* budgetMots), pas la constante budgetMots = 900 codée en dur — c'est
déjà la logique documentée en doc 08 §3 et déjà implémentée côté mockup
(budgetMots() dans rediger.vue) : réutiliser la même formule.
5. Boucler l'appel fusionGemini() sur chaque rubrique, avec gestion des
rubriques theme: null (43 % des rubriques ne se rattachent à aucun thème
aujourd'hui, cf. §7) : soit laisser en a_faire explicite pour rédaction
manuelle, soit tenter un thème par défaut si le libellé matche un mot-clé.
6. Étendre ecrireDocx() pour assembler toutes les sections dans l'ordre
du cadre (ordre de Rubrique), pas une seule.
7. Critère de succès explicite (doc 08 §7) : comparer le résultat à une page
blanche — le juger avec un chargé d'études réel avant de généraliser plus
loin.
Étape 2 — Ingestion : blocs.jsonl → Supabase/KV¶
Objectif : rendre le corpus interrogeable depuis l'app (pas seulement un fichier lu par un script CLI). Livrable : table(s) Supabase peuplées + (option) KV Cloudflare pour un accès bas-latence en prod. Dépend de : étape 1 validée.
Tâches concrètes :
1. Migration 0005_blocs_corpus.sql : créer blocs alignée sur le schéma réel
de blocs.jsonl (doc 08 §6) — theme text, titre text, niveau text,
texte text, mots int, annee int, resultat text check (resultat in
('PERDU','INCONNU','GAGNE','SANS_SUITE')), agence text, source_id text,
source_nom text, source_lien text, canonique boolean default false
(flag qui distingue corpus RAG importé vs bibliothèque canonique curée,
au lieu de deux tables séparées — plus simple à filtrer et fait consensus
avec doc 08 §6 qui laisse le choix).
2. Script d'ingestion scripts/ingest-blocs.ts : lit analyse_out/blocs.jsonl
ligne à ligne (JSONL), valide le schéma, upsert par source_id (idempotent),
insère en batchs de ~500 lignes via supabase-js (service role key, script
exécuté localement, jamais depuis le Worker).
3. Pas de colonne embedding pour le POC (doc 08 §5.4) : filtrage par thème
+ filtres simples (année, agence, mots) suffit ; ne pas ajouter vector(512)
sur blocs tant que le besoin de similarité fine n'est pas confirmé.
4. Option perf : dupliquer blocs.jsonl (3,1 Mo) dans un namespace KV
Cloudflare (BLOCS_KV) au build/déploiement pour éviter une requête
Supabase par génération de rubrique en prod — Supabase reste la source
éditable (bibliothèque canonique), KV un cache de lecture pour le corpus.
5. Clé d'ancrage : code affaire (reference_affaire, format
[Lettre][AA][NNNN]) partout où un bloc est rattaché à une affaire connue —
c'est la clé de jointure avec Zoho plus tard (étape 7).
Étape 3 — Module A (analyse read-only) + Module D (observabilité)¶
Objectif : remplacer les données mockées par les vraies données de
l'audit ; démontrer la valeur sans toucher à la génération.
Livrable : /ao/[id] (onglet Analyse) et /observabilite alimentés en
réel.
Dépend de : étape 2 (ingestion) pour la partie corpus/blocs ; ingestion
des métadonnées Drive (documents, appels_offres) pour la partie AO.
Tâches concrètes :
1. Module D en premier (le plus simple, purement lecture) : importer
l'index.jsonl de l'audit (statistiques par agence, types de documents,
variantes de nomenclature, dossiers vides, doublons probables) dans une
table observabilite_snapshots (jsonb, un snapshot horodaté par run
d'audit) ou directement en fichier statique servi par une route API —
remplacer useObservabilite() (app/composables/useObservabilite.ts) par
un appel useFetch('/api/observabilite/snapshot') qui lit cette source.
Garder le bandeau « lecture seule » du mockup tel quel (observabilite.vue
§0 doc 08 déjà respecté dans le mockup).
2. Module A : ingestion read-only des métadonnées Drive (documents,
dossiers, code affaire, statut via reconnaissance par motifs 4 axes plutôt
que statut_mapping en mécanisme principal — doc 08 §6) dans
appels_offres / documents (schéma déjà présent dans 0001_init.sql).
Extraction Gemini du DCE (carte d'identité, synthèse, contraintes) déjà
prévue par server/utils/generation — brancher un endpoint dédié
server/api/ao/[id]/analyse.get.ts qui déclenche/relit l'extraction.
3. Les deux modules sont parallélisables entre eux (doc 08 §7, ligne 4bis)
mais partagent la même contrainte : aucune écriture sur le Drive, scope
drive.readonly uniquement.
Étape 4 — Bibliothèque canonique (source de vérité)¶
Objectif : figer les blocs stables (présentation groupe, RSE, sécurité,
certifications) en versions de référence, distinctes du corpus RAG bruyant.
Livrable : blocs avec canonique = true pour un premier lot curé
manuellement + blocs_versions pour l'historique daté.
Dépend de : rien (parallélisable avec étapes 2-3, doc 08 §7 ligne 4).
Tâches concrètes :
1. Migration 0006_blocs_versions.sql : table blocs_versions(id, bloc_id →
blocs, version text, contenu text, publie_le date, publie_par → users,
actif boolean).
2. Identifier avec Serpe (ou provisoirement à la main) les blocs candidats :
signal du doc 08 §5.2 — les thèmes « Présentation entreprise » (33 blocs)
et « Références » (6 blocs) sont sous-représentés dans le corpus
précisément parce qu'ils devraient venir d'une source unique curée, pas
d'extraction automatique.
3. Créer en priorité un premier bloc canonique pour le thème
« Continuité/astreinte/urgence », actuellement vide dans le corpus
(angle mort #4, §7) — premier cas où le thème ne peut littéralement rien
proposer sans bibliothèque canonique.
4. UI : réutiliser /bibliotheque/blocs du doc 06 §7 (maquette déjà écrite,
pas encore implémentée) pour lister/gérer ces versions, avec le flag
canonique filtrable.
Étape 5 — Module B : brancher le mockup sur le vrai pipeline¶
Objectif : faire de app/pages/ao/[id]/rediger.vue l'écran réel, pas une
maquette à données mockées.
Livrable : éditeur cadre-piloté fonctionnel de bout en bout (parse cadre →
sélection blocs → génération par rubrique → assemblage .docx).
Dépend de : étapes 1 à 4.
Tâches concrètes (dans l'ordre de câblage) :
1. Parse cadre côté serveur : server/api/ao/[id]/cadre.get.ts (ou
.post.ts si upload à la volée) qui reprend la logique de
loadCadreRubriques() généralisée à l'étape 1, appliquée au DCE de l'AO en
cours (document déjà classé RC/cadre dans documents). Retourne un objet
conforme au type CadreMemoire déjà défini dans
app/composables/useRedactionMemoire.ts (aoReference, totalPoints,
budgetMots, rubriques: Rubrique[]) — ne pas changer ce contrat, le
composable et la page consomment déjà cette forme, il suffit de le
remplacer par un vrai fetch.
2. Sélection de blocs : server/api/blocs/candidats.get.ts?theme=...
remplace la fonction mock blocsParTheme() — interroge la table blocs
(étape 2) filtrée par thème, avec canonique en tête de liste puis corpus
RAG trié comme dans selectBlocs() (non-perdu > récent, dédoublonné).
3. Génération par rubrique : nouvel endpoint
server/api/generation/rubrique.post.ts (détaillé §4 ci-dessous) qui
remplace le setTimeout mocké de genererRubrique() dans rediger.vue
par un vrai appel réseau ($fetch('/api/generation/rubrique', { method:
'POST', body: { rubriqueId, blocsSelectionnes, budgetMots } })).
4. Persistance : chaque génération/validation de rubrique écrit dans
memoire_sections (étendue doc 07 §4 : type_section, statut,
source_generation jsonb pour tracer prompt/tokens/blocs utilisés).
5. Assemblage .docx : bouton « Exporter .docx » de rediger.vue appelle
un endpoint qui reprend ecrireDocx() du vertical slice, généralisé à
toutes les sections validées d'un mémoire (§5 ci-dessous pour le choix
Workers vs job).
6. Conserver l'affichage de traçabilité déjà présent dans la maquette (source
du bloc, lien vers le mémoire d'origine) — c'est le facteur d'adoption n°1
selon l'audit (doc 08 §7).
Étape 6 — Références & visuels¶
Objectif : couvrir les rubriques notées par le cadre mais non-prose
(références chantier, organigrammes, plans, photos, Gantt).
Livrable : sélection de références structurées + insertion de visuels dans
le .docx exporté.
Dépend de : Module B (étape 5) posé.
Tâches concrètes :
1. Migration : table references_clients (doc 07 §5 : maitre_ouvrage,
contact, objet, montant_ht, annee, type_prestation[],
ao_source_id). Alimentée par les 234 fichiers REFERENCE_CHANTIER
identifiés par l'audit — attestations/certificats scannés, pas de la
prose (doc 08 §5.3), donc on ne les traite jamais comme des blocs de
texte à réécrire.
2. Ne pas stocker les attestations elles-mêmes en base : lien vers le
portail Attestations Légales existant (doc 04, rappelé doc 07 §8).
3. UI de sélection : type « sélection » (⊞) déjà prévu dans la maquette doc 06
§5 — filtrer par type de prestation/objet/montant/année, cocher, insérer
en tableau de synthèse dans le .docx (pas de génération LLM ici).
4. Visuels : assets_visuels (déjà en base, 0001_init.sql) — brancher
l'insertion dans ecrireDocx() généralisé (images/plans en pièce jointe de
section, pas dans le corps réécrit par le LLM).
Étape 7 — Win/loss (export Zoho)¶
Objectif : passer du jalon « conforme » au jalon « compétitif ».
Livrable : jointure code affaire → issue branchée sur blocs.resultat
et appels_offres.resultat, utilisée pour prioriser les blocs gagnants dans
la sélection.
Dépend de : donnée Serpe (export Zoho, demande déjà partie côté
analyse_doc/note-serpe-donnees-resultats.md selon l'audit) — hors de notre
contrôle, à relancer/suivre.
Tâches concrètes une fois la donnée obtenue :
1. Script d'import scripts/ingest-winloss.ts : jointure sur
reference_affaire, met à jour appels_offres.resultat et propage à
blocs.resultat (les 1 382 mémoires actuellement PERDU|INCONNU peuvent
être requalifiés pour ceux où la jointure existe).
2. Ajuster selectBlocs() : la priorité PERDU en dernier devient une
vraie priorité GAGNE en tête, avec un signal explicite dans l'UI
(« ce bloc vient d'une affaire gagnée »).
3. Uniquement à ce stade communiquer sur un mémoire « compétitif », pas avant.
4. Détails techniques — génération LLM¶
Où vit l'appel : nouvel endpoint Nitro server/api/generation/rubrique.post.ts,
sœur de l'existant server/api/generation/test.post.ts (déjà en place,
prouve l'auth + le provider). Réutilise getProvider() /
server/utils/generation/index.ts sans modification de contrat — seul
gemini.ts (provider) reste le point d'appel réseau Vertex.
Contrat de l'endpoint (aligné sur le type GenerateSectionParams déjà
défini dans server/utils/generation/provider.ts : { prompt, context: string[],
maxTokens? }) :
// server/api/generation/rubrique.post.ts
// body: { rubriqueLabel, points, contexteCadre, blocs: Bloc[], budgetMots }
// -> { texte, tokensIn, tokensOut, sourcesUtilisees: {blocId, sourceLien}[] }
Forme du prompt (reprend directement fusionGemini() du vertical slice,
à factoriser dans server/utils/generation/prompts/rubrique.ts) :
- Rôle : « chargé d'études SERPE rédigeant un mémoire technique de réponse
à un appel d'offres public » — fixe, ne varie pas par thème.
- Rubrique + attendu client : libellé exact de la rubrique du cadre +
les points + le contexte immédiat (paragraphes suivants dans le cadre, qui
portent souvent les sous-critères).
- Blocs sources : chaque extrait numéroté avec métadonnée (année, agence,
titre) pour que le modèle puisse structurer sans qu'on ait besoin de
reparser la sortie pour retrouver la source — la correspondance
extrait→section se fait en amont (sélection), pas en aval (parsing LLM).
- Consignes non négociables (déjà dans le prompt actuel, à conserver
telles quelles) : fondre plutôt que copier-coller, ne garder que les faits
concrets présents dans les extraits, ne rien inventer (aucun chiffre ou
nom absent des extraits), pas d'intro méta ni de conclusion commerciale.
- Budget : ~${budgetMots} mots, calculé en amont ∝ points de la
rubrique — c'est le prompt qui reçoit un nombre, pas le modèle qui décide
de la longueur.
Streaming : pas nécessaire pour le POC — une génération par rubrique
prend quelques secondes (maxOutputTokens: 4096), l'UI peut se contenter d'un
spinner (nbGeneration déjà dans le mockup). À reconsidérer seulement si on
enchaîne les 10-15 rubriques d'un mémoire en une seule action utilisateur
(auquel cas un état de progression rubrique par rubrique suffit, pas du token
streaming).
Gestion d'erreurs/coût :
- Vertex indisponible (403 billing, quota, timeout) → repli identique au
vertical slice : ne jamais planter la page, renvoyer un statut explicite et
laisser l'utilisateur insérer les blocs bruts non réécrits (mieux qu'un
échec silencieux) ; marquer la rubrique en a_faire avec un badge d'erreur
plutôt que genere.
- Logger tokensIn/tokensOut par génération dans
memoire_sections.source_generation (doc 07 §4) — sert de base à un futur
suivi de coût sans instrumentation supplémentaire.
- Rate limiting côté endpoint si génération en masse (boucle des 10-15
rubriques) : séquentiel avec un petit délai plutôt que parallèle, pour
rester sous les quotas Vertex par minute (§2.1 point 5).
Traçabilité sourceLien : chaque bloc utilisé garde sourceId/
sourceLien de bout en bout (règle d'or doc 08 §0.4) — l'endpoint retourne
sourcesUtilisees en plus du texte, affiché sous la section générée
(« Sources : 3 blocs · adaptée à CD16 / entretien EV » dans la maquette doc
08 §5.1) avec lien cliquable vers le mémoire d'origine.
5. Génération .docx¶
Librairie : docx (déjà utilisée et validée dans le vertical slice,
package.json de la branche feat/vertical-slice-memoire : "docx": "^9.7.1")
— à ajouter au package.json principal lors du merge de l'étape 1.
Respect de la mise en forme du cadre : le vertical slice actuel produit un document générique (titre POC + sections + bloc sources). Pour l'étape 5 (Module B), il faut évoluer vers un gabarit qui respecte le style Serpe : titre du mémoire, en-tête agence/AO, table des matières auto (déjà maquettée doc 06 §6 : « Sommaire auto »), numérotation des rubriques reprenant celle du cadre client (pas une numérotation Serpe interne).
Intégration des visuels : assets_visuels (organigrammes, fiches engins,
Gantt) insérés en ImageRun (docx le supporte nativement) dans les
sections concernées, pas fondus dans le texte réécrit par le LLM — cohérent
avec l'étape 6.
Où l'exécuter :
- Workers (recommandé pour le POC) : docx est une lib JS pure (pas de
binaire natif), compatible nodejs_compat de Cloudflare Workers — le même
environnement que la génération. Générer le .docx dans un endpoint
server/api/memoires/[id]/export.post.ts et streamer le buffer en réponse.
Avantage : pas d'infra supplémentaire, cohérent avec le déploiement Pages/
Workers actuel.
- Job séparé : à envisager seulement si l'assemblage complet (10-15
sections + visuels + mise en page complexe) dépasse les limites CPU/mémoire
d'un Worker (limite d'exécution Cloudflare) — non nécessaire au stade POC,
à surveiller quand les visuels lourds (photos haute résolution) entrent en
jeu.
6. Schéma & ingestion — migrations à ajouter¶
Migrations Supabase à créer, dans l'ordre de dépendance (suite à 0004_harden_functions.sql) :
| Fichier | Contenu | Sert |
|---|---|---|
0005_blocs_corpus.sql |
blocs (theme, titre, niveau, texte, mots, annee, resultat, agence, source_id, source_nom, source_lien, canonique bool) |
Étape 2 — ingestion corpus |
0006_blocs_versions.sql |
blocs_versions (bloc_id, version, contenu, publie_le, publie_par, actif) |
Étape 4 — bibliothèque canonique |
0007_cadres_rubriques.sql |
cadres (ao_id, source_document_id, total_points, budget_mots) + rubriques_cadre (cadre_id, label, points, theme, ordre, statut) |
Étape 5 — pilote de l'éditeur (remplace/complète criteres_ao du doc 07) |
0008_references_clients.sql |
references_clients (maitre_ouvrage, contact, objet, montant_ht, annee, type_prestation[], ao_source_id) |
Étape 6 |
0009_memoire_sections_extend.sql |
ALTER memoire_sections : type_section, critere_id/rubrique_id, bloc_id, statut, source_generation jsonb, assets jsonb |
Étape 5 |
0010_appels_offres_extend.sql |
ALTER appels_offres : reference_affaire, type_prestation[], nature_marche, montant_estime_ht/ttc, ponderations jsonb, extraction_status |
Étape 3 |
statut_mapping existant : ne pas le supprimer, mais le documenter comme
mécanisme de surcharge résiduel (doc 08 §6) — la reconnaissance par motifs
sur les 4 axes de l'audit (docType/phase/statut/resultat) devient le
mécanisme principal, statut_mapping ne sert qu'aux dossiers non reconnus par
motif.
Pipeline d'ingestion read-only depuis le Drive (doc 07 §8) : métadonnées
d'abord (nom, chemin, drive_file_id, code affaire extrait du nom de
dossier/fichier), puis dans un second temps contenu → OCR/extraction →
corpus_chunks + colonnes appels_offres. Toujours scope drive.readonly,
jamais d'écriture — garde-fou à tester (test qui interdit
files.create/update/delete/copy, doc 08 §0.1) avant tout script d'ingestion
réel.
7. Risques & angles morts¶
- Vertex billing (bloqueur actuel, §2) — sans ça, aucune étape suivante ne peut être validée en conditions réelles (seul le repli sans LLM fonctionne, ce qui ne prouve rien sur la valeur produit).
- Formats legacy sur Workers :
textutil(extraction.doc/.odt) n'existe pas sur Cloudflare Workers (c'est un binaire macOS). Pour les documents anciens en formats legacy, soit trouver une lib JS d'extraction (mammothpour.docx, rien de fiable en JS pur pour.doc/.odtbinaire), soit accepter d'écarter le legacy du périmètre d'ingestion automatique et le traiter en conversion manuelle ponctuelle. - Win/loss manquant : 1 382 mémoires perdus/inconnus contre 8 gagnés identifiables — le jalon « compétitif » (étape 7) reste hors de portée tant que Serpe ne fournit pas l'export Zoho. Ne pas sous-estimer ce risque dans la communication : le POC ne peut pas apprendre à mieux gagner sans cette donnée, seulement produire un mémoire complet et conforme.
- 43 % des rubriques non rattachées à un thème : impact direct sur
l'étape 1 (généralisation) et l'étape 5 (Module B) — une rubrique sans
thème ne peut recevoir aucun bloc candidat automatiquement. Traiter comme
un état
a_faireexplicite dans l'UI (déjà prévu dans la maquette : « ⚠ non rattachée », doc 08 §5.1), pas comme une erreur silencieuse. À terme, affiner le mapping rubrique→thème avec de vrais RC/cadres récents annotés (angle mort #2 du doc 08 §8). - Thème « Continuité/astreinte/urgence » vide dans le corpus — premier cas concret où la bibliothèque canonique (étape 4) n'est pas un « nice to have » mais une nécessité : sans bloc canonique, ce thème ne peut littéralement rien proposer.
- Trois branches non convergées (§0) : risque de dérive si le mockup UI
(
feat/refonte-mockup) évolue sans que son contrat de données (CadreMemoire,Bloc,RubriquedansuseRedactionMemoire.ts) reste stable — c'est ce contrat qui sert de spec d'API pour l'étape 5. Le documenter comme gelé dès que le branchement réel commence, pour éviter d'avoir à resynchroniser UI et backend deux fois.
8. Jalons datables¶
Base : aujourd'hui 21/07/2026. Estimations en jours ouvrés, POC à effectif réduit (ordre de grandeur, pas un engagement contractuel).
Jalon 0 — Débloquer Vertex (J+2 à J+5)¶
- Facturation + API Vertex AI activées sur
kayro-502614. - Rôle IAM du service account vérifié,
europe-west4/gemini-2.5-flashconfirmés accessibles. npx tsx scripts/vertical-slice-memoire.tsproduit une vraie section générée (pas le repli concaténation).- Démontrable : un
.docxréel avec une section « Moyens humains » rédigée par Gemini, traçable, montré à un chargé d'études pour un premier avis qualitatif (« mieux qu'une page blanche ? »).
Jalon 1 — Vertical slice généralisé + ingestion (J+5 à J+15)¶
- Étapes 1 et 2 complètes : cadre parsé en toutes ses rubriques, blocs ingérés en Supabase, génération multi-thèmes fonctionnelle en CLI/script.
- Démontrable : un mémoire complet (10-15 rubriques) généré de bout en bout pour un vrai AO en cours, comparé côte à côte avec un mémoire déposé historique sur une affaire similaire.
Jalon 2 — Modules A/D + bibliothèque canonique (J+15 à J+25)¶
- Étapes 3 et 4, parallélisables.
- Démontrable :
/observabiliteet l'onglet Analyse d'un AO terminé branchés sur les vraies données d'audit — démo direction sans risque (read-only), plus un premier lot de blocs canoniques validés dont celui du thème « Continuité/astreinte/urgence ».
Jalon 3 — Module B en production (J+25 à J+40)¶
- Étape 5 complète :
/ao/[id]/redigerbranché sur le vrai pipeline, export.docxfonctionnel aux standards Serpe. - Démontrable : un chargé d'études rédige un mémoire réel dans l'app, de la sélection de rubriques à l'export, sans repasser par le script CLI. C'est la fin du jalon « conforme ».
Jalon 4 — Références/visuels + win-loss si donnée obtenue (au-delà de J+40)¶
- Étape 6 systématiquement ; étape 7 conditionnelle à la réception de l'export Zoho (hors de notre calendrier).
- Démontrable : mémoire exporté avec références structurées et visuels intégrés ; et, seulement si la donnée win/loss arrive, un premier signal « ce bloc vient d'une affaire gagnée » dans la sélection — début du jalon « compétitif ».