Le RAG n'avait tourne que sur une base a une seule instance -- le cas ou le
post-filtrage HNSW ne se voit pas. 14 tests Testcontainers sur l'image de
`Deployment/Dockerfile.postgres`, donc le meme PostGIS + pgvector que la
prod. Ils se sautent proprement (SkippableFact) sans demon Docker.
Ce que ca etablit :
- pgvector 0.8.6 : `SET hnsw.iterative_scan` est accepte. Ce SET est pose a
chaque recherche par SearchAsync et n'existe qu'a partir de 0.8 -- sur une
version anterieure, toute recherche echouait en production.
- Les extensions vector et postgis sont bien creees par les migrations.
- A deux instances, l'une saturant l'axe de la question a 30 contre 1,
SearchAsync rend le bon nombre de resultats, tous de la bonne instance.
Aucune fuite d'un client vers un autre.
⚠️ DEUX RESULTATS INATTENDUS, contraires a ce que le plan supposait.
1. Ce qui protege du post-filtrage n'est PAS le parcours iteratif, c'est
l'index sur (InstanceId, ContentType). Le planificateur filtre par
instance d'abord et trie exactement : l'index HNSW n'est jamais touche,
donc il n'y a rien a post-filtrer. Verifie a 620 lignes (test) et a
22 000 hors suite, avec 2 000 lignes pour l'instance cible. Un test fige
ce plan -- si cet index disparaissait, la recherche se degraderait sans
qu'aucune erreur ne le dise.
2. Et si on retire cet index, `hnsw.iterative_scan = relaxed_order` NE
RATTRAPE RIEN : le parcours s'epuise apres ~335 lignes sans avoir atteint
l'instance minoritaire, et rend zero. Le meme jeu de donnees avec l'index
HNSW construit APRES l'insertion rend bien ses 20 lignes -- c'est donc la
connectivite du graphe qui decide, et nos migrations creent l'index sur
une table vide. Le SET de SearchAsync est donc une assurance qui ne couvre
pas ce qu'on croyait ; on le garde (il est correct et sans cout), mais
c'est l'index qui fait le travail.
Deux notes d'outillage :
- Testcontainers est epingle en 3.10.0 : la 4.x parle l'API Docker 1.44 et
l'engine local plafonne a 1.43.
- L'image est construite par le CLI docker, pas par le constructeur d'images
de Testcontainers : celui-ci relit le FROM pour pre-tirer l'image de base
et ne sait pas parser `tag@sha256:`. Ce digest protege la base d'un
changement de glibc sous ses index -- il ne se retire pas pour un test.
S'ajoutent 4 tests de GetSummary contre le vrai Postgres, qui levent le
caveat du commit precedent : le provider InMemory evalue tout cote client,
donc il ne disait rien de la traduction SQL. Les agregats du plan de base,
ceux du plan avance, le filtre appType et la fenetre vide traduisent tous.
dotnet test 200/200, aucun saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
C2 — POST /api/Resource/backfill-storage, SuperAdmin, dryRun à true par
défaut : la migration se joue sur une base vide, ce backfill sur des lignes
de production. StoragePath par ResourceStorage.PathFor, SizeBytes par HEAD.
La méthode annoncée au plan — « SizeBytes par listing du bucket Firebase » —
était inapplicable : le serveur n'avait aucun client de stockage. Le sondage
passe donc par HEAD sur l'URL publique, comme le fait déjà la migration, et
le sondeur est extrait plutôt que recopié (Helpers/ResourceSizeProbe,
consommé par MigrationController et par le backfill). Même raisonnement que
pour ResourceStorage : deux copies auraient divergé sur ce qui compte, le
sort réservé aux échecs.
L'extraction a bouché un trou que personne ne cherchait. L'original ne notait
l'échec que dans son catch, or un HEAD sur un blob absent ne lève pas : il
répond 404, sans Content-Length. Ces ressources arrivaient à 0 octet sans
figurer dans le rapport — invisibles au quota et invisibles au diagnostic,
exactement ce que le commentaire d'origine voulait empêcher.
Le « 37 lignes sur 45 » du plan n'étant pas vérifiable, le backfill rend son
propre inventaire : Orphans (aucune URL, blob peut-être jamais téléversé) et
Unsized (URL présente, bucket muet) restent séparés, ce sont deux causes
distinctes.
C3 — pré-vol du quota sur les deux chemins de création, suppression du blob
à Delete, angle mort d'Update tranché.
Deux défauts trouvés en câblant, qui n'étaient documentés nulle part :
- Le pré-vol existait déjà à moitié. Upload (multipart) contrôlait et
renvoyait 413, Create (JSON) ne contrôlait rien — or c'est le chemin
qu'emprunte manager-app, qui crée la ligne puis téléverse.
- Les deux lectures du quota divergeaient. Upload lisait le quota du plan,
GetQuota celui de l'instance avec le plan en repli. Une instance à quota
surchargé — le mécanisme même de l'add-on — affichait un chiffre à l'écran
et se faisait bloquer sur un autre. Helpers/StorageQuota devient la seule
source de vérité pour les deux.
Delete supprime le blob AVANT la ligne et renvoie 502 en conservant la ligne
si le bucket échoue. manager-app faisait l'inverse en avalant l'échec dans un
print : la ligne disparaissait, le blob restait, et n'ayant plus de ligne il
devenait invisible au quota tout en restant facturé. Une ressource encore
listée se rattrape ; un blob que plus aucune ligne ne désigne, non.
L'angle mort laissé ouvert par C1 était une fausse crainte : PathFor ne
construit qu'un pictures/{instanceId}/{resourceId}, le type n'entre pas dans
le chemin, il décide seulement s'il y en a un. Recalculer ne peut donc pas
pointer ailleurs, et Update rejoue Apply.
Aucun secret nouveau : FirebaseAdmin était déjà référencé pour les
notifications push et Startup charge déjà un service account, donc
Google.Cloud.Storage.V1 réutilise le même GoogleCredential. Seule s'ajoute la
clé Firebase:StorageBucket, vide par défaut — à renseigner en prod (I9),
sans quoi Delete ne supprime rien et ne prétend pas le contraire.
dotnet build vert, dotnet test 163/163 (148 au départ, +7 pour C2, +8 pour C3).
Contient aussi le correctif d'indexation préparé en parallèle : un job
Hangfire par section dans BackfillInstanceAsync au lieu d'une boucle, et un
backoff sur 429/503 dans GoogleEmbeddingService.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ingestion et indexation
- IIngestionService/IngestionService : chargement des collections filles par
sous-type, un jeu de morceaux par langue, ChunkIndex continu.
- SectionIndexingInterceptor retenu comme unique déclencheur : les 5
sous-contrôleurs totalisaient 30 SaveChanges et 0 Enqueue, donc ajouter des
points d'intérêt à une carte ne réindexait rien.
- HTML retiré avant l'embedding et lignes trop longues recoupées : sans cela un
article dépassait l'entrée max du modèle et emportait son lot de 50 morceaux.
- Gabarits de LanguageInit filtrés, DistinctBy(Text) avant le Take : ils
occupaient les cinq premiers résultats d'une recherche en néerlandais.
Endpoints du guide IA
- GET /api/Ai/knowledge/{id} : agrégats sur ContentEmbedding, donc sur ce qui
est réellement indexé — compter les sections publiées serait plus flatteur et faux.
- GET /api/Ai/insights/{id} : miroir de GuideIaInsights côté manager-app, c'est
l'écran qui a fixé la forme pour que le job de thèmes la remplisse.
RGPD
- VisitorQuestion journalisée dans AiController.Chat. HasAnswer se déduit des
sources du retrieval, pas du texte : un repli poli ressemble à une réponse.
L'écriture n'échoue jamais la réponse au visiteur.
- VisitorQuestionPurgeService, 90 jours, actif sans condition de configuration :
une durée écrite dans les CGU n'est pas un réglage commercial.
Corrections
- Updateinstance ne recopiait pas les quotas du nouveau plan.
- CheckQuota ne bloquait ni ne comptait à quota 0 — IA gratuite non comptée.
- StoragePath et SizeBytes renseignés à Create, types URL exclus.
dotnet build 0 erreur, dotnet test 130/130.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75 fichiers, ~1 mois de travail depuis f222861 (17/07). Contenu :
Onboarding self-service — jamais exécuté de bout en bout
OnboardingController, StripeWebhookController, StripeService, ResendEmailService
+ IEmailService (10 templates), EmailTemplates/, TrialLifecycleService (Hangfire),
PasswordTokenHelper, SlugHelper, 5 DTOs. Essai 14 j, Stripe customer/Checkout/Tax,
mot de passe oublié + invitation user, plafond IA d'essai.
Schéma Postgres v3 — passe pendant que la base est vide
ContentEmbedding + index HNSW (vector_cosine_ops), IEmbeddingService +
GoogleEmbeddingService (gemini-embedding-001, 768 dims), Deployment/Dockerfile.postgres
(postgis 3.4.3 + pgvector 0.8.6, épinglé par digest — un tag mobile rejouerait le
warning de collation glibc). Colonnes Resource : StoragePath, FileName,
IncludeInAiKnowledge, AiIndexStatus + nouveaux ResourceType ajoutés EN FIN d'enum.
Guide IA
Champs Guide* sur Instance + InstanceDTO, AssistantService lit la configuration client
dans les 4 blocs de prompt (ton codé en dur retiré, règle hors-sujet ajoutée aux deux
variantes qui n'en avaient pas), IHttpClientFactory à la place des new HttpClient().
Table VisitorQuestion + ConversationId sur AiChatRequest.
Stats
Rétention unifiée à 13 mois (instances ET plans), VisitEventPurgeService.
Nettoyage
IsStepLocked / IsHiddenInitially / FactContent supprimés de GuidedStep — IsStepLocked
rendait une étape définitivement infranchissable même après réussite.
SectionMap allégé (-57 lignes).
Tests
SectionParcoursControllerTests, FakeConfiguration, FakeEmailService.
ContentEmbedding a cassé 116 tests sur 124 (EF InMemory ne connaît pas Vector) :
l'entité est exclue quand le provider n'est pas Npgsql. Conséquence assumée —
le vector store n'est couvert par aucun test. dotnet test 124/124.
10 migrations EF. Base locale à jour, dotnet build 0 erreur.
Rien n'est en prod : la bascule Mongo → Postgres est décrite dans DOCS/STATUS.md §1quinquies.
⚠️ appsettings.json contient les clés Stripe (test) et Resend (prod) en clair — à rotationner.