[EF-05] Les alertes de la source entrent en zone argent (#165) #167

Merged
olivier merged 6 commits from marvin/165-alertes-zone-argent into develop 2026-09-07 12:23:07 +00:00
Member

Ce que ça change

Les alertes de la source s'arrêtaient en zone bronze : 11 546 objets collectés au fil de l'eau depuis le 3 septembre, que rien ne transformait. Cette demande ajoute silver.alerte, quatrième table de la zone argent, et l'écrit à chaque passe du job silver_daily.

Preuve

$ pytest tests/unit/silver/test_alertes_silver.py -q
30 passed in 0.03s

$ pytest tests/unit -q
628 passed

$ pytest tests/unit/silver --cov=services/etl/etl/silver
services/etl/etl/silver/alertes.py    70    0   100%
services/etl/etl/silver/job.py       124    9    93%

La chaine complete du job sur bronze reel, en lecture seule, sans rien ecrire dans MinIO :

alertes retenues pour 2026-09-06 : 2984
controle des enumerations : passe
schema == ligne produite  : True
dt tous sur la journee    : {'2026-09-06'}
alert_id uniques          : 2984 / 2984
triees par horodatage     : True

2 984, soit exactement le nombre d'objets d'alerte que bronze porte pour ce jour-la. Aucune perte, aucun doublon.

Relecture

  • Un pair a relu et laissé un commentaire, même court
  • Ses remarques sont traitées, ou une réponse explique pourquoi elles ne le sont pas

Ce qui suit le code

  • docs/runbooks/ mis à jour — etl.md annonce quatre objets par journée au lieu de trois, et dit qu'une partition d'alertes vide n'est pas une panne. C'est ce qu'un exploitant voit en premier avec mc ls.

Où regarder en priorité

1. Le choix d'architecture : pourquoi la zone argent et pas directement la zone or. C'est le point qui mérite le plus votre avis. Le §4 du pipeline définissait la zone argent comme une « série régulière », et une alerte est un événement : ni grille, ni imputation, ni validation physique. L'argument qui a tranché n'est pas la symétrie du médaillon, c'est que le collecteur délègue déjà un traitement à cette zonecollector/alertes.py range sous dt=inconnu une alerte qu'il n'a pas su dater, et son docstring annonce que « la zone argent la replacera d'après son contenu ». Personne ne l'avait écrit. charger_alertes lit donc deux partitions par site et replace d'après le payload.

Le §4, le §10, le §11 et le §14 du pipeline sont réécrits en conséquence. Si vous préférez un chemin bronze → or direct, c'est maintenant qu'il faut le dire : la table est isolée, la défaire coûte peu.

2. Le nom du fichier de test. test_alertes_silver.py et non test_alertes.py, parce que tests/unit/collector/test_alertes.py porte déjà ce nom et que pytest refuse deux modules homonymes — la collecte de la suite entière échoue, alors que le fichier passe isolément. Les deux correctifs globaux, poser des __init__.py ou passer en --import-mode=importlib, casseraient l'import des aides _echantillons et _doubles partout. Le suffixe coûte moins, mais si vous voyez mieux, je prends.

3. L'horodatage n'est pas tronqué à la minute, contrairement à celui d'une mesure. public.alerte est unique sur (site_id, horodatage, type, source) : tronquer ferait de deux alertes du même type à douze secondes d'écart une seule alerte, et la seconde disparaîtrait en silence.

Deux choses relevées en chemin, hors de ce lot

Un tiers à la moitié des alertes de la source ont une valeur en dessous de leur seuil. Sur 500 échantillonnées : threshold 34/102, spike 48/114, outage 38/96, anomaly 44/86, sensor 53/102. Une alerte « Seuil de consommation dépassé » avec value = 70.34 et threshold = 135.0 se verra sur l'écran Qualité. C'est la donnée que la source sert, et l'historiser telle quelle est ce que demande l'EF-05 — mais ça vaut un coup d'oeil avant la démo.

docs/data est ignoré par .gitignore. La règle data/ matche à tous les niveaux, donc git add docs/data/etl-pipeline.md sort un avertissement. Le fichier existant reste suivi, mais tout nouveau document déposé là serait invisible sans le moindre message. Déjà signalé par @olivier en fin de commentaire du #35. Le correctif tient en un caractère — /data/ — et ne relève pas de cette demande.

## Ce que ça change Les alertes de la source s'arrêtaient en zone bronze : 11 546 objets collectés au fil de l'eau depuis le 3 septembre, que rien ne transformait. Cette demande ajoute `silver.alerte`, quatrième table de la zone argent, et l'écrit à chaque passe du job `silver_daily`. ## Preuve ``` $ pytest tests/unit/silver/test_alertes_silver.py -q 30 passed in 0.03s $ pytest tests/unit -q 628 passed $ pytest tests/unit/silver --cov=services/etl/etl/silver services/etl/etl/silver/alertes.py 70 0 100% services/etl/etl/silver/job.py 124 9 93% ``` La chaine complete du job sur bronze reel, en lecture seule, sans rien ecrire dans MinIO : ``` alertes retenues pour 2026-09-06 : 2984 controle des enumerations : passe schema == ligne produite : True dt tous sur la journee : {'2026-09-06'} alert_id uniques : 2984 / 2984 triees par horodatage : True ``` **2 984, soit exactement le nombre d'objets d'alerte que bronze porte pour ce jour-la.** Aucune perte, aucun doublon. ## Relecture - [ ] Un pair a relu et laissé un commentaire, même court - [ ] Ses remarques sont traitées, ou une réponse explique pourquoi elles ne le sont pas ## Ce qui suit le code - [x] `docs/runbooks/` mis à jour — `etl.md` annonce quatre objets par journée au lieu de trois, et dit qu'une partition d'alertes vide n'est pas une panne. C'est ce qu'un exploitant voit en premier avec `mc ls`. ## Où regarder en priorité **1. Le choix d'architecture : pourquoi la zone argent et pas directement la zone or.** C'est le point qui mérite le plus votre avis. Le §4 du pipeline définissait la zone argent comme une « série régulière », et une alerte est un événement : ni grille, ni imputation, ni validation physique. L'argument qui a tranché n'est pas la symétrie du médaillon, c'est que **le collecteur délègue déjà un traitement à cette zone** — `collector/alertes.py` range sous `dt=inconnu` une alerte qu'il n'a pas su dater, et son docstring annonce que « la zone argent la replacera d'après son contenu ». Personne ne l'avait écrit. `charger_alertes` lit donc deux partitions par site et replace d'après le payload. Le §4, le §10, le §11 et le §14 du pipeline sont réécrits en conséquence. Si vous préférez un chemin bronze → or direct, c'est maintenant qu'il faut le dire : la table est isolée, la défaire coûte peu. **2. Le nom du fichier de test.** `test_alertes_silver.py` et non `test_alertes.py`, parce que `tests/unit/collector/test_alertes.py` porte déjà ce nom et que pytest refuse deux modules homonymes — **la collecte de la suite entière échoue**, alors que le fichier passe isolément. Les deux correctifs globaux, poser des `__init__.py` ou passer en `--import-mode=importlib`, casseraient l'import des aides `_echantillons` et `_doubles` partout. Le suffixe coûte moins, mais si vous voyez mieux, je prends. **3. L'horodatage n'est pas tronqué à la minute**, contrairement à celui d'une mesure. `public.alerte` est unique sur `(site_id, horodatage, type, source)` : tronquer ferait de deux alertes du même type à douze secondes d'écart une seule alerte, et la seconde disparaîtrait en silence. ## Deux choses relevées en chemin, hors de ce lot **Un tiers à la moitié des alertes de la source ont une valeur en dessous de leur seuil.** Sur 500 échantillonnées : `threshold` 34/102, `spike` 48/114, `outage` 38/96, `anomaly` 44/86, `sensor` 53/102. Une alerte « Seuil de consommation dépassé » avec `value = 70.34` et `threshold = 135.0` se verra sur l'écran Qualité. C'est la donnée que la source sert, et l'historiser telle quelle est ce que demande l'EF-05 — mais ça vaut un coup d'oeil avant la démo. **`docs/data` est ignoré par `.gitignore`.** La règle `data/` matche à tous les niveaux, donc `git add docs/data/etl-pipeline.md` sort un avertissement. Le fichier existant reste suivi, mais tout nouveau document déposé là serait invisible sans le moindre message. Déjà signalé par @olivier en fin de commentaire du #35. Le correctif tient en un caractère — `/data/` — et ne relève pas de cette demande.
marvin self-assigned this 2026-09-07 10:48:16 +00:00
Les alertes de la source s'arrêtaient en zone bronze : 11 546 objets collectés
au fil de l'eau depuis le 3 septembre, que rien ne transformait. Ce commit pose
la quatrième table de la zone argent, `silver.alerte`, et rien d'autre : le
parseur d'enveloppe et l'écriture suivent.

`alerte` est la seule des quatre tables qui ne soit pas une série régulière —
un journal d'événements, sans grille ni imputation. Elle est en zone argent et
non directement en zone or parce que le collecteur lui délègue déjà un
traitement : une alerte à l'horodatage illisible est rangée sous « dt=inconnu »,
et `collector/alertes.py` écrit que la zone argent « la replacera d'après son
contenu ». Personne ne l'avait écrit.

Les noms de colonnes restent ceux de la source, comme le fait `silver.mesure` :
c'est la zone or qui traduit en `severite` et `valeur_kw`, à un seul endroit.
`alert_type` et non `type`, qui est un mot réservé de DuckDB. `site_type` et
`capacity_kw` sont recopiés du référentiel pour que la zone or obtienne le taux
de charge de `public.alerte` sans jointure. Ni `_raw` ni `_method` : une alerte
n'est ni validée ni imputée, elle est historisée telle que servie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7YA7QikrPg6wU3U1ur9M
Suite du schéma posé au commit précédent. Le job `silver_daily` écrit désormais
quatre tables : la quatrième porte les alertes que la source sert et que rien ne
transformait depuis bronze.

`charger_alertes` lit DEUX partitions par site — celle du jour et « dt=inconnu »
— puis retient chaque alerte sur la date de SON horodatage, jamais sur le
segment « dt= » de sa clé bronze. C'est le replacement que `collector/alertes.py`
délègue à cette zone depuis le #107 et que personne n'avait écrit. Le corollaire
est qu'une alerte lue sous « dt=inconnu » mais datée d'un autre jour est écartée
ici : elle appartient à la passe de ce jour-là, qui la retrouvera au même
endroit. Rien ne se perd, rien ne se duplique.

`verifier_enumerations_alertes` refuse un type ou une sévérité que la contrainte
de `public.alerte` rejetterait, avant d'écrire — même parti pris que
`gold.agregation` : attrapée ici, la faute nomme l'alerte ; attrapée au
chargement, elle sort deux jobs plus loin sur un numéro de ligne.

La partition des alertes est réécrite même vide. Une journée dont les alertes
ont disparu de bronze doit voir sa partition se vider, sinon la zone or
chargerait indéfiniment celles d'un rejeu antérieur. Un test le garde.

Vérifié sur les alertes réelles du 2026-09-06, sept sites, en lecture seule :
2 984 alertes retenues, soit exactement le nombre d'objets que bronze porte pour
ce jour ; 2 984 `alert_id` distincts ; toutes datées du jour traité ; le schéma
déclaré et la ligne produite portent les mêmes colonnes dans le même ordre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7YA7QikrPg6wU3U1ur9M
Trente cas sur `etl.silver.alertes`, qui passe à 100 % de couverture, et sur les
deux fonctions du job qui s'en servent.

Ce que les cas gardent, au delà du nominal :

- l'horodatage n'est PAS tronqué à la minute, contrairement à celui d'une
  mesure. `public.alerte` est unique sur (site_id, horodatage, type, source) :
  tronquer ferait de deux alertes du même type à douze secondes d'écart une
  seule alerte, et la seconde disparaîtrait sans que rien ne le dise ;
- une alerte rangée sous « dt=inconnu » est replacée sur la journée de son
  horodatage, et une alerte datée d'un autre jour est laissée à la passe de ce
  jour-là — rien ne se perd, rien ne se duplique ;
- une alerte sans identifiant, sans site ou sans date lisible est écartée et
  non inventée : elle reste en bronze, et un identifiant fabriqué casserait la
  déduplication que le collecteur garantit ;
- un objet corrompu, une enveloppe d'échec ou un élément non-objet dans un
  tableau ne font pas perdre le reste du lot ;
- la ligne produite porte exactement les colonnes du schéma déclaré ;
- la sentinelle « inconnu » est recopiée et non importée du paquet du
  collecteur : un cas garde l'identité des deux valeurs.

Le fichier s'appelle `test_alertes_silver.py` et non `test_alertes.py` :
`tests/unit/collector/test_alertes.py` porte déjà ce nom, les répertoires de
test n'ont pas d'`__init__.py`, et pytest refuse deux modules homonymes — la
collecte de la suite entière échoue, alors que le fichier passe isolément. Les
deux correctifs globaux, poser des `__init__.py` ou passer en
`--import-mode=importlib`, casseraient l'import des aides `_echantillons` et
`_doubles`, qui sont des modules de premier niveau. Le suffixe coûte moins, et
la raison est écrite en tête du fichier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7YA7QikrPg6wU3U1ur9M
docs: la zone argent porte quatre tables, dont le journal des alertes (#165)
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 6s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 29s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m35s
572a08125d
Le §4 définissait la zone argent comme une « série régulière ». Elle porte
désormais aussi un journal d'événements, et un lecteur qui s'arrête au §4 doit
le savoir.

`docs/data/etl-pipeline.md`
- §2 : deux lignes d'état séparées pour les alertes, bronze d'un côté, argent de
  l'autre, la seconde disant ce qui manque encore — la zone or ne les lit pas ;
- §4 : la zone argent n'est plus décrite comme une seule série régulière ;
- §10 : le schéma de `silver.alerte`, et les quatre décisions qui l'expliquent —
  pourquoi elle est en argent et pas en or, pourquoi sa clé n'est pas
  `(site_id, ts)`, pourquoi son horodatage n'est pas tronqué à la minute, et
  pourquoi une valeur d'énumération étrangère fait échouer la passe ;
- §11 : quatre chemins d'écriture, et la règle de la partition réécrite même
  vide ;
- §14 : le point « chemin des alertes au delà de bronze » est refermé, avec ce
  qu'il reste à faire ;
- §15 : l'EF-05 pointe désormais sur la zone argent et son fichier de test.

`docs/runbooks/etl.md` : quatre objets par journée et non trois, et le fait
qu'une partition d'alertes vide n'est pas une panne — c'est ce qu'un exploitant
verra en premier avec `mc ls`.

`services/etl/README.md` : la quatrième table et ce qui la distingue des trois
autres.

Un commentaire de `job.py` disait encore « trois tables vides ».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7YA7QikrPg6wU3U1ur9M
olivier approved these changes 2026-09-07 12:07:46 +00:00
Dismissed
olivier left a comment

Relu en entier. J'approuve : la CI est verte (4/4), le module est net, les 30 cas sont bien nommés et le schéma est justifié colonne par colonne. Rien de ce qui suit ne bloque la fusion — le chemin qui s'exécute réellement est correct. Mais deux points méritent une suite, dont un qui touche l'argument d'architecture que tu mets en avant.

1. Le replacement depuis dt=inconnu ne peut rien replacer

C'est le point sur lequel tu demandes un avis, et il se démontre sans exécuter le code.

Le collecteur range sous dt=inconnu si et seulement si horodatage_de_la_source rend None (collector/alertes.py:109-111). Or ce prédicat est le même, à l'octet près, que celui de la zone argent :

# collector/cles.py — horodatage_de_la_source
if not isinstance(valeur, str): return None
try: return datetime.fromisoformat(valeur.replace("Z", "+00:00"))
except ValueError: return None

# silver/alertes.py — _en_utc
if not isinstance(valeur, str): return None
try: instant = datetime.fromisoformat(valeur.replace("Z", "+00:00"))
except ValueError: return None

Et collecter_alertes écrit payload=alerte, la charge d'origine non modifiée (collector/alertes.py:156) : le timestamp illisible dans l'objet est celui-là même qui a envoyé l'alerte sous dt=inconnu.

Donc toute alerte présente sous dt=inconnu porte un timestamp que _en_utc rejette → alerte_depuis_payload rend None (ts is None) → lire_alertes l'écarte, avant que charger_alertes n'ait l'occasion de la replacer. La partition est lue à chaque passe, jamais moissonnée.

Le test qui semble l'établir s'appuie sur un état que le collecteur ne peut pas produire :

def test_une_alerte_rangee_sous_inconnu_est_replacee_sur_sa_journee():
    source = SourceDouble({cle(partition=PARTITION_INCONNUE): objet_bronze(alerte())})

alerte() porte un timestamp valide sous une clé inconnu — soit l'inverse exact de la condition de rangement.

Coût réel : 7 lectures de préfixe de plus par passe, toutes les heures, sur une partition qui ne fait que s'accumuler et dont chaque objet est retéléchargé et reparsé indéfiniment pour être jeté. Ça explique aussi que ton contrôle sur bronze réel tombe exactement sur 2 984 : la seconde partition n'apporte rien.

À noter que test_la_partition_sentinelle_est_la_meme_des_deux_cotes garde la moitié cosmétique de l'accord avec le collecteur — la valeur "inconnu" — et laisse sans garde la moitié porteuse : la relation entre les deux analyseurs. C'est la duplication de _en_utc qui est le point de rupture, pas celle de la sentinelle.

Sur le placement lui-même, je ne reviens pas en arrière. Je garderais alerte en zone argent, mais pour la raison qu'écrit déjà ton propre docstring de _colonnes_alerte : site_type et capacity_kw y sont dénormalisés pour que la zone or obtienne le taux_de_charge de public.alerte sans joindre le référentiel. Argument suffisant et vérifiable. Ce sont les §4, §10, §11 et §14 du pipeline qu'il faut réécrire sur cette base plutôt que sur la promesse déléguée — et vu que c'est une décision structurante, la « définition de terminé » de docs/CONVENTIONS.md demanderait plutôt une fiche dans docs/adr/.

2. Une alerte hors énumération bloque aussi les mesures

executer appelle verifier_enumerations_alertes avant verifier_taux_de_trous et avant ecrire. Une seule alerte au type ou à la severity inattendus fait donc échouer la passe entière : ni mesure, ni journal_capteur, ni disponibilite_jour ne sont écrites, et ça se reproduit à chaque heure jusqu'à intervention humaine. C'est un couplage nouveau — jusqu'ici un problème d'alerte ne pouvait pas arrêter les mesures, qui n'ont rien à voir avec elles.

La source étant collectée au fil de l'eau, et vu que tu relèves toi-même qu'elle sert des données incohérentes (un tiers à la moitié des valeurs sous leur seuil), une sixième valeur de type un matin arrête toute la zone argent. Deux sorties : écarter les alertes fautives en les journalisant, ou déplacer le contrôle après l'écriture des trois autres tables. La première me paraît plus fidèle à code_de_sortie du collecteur, qui refuse déjà de faire échouer cron « pour une donnée qu'on ne maîtrise pas ».

Point annexe qui trompera un relecteur : le docstring annonce « même parti pris que gold.agregation._controler_enumerations ». Ce n'est pas le même — gold écarte explicitement les nuls (WHERE {colonne} IS NOT NULL AND {colonne} NOT IN (...)), ici None est refusé. Refuser None est défendable, les colonnes étant not null en 0012, et ton test le documente ; c'est la comparaison à la zone or qui est fausse et la divergence qui n'est pas dite.

3. Points mineurs

  • ecrire(..., alertes=()) : le défaut vide efface silencieusement la partition d'alertes pour tout appelant qui l'oublie, et test_les_quatre_tables_sont_ecrites_dans_la_partition_du_jour s'appuie précisément sur ce défaut — c'est la forme exacte de l'accident. En argument nommé obligatoire, ça coûte un alertes=[] dans deux tests et supprime le piège.
  • _en_utc dit « à la seconde près » alors que les microsecondes sont conservées — et test_l_horodatage_n_est_pas_tronque_a_la_minute assène microsecond == 987654. Dérive de docstring sur le seul comportement dont le module fait un point d'honneur.
  • docs/runbooks/etl.md : la ligne Lit annonce toujours « préfixes endpoint=current et endpoint=readings ». Le job lit maintenant aussi endpoint=alerts, et c'est la ligne que l'exploitant lit en premier.
  • Aucun test ne traverse executer avec des alertes non vides : alertes_ecrites, la compréhension ligne_alerte et l'accès sites[alerte.site_id] ne sont couverts qu'isolément. C'est vraisemblablement les 7 % manquants de job.py.

Tes deux autres questions

Le nom de fichier : test_alertes_silver.py me va. La contrainte pytest est réelle, les deux correctifs globaux coûtent effectivement plus cher, et le suffixe se lit.

Ne pas tronquer : d'accord, et bien argumenté. unique (site_id, horodatage, type, source) en 0012 le confirme — tronquer perdrait la seconde alerte en silence.

Méthode

Je n'ai pas rejoué la suite : le venv du poste n'a aucune dépendance applicative (duckdb absent), donc je m'appuie sur la CI verte du commit 572a081 et sur ta preuve pour les 628 tests. Le point 1 est une lecture de code, pas une observation : si tu as sous dt=inconnu des objets réels dont le timestamp se parse, montre-les, ça invaliderait mon raisonnement.

Je fusionne. Les points 1 et 2 valent un ticket de suite — le 2 avant la démo.

Relu en entier. **J'approuve** : la CI est verte (4/4), le module est net, les 30 cas sont bien nommés et le schéma est justifié colonne par colonne. Rien de ce qui suit ne bloque la fusion — le chemin qui s'exécute réellement est correct. Mais deux points méritent une suite, dont un qui touche l'argument d'architecture que tu mets en avant. ## 1. Le replacement depuis `dt=inconnu` ne peut rien replacer C'est le point sur lequel tu demandes un avis, et il se démontre sans exécuter le code. Le collecteur range sous `dt=inconnu` **si et seulement si** `horodatage_de_la_source` rend `None` (`collector/alertes.py:109-111`). Or ce prédicat est le même, à l'octet près, que celui de la zone argent : ```python # collector/cles.py — horodatage_de_la_source if not isinstance(valeur, str): return None try: return datetime.fromisoformat(valeur.replace("Z", "+00:00")) except ValueError: return None # silver/alertes.py — _en_utc if not isinstance(valeur, str): return None try: instant = datetime.fromisoformat(valeur.replace("Z", "+00:00")) except ValueError: return None ``` Et `collecter_alertes` écrit `payload=alerte`, la charge d'origine non modifiée (`collector/alertes.py:156`) : le `timestamp` illisible dans l'objet est celui-là même qui a envoyé l'alerte sous `dt=inconnu`. Donc toute alerte présente sous `dt=inconnu` porte un `timestamp` que `_en_utc` rejette → `alerte_depuis_payload` rend `None` (`ts is None`) → `lire_alertes` l'écarte, **avant** que `charger_alertes` n'ait l'occasion de la replacer. La partition est lue à chaque passe, jamais moissonnée. Le test qui semble l'établir s'appuie sur un état que le collecteur ne peut pas produire : ```python def test_une_alerte_rangee_sous_inconnu_est_replacee_sur_sa_journee(): source = SourceDouble({cle(partition=PARTITION_INCONNUE): objet_bronze(alerte())}) ``` `alerte()` porte un `timestamp` valide sous une clé `inconnu` — soit l'inverse exact de la condition de rangement. Coût réel : 7 lectures de préfixe de plus par passe, toutes les heures, sur une partition qui ne fait que s'accumuler et dont chaque objet est retéléchargé et reparsé indéfiniment pour être jeté. Ça explique aussi que ton contrôle sur bronze réel tombe *exactement* sur 2 984 : la seconde partition n'apporte rien. À noter que `test_la_partition_sentinelle_est_la_meme_des_deux_cotes` garde la moitié cosmétique de l'accord avec le collecteur — la valeur `"inconnu"` — et laisse sans garde la moitié porteuse : la relation entre les deux analyseurs. C'est la duplication de `_en_utc` qui est le point de rupture, pas celle de la sentinelle. **Sur le placement lui-même, je ne reviens pas en arrière.** Je garderais `alerte` en zone argent, mais pour la raison qu'écrit déjà ton propre docstring de `_colonnes_alerte` : `site_type` et `capacity_kw` y sont dénormalisés pour que la zone or obtienne le `taux_de_charge` de `public.alerte` sans joindre le référentiel. Argument suffisant et vérifiable. Ce sont les §4, §10, §11 et §14 du pipeline qu'il faut réécrire sur cette base plutôt que sur la promesse déléguée — et vu que c'est une décision structurante, la « définition de terminé » de `docs/CONVENTIONS.md` demanderait plutôt une fiche dans `docs/adr/`. ## 2. Une alerte hors énumération bloque aussi les mesures `executer` appelle `verifier_enumerations_alertes` **avant** `verifier_taux_de_trous` et avant `ecrire`. Une seule alerte au `type` ou à la `severity` inattendus fait donc échouer la passe entière : ni `mesure`, ni `journal_capteur`, ni `disponibilite_jour` ne sont écrites, et ça se reproduit à chaque heure jusqu'à intervention humaine. C'est un couplage nouveau — jusqu'ici un problème d'alerte ne pouvait pas arrêter les mesures, qui n'ont rien à voir avec elles. La source étant collectée au fil de l'eau, et vu que tu relèves toi-même qu'elle sert des données incohérentes (un tiers à la moitié des valeurs sous leur seuil), une sixième valeur de `type` un matin arrête toute la zone argent. Deux sorties : écarter les alertes fautives en les journalisant, ou déplacer le contrôle après l'écriture des trois autres tables. La première me paraît plus fidèle à `code_de_sortie` du collecteur, qui refuse déjà de faire échouer cron « pour une donnée qu'on ne maîtrise pas ». Point annexe qui trompera un relecteur : le docstring annonce « même parti pris que `gold.agregation._controler_enumerations` ». Ce n'est pas le même — gold écarte explicitement les nuls (`WHERE {colonne} IS NOT NULL AND {colonne} NOT IN (...)`), ici `None` est refusé. Refuser `None` est défendable, les colonnes étant `not null` en 0012, et ton test le documente ; c'est la comparaison à la zone or qui est fausse et la divergence qui n'est pas dite. ## 3. Points mineurs - **`ecrire(..., alertes=())`** : le défaut vide efface silencieusement la partition d'alertes pour tout appelant qui l'oublie, et `test_les_quatre_tables_sont_ecrites_dans_la_partition_du_jour` s'appuie précisément sur ce défaut — c'est la forme exacte de l'accident. En argument nommé obligatoire, ça coûte un `alertes=[]` dans deux tests et supprime le piège. - **`_en_utc`** dit « à la seconde près » alors que les microsecondes sont conservées — et `test_l_horodatage_n_est_pas_tronque_a_la_minute` assène `microsecond == 987654`. Dérive de docstring sur le seul comportement dont le module fait un point d'honneur. - **`docs/runbooks/etl.md`** : la ligne `Lit` annonce toujours « préfixes `endpoint=current` et `endpoint=readings` ». Le job lit maintenant aussi `endpoint=alerts`, et c'est la ligne que l'exploitant lit en premier. - **Aucun test ne traverse `executer` avec des alertes non vides** : `alertes_ecrites`, la compréhension `ligne_alerte` et l'accès `sites[alerte.site_id]` ne sont couverts qu'isolément. C'est vraisemblablement les 7 % manquants de `job.py`. ## Tes deux autres questions **Le nom de fichier** : `test_alertes_silver.py` me va. La contrainte pytest est réelle, les deux correctifs globaux coûtent effectivement plus cher, et le suffixe se lit. **Ne pas tronquer** : d'accord, et bien argumenté. `unique (site_id, horodatage, type, source)` en 0012 le confirme — tronquer perdrait la seconde alerte en silence. ## Méthode Je n'ai **pas** rejoué la suite : le venv du poste n'a aucune dépendance applicative (`duckdb` absent), donc je m'appuie sur la CI verte du commit `572a081` et sur ta preuve pour les 628 tests. Le point 1 est une lecture de code, pas une observation : si tu as sous `dt=inconnu` des objets réels dont le `timestamp` se parse, montre-les, ça invaliderait mon raisonnement. Je fusionne. Les points 1 et 2 valent un ticket de suite — le 2 avant la démo.
Merge branch 'develop' into marvin/165-alertes-zone-argent (#165)
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 6s
Intégration / Aucun secret commité (pull_request) Successful in 4s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 33s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 3m41s
70dbfc0761
La #159 a réécrit `docs/runbooks/etl.md` pendant la relecture. Les deux
intentions sont gardées, aucune n'écrase l'autre :

- le tableau de la #159 couvre les trois passes, on le prend, en y ajoutant
  `endpoint=alerts` que la zone argent lit maintenant aussi ;
- le compte d'objets de la zone argent passe à quatre là où la #159 l'avait
  déplacé, dans le commentaire de l'étape 1 ;
- les deux paragraphes disent deux choses différentes — le piège du chargement
  et la partition d'alertes vide — donc les deux restent.

Résolution écrite par Olivier en relecture de la #167.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
olivier dismissed olivier's review 2026-09-07 12:14:10 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

olivier approved these changes 2026-09-07 12:23:02 +00:00
olivier merged commit d9d6d1e27b into develop 2026-09-07 12:23:07 +00:00
olivier deleted branch marvin/165-alertes-zone-argent 2026-09-07 12:23:07 +00:00
Sign in to join this conversation.
No reviewers
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".

No due date set.

Dependencies

No dependencies set

Reference
g2/enervision!167
No description provided.