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

50 lines
2.0 KiB
C#

using System.Collections.Generic;
using System.Linq;
namespace ManagerService.Services
{
/// <summary>
/// Liste fixe des thèmes de questions visiteurs.
///
/// ⚠️ **Fixe, et c'est le point de conception.** Des thèmes découverts librement par l'IA
/// colleraient mieux à chaque lieu, mais les agrégats mensuels ne se compareraient plus :
/// « Horaires » en janvier et « Questions d'horaires » en février seraient deux lignes
/// distinctes, et la courbe que le client regarde ne voudrait rien dire. Or la table
/// d'agrégats existe précisément pour survivre à la purge et porter cet historique.
///
/// ⚠️ **Ajouter un thème est possible, en renommer un ne l'est pas** : le libellé est la
/// clé de <see cref="Data.QuestionThemeMonthly.Theme"/>, donc le renommer coupe l'historique
/// en deux. Un nouveau thème, lui, apparaît simplement à partir du mois où il est ajouté.
/// </summary>
public static class QuestionThemes
{
public const string Other = "Autre";
public static readonly IReadOnlyList<string> All = new[]
{
"Horaires et tarifs",
"Accès et transport",
"Accessibilité",
"Œuvres et contenu",
"Services sur place",
"Événements et animations",
"Orientation dans le lieu",
Other,
};
/// <summary>
/// Ramène la réponse du modèle sur un thème connu. Une valeur inattendue tombe dans
/// « Autre » plutôt que de créer une ligne d'agrégat parasite — le modèle peut
/// paraphraser, la table ne doit pas en souffrir.
/// </summary>
public static string Normalize(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return Other;
var trimmed = raw.Trim().Trim('"', '.', '-', ' ');
return All.FirstOrDefault(t => string.Equals(t, trimmed, System.StringComparison.OrdinalIgnoreCase))
?? Other;
}
}
}