186 lines
24 KiB
Markdown
186 lines
24 KiB
Markdown
# 1quater. Chantier « Guide IA V1 » — ordre d'exécution (arrêté le 2026-08-07)
|
||
|
||
> ← Section **§1quater** du tableau de bord : [STATUS.md](../STATUS.md)
|
||
|
||
> Cette section ne décrit **rien de neuf** : elle donne l'ordre dans lequel exécuter ce que trois plans détaillent déjà, parce que le chantier les traverse tous les trois et qu'aucun ne porte la vue d'ensemble.
|
||
>
|
||
> **Décision : tout est fait d'un coup, sans mise en prod intermédiaire.** Rien ne part en prod avant que le backlog (kanban + ce fichier) soit terminé — voir §1ter, la base Postgres est vide et c'est une fenêtre à ne pas refermer.
|
||
|
||
### ✅ Lot 1 — Rendre vrai ce qui est déjà livré · **fait le 2026-08-07**
|
||
|
||
→ **[v2/guide-ia-screen-plan.md](../v2/guide-ia-screen-plan.md)** § « Configuration branchée sur `AssistantService` »
|
||
|
||
`BuildGuideIdentity()` et `BuildFallbackInstruction()` dans `AssistantService`, consommés par les **4** blocs de prompt — le relevé initial n'en annonçait que deux, il en existe quatre (scope configuration / scope instance × vocal / texte). Ton codé en dur retiré, seconde politique de repli de la ligne 550 réconciliée, règle hors-sujet **ajoutée aux deux variantes texte qui n'en avaient aucune**. Les 4 `new HttpClient()` des outils d'agenda passent par `IHttpClientFactory` (§3, priorité haute).
|
||
|
||
`dotnet build` 0 erreur · `dotnet test` **124/124**.
|
||
|
||
⚠️ **Pas encore vu tourner** : aucun échange réel joué contre un guide configuré. À couvrir au moment du plan de test.
|
||
|
||
### ✅ Lot 2 — Le schéma · **livré le 2026-08-09**
|
||
|
||
→ **[v2/rag-pgvector-integration-plan.md](../v2/rag-pgvector-integration-plan.md)** · **[v2/media-storage-plan.md](../v2/media-storage-plan.md)**
|
||
|
||
| # §1ter | Livré | Vérifié en base |
|
||
|---|---|---|
|
||
| 1 | Image `postgis` + `pgvector` — `Deployment/Dockerfile.postgres`, base **épinglée par digest** | `postgis 3.4.3` + `vector 0.8.6` |
|
||
| 2 | Table `ContentEmbedding` + 3 index (dont HNSW `vector_cosine_ops`) | migration `AddContentEmbedding` |
|
||
| 3 | `IEmbeddingService` + `GoogleEmbeddingService` (batch par 50) | **768 validé contre l'API réelle**, insertion + recherche jouées en base |
|
||
| 4 | Colonnes `Resource` (7) + `Word`/`PowerPoint`/`Text` en **fin** d'enum | migration `AddResourceStorageAndAiColumns` |
|
||
| 7 | Table `VisitorQuestion` + `ConversationId` sur `AiChatRequest` | migration `AddVisitorQuestion` |
|
||
|
||
`dotnet build` 0 erreur · `dotnet test` **124/124** · 62 migrations, 26 tables · données intactes (52 sections, 45 ressources).
|
||
|
||
**Ce que l'appel réel à l'API a appris (2026-08-09)** — trois choses qu'aucun plan ne disait :
|
||
|
||
1. **`gemini-embedding-001` renvoie 3072 dimensions par défaut.** Le paramètre `dimensions: 768` est **obligatoire** dans la requête, sinon la colonne `vector(768)` rejette l'insertion.
|
||
2. **Les vecteurs réduits ne sont pas normalisés.** Norme L2 mesurée : **0,578**, pas 1. La distance cosinus de pgvector suppose des vecteurs normalisés — sans normalisation côté client, le classement des résultats serait faussé **sans qu'aucune erreur ne soit levée**. `GoogleEmbeddingService.Normalize()` s'en charge.
|
||
3. **Google ne renvoie pas le champ `index`** du contrat OpenAI (chaque item ne porte que `embedding` et `object`). Un `OrderBy(d => d.Index)` trierait sur des zéros : retiré, l'ordre du tableau fait foi.
|
||
|
||
**Recherche cross-lingue vérifiée en base**, question posée en néerlandais sur du contenu FR/NL :
|
||
|
||
| Contenu | Similarité |
|
||
|---|---|
|
||
| NL, même sujet | **0,8145** |
|
||
| FR, même sujet | **0,7982** |
|
||
| FR, hors sujet | 0,4818 |
|
||
|
||
Le pari « pas de filtre de langue » tient : le FR pertinent bat largement le FR hors-sujet. Et l'écart NL/FR n'est que de **0,016** — trop faible pour trier tout seul, ce qui confirme la nécessité du **bonus de score** par langue plutôt que de s'en remettre à la similarité brute.
|
||
|
||
Vérifié aussi : la contrainte anti-doublon rejette bien un rejeu Hangfire, et un vecteur de 512 dimensions est refusé par la colonne.
|
||
|
||
**Schéma `ContentEmbedding` validé le 2026-08-09** — il diverge du plan écrit sur trois points, sur la base du code réel :
|
||
- `string InstanceId` et non `int VenueId` : tous les Id du projet sont des `string` (héritage ObjectId Mongo). **Filtre dur** — frontière entre deux clients.
|
||
- `string? ConfigurationId` **nullable**, absent du plan : `Resource` ne porte pas de `ConfigurationId`. Sert de **bonus de score**, jamais de filtre — décision produit : le guide connaît tout le lieu et priorise la visite en cours. Même patron que le multilingue.
|
||
- `long Id` : table technique à fort volume, jamais référencée ailleurs.
|
||
|
||
**Relevé en chemin :**
|
||
- ⚠️ **La base locale avait une migration de retard.** `AddGuideIaConfigToInstance` (lot 1) n'était pas appliquée : les colonnes `Guide*` n'existaient pas, donc **toute requête sur `Instances` aurait planté** — l'API entière, dès le login. Appliqué le 2026-08-09. Le compilateur ne dit rien de l'état de la base.
|
||
- ⚠️ **Warning de collation** sur la base locale (`glibc 2.36` → `2.31`), corrigé par `REINDEX` **puis** `REFRESH COLLATION VERSION` — dans cet ordre, le refresh seul masque le symptôme sans réparer les index. C'est la raison de l'épinglage par digest : un tag mobile rejouerait ça sur des données clients.
|
||
- `SizeBytes` **existait déjà** sur `Resource`, contrairement à ce qu'annonçait le plan médias — mais jamais renseigné (`= 0` partout). Le backfill (point 5) reste entier.
|
||
- `ResourceDTO` volontairement non modifié : exposer les nouvelles colonnes désynchroniserait `manager_api_new`, qui s'édite à la main. L'UI est V2.
|
||
- pgvector **0.8.6** et non 0.5 : les *iterative index scans* existent (`hnsw.iterative_scan`), vraie parade au post-filtrage HNSW là où le plan ne prévoyait que de monter `ef_search`. À régler au lot 3.
|
||
|
||
### ⚠️ Dette ouverte — aucun test ne couvrira le RAG
|
||
|
||
Ajouter `ContentEmbedding` a cassé **116 des 124 tests** d'un coup : ils tournent sur **EF InMemory**, qui ne connaît pas le type `Vector` et refuse de valider le modèle. Contournement appliqué — l'entité est exclue quand le provider n'est pas Npgsql (`Database.IsNpgsql()` dans `OnModelCreating`).
|
||
|
||
Les tests repassent, mais **la conséquence est structurelle** : vector store, recherche cosinus, index HNSW, contrainte anti-doublon Hangfire — rien de tout ça n'est testable tant que la suite ne tourne pas sur un vrai Postgres. C'est la dette « tests EF InMemory peu représentatifs » du §3, qui cesse d'être théorique : elle rend le cœur du lot 3 non couvert.
|
||
|
||
Piste : Testcontainers (`Testcontainers.PostgreSql`) sur l'image `Dockerfile.postgres`, au moins pour les tests du vector store. Pas chiffré, pas planifié.
|
||
|
||
### Lot 3 — Le RAG · 1 à 2 semaines
|
||
|
||
→ **[v2/rag-pgvector-integration-plan.md](../v2/rag-pgvector-integration-plan.md)** § « Ensuite — incrément indépendant, à froid »
|
||
|
||
`IVectorStoreService` (delete-then-insert transactionnel) · jobs Hangfire sur les saves de `SectionController`, queue dédiée · **respect d'`IsActive` avec purge des embeddings à la désactivation** (non négociable) · endpoint `/ask`, recherche cross-lingue sans filtre de langue avec bonus de score, réglage du post-filtrage HNSW, **sources renvoyées dans tous les cas**.
|
||
|
||
> **Le point de charge caché du chantier.** `GetEmbeddableText()` est à écrire pour chacun des **13 sous-types de section** — ingrat, sans difficulté technique, mais c'est lui qui détermine la qualité des réponses : ce qu'on n'extrait pas, le guide ne le connaîtra jamais.
|
||
>
|
||
> Il se double de `GetReferencedResourceIds()`, réclamé par [v2/offline-visit-plan.md](../v2/offline-visit-plan.md) — mêmes sous-types, mêmes structures parcourues. **À écrire dans la même passe.** Les faire séparément, c'est payer deux fois.
|
||
>
|
||
> ✅ **La passe commune a bien eu lieu** — vérifié le 2026-08-11 : `GetReferencedResourceIds()` existe dans les 13 sous-types de `Data/SubSection/`. Mais il n'est **appelé nulle part** : le switch de collecte de `ConfigurationController.Export` est toujours commenté en bloc (~180 lignes mortes). Le bug offline nº1 coûte donc le branchement d'une méthode existante, pas une passe sur 13 sous-types — il descend en coût, pas en priorité.
|
||
|
||
> **✅ Pipeline d'ingestion livré le 2026-08-10.** `IIngestionService` / `IngestionService` : chargement des collections filles par sous-type (sans `Include`, `GetEmbeddableText` rendait un texte amputé de ses événements, étapes et questions **sans rien signaler**), un jeu de morceaux par langue renseignée, `ChunkIndex` continu toutes langues confondues pour respecter la contrainte d'unicité. Branché sur les trois chemins de `SectionController` (création, mise à jour, suppression), sur `AgendaSyncService.SyncSectionAsync` — après son `SaveChanges`, sinon on indexerait les anciennes dates — et servi par un **serveur Hangfire séparé** (`ingestion`, 2 workers) pour que les extractions ne prennent pas les workers de la file générale.
|
||
>
|
||
> **✅ Garde par plan.** `AiTokensPerMonth > 0` vérifié dans le job, pas seulement à l'enqueue : le plan peut changer entre les deux, et c'est ce contrôle-là qui décide de l'appel facturé.
|
||
>
|
||
> **✅ Rattrapage à l'upgrade branché le 2026-08-10.** `BackfillInstanceAsync` part automatiquement depuis `InstanceController.Updateinstance` quand `AiTokensPerMonth` passe de 0 à une valeur, et manuellement via `POST /api/Ai/reindex/{instanceId}` — **SuperAdmin uniquement**, avec le bouton correspondant dans l'écran Guide IA de `manager-app`. Réservé au support, volontairement : exposé au client, il serait cliqué à chaque réponse décevante du guide, pour un coût d'embedding complet et sans rien améliorer.
|
||
>
|
||
> ⚠️ **Le webhook Stripe n'est pas un point d'accroche**, contrairement à ce que supposait le plan : `checkout.session.completed` ne touche ni `SubscriptionPlanId` ni `AiTokensPerMonth`, il bascule `IsTrialActive` et stocke l'id d'abonnement. L'attribution de plan est manuelle. Le jour où le webhook changera le plan, il devra reprendre la même garde 0 → >0.
|
||
>
|
||
> 🐛 **`Updateinstance` ne recopiait pas les quotas du nouveau plan** — `CreateInstance` le faisait, pas lui. Passer un client de Starter à Premium changeait `SubscriptionPlanId` et rien d'autre : 1 Go de stockage et 0 jeton IA conservés. L'endpoint `/quota` masquait la moitié du problème en retombant sur le plan à la lecture, mais `AiController` lit `instance.AiTokensPerMonth` — **le client payait un plan avec IA et restait sans IA**. Extrait dans `ApplyPlanQuotas`, appelé par les deux chemins, avec test.
|
||
>
|
||
> **✅ `SearchKnowledge`** ajouté aux outils d'`AssistantService` — pas d'endpoint `/ask` séparé, qui aurait donné deux assistants concurrents : l'un sachant interroger l'agenda mais ignorant le contenu, l'autre l'inverse. Rend une phrase explicite quand la recherche ne trouve rien, sinon le modèle comble le silence avec ses connaissances générales.
|
||
>
|
||
> **✅ Deux angles morts du découpage corrigés le 2026-08-10.** Ils frappaient le même contenu — l'article, le texte le plus riche du CMS :
|
||
> - **HTML retiré** avant l'embedding (`StripHtml`, remplacement par une espace + `HtmlDecode`). Les balises sont identiques dans tous les contenus : elles tiraient les vecteurs vers un fond commun et écrasaient les écarts de score, bien plus gênant que leur coût en jetons. Volontairement distinct du `StripHtml` d'`AssistantService`, qui remplace par `""` — correct pour de l'affichage, mais y accoler deux paragraphes fusionnerait leurs mots.
|
||
> - **ligne plus longue que `MaxChunkChars` découpée** (`SplitLongLine`, coupe à la dernière fin de phrase, repli sur l'espace). Le HTML ne contient pas de `\n` : une fois les balises retirées, un article formait une seule ligne de toute sa longueur, dépassait l'entrée maximale du modèle, et l'échec emportait les 49 autres morceaux de son lot d'embedding. Symptôme : « le guide ne connaît pas mes articles », visible nulle part ailleurs que dans le dashboard `/hangfire`.
|
||
>
|
||
> ⚠️ Vérifier l'entrée maximale réelle de `gemini-embedding-001` dans la doc Google au moment du test de charge — `MaxChunkChars = 1200` (~300 jetons) a de la marge, mais la valeur n'a pas été confirmée.
|
||
>
|
||
> **✅ Downgrade tranché le 2026-08-10 : on conserve.** Le code purgeait les embeddings dès que l'instance perdait l'IA. Rétabli sur la décision du plan — quelques Mo de vecteurs ne pèsent rien, l'usage est bloqué en amont, et purger ferait payer une réindexation complète pour un incident de paiement réglé le lendemain. La purge reste sur les deux cas qui la justifient : section supprimée, section désactivée (`IsActive`).
|
||
>
|
||
> 🐛 **Trou de facturation trouvé et corrigé le 2026-08-10.** `CheckQuota` ne bloquait que si `quota > 0 && AiTokensThisMonth >= quota` : avec `AiTokensPerMonth = 0`, **aucun blocage et aucun compteur**. L'ambiguïté vient du code lui-même — `0` veut dire *illimité* pour `StorageQuotaBytes` mais *pas d'IA* pour `AiTokensPerMonth`. Comme `IsAssistant` est un drapeau manuel indépendant du plan (`InstanceController:197`), une instance `plan-starter` avec `IsAssistant = true` consommait de l'IA gratuite et non comptée. **Pas théorique** : `MigrationController` ne reprend pas `SubscriptionPlanId` (écart **c** du §1quinquies), donc toute instance migrée depuis Mongo arrive à 0 — rien ne s'indexe *et* l'IA tourne sans compteur. Corrigé en 403 avant tout appel au modèle, avec un test dédié.
|
||
>
|
||
> **✅ Déclenchement unifié le 2026-08-10 → [v2/rag-indexing-trigger-decision.md](../v2/rag-indexing-trigger-decision.md).** Deux mécanismes concurrents coexistaient (un `Enqueue` par contrôleur *et* un intercepteur EF), ce qui doublait chaque enqueue et cassait 2 tests — `BackgroundJob.Enqueue` est statique et lève sans `JobStorage.Current`. **L'enqueue par contrôleur seul ne pouvait pas marcher** : mesuré, les 5 sous-contrôleurs totalisent **30 `SaveChanges` et 0 `Enqueue`**, et ils enregistrent l'entité fille sans jamais toucher la ligne `Section` (`SectionMapController:148`). Un client ajoutant 40 points d'intérêt à une carte n'aurait rien réindexé — précisément le contenu que les 13 `GetEmbeddableText()` savent extraire. Retenu : **`SectionIndexingInterceptor`** seul, avec sa table de résolution enfant → section et un filtre `{Order, DateUpdate}` qui évite de payer un embedding par section déplacée lors d'un réordonnancement.
|
||
|
||
> **✅ Première exécution réelle — 2026-08-10.** Le pipeline a tourné contre la base locale et l'API Google, hors HTTP (harnais jetable, pas de contournement d'auth). **52 sections → 2150 morceaux en 53 s.** L'index se remplit, la recherche cosinus répond, le parcours itératif HNSW ne bronche pas, et le bonus de langue fait son travail : question NL → morceaux NL en tête, avec du contenu d'une autre langue qui remonte quand même.
|
||
>
|
||
> ⚠️ **Constat en passant, en données réelles** : l'instance était sur `plan-standard` (5 M jetons) avec `AiTokensPerMonth = 0` sur la ligne `Instance`. C'est le bug `Updateinstance` en vrai, pas en théorie — sans le correctif, la garde d'indexation aurait écarté les 52 sections en silence.
|
||
>
|
||
> **Deux défauts que seule l'exécution pouvait montrer, corrigés :**
|
||
> - **Les gabarits étaient indexés.** `LanguageInit.Init` pose « FR - Title », « NL - Description » à la création d'une section, et ils restent tant que la langue n'est pas remplie. Résultat mesuré : les **cinq** premiers résultats d'une question en néerlandais étaient des `NL - Title NL - Description`. Le bonus de langue suffit à faire passer un gabarit vide devant du vrai contenu français — soit exactement le cas que la recherche cross-lingue devait servir. Filtrés à l'indexation (`WithoutPlaceholders`) : une langue qui n'a que des gabarits ne produit plus aucun morceau. −255 morceaux sur 2405.
|
||
> - **Le même texte occupait trois des cinq résultats**, au même score : une description recopiée sur chacun des événements d'un agenda. `DistinctBy(Text)` avant le `Take(topK)` — sinon c'est 60 % du contexte du modèle gâché.
|
||
>
|
||
> Après correction, les cinq premiers résultats FR **et** NL sont du contenu réel et distinct.
|
||
>
|
||
> ⚠️ Restent non vérifiés : l'entrée maximale réelle de `gemini-embedding-001` (aucun morceau n'a atteint la limite sur ce jeu) et le comportement du post-filtrage HNSW **à plusieurs instances** — il n'y en a qu'une en base locale, ce qui est précisément le cas où le problème ne se voit pas.
|
||
|
||
**Jalon démontrable en clientèle** : à ce point le guide répond correctement sur le contenu CMS seul. Tout le reste est de l'amélioration.
|
||
|
||
### Lot 4 — Le journal et le reste de l'écran · ~1 semaine
|
||
|
||
→ **[v2/guide-ia-screen-plan.md](../v2/guide-ia-screen-plan.md)** §7 et § « Ensuite »
|
||
|
||
Insertion `VisitorQuestion` dans `AiController.Chat`, à côté du `RecordUsage` existant · job de regroupement en thèmes · job de purge à 90 jours · onglet « Ce que demandent vos visiteurs » · aperçu de conversation (à faire converger avec le « Mode preview / cible Assistant-Persona » de [todo-features.md](../todo-features.md), pas construire deux fois) · onglet « Vocal » dans les stats. **Volet RGPD dans les CGU avant mise en service.**
|
||
|
||
### ⚠️ Ce que les 4 lots ne couvrent PAS
|
||
|
||
**Les lots 1 à 4 découpent le chantier Guide IA, pas tout le §1ter.** Le lot 2 a pris le point 4 (colonnes `Resource`) parce que c'est du schéma partagé avec le RAG, mais **les points 5, 6, 8 et 9 n'appartiennent à aucun lot** — ils relèvent du chantier médias & stockage, jamais découpé. Constaté le 2026-08-09 : le tableau du lot 2 liste `1, 2, 3, 4, 7` sans le dire.
|
||
|
||
### Lot médias — orphelin de lot jusqu'au 2026-08-09
|
||
|
||
→ **[v2/media-storage-plan.md](../v2/media-storage-plan.md)** § « État mesuré en base le 2026-08-09 »
|
||
|
||
Ordre à respecter, sous peine de travailler deux fois :
|
||
|
||
| # | Quoi | Note |
|
||
|---|---|---|
|
||
| 1 | `IStorageService` + **`StoragePath` écrit à `Create`** | La colonne existe mais **n'est affectée nulle part** : elle reste vide même pour les nouvelles ressources |
|
||
| 2 | **`SizeBytes` renseigné à `Create`** | Aujourd'hui alimenté dans `MigrationController`, l'endpoint legacy `Upload` et `Update` — jamais dans `Create`, le chemin réel |
|
||
| 3 | Backfill (§1ter point 5) | **`StoragePath` est un simple `UPDATE` SQL** (chemin déterministe) ; seul `SizeBytes` exige de lister Firebase |
|
||
| 4 | Flag watermark (§1ter point 6) | Le `if instanceId == "633ee379…"` de `ResourceController:265` est intact, TODO d'origine compris |
|
||
| 5-7 | Compression images, quota autoritaire, alerte budget GCP (§1ter 7-9) | Hors schéma, avant la release |
|
||
|
||
⚠️ **Le backfill ne porte pas sur « chaque ligne `Resource` ».** Mesuré : sur 45 lignes, **37** ont un blob, **7** sont des types URL (`ImageUrl`/`VideoUrl`/`JSONUrl`) sans aucun fichier — leur donner un `StoragePath` ou les compter dans le quota serait faux — et **1** est de type fichier sans URL du tout. `SizeBytes` vaut **0 partout** aujourd'hui : le quota de stockage ne veut rien dire pour personne.
|
||
|
||
### Restes non chiffrés, à caser en cours de route
|
||
|
||
~~Ratio jetons → questions (`/1000`) à caler sur du réel~~ ✅ **des deux côtés le 2026-08-13.** Serveur : `InstanceQuotaDTO.aiTokensPerQuestion`, mesuré sur les `VisitorQuestion.TokensUsed` de l'instance, avec un seuil de 20 questions avant de faire foi ; en dessous, repli sur l'hypothèse de la grille tarifaire. Front : `guide_ia_screen` lit ce diviseur au lieu du `/1000` codé en dur, et **masque la ligne « ~N questions »** si l'appel échoue. ⚠️ **Le `/1000` était faux d'un facteur 10** — la grille vend Premium à 20 M de jetons pour ~2 000 questions, soit **10 000 par question** — et c'est un chiffre montré au client : le crédit restant affiché était dix fois trop généreux.
|
||
|
||
⛔ ~~Endpoint de mise à jour d'`ApplicationInstance`~~ **n'a jamais manqué** (vérifié le 2026-08-13) : `ApplicationInstanceController` porte un CRUD complet (`POST :75`, `PUT :115`) et `InstanceController.Updateinstance` écrit déjà `IsMobile`/`IsTablet`/`IsWeb` (`:209-211`). Activer un canal est faisable par l'API depuis le début ; ce qui manque est l'**écran** SuperAdmin, parqué en V2 (voir §« Add-on IA »). ⛔ ~~Quotas seed 5M/20M~~ **déjà ajustés** par `AlignSubscriptionPlansWithPricing` : Essentiel 0, Pro 0, Premium 20 M, Enterprise `long.MaxValue`. Le « 5M » datait d'avant l'alignement.
|
||
|
||
~~`features.reqPerMonth` de la landing~~ ✅ corrigé, la clé n'existe plus dans `myinfomate-landing/src` (vérifié le 2026-08-11).
|
||
|
||
### Hors périmètre V1, assumé
|
||
|
||
Ingestion documentaire (PdfPig + OCR + formats + UI `IncludeInAiKnowledge`, avec quota et `StoragePath` en prérequis), TTS pré-généré, guides multiples et thématiques, talking head, génération de visites.
|
||
|
||
### Décisions figées — ne pas rouvrir
|
||
|
||
`gemini-embedding-001` à 768 dims · voix **Viva** (`Sulafat`) / **Marco** (`Umbriel`), ✅ **écoutées et validées le 2026-08-07** · découpage V1/V2 · pas de bascule « répondre avec ses connaissances générales » · sources visibles côté client, pas côté visiteur.
|
||
|
||
**Ordre de grandeur total : ~3 semaines**, dont une bonne moitié dans le lot 3 et les 13 sous-types.
|
||
|
||
### 📍 ~~Reprendre ici~~ — périmé, conservé pour la trace (ordre arrêté le 2026-08-10)
|
||
|
||
> ⛔ **Ne plus repartir d'ici : les trois étapes ci-dessous sont terminées** (A le 10/08, B le 10/08, C le 12/08), et le développement V1 est clos depuis le 2026-08-13. **Le point d'entrée est [v1-plan.md](../v1-plan.md)** ; l'état du jour est au § « Les lots F et J sont clos » ci-dessus. Ce qui reste avant la prod est du test, du juridique et de l'infra — plus une ligne de code applicatif.
|
||
|
||
Les lots 1 et 2 sont terminés. **Le lot 3 et le lot médias sont tous les deux obligatoires** — l'ordre ci-dessous ne trie pas par importance mais par coût de report. Ni l'un ni l'autre ne bloque plus la bascule : ce qui devait partir avec la migration, c'était le schéma, et il est en place.
|
||
|
||
| Étape | Quoi | Pourquoi dans cet ordre |
|
||
|---|---|---|
|
||
| ~~**A**~~ ✅ 2026-08-10 | **`StoragePath` + `SizeBytes` renseignés à `Create`** (`ResourceController:340`, types URL 2/3/7 exclus) + `manager-app` envoie `sizeBytes` **avant** l'upload (`resources_screen.dart:210`) | Ce n'est pas un chantier, c'est une fuite : chaque upload créait une ligne incomplète de plus. Corrigée, le backfill de l'étape C est **définitif**. Reste non autoritaire : la taille vient du client (§4 point 2 du plan médias) |
|
||
| **B** ✅ **exécuté** 2026-08-10 | **Lot 3 — le RAG**, livré *et* joué contre la vraie base + la vraie API Google : **52 sections → 2150 morceaux en 53 s**, recherche cosinus et bonus de langue vérifiés en conditions réelles. Voir le § « Première exécution réelle » ci-dessous | Le jalon démontrable en clientèle est atteint sur le contenu CMS. Reste le jugement produit — la qualité des réponses — qu'aucun test ne donnera |
|
||
| **C** | **Reste du lot médias** : backfill des 37 lignes + inventaire des orphelins, watermark, compression, quota autoritaire, alerte budget | Le backfill exige de lister le bucket Firebase — plus long, et aucun intérêt à le jouer deux fois |
|
||
|
||
**Amorce pour l'étape A**, utilisable telle quelle :
|
||
|
||
> *« Lis `DOCS/v2/media-storage-plan.md` §5. Renseigne `StoragePath` et `SizeBytes` à la création d'une `Resource` dans `ResourceController.Create` — c'est le chemin réellement emprunté depuis la bascule Firebase, et aucun des deux n'y est écrit aujourd'hui. Le chemin est `pictures/{instanceId}/{resourceId}`. Ne rien écrire pour les types URL (`ImageUrl`, `VideoUrl`, `JSONUrl`) : ils n'ont pas de blob. »*
|
||
|
||
**Amorce pour l'étape B** :
|
||
|
||
> *« Lis `DOCS/STATUS.md` §1quater puis `DOCS/v2/rag-pgvector-integration-plan.md`. Le schéma, `IEmbeddingService`, `IVectorStoreService`, les 13 `GetEmbeddableText()`, le pipeline Hangfire et l'outil `SearchKnowledge` sont livrés et compilent. Reste à **faire tourner la chaîne pour de vrai** contre une base Postgres avec pgvector : vérifier qu'une section enregistrée produit bien ses morceaux, que le guide les retrouve, et régler les deux angles morts du découpage (HTML non retiré, ligne plus longue que la limite non coupée). »*
|
||
|
||
---
|