[EF-09] Spécification métier des trois règles de recommandation #153
Labels
No labels
Compat/Breaking
EC01
EC02
EC03
EC04
EC05
EC06
Kind/BDD
Kind/Back
Kind/Bug
Kind/CICD
Kind/Cloud
Kind/Contenu
Kind/Data
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Front
Kind/IA
Kind/Infra
Kind/Monitoring
Kind/Security
Kind/Testing
Portée/Post-jury
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
ops/alerte
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
g2/enervision#153
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Exigence couverte
EF-09 — Émettre des recommandations issues de règles versionnées, horodatées, justifiées par leurs valeurs déclenchantes.
Épreuve servie
EC06 · IA et automatisation
Charge estimée
0,5 j.h (rédaction PO). L'implémentation est portée par #39.
Ce qu'on veut obtenir
Le contenu métier des trois règles de recommandation attendu par le critère 3 de
#39 : pour chacune, ce qu'elle observe et sur quelle fenêtre, la condition exacte
qui la déclenche, la phrase rendue à l'utilisateur, et l'action recommandée.
Les seuils ci-dessous (0,90 / 0,85 / 0,80) sont des valeurs de départ assumées,
choisies pour être explicables devant un exploitant. Leur recalage après 24 à
48 h de données réelles fait l'objet d'un ticket de suivi distinct ; le module
est conçu pour qu'un changement de seuil ne touche pas la mécanique.
Cadre commun aux trois règles
prevision,mesure_horaire,qualite_jour) et le référentielsites(capacity_kw). Aucune lecture deszones argent ou bronze, ni de l'API de simulation.
nouvelle prévision est disponible.
condition tient. Elle passe à
resolved(avecresolved_at) quand lacondition retombe. Même schéma que les alertes de #42.
porte
rule_id,rule_version(sémantique),evaluated_at(UTC), et un objettriggering_valueslistant les valeurs comparées, nommées.r1,r2,r3) et uneversion sémantique. Un changement de seuil incrémente le correctif ; l'ancienne
version reste consultable, pour qu'une recommandation passée reste rattachable
au texte exact qui l'a produite.
Placeholders des gabarits :
{site_name}(référentiel),{heure_locale}(Europe/Paris à l'affichage), les valeurs numériques arrondies à l'entier, les
pourcentages sans décimale.
R1 — Dépassement prévisionnel de capacité
prevision), rapportée àcapacity_kwdu site.prevision_kw >= 0,90 * capacity_kwprevision_kw,capacity_kw,ratio(= prevision_kw / capacity_kw),seuil(0,90)R1 est l'étage anticipation. Le signal de dépassement ferme (≥ 100 % de la
capacité) relève d'EF-08 / #37 : à coordonner avec Justine pour que les deux ne
disent pas la même chose au même moment, mais R1 reste distinct.
R2 — Pointe de charge persistante
mesure_horaire.moyenne / sites.capacity_kw) sur les trois dernières heures complètes.>= 0,85 * capacity_kw.charges_horaires(liste des 3 ratios, du plus ancien au plus récent),capacity_kw,seuil(0,85)R3 — Fiabilité des données insuffisante
qualite_jourdu site pour la journée en cours : taux de disponibilité et nombre de relevés manquants ou reconstitués.taux_disponibilite_jour < 0,80taux_disponibilite,releves_manquants,releves_imputes,seuil(0,80)Critères d'acceptation
sites.rule_id,rule_version,evaluated_at(UTC) et un objettriggering_valuesavec les valeurs comparées nommées.resolvedavecresolved_atquand la condition retombe.Comment on le vérifie
Hors périmètre
Spécification claire et complète, il n'y avait rien à deviner. Les trois règles
sont implémentées avec les seuils exacts, les gabarits substitués et les
triggering_valuesnommées : c'est la #154.Deux écarts entre ce ticket et le schéma de la zone or, que je n'ai pas
tranchés seul parce qu'ils touchent des migrations qui ne sont pas à moi.
1.
releves_imputesn'est pas une colonneR3 doit citer
releves_manquantsetreleves_imputes. La migration 0013 donne :Le nombre de relevés reconstitués vit donc dans
repartition_methode, pas dansune colonne à lui. Le contrat d'entrée expose le champ et la règle s'en sert ;
c'est la requête qui l'alimentera qui devra l'extraire du jsonb.
Rien de bloquant, mais autant que ce soit écrit quelque part avant que quelqu'un
cherche une colonne qui n'existe pas.
2. La table
recommandationne sait pas encore décrire un cycle de vieLe ticket demande qu'une recommandation « passe à
resolved, avecresolved_at, quand la condition retombe ». La migration 0014 donne :Ni statut, ni
resolved_at, ni champ d'action : le message et l'actionrecommandée devraient tenir dans le seul
libelle.Et la contrainte d'unicité décrit un autre modèle :
(site_id, horodatage, regle_id, regle_version)sert un rejeu idempotent par horodatage, c'est-à-direune ligne par évaluation. Une recommandation qui s'ouvre une fois et se referme
trois heures plus tard n'a pas d'horodatage unique, elle a une plage.
Les deux modèles sont défendables, ils ne sont simplement pas le même.
Côté code, le rapprochement est prêt et éprouvé :
cycle.rapprocherdit quoiouvrir, quoi garder, quoi résoudre, sans stocker d'état. Il attend juste de savoir
où écrire.
Ce que ça demande : une décision de schéma, donc @olivier pour la zone or. Soit
on ajoute statut,
resolue_aetaction, soit on assume une ligne parévaluation et le « pas de ré-émission » se règle à la lecture plutôt qu'à
l'écriture.
Un point de coordination, comme tu le demandais
R1 prévient à 90 %, l'EF-08 constate à 100 %. Pour qu'ils ne parlent pas en même
temps sans se contredire, il faut que les deux sortent la même valeur de capacité
et la même prévision. R1 lit
prevision.valeur_prevue_kwetsite.capacite_kw,rien d'autre. À caler avec @justine sur le #37.
Une précision sur les seuils
Les trois vivent chacun dans une constante en tête de leur module, et les tests
éprouvent les bornes des deux côtés : 90 % pile déclenche R1, le ticket disant
« >= » ; 80 % pile ne déclenche pas R3, le ticket disant « < ». Le recalage
d'après observation coûtera un nombre et un incrément de correctif.
Repris les deux.
1.
releves_imputesPas de colonne dédiée, pas de souci : c'est
repartition_methode(jsonb) de 0013 qui porte l'info, la requête qui alimenteObservation.releves_imputesl'en extrait. Pas de migration à faire pour ça, juste à documenter dans le module recommandations que ce champ dérive du jsonb et non d'une colonne. Rien à trancher, le ticket reste tel quel.2. Cycle de vie de
recommandationLe ticket est explicite (« passe à
resolvedavecresolved_at») et l'implémentation de #39 est écrite pour ce modèle-là (cycle.rapprochercompare l'existant à ce qu'une passe vient de produire, à l'identité site + règle). Donc on ajoute les colonnes plutôt que de recoder le rapprochement pour tenir dans le rejeu idempotent actuel.En revérifiant : l'
alertede 0012, à laquelle je pensais en écrivant « même schéma que #42 », est elle-même idempotente par horodatage — la référence, c'était le cycle firing/resolved de l'alerting Grafana du #42, pas cette table. Il n'y a donc pas de schéma existant à copier ici, seulement le principe à reproduire.Migration à ouvrir sur
recommandation(0014) :statut:active/resolue, défautactiveresolue_a:timestamptz, nullableaction:text not null— aujourd'hui seullibelleexiste, mais message et action sont deux choses distinctes dans le contrat de #39,libellene doit pas porter les deux concaténés(site_id, horodatage, regle_id, regle_version)ne peut plus servir d'identité : celle d'une recommandation active est(site_id, regle_id)d'après #39. À remplacer par un index unique partiel(site_id, regle_id) where statut = 'active', qui garantit une seule recommandation ouverte par site et par règle sans empêcher l'historique des résolues.@olivier pour la migration — dites si ça convient côté zone or ou si vous voyez un obstacle, sinon j'ouvre le ticket de suivi.
Validé côté zone or, même conclusion que @gabriel sur les deux points.
1.
releves_imputes: pas de migration, et pas par facilitéLe jsonb de 0013 a été choisi exactement pour ça — le jeu des méthodes suit les
trois régimes de l'ADR 0006 et bougera encore, une colonne par méthode imposerait
une migration à chaque nouveau régime. Une colonne dédiée serait en plus le
doublon d'une valeur déjà dérivable, donc une seconde source de vérité à tenir
cohérente :
À documenter dans le module recommandations, comme proposé. Le ticket reste tel quel.
2. Cycle de vie : on ajoute les colonnes
Trois raisons, dans l'ordre où elles pèsent.
idempotent par horodatage, « la condition est retombée » se lit « pas de ligne à
cette heure » — indistinguable de « le job horaire n'a pas tourné ». EF-09 veut
une recommandation relisible six jours plus tard : on n'audite pas une absence.
statutetresolue_aen font un fait écrit.règle » est une contrainte d'écriture, et l'index unique partiel la garantit.
Reportée à la lecture, elle serait à reconstruire par fenêtre dans chaque
requête — tableau de bord, API, export — et céderait au premier appelant qui
l'oublie. Accessoirement : 7 sites × 3 règles × 24 h, ~500 lignes/jour de
quasi-doublons pour trois états utiles, et aucun
reco_idstable à désigner.recommandationn'est pas une hypertable — seulesmesureetprevisionle sont (0007) : l'altern'a pas le coût dedécompression qui rend une migration jouée au démarrage imprévisible. Ajout de
colonne, donc métadonnée.
Trois points d'exécution pour le ticket de suivi
0017_zone_or_recommandation_cycle.sql, enalterseulement,0014 non retouché. Base neuve = 0014 puis 0017, bases du serveur = 0017 seul :
les deux chemins convergent sans le risque de dérive entre
createetalterdu motif 0008/0010.
action text not nulléchoue si la table porte déjà des lignes :default ''puis
drop default, ou nullable — à trancher en regardant préprod. Et lacontrainte à retirer porte son nom généré, à relever avant d'écrire le
drop constraint.NOUVEAUX, danstests/unit/db/test_migrations_zone_or.py, est une listefigée : y ajouter 0017, sinon sa section « Retour arrière » et ses
if not existsne sont gardés par rien.Pas d'obstacle côté zone or : @gabriel, ouvre le ticket de suivi.