4 Commits

Author SHA1 Message Date
Thomas Fransolet
3d23e05351 Misc update ressource to mediatheque ! 2026-09-04 16:49:26 +02:00
Thomas Fransolet
cb76df5ddf Purge du journal d'audit — poste du lot J oublié à la première passe
AuditLog porte un UserId, et les valeurs avant/après d'une modification de User
contiennent e-mail, prénom et nom : conservés sans limite jusqu'ici, quand
VisitEvent purge à 13 mois et VisitorQuestion à 90 jours. Le plan l'assignait
explicitement au lot J, il n'avait pas été fait.

12 mois, uniforme. Pas 13 : les 13 mois des statistiques existent pour comparer
une saison à la précédente, les recopier ici serait du mimétisme — un test fige
l'écart voulu. Écarté aussi : garder les suppressions plus longtemps que les
créations, ça double les règles pour un cas qu'une sauvegarde couvre déjà.

Inerte tant qu'Audit:RetentionDays n'est pas défini, même verrou que les
VisitEvent : c'est une suppression définitive et le pg_dump n'est pas en place.
Volontairement différent de VisitorQuestionPurgeService, qui tourne sans
condition parce que ses 90 jours sont un engagement des CGU, pas un réglage.

La suppression se teste contre un vrai Postgres : ExecuteDeleteAsync n'est pas
traduisible par le provider InMemory, et l'écrire en chargeant les lignes puis
RemoveRange l'aurait rendue testable en mémoire au prix de charger un an de
journal — dégrader le code de production pour satisfaire un provider de test.

dotnet test : 213 passés, 16 sautés, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:29:35 +02:00
Thomas Fransolet
bd484db48a Lot J : agrégats de thèmes, job de regroupement, interrupteur de collecte
Table QuestionThemeMonthly (instance, mois, thème, compteur). C'est elle qui rend
tenable le §8.4 des CGU : le regroupement vivait dans ThemeId, colonne de la ligne
VisitorQuestion, donc la purge du 90e jour l'emportait avec la question et le
client perdait tout au 91e. Elle ne porte que des compteurs — aucune donnée
personnelle, ce qui est précisément ce qui l'autorise à survivre. Insights lit
désormais les thèmes dans cette table, pas dans les questions de la fenêtre.

Liste fixe de 8 thèmes, pas de thèmes découverts par l'IA : des libellés
régénérés à chaque passage donneraient « Horaires » en janvier et « Questions
d'horaires » en février, deux lignes distinctes et une courbe qui ne veut rien
dire — alors que la table existe pour porter cet historique.

Le job tourne à 2 h, la purge à 3 h 30 : une question purgée avant d'avoir été
classée ne compte dans aucun agrégat et rien ne peut la rattraper. Le plafond de
500 par passage ne perd rien, il retarde — les plus anciennes d'abord, un passage
par jour, avertissement si le retard dépasse un passage. Les jetons ne sont pas
décomptés du quota client : il n'a pas demandé ces appels. Un lot en échec n'est
pas marqué « Autre » pour s'en débarrasser, ce serait une perte définitive
maquillée en résultat ; et la relecture se fait par numéro, jamais par position,
pour qu'une ligne manquante ne décale pas les suivantes.

Instance.IsVisitorQuestionCollectionEnabled (défaut true) + garde dans Chat : le
client est responsable de traitement, la collecte était inconditionnelle.

Ajout d'une fabrique design-time : EF construisait tout l'hôte pour trouver le
contexte, et l'hôte ouvre une connexion au démarrage — générer une migration
exigeait donc une base joignable, impossible sur une machine sans Postgres ni
Docker.

dotnet test : 211 passés, 15 sautés, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:58:35 +02:00
Thomas Fransolet
2b1b20cd49 Le vector store est enfin eprouve a DEUX instances, sur un vrai Postgres
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>
2026-08-12 15:34:56 +02:00