Backend / Tests exigeant une base (push) Failing after 34s
Backend / Lint, typage et tests (push) Successful in 1m24s
Backend / Audit des dépendances (push) Successful in 57s
SonarQube / build-back (push) Successful in 1m5s
SonarQube / build-front (push) Successful in 9m39s
SonarQube / test-back (push) Failing after 51s
SonarQube / test-front (push) Failing after 5m6s
SonarQube / SonarQube (push) Skipped
`create_missing()` construisait un seul `INSERT ... VALUES` pour la totalite des propositions. Avec quatre colonnes par ligne et le plafond asyncpg de 32 767 parametres, la route echouait au-dela de 8 191 recommandations par appel, cas devenu realiste maintenant que la detection interne (#104) alimente `alert` en continu. L'insertion passe par des lots de `TAILLE_DE_LOT` lignes, sur le patron de `app/etl/historical_import.py`. L'ADR 0006, `20-backend.md` et la description de la PR annoncaient qu'aucune source n'alimentait `alert` et que #104 n'etait pas commencee. #104 est livree sur `dev` depuis la #113 : les phrases sont corrigees plutot que laissees a vieillir dans un ADR.
82 lines
5.0 KiB
Markdown
82 lines
5.0 KiB
Markdown
# 0006 - Le moteur de règles de recommandation vit dans le backend
|
|
|
|
- Statut : accepté
|
|
- Date : 2026-09-18
|
|
|
|
## Contexte
|
|
|
|
L'issue #38 demande un « moteur de règles pour recommandations », portée par le label `ml`. Le
|
|
schéma tranche déjà la forme du résultat : `recommendation(alert_id, action, explanation,
|
|
rule_reference)`, avec `alert_id` en clé étrangère `NOT NULL` et une contrainte d'unicité
|
|
`uq_recommendation_alert_rule` sur `(alert_id, rule_reference)`. Une recommandation est donc
|
|
**dérivée d'une alerte**, jamais d'une mesure brute ni d'une prévision.
|
|
|
|
Deux emplacements se disputaient le code :
|
|
|
|
1. `ml/enervision_ml/`, sur le patron de `enervision_ml.score` livré par #37 : un script autonome
|
|
qui se connecte par `ML_DATABASE_URL`, écrit une table, et que l'API se contente de lire.
|
|
L'[ADR 0005](0005-modele-prediction-lightgbm.md) annonce d'ailleurs #38 de ce côté, en écrivant
|
|
que le scoring, le moteur de recommandations et les tests de dérive « consommeront le même
|
|
module `enervision_ml.features` ».
|
|
2. `apps/backend/app/services/`, où `apps/backend/README.md` place les « regles metier ».
|
|
|
|
## Décision
|
|
|
|
**Le moteur vit dans `apps/backend/app/services/`**, sous la forme d'un module pur
|
|
`recommendation_rules.py` (le catalogue `REGLES`) et d'une méthode `RecommendationService.generate()`
|
|
qui l'applique, persiste et valide la transaction.
|
|
|
|
Trois raisons :
|
|
|
|
- **Il n'utilise rien du ML.** Le catalogue lit `alert.type`, `alert.severity`, `alert.value` et
|
|
`alert.threshold`. Aucun modèle, aucune feature, aucun `enervision_ml.features` : la phrase de
|
|
l'ADR 0005 vaut pour le scoring (#37) et les tests de dérive (#44/#45), qui manipulent bien des
|
|
features, pas pour des règles sur alertes. Le label `ml` de #38 désigne le lot fonctionnel
|
|
« prédiction et recommandation », pas l'emplacement du code.
|
|
- **Il lit et écrit deux tables déjà couvertes par des repositories.** `AlertRepository` sait déjà
|
|
filtrer par site. Le placer dans `ml/` obligerait à réécrire ces accès en SQL brut, et à
|
|
maintenir deux représentations du même domaine.
|
|
- **Le déclencheur HTTP n'a de sens que dans l'API.** `POST /recommendations/generate` doit passer
|
|
par `require_role(Role.ADMIN)` et par la session injectée : cela suppose d'être dans
|
|
l'application FastAPI.
|
|
|
|
Le moteur reste néanmoins **déclenchable hors HTTP**, par `python -m app.cli
|
|
generate-recommendations` (cible `make recommendations`), sur le patron de `make ml-score` : rien
|
|
n'oblige à exposer un port pour régénérer des recommandations.
|
|
|
|
## Conséquences
|
|
|
|
- L'API gagne sa première route d'écriture métier. La checklist de `20-backend.md` s'applique :
|
|
entrée dans `ROLE_MINIMUM` de `tests/api/acces.py`, et `openapi.json` régénéré dans le même
|
|
commit.
|
|
- `RecommendationService` n'est plus en lecture seule : il reçoit le `Transaction` Protocol déjà
|
|
utilisé par `AuthService` et `UserService`, et commite lui-même. Les repositories continuent de
|
|
ne pas commiter.
|
|
- **L'idempotence est déléguée à la base.** `create_missing()` insère en `ON CONFLICT DO NOTHING`
|
|
sur `uq_recommendation_alert_rule` plutôt que de relire avant d'écrire, ce qui supprime la
|
|
fenêtre entre le contrôle et l'insertion. Corollaire : `rule_reference` est une clé fonctionnelle.
|
|
Une règle dont le sens change prend une référence `-v2` ; renommer une référence livrée
|
|
ferait réapparaître ses recommandations à côté des anciennes.
|
|
- **Le moteur est branché sur la détection interne, et sur elle seule.** `alert` est alimentée
|
|
par `app/detection/internal_alerts.py` (#104), lancée à la main comme `enervision_ml.score` ;
|
|
l'ingestion de l'API Mock `/alerts` reste à faire. Le rapport de génération est donc à zéro tant
|
|
que la détection n'a pas tourné, sans que le moteur soit à retoucher.
|
|
- **L'insertion est découpée en lots.** `create_missing()` écrit par paquets de `TAILLE_DE_LOT`
|
|
lignes : asyncpg plafonne une requête à 32 767 paramètres, soit 8 191 lignes de quatre colonnes,
|
|
et la détection interne peut alimenter `alert` au fil de l'eau.
|
|
- Si le projet devait un jour pondérer les recommandations par un score appris, la décision serait
|
|
à rouvrir : le moteur redeviendrait consommateur du pipeline ML.
|
|
|
|
## Alternatives écartées
|
|
|
|
- **Module et CLI dans `ml/enervision_ml/`** : cohérent avec le label `ml` et avec la lettre de
|
|
l'ADR 0005, mais impose du SQL brut là où deux repositories existent, et laisse la génération
|
|
hors de portée de l'API. Redeviendrait le bon choix si les règles se mettaient à consommer des
|
|
features ou un modèle.
|
|
- **Génération à la volée, sans persistance**, calculée à chaque `GET /recommendations` : supprime
|
|
le besoin d'écriture, mais rend la table `recommendation` et sa contrainte d'unicité inutiles,
|
|
et interdit toute trace de ce qui a été proposé et quand.
|
|
- **Table de configuration des règles en base**, plutôt qu'un catalogue en Python : plus souple,
|
|
mais déplace la logique métier hors de la revue de code et hors des tests, pour un besoin que
|
|
rien n'exprime à ce stade.
|