Recommandations — moteur générique de règles chargées sans redéploiement #116

Closed
opened 2026-09-03 12:25:30 +00:00 by gabriel · 1 comment
Member

Issu de la réduction de périmètre du 03/09 (voir docs/BACKLOG.md, §« Réductions de périmètre assumées », décision 1).

Contexte. Pour la fenêtre du 11/09, #39 est réduit à trois règles écrites dans le code. Ce ticket porte le reste : un moteur générique.

Ce qu'il faut faire.

  • Charger un jeu de règles depuis un fichier ou une table, versionné.
  • Évaluer les règles de façon générique, sans code par règle.
  • Ajouter, désactiver ou modifier une règle sans redéploiement.

Critères d'acceptation.

  • Une règle ajoutée au fichier de règles est prise en compte sans redémarrage du service.
  • Chaque recommandation émise cite la règle et les valeurs qui l'ont déclenchée.
  • Le jeu de règles actif est horodaté et traçable à une version.

Exigence couverte : EF-09. Épreuve : EC06. Hors fenêtre (Portée/Post-jury).
Vérification : tests/unit/test_moteur_regles.py, joué par pytest tests/unit/test_moteur_regles.py -v.

Issu de la réduction de périmètre du 03/09 (voir `docs/BACKLOG.md`, §« Réductions de périmètre assumées », décision 1). **Contexte.** Pour la fenêtre du 11/09, #39 est réduit à trois règles écrites dans le code. Ce ticket porte le reste : un moteur générique. **Ce qu'il faut faire.** - Charger un jeu de règles depuis un fichier ou une table, versionné. - Évaluer les règles de façon générique, sans code par règle. - Ajouter, désactiver ou modifier une règle sans redéploiement. **Critères d'acceptation.** - Une règle ajoutée au fichier de règles est prise en compte sans redémarrage du service. - Chaque recommandation émise cite la règle et les valeurs qui l'ont déclenchée. - Le jeu de règles actif est horodaté et traçable à une version. Exigence couverte : EF-09. Épreuve : EC06. Hors fenêtre (`Portée/Post-jury`). Vérification : `tests/unit/test_moteur_regles.py`, joué par `pytest tests/unit/test_moteur_regles.py -v`.
gabriel added the due date 2026-09-11 2026-09-03 12:41:08 +00:00
gabriel added this to the Post-jury milestone 2026-09-03 15:06:07 +00:00
gabriel added this to the EnerVision project 2026-09-03 15:19:32 +00:00
Member

Développé le 09/09, demande de fusion #241. Le ticket reste Portée/Post-jury : la décision du 03/09 n'est pas rouverte par ce développement, la fusion est une décision d'équipe.

Les trois critères d'acceptation

« Une règle ajoutée au fichier de règles est prise en compte sans redémarrage du service. »

Le job tourne en cron à :45, donc chaque passe est un processus neuf qui relit le fichier. Éprouvé aussi une couche plus bas — deux chargements successifs dans la même instance voient bien deux jeux différents, donc rien n'est mémorisé au niveau du module (test_une_regle_ajoutee_au_fichier_est_prise_en_compte_sans_redemarrage). La désactivation par retrait du fichier est couverte par le cas voisin.

Relevé sur le serveur, en pointant le lanceur sur un fichier posé à la main, sans rien redémarrer :

INFO recommendations.job jeu de règles 1.0.0 (empreinte e3208461c9d140c0)
     chargé depuis /tmp/regles-actif.toml : r1 v1.0.0, r2 v1.0.0, r3 v1.0.0

« Chaque recommandation émise cite la règle et les valeurs qui l'ont déclenchée. »

Acquis au #154 et conservé au caractère près, ce qui était le vrai risque de ce ticket. tests/unit/rules/verdicts-de-reference.json porte les 84 verdicts produits par les trois classes Python sur 28 observations couvrant chaque branche, capturés avant leur retrait puis confrontés aux mêmes classes extraites de develop : aucun écart. Message, action, gravité, valeurs déclenchantes et motif d'indécision sont comparés à l'identique.

« Le jeu de règles actif est horodaté et traçable à une version. »

Migration 0021 : jeu_version et jeu_empreinte sur public.recommandation. La version est déclarative donc faillible ; l'empreinte est un sha-256 des octets lus. Jouée sur enervision_prod — 11 lignes réelles — en transaction annulée :

BEGIN / ALTER TABLE / ALTER TABLE / UPDATE 11 / ALTER TABLE / ALTER TABLE / DO
 jeu_version | jeu_empreinte | statut  | count
-------------+---------------+---------+-------
 avant-116   | avant-116     | resolue |     9
 avant-116   | avant-116     | active  |     2
ROLLBACK

Rejeu vérifié dans la même transaction : UPDATE 0, aucune contrainte dupliquée.

Vérification

pytest tests/unit/rules/ (le ticket annonçait tests/unit/test_moteur_regles.py ; tout l'existant vit dans tests/unit/rules/ depuis le #153, la convention l'emporte).

Rejoué sur le serveur en Python 3.12, le venv du poste n'ayant aucune dépendance applicative :

353 passed
TOTAL  702  42  94%      (palier des zones sensibles : 85 %)

ruff, ruff format, mypy --strict, shellcheck, yamllint, ansible-lint (profil production) propres. Suite complète du dépôt : mêmes 5 échecs et 149 erreurs que develop avec le même venv — aucune régression.

