[EF-09] Spécification métier des trois règles de recommandation #153

Closed
opened 2026-09-04 12:10:19 +00:00 by gabriel · 3 comments
Member

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

  • Sources : uniquement la zone or (prevision, mesure_horaire,
    qualite_jour) et le référentiel sites (capacity_kw). Aucune lecture des
    zones argent ou bronze, ni de l'API de simulation.
  • Cadence : évaluation à chaque heure, quand un nouvel agrégat ou une
    nouvelle prévision est disponible.
  • Cycle de vie : une recommandation active n'est pas ré-émise tant que sa
    condition tient. Elle passe à resolved (avec resolved_at) quand la
    condition retombe. Même schéma que les alertes de #42.
  • Traçabilité de sortie (critère 2 de #39) : chaque recommandation émise
    porte rule_id, rule_version (sémantique), evaluated_at (UTC), et un objet
    triggering_values listant les valeurs comparées, nommées.
  • Versionnage : chaque règle porte un identifiant (r1, r2, r3) et une
    version 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é

Observe La consommation prévue pour l'heure H+1 (table prevision), rapportée à capacity_kw du site.
Fenêtre L'heure suivante, réévaluée à chaque prévision servie.
Déclenche si prevision_kw >= 0,90 * capacity_kw
triggering_values prevision_kw, capacity_kw, ratio (= prevision_kw / capacity_kw), seuil (0,90)
Message « Site {site_name} : la consommation prévue à {heure_locale} atteint {prevision_kw} kW, soit {ratio_pct} % de la capacité déclarée ({capacity_kw} kW). Marge faible sur la prochaine heure. »
Action « Reporter ou délester les usages non prioritaires du site sur l'heure à venir. À défaut, confirmer avec l'exploitant que la pointe reste dans la capacité contractuelle. »

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

Observe La charge horaire moyenne (mesure_horaire.moyenne / sites.capacity_kw) sur les trois dernières heures complètes.
Fenêtre Trois heures glissantes.
Déclenche si Les trois moyennes horaires consécutives sont toutes >= 0,85 * capacity_kw.
triggering_values charges_horaires (liste des 3 ratios, du plus ancien au plus récent), capacity_kw, seuil (0,85)
Message « Site {site_name} : la charge moyenne dépasse 85 % de la capacité depuis 3 heures ({h1_pct} %, {h2_pct} %, {h3_pct} %). La marge de sécurité est entamée. »
Action « Planifier un lissage de charge sur les heures creuses de la journée et vérifier qu'aucun équipement n'est resté en marche inutilement. Si la situation se répète sur plusieurs jours, réexaminer le contrat de fourniture du site. »

R3 — Fiabilité des données insuffisante

Observe qualite_jour du site pour la journée en cours : taux de disponibilité et nombre de relevés manquants ou reconstitués.
Fenêtre Journée calendaire en cours (UTC).
Déclenche si taux_disponibilite_jour < 0,80
triggering_values taux_disponibilite, releves_manquants, releves_imputes, seuil (0,80)
Message « Site {site_name} : {taux_pct} % seulement des relevés du jour sont exploitables ({releves_manquants} manquants, {releves_imputes} reconstitués). Prévision et charge de ce site à interpréter avec prudence. »
Action « Faire contrôler les capteurs du site. Tant que la collecte n'est pas rétablie, ne pas fonder de décision d'exploitation sur les seules valeurs de ce site. »

Critères d'acceptation

  • Les trois règles R1, R2, R3 sont implémentées avec les seuils exacts de ce ticket (0,90 / 0,85 / 0,80) et ne lisent que des tables de la zone or et le référentiel sites.
  • Chaque règle porte son identifiant et sa version ; un changement de seuil incrémente le correctif et l'ancienne version reste consultable.
  • Chaque recommandation émise cite rule_id, rule_version, evaluated_at (UTC) et un objet triggering_values avec les valeurs comparées nommées.
  • Une recommandation active n'est pas ré-émise tant que sa condition tient ; elle passe à resolved avec resolved_at quand la condition retombe.
  • Les formulations rendues (message et action) correspondent aux gabarits de ce ticket, placeholders substitués.

Comment on le vérifie

Tests      tests/unit/rules/
Commande   pytest tests/unit/rules --cov
Preuve     rapport de couverture (>= 85 %), plus trois recommandations produites,
           une par règle, avec leur justification complète

Hors périmètre

  • Le moteur générique de règles chargées sans redéploiement : #116 (Portée/Post-jury).
  • L'affichage des recommandations dans le tableau de bord.
  • Le signal de dépassement EF-08 (>= 100 % de la capacité) : #37, à coordonner mais distinct.
  • Le recalage des seuils après observation réelle : ticket de suivi dédié à ouvrir.
### 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 - **Sources** : uniquement la zone or (`prevision`, `mesure_horaire`, `qualite_jour`) et le référentiel `sites` (`capacity_kw`). Aucune lecture des zones argent ou bronze, ni de l'API de simulation. - **Cadence** : évaluation à chaque heure, quand un nouvel agrégat ou une nouvelle prévision est disponible. - **Cycle de vie** : une recommandation active n'est pas ré-émise tant que sa condition tient. Elle passe à `resolved` (avec `resolved_at`) quand la condition retombe. Même schéma que les alertes de #42. - **Traçabilité de sortie** (critère 2 de #39) : chaque recommandation émise porte `rule_id`, `rule_version` (sémantique), `evaluated_at` (UTC), et un objet `triggering_values` listant les valeurs comparées, nommées. - **Versionnage** : chaque règle porte un identifiant (`r1`, `r2`, `r3`) et une version 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é | | | |---|---| | **Observe** | La consommation prévue pour l'heure H+1 (table `prevision`), rapportée à `capacity_kw` du site. | | **Fenêtre** | L'heure suivante, réévaluée à chaque prévision servie. | | **Déclenche si** | `prevision_kw >= 0,90 * capacity_kw` | | **triggering_values** | `prevision_kw`, `capacity_kw`, `ratio` (= prevision_kw / capacity_kw), `seuil` (0,90) | | **Message** | « Site {site_name} : la consommation prévue à {heure_locale} atteint {prevision_kw} kW, soit {ratio_pct} % de la capacité déclarée ({capacity_kw} kW). Marge faible sur la prochaine heure. » | | **Action** | « Reporter ou délester les usages non prioritaires du site sur l'heure à venir. À défaut, confirmer avec l'exploitant que la pointe reste dans la capacité contractuelle. » | 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 | | | |---|---| | **Observe** | La charge horaire moyenne (`mesure_horaire.moyenne / sites.capacity_kw`) sur les trois dernières heures complètes. | | **Fenêtre** | Trois heures glissantes. | | **Déclenche si** | Les trois moyennes horaires consécutives sont toutes `>= 0,85 * capacity_kw`. | | **triggering_values** | `charges_horaires` (liste des 3 ratios, du plus ancien au plus récent), `capacity_kw`, `seuil` (0,85) | | **Message** | « Site {site_name} : la charge moyenne dépasse 85 % de la capacité depuis 3 heures ({h1_pct} %, {h2_pct} %, {h3_pct} %). La marge de sécurité est entamée. » | | **Action** | « Planifier un lissage de charge sur les heures creuses de la journée et vérifier qu'aucun équipement n'est resté en marche inutilement. Si la situation se répète sur plusieurs jours, réexaminer le contrat de fourniture du site. » | --- ### R3 — Fiabilité des données insuffisante | | | |---|---| | **Observe** | `qualite_jour` du site pour la journée en cours : taux de disponibilité et nombre de relevés manquants ou reconstitués. | | **Fenêtre** | Journée calendaire en cours (UTC). | | **Déclenche si** | `taux_disponibilite_jour < 0,80` | | **triggering_values** | `taux_disponibilite`, `releves_manquants`, `releves_imputes`, `seuil` (0,80) | | **Message** | « Site {site_name} : {taux_pct} % seulement des relevés du jour sont exploitables ({releves_manquants} manquants, {releves_imputes} reconstitués). Prévision et charge de ce site à interpréter avec prudence. » | | **Action** | « Faire contrôler les capteurs du site. Tant que la collecte n'est pas rétablie, ne pas fonder de décision d'exploitation sur les seules valeurs de ce site. » | --- ### Critères d'acceptation - [ ] Les trois règles R1, R2, R3 sont implémentées avec les seuils exacts de ce ticket (0,90 / 0,85 / 0,80) et ne lisent que des tables de la zone or et le référentiel `sites`. - [ ] Chaque règle porte son identifiant et sa version ; un changement de seuil incrémente le correctif et l'ancienne version reste consultable. - [ ] Chaque recommandation émise cite `rule_id`, `rule_version`, `evaluated_at` (UTC) et un objet `triggering_values` avec les valeurs comparées nommées. - [ ] Une recommandation active n'est pas ré-émise tant que sa condition tient ; elle passe à `resolved` avec `resolved_at` quand la condition retombe. - [ ] Les formulations rendues (message et action) correspondent aux gabarits de ce ticket, placeholders substitués. ### Comment on le vérifie ``` Tests tests/unit/rules/ Commande pytest tests/unit/rules --cov Preuve rapport de couverture (>= 85 %), plus trois recommandations produites, une par règle, avec leur justification complète ``` ### Hors périmètre - Le moteur générique de règles chargées sans redéploiement : #116 (Portée/Post-jury). - L'affichage des recommandations dans le tableau de bord. - Le signal de dépassement EF-08 (>= 100 % de la capacité) : #37, à coordonner mais distinct. - Le recalage des seuils après observation réelle : ticket de suivi dédié à ouvrir.
gabriel self-assigned this 2026-09-04 12:10:19 +00:00
gabriel removed their assignment 2026-09-04 12:21:05 +00:00
Owner

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_values nommé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_imputes n'est pas une colonne

R3 doit citer releves_manquants et releves_imputes. La migration 0013 donne :

taux_disponibilite   numeric(5,...)
releves_attendus     integer
releves_manquants    integer
repartition_methode  jsonb

Le nombre de relevés reconstitués vit donc dans repartition_methode, pas dans
une 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 recommandation ne sait pas encore décrire un cycle de vie

Le ticket demande qu'une recommandation « passe à resolved, avec
resolved_at, quand la condition retombe ». La migration 0014 donne :

reco_id, site_id, horodatage, regle_id, regle_version,
valeurs_declenchantes, libelle, alerte_id
unique (site_id, horodatage, regle_id, regle_version)

Ni statut, ni resolved_at, ni champ d'action : le message et l'action
recommandé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-à-dire
une 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.rapprocher dit quoi
ouvrir, 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_a et action, 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_kw et site.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.

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_values` nommé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_imputes` n'est pas une colonne R3 doit citer `releves_manquants` et `releves_imputes`. La migration 0013 donne : ```sql taux_disponibilite numeric(5,...) releves_attendus integer releves_manquants integer repartition_methode jsonb ``` Le nombre de relevés reconstitués vit donc dans `repartition_methode`, pas dans une 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 `recommandation` ne sait pas encore décrire un cycle de vie Le ticket demande qu'une recommandation « passe à `resolved`, avec `resolved_at`, quand la condition retombe ». La migration 0014 donne : ```sql reco_id, site_id, horodatage, regle_id, regle_version, valeurs_declenchantes, libelle, alerte_id unique (site_id, horodatage, regle_id, regle_version) ``` **Ni statut, ni `resolved_at`, ni champ d'action** : le message et l'action recommandé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-à-dire une 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.rapprocher` dit quoi ouvrir, 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_a` et `action`, 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_kw` et `site.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.
Author
Member

Repris les deux.

1. releves_imputes

Pas de colonne dédiée, pas de souci : c'est repartition_methode (jsonb) de 0013 qui porte l'info, la requête qui alimente Observation.releves_imputes l'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 recommandation

Le ticket est explicite (« passe à resolved avec resolved_at ») et l'implémentation de #39 est écrite pour ce modèle-là (cycle.rapprocher compare 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'alerte de 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éfaut active
  • resolue_a : timestamptz, nullable
  • action : text not null — aujourd'hui seul libelle existe, mais message et action sont deux choses distinctes dans le contrat de #39, libelle ne doit pas porter les deux concaténés
  • la contrainte d'unicité (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.

Repris les deux. ### 1. `releves_imputes` Pas de colonne dédiée, pas de souci : c'est `repartition_methode` (jsonb) de 0013 qui porte l'info, la requête qui alimente `Observation.releves_imputes` l'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 `recommandation` Le ticket est explicite (« passe à `resolved` avec `resolved_at` ») et l'implémentation de #39 est écrite pour ce modèle-là (`cycle.rapprocher` compare 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'`alerte` de 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éfaut `active` - `resolue_a` : `timestamptz`, nullable - `action` : `text not null` — aujourd'hui seul `libelle` existe, mais message et action sont deux choses distinctes dans le contrat de #39, `libelle` ne doit pas porter les deux concaténés - la contrainte d'unicité `(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.
florian added this to the EnerVision project 2026-09-07 07:18:20 +00:00
Member

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 :

coalesce((repartition_methode->>'interpolated')::int, 0)
  + coalesce((repartition_methode->>'forward_fill')::int, 0)

À 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.

  • Une résolution ne peut pas être une absence de ligne. Avec le rejeu
    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.
    statut et resolue_a en font un fait écrit.
  • L'invariant se tient là où il est vrai. « Une seule active par site et par
    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_id stable à désigner.
  • Le coût est nul ici. recommandation n'est pas une hypertable — seules
    mesure et prevision le sont (0007) : l'alter n'a pas le coût de
    dé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

  • Nouveau fichier 0017_zone_or_recommandation_cycle.sql, en alter seulement,
    0014 non retouché.
    Base neuve = 0014 puis 0017, bases du serveur = 0017 seul :
    les deux chemins convergent sans le risque de dérive entre create et alter
    du 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 la
    contrainte à retirer porte son nom généré, à relever avant d'écrire le
    drop constraint.
  • NOUVEAUX, dans tests/unit/db/test_migrations_zone_or.py, est une liste
    figée
    : y ajouter 0017, sinon sa section « Retour arrière » et ses
    if not exists ne sont gardés par rien.

Pas d'obstacle côté zone or : @gabriel, 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 : ```sql coalesce((repartition_methode->>'interpolated')::int, 0) + coalesce((repartition_methode->>'forward_fill')::int, 0) ``` À 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. - **Une résolution ne peut pas être une absence de ligne.** Avec le rejeu 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. `statut` et `resolue_a` en font un fait écrit. - **L'invariant se tient là où il est vrai.** « Une seule active par site et par 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_id` stable à désigner. - **Le coût est nul ici.** `recommandation` n'est pas une hypertable — seules `mesure` et `prevision` le sont (0007) : l'`alter` n'a pas le coût de dé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 - **Nouveau fichier `0017_zone_or_recommandation_cycle.sql`, en `alter` seulement, 0014 non retouché.** Base neuve = 0014 puis 0017, bases du serveur = 0017 seul : les deux chemins convergent sans le risque de dérive entre `create` et `alter` du 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 la contrainte à retirer porte son nom généré, à relever avant d'écrire le `drop constraint`. - **`NOUVEAUX`, dans `tests/unit/db/test_migrations_zone_or.py`, est une liste figée** : y ajouter 0017, sinon sa section « Retour arrière » et ses `if not exists` ne sont gardés par rien. Pas d'obstacle côté zone or : @gabriel, ouvre le ticket de suivi.
Sign in to join this conversation.
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
g2/enervision#153
No description provided.