Ce qui n'était pas dans le ticket et a été fait

  • ADR 0014 : formes paramétrées plutôt qu'un langage d'expressions, avec les deux alternatives écartées et leur motif.
  • docs/runbooks/recommandations.md : il n'existait pas de manuel pour ce service. Modifier une règle, la vérifier avant de partir, lire un code 5, savoir quel jeu a produit quoi. Chaque commande a été exécutée sur le serveur.
  • Le §13 de docs/data/etl-pipeline.md ne listait pas cette passe, alors que le lanceur y renvoie pour sa cadence. Corrigé, et la minute du lanceur avec : son commentaire annonçait :37 derrière un chargement à :27, deux chiffres périmés depuis le passage au quart d'heure (#206). La crontab réelle porte :45.
  • Six défauts trouvés en relecture, corrigés avec leurs cas de test. Le plus grave était bloquant : la tâche Ansible validait le jeu avec un venv créé plus loin dans le même rôle, ce qui arrêtait le déploiement avant les migrations sur un hôte neuf, sans se réparer aux relances.

Réserve

Modifier une règle demande sudo sur le serveur : le fichier est root:deploy 0640, comme les quatre fichiers voisins. « Sans redéploiement » n'est donc pas « sans droits » — c'est le prix de l'auditabilité, et il est assumé dans l'ADR. À dire si quelqu'un attendait une édition libre.

Développé le 09/09, demande de fusion #241. **Le ticket reste `Portée/Post-jury`** : la décision du 03/09 n'est pas rouverte par ce développement, la fusion est une décision d'équipe. ## Les trois critères d'acceptation **« Une règle ajoutée au fichier de règles est prise en compte sans redémarrage du service. »** Le job tourne en cron à `:45`, donc chaque passe est un processus neuf qui relit le fichier. Éprouvé aussi une couche plus bas — deux chargements successifs dans la *même* instance voient bien deux jeux différents, donc rien n'est mémorisé au niveau du module (`test_une_regle_ajoutee_au_fichier_est_prise_en_compte_sans_redemarrage`). La désactivation par retrait du fichier est couverte par le cas voisin. Relevé sur le serveur, en pointant le lanceur sur un fichier posé à la main, sans rien redémarrer : ``` INFO recommendations.job jeu de règles 1.0.0 (empreinte e3208461c9d140c0) chargé depuis /tmp/regles-actif.toml : r1 v1.0.0, r2 v1.0.0, r3 v1.0.0 ``` **« Chaque recommandation émise cite la règle et les valeurs qui l'ont déclenchée. »** Acquis au #154 et **conservé au caractère près**, ce qui était le vrai risque de ce ticket. `tests/unit/rules/verdicts-de-reference.json` porte les 84 verdicts produits par les trois classes Python sur 28 observations couvrant chaque branche, capturés avant leur retrait puis confrontés aux mêmes classes extraites de `develop` : aucun écart. Message, action, gravité, valeurs déclenchantes et motif d'indécision sont comparés à l'identique. **« Le jeu de règles actif est horodaté et traçable à une version. »** Migration `0021` : `jeu_version` et `jeu_empreinte` sur `public.recommandation`. La version est déclarative donc faillible ; l'empreinte est un sha-256 des octets lus. Jouée sur `enervision_prod` — 11 lignes réelles — en transaction annulée : ``` BEGIN / ALTER TABLE / ALTER TABLE / UPDATE 11 / ALTER TABLE / ALTER TABLE / DO jeu_version | jeu_empreinte | statut | count -------------+---------------+---------+------- avant-116 | avant-116 | resolue | 9 avant-116 | avant-116 | active | 2 ROLLBACK ``` Rejeu vérifié dans la même transaction : `UPDATE 0`, aucune contrainte dupliquée. ## Vérification `pytest tests/unit/rules/` (le ticket annonçait `tests/unit/test_moteur_regles.py` ; tout l'existant vit dans `tests/unit/rules/` depuis le #153, la convention l'emporte). Rejoué **sur le serveur en Python 3.12**, le venv du poste n'ayant aucune dépendance applicative : ``` 353 passed TOTAL 702 42 94% (palier des zones sensibles : 85 %) ``` ruff, `ruff format`, mypy `--strict`, shellcheck, yamllint, ansible-lint (profil `production`) propres. Suite complète du dépôt : mêmes 5 échecs et 149 erreurs que `develop` avec le même venv — aucune régression. ## Ce qui n'était pas dans le ticket et a été fait - **ADR 0014** : formes paramétrées plutôt qu'un langage d'expressions, avec les deux alternatives écartées et leur motif. - **`docs/runbooks/recommandations.md`** : il n'existait pas de manuel pour ce service. Modifier une règle, la vérifier avant de partir, lire un code 5, savoir quel jeu a produit quoi. Chaque commande a été exécutée sur le serveur. - Le §13 de `docs/data/etl-pipeline.md` ne listait pas cette passe, alors que le lanceur y renvoie pour sa cadence. Corrigé, et la minute du lanceur avec : son commentaire annonçait `:37` derrière un chargement à `:27`, deux chiffres périmés depuis le passage au quart d'heure (#206). La crontab réelle porte `:45`. - Six défauts trouvés en relecture, corrigés avec leurs cas de test. Le plus grave était bloquant : la tâche Ansible validait le jeu avec un venv créé plus loin dans le même rôle, ce qui arrêtait le déploiement avant les migrations sur un hôte neuf, sans se réparer aux relances. ## Réserve Modifier une règle demande `sudo` sur le serveur : le fichier est `root:deploy 0640`, comme les quatre fichiers voisins. « Sans redéploiement » n'est donc pas « sans droits » — c'est le prix de l'auditabilité, et il est assumé dans l'ADR. À dire si quelqu'un attendait une édition libre.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
2026-09-11
Dependencies

No dependencies set

Reference
g2/enervision#116
No description provided.