Mise en production : la dérive du modèle, la chaîne aux dix minutes, les règles hors du code #252

Merged
lenaic merged 45 commits from develop into main 2026-09-09 17:42:14 +00:00
Member

Ce que ça change

Mise en production de tout ce qui est entré dans develop depuis la livraison
du 09/09 au matin (#233) : la détection de dérive du modèle de prévision, la
chaîne ETL ramenée aux dix minutes, les règles de recommandation sorties du
code, la garde Terraform en intégration, le bundle Git du dépôt, le jeton SAS
de l'archive de secours, et la grille de recette qui manquait au dossier.
main = develop.

Refs #48

Preuve

11 demandes de fusion, 45 commits, 20 fusions
100 fichiers, +10 166 / −805

Les demandes livrées :

#234  #236  #238  #239  #240  #241
#242  #245  #247  #248  #251

Elles servent les tickets #47, #48, #70, #72, #74, #116, #117, #244 et #246.
Chacune est entrée dans develop avec ses tâches de chaîne au vert et
l'approbation d'un pair. Aucune n'a été forcée. La chaîne est au vert sur la
tête de develop (7344d44), les six tâches.

Deux migrations dans cette livraison, et c'est la différence avec la #233 :
0021_recommandation_jeu_de_regles.sql (deux colonnes sur
public.recommandation) et 0022_cadence_dix_minutes.sql (la politique de
rafraîchissement de mesure_horaire). Le retour arrière n'est donc pas un
simple git revert du commit de fusion : voir le paragraphe 1 ci-dessous.
.forgejo/workflows/ci.yml change aussi — deux tâches d'intégration en plus,
couvertes par le contrôle requis Intégration / * qui est un motif.

Ce qui change pour la démonstration

  • Le modèle dit quand il se démode (#117). La passe horaire compare les
    entrées et l'erreur du modèle promu à une référence figée à sa promotion,
    et rend un verdict à trois valeurs — stable, surveillance, dérive
    publié pour Prometheus. Onzième règle d'alerte : « Modèle de prévision en
    dérive ». Deux correctifs de relecture sont dans le lot : une passe sans
    entrée ne peut plus rendre « stable », et la surveillance cesse d'annoncer
    « stable » quand elle ne mesure plus.
  • La chaîne passe aux dix minutes (#246). L'âge de la dernière minute dans
    public.mesure — ce que le pavé « état des sites » affiche — tombe de 9-23
    minutes à 6-15, et le seau horaire devient lisible à (H+1):10. La crontab
    et la politique de l'agrégat continu partent dans le même lot, dans cet
    ordre.
  • Les règles de recommandation quittent le code (#116). Les trois règles du
    #153 étaient trois classes Python à seuils constants ; elles deviennent des
    données dans un regles.toml de formes paramétrées, modifiable sans
    redéploiement. La version et l'empreinte du jeu appliqué sont écrites sur
    chaque recommandation (migration 0021), donc on peut toujours dire quel jeu a
    produit quoi.
  • Terraform est validé et audité à chaque demande de fusion (#72). fmt,
    validate, tflint et checkov tournent sans jamais joindre Azure —
    init -backend=false, aucun secret, aucune variable ARM_*. Les tâches se
    désistent quand la demande ne touche pas Terraform, et les neuf constats de
    checkov portent désormais leur motif écrit sur la ressource.
  • Le dépôt survit à la perte du serveur (#74). git bundle --all tous les
    jours à 03:10, sept bundles gardés dans /var/backups/depot. Le serveur ne
    pousse rien vers l'extérieur : c'est un poste d'équipier qui vient chercher.
  • Le jeton SAS de l'archive de secours (#70). Écriture seule, adossé à une
    stored access policy révocable sans toucher à la clé du compte, au coffre
    ansible-vault, avec son runbook de rotation et de révocation.
  • La recette existe (#47). docs/RECETTE.md, la grille des 28 exigences,
    jouée contre la production 415af1a : 22 tenus, 5 tenus avec réserve, 1 non
    tenu, 2 non joués. Le chemin complet, de la minute collectée à la
    recommandation affichée, rejoué d'un seul tenant en 3 min 25 s.
  • La session du tableau de bord est glissante (#234). Le cookie d'accès
    vivait quinze minutes sans rien pour le renouveler : un écran actif était
    coupé quinze minutes après la connexion. La coupe se fait maintenant sur
    quinze minutes d'inactivité.

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

Où regarder en priorité

1. Le retour arrière n'est plus gratuit. Les deux migrations partent avec le
déploiement, appliquées par le rôle app. La 0021 est additive et se défait
sans perte (deux colonnes nullables, les lignes antérieures marquées
avant-116). La 0022 change une politique de rafraîchissement continu : la
défaire demande de reposer les offsets du #205, et il faut le faire dans le même
geste que le retour de la crontab, sinon l'agrégat reste en arrière de la série
qu'il résume. Un git revert seul remettrait la crontab au quart d'heure en
laissant la politique aux dix minutes — l'inverse exact du défaut d'origine du
#205. À dire avant de fusionner, pas pendant l'incident.

2. L'ordre crontab → politique est un invariant, pas une préférence. Le seau
H:00 entre dans la fenêtre de rafraîchissement à (H+1):10 ; la chaîne achève
l'heure H en base à (H+1):05 au pire. Cinq minutes séparent les deux. Le
déploiement pose les deux ensemble, tests/ci/test-fraicheur-chaine.sh tient
l'invariant en lisant la cadence déclarée. Le point à surveiller après la
fusion est le premier rafraîchissement qui suit le passage de la crontab.

3. regles.toml est posé avec force: false, et le serveur diverge du dépôt
à partir de là.
C'est la condition du #116 — le déploiement continu rejoue
--tags app,proxy,backup à chaque fusion, une copie qui écrase effacerait toute
règle ajoutée en exploitation. Conséquence à assumer : après cette mise en
production, services/recommendations/regles.toml est un jeu de référence, et
ce qui tourne est /etc/enervision/regles.toml. recommandation.jeu_empreinte
dit lequel a produit une recommandation donnée. La tâche est placée après
l'installation des dépendances Python parce que son validate lance
l'interpréteur du venv — plus haut, elle arrêtait le rôle avant les migrations
sur un hôte neuf.

4. Onze alertes chargées, dix annoncées. rules.yaml porte onze règles
depuis le #117, mais docs/PRD.md (ENF-08), docs/BACKLOG.md et
docs/runbooks/supervision.md disent encore dix — le même écart que la #232
avait corrigé de cinq à dix. C'est de la documentation seule, rien ne casse,
mais l'ENF-08 est une exigence sur laquelle le jury compte, et le dossier doit
dire le vrai. À corriger avant le 11, dans ce lot ou juste après.

5. Le #116 est un ticket Portée/Post-jury, fusionné quand même. Il est
hors fenêtre par la décision du 03/09 (réduction 1 de docs/BACKLOG.md). Le
travail a été fait le 09 à la demande, le libellé du ticket n'a pas bougé, et le
backlog comme le PRD portent l'écart en clair. Le signaler ici parce que c'est
la fusion vers main qui le met en production, et que c'est une décision
d'équipe, pas un acquis.

6. Le déploiement n'est toujours pas gardé par la chaîne. deploy.yml se
déclenche sur push: main, sans workflow_run ni needs. Sur un exécuteur
unique, le déploiement peut partir avant le verdict. La barrière reste une
convention de fusion. Inchangé depuis la #220, redit ici parce que cette
livraison porte deux migrations et que le coût d'un départ anticipé n'est plus
le même.

7. Le rôle app déploie repo_version: develop, pas main. La fusion vers
main déclenche donc un déploiement dont le contenu vient de develop
identique à ce qui est fusionné à cet instant, mais seulement parce que les deux
branches coïncident au moment du push. Constat antérieur, sans ticket, redit ici
comme à la #233.

## Ce que ça change Mise en production de tout ce qui est entré dans `develop` depuis la livraison du 09/09 au matin (#233) : la détection de dérive du modèle de prévision, la chaîne ETL ramenée aux dix minutes, les règles de recommandation sorties du code, la garde Terraform en intégration, le bundle Git du dépôt, le jeton SAS de l'archive de secours, et la grille de recette qui manquait au dossier. `main` = `develop`. Refs #48 ## Preuve ``` 11 demandes de fusion, 45 commits, 20 fusions 100 fichiers, +10 166 / −805 ``` Les demandes livrées : ``` #234 #236 #238 #239 #240 #241 #242 #245 #247 #248 #251 ``` Elles servent les tickets #47, #48, #70, #72, #74, #116, #117, #244 et #246. Chacune est entrée dans `develop` avec ses tâches de chaîne au vert et l'approbation d'un pair. Aucune n'a été forcée. La chaîne est au vert sur la tête de `develop` (`7344d44`), les six tâches. **Deux migrations dans cette livraison**, et c'est la différence avec la #233 : `0021_recommandation_jeu_de_regles.sql` (deux colonnes sur `public.recommandation`) et `0022_cadence_dix_minutes.sql` (la politique de rafraîchissement de `mesure_horaire`). Le retour arrière n'est donc pas un simple `git revert` du commit de fusion : voir le paragraphe 1 ci-dessous. `.forgejo/workflows/ci.yml` change aussi — deux tâches d'intégration en plus, couvertes par le contrôle requis `Intégration / *` qui est un motif. ## Ce qui change pour la démonstration - **Le modèle dit quand il se démode** (#117). La passe horaire compare les entrées et l'erreur du modèle promu à une référence figée à sa promotion, et rend un verdict à trois valeurs — `stable`, `surveillance`, `dérive` — publié pour Prometheus. Onzième règle d'alerte : « Modèle de prévision en dérive ». Deux correctifs de relecture sont dans le lot : une passe sans entrée ne peut plus rendre « stable », et la surveillance cesse d'annoncer « stable » quand elle ne mesure plus. - **La chaîne passe aux dix minutes** (#246). L'âge de la dernière minute dans `public.mesure` — ce que le pavé « état des sites » affiche — tombe de 9-23 minutes à 6-15, et le seau horaire devient lisible à `(H+1):10`. La crontab et la politique de l'agrégat continu partent dans le même lot, dans cet ordre. - **Les règles de recommandation quittent le code** (#116). Les trois règles du #153 étaient trois classes Python à seuils constants ; elles deviennent des données dans un `regles.toml` de formes paramétrées, modifiable sans redéploiement. La version et l'empreinte du jeu appliqué sont écrites sur chaque recommandation (migration 0021), donc on peut toujours dire quel jeu a produit quoi. - **Terraform est validé et audité à chaque demande de fusion** (#72). `fmt`, `validate`, `tflint` et `checkov` tournent sans jamais joindre Azure — `init -backend=false`, aucun secret, aucune variable `ARM_*`. Les tâches se désistent quand la demande ne touche pas Terraform, et les neuf constats de checkov portent désormais leur motif écrit sur la ressource. - **Le dépôt survit à la perte du serveur** (#74). `git bundle --all` tous les jours à 03:10, sept bundles gardés dans `/var/backups/depot`. Le serveur ne pousse rien vers l'extérieur : c'est un poste d'équipier qui vient chercher. - **Le jeton SAS de l'archive de secours** (#70). Écriture seule, adossé à une stored access policy révocable sans toucher à la clé du compte, au coffre ansible-vault, avec son runbook de rotation et de révocation. - **La recette existe** (#47). `docs/RECETTE.md`, la grille des 28 exigences, jouée contre la production `415af1a` : 22 tenus, 5 tenus avec réserve, 1 non tenu, 2 non joués. Le chemin complet, de la minute collectée à la recommandation affichée, rejoué d'un seul tenant en 3 min 25 s. - **La session du tableau de bord est glissante** (#234). Le cookie d'accès vivait quinze minutes sans rien pour le renouveler : un écran actif était coupé quinze minutes après la connexion. La coupe se fait maintenant sur quinze minutes d'inactivité. ## 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 ## Où regarder en priorité **1. Le retour arrière n'est plus gratuit.** Les deux migrations partent avec le déploiement, appliquées par le rôle `app`. La 0021 est additive et se défait sans perte (deux colonnes nullables, les lignes antérieures marquées `avant-116`). La 0022 change une politique de rafraîchissement continu : la défaire demande de reposer les offsets du #205, et il faut le faire dans le même geste que le retour de la crontab, sinon l'agrégat reste en arrière de la série qu'il résume. Un `git revert` seul remettrait la crontab au quart d'heure en laissant la politique aux dix minutes — l'inverse exact du défaut d'origine du #205. À dire avant de fusionner, pas pendant l'incident. **2. L'ordre crontab → politique est un invariant, pas une préférence.** Le seau `H:00` entre dans la fenêtre de rafraîchissement à `(H+1):10` ; la chaîne achève l'heure `H` en base à `(H+1):05` au pire. Cinq minutes séparent les deux. Le déploiement pose les deux ensemble, `tests/ci/test-fraicheur-chaine.sh` tient l'invariant en lisant la cadence déclarée. Le point à surveiller après la fusion est le premier rafraîchissement qui suit le passage de la crontab. **3. `regles.toml` est posé avec `force: false`, et le serveur diverge du dépôt à partir de là.** C'est la condition du #116 — le déploiement continu rejoue `--tags app,proxy,backup` à chaque fusion, une copie qui écrase effacerait toute règle ajoutée en exploitation. Conséquence à assumer : après cette mise en production, `services/recommendations/regles.toml` est un jeu de *référence*, et ce qui tourne est `/etc/enervision/regles.toml`. `recommandation.jeu_empreinte` dit lequel a produit une recommandation donnée. La tâche est placée après l'installation des dépendances Python parce que son `validate` lance l'interpréteur du venv — plus haut, elle arrêtait le rôle avant les migrations sur un hôte neuf. **4. Onze alertes chargées, dix annoncées.** `rules.yaml` porte onze règles depuis le #117, mais `docs/PRD.md` (ENF-08), `docs/BACKLOG.md` et `docs/runbooks/supervision.md` disent encore dix — le même écart que la #232 avait corrigé de cinq à dix. C'est de la documentation seule, rien ne casse, mais l'ENF-08 est une exigence sur laquelle le jury compte, et le dossier doit dire le vrai. À corriger avant le 11, dans ce lot ou juste après. **5. Le #116 est un ticket `Portée/Post-jury`, fusionné quand même.** Il est hors fenêtre par la décision du 03/09 (réduction 1 de `docs/BACKLOG.md`). Le travail a été fait le 09 à la demande, le libellé du ticket n'a pas bougé, et le backlog comme le PRD portent l'écart en clair. Le signaler ici parce que c'est la fusion vers `main` qui le met en production, et que c'est une décision d'équipe, pas un acquis. **6. Le déploiement n'est toujours pas gardé par la chaîne.** `deploy.yml` se déclenche sur `push: main`, sans `workflow_run` ni `needs`. Sur un exécuteur unique, le déploiement peut partir avant le verdict. La barrière reste une convention de fusion. Inchangé depuis la #220, redit ici parce que cette livraison porte deux migrations et que le coût d'un départ anticipé n'est plus le même. **7. Le rôle `app` déploie `repo_version: develop`, pas `main`.** La fusion vers `main` déclenche donc un déploiement dont le contenu vient de `develop` — identique à ce qui est fusionné à cet instant, mais seulement parce que les deux branches coïncident au moment du push. Constat antérieur, sans ticket, redit ici comme à la #233.
front : session glissante, coupe sur 15 min d'inactivite et non 15 min tout court
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 46s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 19s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m7s
4a9b18f0d4
Le cookie d'acces vit quinze minutes et rien ne le renouvelait en cours de
session : un ecran actif etait coupe quinze minutes apres la connexion, action
ou pas.

surSessionGlissante(), monte une fois dans EnteteApplication a cote de
ModalReconnexion :
 - ecoute l'activite (pointeur, clavier, molette, defilement, toucher) ;
 - toutes les dix minutes, tant que l'appelant agit, fait tourner le cookie
   d'acces via /auth/refresh avant qu'il n'expire ;
 - au-dela de quinze minutes sans interaction, ferme la session (revocation
   cote API) et ouvre la modale de reprise ;
 - une rotation en echec ouvre la modale, comme un 401 en cours de route.

Inerte en mode demonstration et hors navigateur. Nouvelle action de depot
expirerParInactivite().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Es6bHF86WXpqXpRKnwqaUh
Un modèle ne tombe pas en panne, il se démode. La passe horaire compare
désormais les entrées et l'erreur du modèle promu à une référence FIGÉE À SA
PROMOTION, lève un signal au-delà de seuils déclarés, et publie son verdict
pour Prometheus.

Ce que le ticket demandait, et où c'est :

- `inference/derive.py` — la règle, en Python pur. Trois écarts : déplacement
  de la moyenne des entrées en écarts-types de la référence, rapport des
  dispersions (rendu dans les deux sens, un capteur bloqué fait chuter la
  dispersion autant qu'une agitation la fait monter), rapport des erreurs.
  Verdict à trois valeurs et non deux : « surveillance » à mi-chemin de la
  borne évite qu'une mesure oscillante allume et éteigne l'alerte.
- `model/entrainement.py` — journalise la référence (moyenne, dispersion,
  effectif des entrées apprises) dans l'exécution MLflow. Elle est donc
  adressée par (modèle, version) et ne peut pas survivre au modèle.
- `inference/reference_promue.py` — la relit par le protocole `Registre` du
  #36. Une version sans référence rend None et le dit : la prévision continue
  d'être servie, la dérive n'est pas mesurée.
- `inference/realise.py` — remplit `public.prevision.valeur_reference_kw`, la
  colonne que la migration 0017 réservait et que l'ADR 0013 laissait vide « tant
  que ce ticket n'est pas ouvert ». C'est la moitié manquante : sans réalisé,
  aucune erreur récente n'est mesurable.
- `inference/publication.py` — dépose un `.prom` pour le collecteur textfile de
  node-exporter, motif de `pg-backup.sh` (#42), y compris son `chmod 0644` : en
  0640 node-exporter ne lit pas et l'alerte se déclenche sur une absence.
- `rules.yaml` — sixième alerte, sur le verdict 2 tenu deux heures.
  `noDataState: OK` : une dérive non mesurée n'est pas une dérive.

Seuils déclarés (critère 2), surchargeables sans toucher au code :
ENERVISION_INFERENCE_DERIVE_{DEPLACEMENT_MAX,DISPERSION_MAX,ERREUR_MAX,
COUPLES_MINIMUM}.

Deux corrections qu'un essai contre le vrai serveur a imposées :

- la requête visait une colonne `valeur_kw` qui n'existe pas ; l'agrégat porte
  `moyenne_kw`, la cible même du modèle. Un test verrouille le nom, parce que
  viser `max_kw` serait passé sans rien casser en comparant une prévision de
  moyenne à un maximum ;
- psycopg rend une colonne `numeric` en `Decimal` : un filtre naïf sur
  (int, float) aurait laissé l'erreur « non mesurée » pour toujours, sans
  message. mypy strict a forcé à regarder ce que le pilote rend vraiment.

`test_job_inference` lisait `caplog.records` en entier alors que son propre
`at_level` vise `inference.emission` : n'importe quel avertissement d'un autre
module le cassait. Filtré sur le journal qu'il annonce.
Merge pull request 'front : session glissante, coupe sur 15 min d inactivite et non 15 min tout court' (#234) from marvin/front-session-glissante-inactivite into develop
All checks were successful
Intégration / Contrôles statiques du dépôt (push) Successful in 8s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 26s
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 44s
Intégration / Python — qualité, tests et dépendances (push) Successful in 5m40s
53263f0b4e
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/234
Reviewed-by: gabriel <gabriel@noreply.10.105.200.41>
Les trois regles du #153 etaient trois classes Python, leurs seuils en
constantes de module : les changer demandait un redeploiement. Le #116 les
remplace par un jeu de regles charge d'un fichier, versionne et modifiable
sans redeploiement.

Des FORMES parametrees, pas un langage d'expressions. Chaque forme est un
raisonnement ecrit une fois en Python — un seuil, un ratio soumis a une
echeance, une serie de N points consecutifs — que le fichier instancie. Rien
de ce que le fichier contient n'est execute : il est modifiable sans revue,
donc c'est une entree non fiable. Motif complet dans l'ADR 0014.

CE QUI GARANTIT QUE RIEN N'A CHANGE. Le risque n'etait pas qu'un test
rougisse, mais que tout passe et que les recommandations aient quand meme
change. tests/unit/rules/verdicts-de-reference.json porte les 84 verdicts
produits par les trois classes sur 28 observations couvrant chaque branche,
captures avant leur retrait puis confrontes aux memes classes extraites de
develop : aucun ecart. Le test d'equivalence compare message, action, gravite,
valeurs declenchantes et motif d'indecision au caractere pres — c'est pour
tenir ce dernier point que les motifs sont des gabarits du fichier.

Un jeu invalide n'est pas rattrape : la passe sort en code 5 sans rien ecrire.
Le prix se paie en amont — `--verifier` a la main, `validate:` sur la tache
Ansible, et le chargement du jeu de reference par la chaine.

La profondeur de lecture de la zone or se derive maintenant du jeu, au lieu
d'etre une constante d'entrepot.py : une regle ajoutee a chaud ajuste la
fenetre lue. C'est le piege du #175, referme.

Ansible pose le fichier une seule fois (force: false) : le deploiement continu
joue --tags app a chaque fusion et aurait efface les regles ajoutees sur le
serveur.

Verifie en 3.12 sur le serveur : 345 tests, 94 % de couverture sur le paquet
(palier 85). Migration 0021 jouee sur enervision_prod en transaction annulee,
11 lignes existantes rattrapees en `avant-116`.

Le ticket reste Portee/Post-jury : la decision du 03/09 n'est pas rouverte par
ce developpement, la fusion est une decision d'equipe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
L'ADR 0014 et le manuel concluaient qu'aucune alerte ne regardait les passes de
recommandation, donc qu'une suite de codes 5 ne reveillerait personne. C'etait
vrai en l'ecrivant, et faux en fusionnant : le #228 a instrumente les sept
taches planifiees entre-temps.

Le lanceur publie `ev_ops_tache_dernier_code` et la date de la derniere
REUSSITE ; l'alerte « Tache planifiee en retard » se declenche a trois fois la
cadence, soit trois heures ici. Ce n'est pas immediat — les deux ou trois
premieres passes echouent en silence — d'ou la verification avant de quitter le
fichier, que le manuel place a l'etape 3.

Constate en fusionnant develop, pas suppose : les deux blocs du lanceur, le
mien et celui du #228, se fusionnent sans conflit et
tests/ci/test-supervision.sh passe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
adr 0013 : la détection de dérive n'est plus hors périmètre (#117)
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 49s
Intégration / Contrôles statiques du dépôt (pull_request) Failing after 5s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 20s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m27s
e1e92b8b81
La fiche disait « personne ne remplit valeur_reference_kw tant que ce ticket
n'est pas ouvert ». Il l'est, et la colonne est remplie : la ligne était
devenue fausse du fait même du travail qu'elle annonçait.
docs: le backlog dit l etat du 09 sur les tickets du Tech Lead (#48)
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 26s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 45s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m8s
1b4d46604b
Le fichier datait du 08 a 19 h et annoncait encore « a faire » sur du travail
livre, deploye et verifie : le rapport EC02, la tracabilite, la mise en
production, les deux decisions d exploitation. C est le document qu un jury lit
pour juger le suivi, et il disait le contraire de la forge.

Seuls les FAITS sont repris — comptes, etats, preuves. Rien de l arbitrage du
PO : l ordre des rangs, les jalons et la clause de gel sont ceux de Gabriel et
ne bougent pas. Les entrees faites sont barrees plutot que supprimees, pour que
l ecart entre le prevu et le realise reste lisible.

Chiffres releves sur la forge au 09/09 : 76 tickets fermes, 16 ouverts dont 13
Post-jury, donc TROIS reellement ouverts (#47, #59, #235). 125 demandes de
fusion fusionnees, zero ouverte. Le jalon J2 est clos, le J3 n a plus qu un
livrable, #47.
runbook : le jeton SAS de l'archive de secours (#70)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 48s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 17s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m19s
553eddda29
Manuel des gestes autour du jeton SAS que le serveur utilise pour deposer
l'archive quotidienne : generation adossee a la stored access policy
depot-archive, rotation, revocation sans rotation de cle, chiffrement age/SOPS.

La stored access policy et le jeton sont poses sur Azure (09/09). Le
chiffrement SOPS (.sops.yaml + fichier chiffre) et la consommation Ansible
(#71) restent a faire : les sections concernees le signalent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9GaXoZ17B3o9kxsb3P973
infra: bundle Git quotidien du depot, hors serveur (#74)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 45s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 6s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 18s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m19s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m8s
08e5534e04
L archive de secours emporte les donnees, pas le code. Si le serveur disparait,
la forge disparait avec lui : le depot, les tickets et l historique de la chaine
vivent tous les trois dessus. Un « git bundle --all » quotidien repond a la
moitie qui compte le plus, et le ticket assume le reste : ce qui ne se
reconstitue pas, c est le code.

SOUS deploy ET NON SOUS root. Le depot clone appartient a deploy, et Git refuse
d operer sur un depot dont l utilisateur courant n est pas proprietaire depuis
CVE-2022-24765. Un cron root echouait sur « detected dubious ownership » —
constate en essai reel sur le serveur, pas deduit. Le script pose quand meme
safe.directory pour qu un rejeu a la main sous sudo, le geste naturel en
incident, ne casse pas au pire moment.

LE SERVEUR NE POUSSE RIEN VERS L EXTERIEUR. Le critere « aucun secret en clair
dans la conf du cron » se tient le plus simplement en n en ayant aucun : le cron
ecrit un fichier local, et c est le poste d un equipier qui vient le CHERCHER
avec sa propre cle. Une machine compromise ne donne alors acces a rien de plus
qu elle-meme.

Un bundle qui ne passe pas « git bundle verify » NE PREND PAS LA PLACE du
precedent : ecriture sous un nom temporaire, verification, renommage seulement
apres. Un bundle tronque a la bonne taille apparente et ne se voit qu au
clonage.

La rotation compte des FICHIERS et non des jours. Sept jours sans passage de
cron laisseraient sinon zero sauvegarde, ce qui est exactement le cas ou on en a
besoin.

Eprouve de bout en bout le 09/09 : bundle de 2 661 167 octets produit sur le
serveur, recupere sur le poste, CLONE pour de vrai — 584 commits, 3 branches,
dernier commit 415af1a. Rotation eprouvee : quatre passages avec une garde de
deux ne laissent que les deux plus recents.

Un piege evite en route : la ligne de cron depassait cent caracteres et
ansible-lint la refusait. La couper par un antislash aurait produit une ligne de
cron INVALIDE, cron ne connaissant pas la continuation de ligne — la sauvegarde
quotidienne aurait ete cassee en silence pour satisfaire un linter. C est la
variable qui a raccourci.
recommandations : six defauts trouves en relecture du moteur de regles
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 48s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 8s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 18s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m20s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m9s
6a2416f2a5
1. BLOQUANT — la tache Ansible validait le jeu avec {{ collector_venv }}/bin/
python, place 220 lignes AVANT la tache qui cree ce venv. Sur un hote neuf la
commande n'a pas d'interpreteur, `validate` echoue, et le role s'arrete avant
de demarrer les piles et d'appliquer les migrations — dont la 0021 dont
l'insertion depend. Ce n'etait pas auto-reparant : chaque relance s'arretait au
meme endroit. La tache passe apres l'installation des dependances et avant la
pose de la crontab, et le commentaire dit pourquoi les deux bornes comptent.

2. Une table ecrite en liste — `valeurs = ["seuil"]`, confusion de syntaxe TOML
facile a la main — levait AttributeError, que _construire ne rattrape pas : la
passe mourait sur une trace en code 1, quand le journal, le manuel et le
validate d'Ansible annoncent tous un code 5. Meme cas pour `motifs` et pour
`requises` donne en scalaire.

3. `Template.get_identifiers` IGNORE les placeholders invalides — seul
`is_valid` les refuse. Un « $ » litteral (« 30 $/MWh ») passait le chargement,
`--verifier` et le validate, puis faisait lever `substitute` au premier RENDU :
la regle tombait le jour ou le site etait en pointe, et nulle part avant. C'est
le mode de panne que cette validation existe pour supprimer.

4. `profondeur_heures` ajoutait un NOMBRE DE POINTS a des HEURES et ignorait le
pas : juste par coincidence au pas horaire du #153, faux des qu'une regle
declare autre chose. Au pas de trois heures, la regle avait besoin de douze
heures et n'en demandait que neuf — le #175 refermé d'un cote, reouvert de
l'autre, atteignable par une modification du fichier. Le jeu de reference reste
a 9 h : la production ne change pas.

5. Trois appels a `charger()` sans chemin honoraient
ENERVISION_REGLES_FICHIER alors qu'ils documentent lire le jeu du depot. Le
temoin d'equivalence aurait ete regenere depuis /etc — le fichier meme dont la
conception dit qu'il peut diverger.

6. Le repli du lanceur se declenchait sur `! -r`, qui couvre « illisible »
autant que « absent ». Un fichier reste en 0600 root:root apres une
modification a la main aurait fait tourner la passe sur les regles du DEPOT
sous une autre empreinte. Absent : repli, comme avant. Present et illisible :
code 2, comme le controle de postgres.env juste au-dessus.

Le temoin est inchange apres les six correctifs : aucun verdict n'a bouge.
292 tests sur le service, ruff, mypy strict, shellcheck, yamllint et
ansible-lint propres.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
runbook + coffre : le jeton SAS passe par ansible-vault, pas SOPS (#70)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 1m17s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 14s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 29s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m34s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m0s
46b7446204
Un seul secret ne justifie pas un second outil de chiffrement. Le jeton SAS
rejoint le coffre ansible-vault du projet comme tous les autres secrets
d'exploitation.

- secrets.md reecrit autour de vault_sas_archive_depot
- vault.yml.example : la variable remplace la note d'attente

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9GaXoZ17B3o9kxsb3P973
ci : toute demande de fusion touchant Terraform est validée et auditée, sans Azure (#72)
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 28s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 46s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 22s
Intégration / Checkov — audit de la configuration (pull_request) Successful in 49s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 2m13s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m50s
ed0c86bf88
Deux tâches de plus dans « Intégration », aux outillages sans recouvrement :

  - terraform : `fmt -check -recursive`, `init -backend=false`, `validate`, `tflint` ;
  - checkov : audit de la configuration, rapport JUnit en artefact.

DANS ci.yml, ET NON DANS UN WORKFLOW FILTRÉ À PART. Le ticket demandait « un job
filtré sur infra/terraform/** », et le travail a d'abord pris cette forme. Elle
et le blocage réel s'excluent, pour la raison écrite en tête de ci.yml : un
contrôle obligatoire attend un statut, le statut n'est écrit que si le workflow
démarre, et `on.paths` agit avant le démarrage. Sur une demande de fusion qui ne
touche pas Terraform, un requis « Infra Terraform / * » resterait « en attente »
pour toujours. Rapatriées ici, les deux tâches tombent sous le joker
« Intégration / * » déjà déclaré dans la protection des branches : rien à
configurer sur la forge, et une faute Terraform empêche vraiment la fusion.

La contrepartie est assumée : elles tournent sur chaque demande de fusion, comme
Ruff, mypy et npm audit, qui n'ont jamais regardé non plus ce que la demande
changeait. Le fournisseur azurerm est mis en cache sur la clé du lock pour que ce
prix reste de l'ordre de la minute. Chaque tâche se désiste proprement si
infra/terraform disparaît, et le dit dans son tableau de synthèse.

AUCUN SECRET, AUCUNE VARIABLE ARM_*. `terraform init` est joué en
`-backend=false` : le bloc backend azurerm réclamerait un jeton dès
l'initialisation, alors que `validate` n'a besoin que du schéma du fournisseur,
qui vient du registre public. N'importe quel membre obtient donc un verdict sur
l'infrastructure sans rôle sur l'abonnement école.

Terraform 1.16.1 et TFLint 0.64.0 épinglés par version ET par somme de contrôle,
checkov 3.3.16 par un requirements-ci.txt versionné. Un téléchargement échoué
avertit et dit ce qui n'a PAS été contrôlé ; une somme différente bloque. Même
doctrine qu'actionlint.

Bloquant dès la première exécution : le ticket demandait « informatif avant le
jour 5, bloquant après », et le jour 5 était le vendredi 4 septembre. La bascule
CHECKOV_BLOQUANT reste, pour checkov seul, parce que c'est lui qui porte le
risque de faux positifs — et la tourner exige une demande de fusion.

Les exceptions vivent dans infra/terraform/.checkov.yml, une par ligne, motif
écrit au-dessus. Une seule aujourd'hui, CKV2_AZURE_21 : ce dépôt ne gère aucun
compte de stockage, celui qui porte le conteneur d'archive ayant été créé hors
Terraform par bootstrap.sh (#67).

LA GARDE. Un contrôle vert ne prouve pas qu'il regarde.
tests/ci/test-garde-terraform.sh rejoue fmt et checkov — avec le vrai fichier
d'exceptions — sur un fichier volontairement mal formaté et volontairement
dangereux (`allow_blob_public_access = true`), et échoue si celui-ci passe. La
preuve demandée par le ticket est ainsi rejouée à chaque exécution au lieu
d'avoir été faite une fois dans une demande de fusion d'essai que personne ne
rouvrira. Elle rougit aussi le jour où une exception devient trop large. Le banc
tourne dans les deux tâches, chacune n'ayant qu'un des deux outils, et annonce
celui qui lui manque plutôt que de se taire.

Mesuré, pas supposé, sur l'état du dépôt au 09/09 : fmt, init -backend=false,
validate, tflint (preset `all`) et checkov passent ; le fichier fautif est refusé
par les deux gardes, checkov nommant CKV_AZURE_59 ; actionlint, shellcheck,
yamllint et zizmor sont propres sur ci.yml ; le banc d'hygiène compte six tâches,
six résumés et trente-six noms de contrôle qui correspondent tous.

Manuel d'exploitation : docs/runbooks/ci.md, section « Terraform et sécurité
IaC », et le tableau des tâches passé de quatre à six.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cc9tidxhXLh4pyLxVar8es
coffre : le jeton SAS d'ecriture sur l'archive Azure (#70)
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 42s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 32s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m51s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m15s
7b78f288c5
vault_sas_archive_depot : jeton SAS de service adosse a la stored access
policy depot-archive (permissions cwl, HTTPS, expiration 2026-10-05). Genere
le 09/09 depuis un poste, consomme par le role backup (#71).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9GaXoZ17B3o9kxsb3P973
Le #47 n'avait aucune trace dans le dépôt. Il en a deux.

docs/RECETTE.md — la grille des 28 exigences, avec pour chacune ce qu'on
observe écrit pour quelqu'un qui ne connaît pas le code, où le voir, et un
verdict daté du passage du 09/09 sur main = 415af1a. 22 tenus, 5 tenus avec
réserve, 1 non tenu, 2 non joués. La section 4 liste sept constats numérotés
avec leurs preuves, plutôt que de les contourner en silence.

docs/DEMONSTRATION.md — neuf étapes chronométrées, cible 20 minutes, qui suivent
une seule mesure de la minute où elle est relevée jusqu'à la recommandation
qu'elle déclenche. Chaque étape a son critère observable et son porte-parole.
Quatre réserves à annoncer avant que le jury les trouve.

Trois lignes du PRD sont corrigées parce que la recette les contredit :

- ENF-02 passe de « Fait » à « Non tenu ». Le fichier que le PRD citait comme
  preuve dit dans sa propre documentation que tout utilisateur authentifié voit
  les sept sites, et ops.acces_site est vide pour les huit comptes.
- ENF-04 passe de « À publier » à publié : 85 ms de p95 sur la série 24 h pour
  une cible de 400, 23 ms sur les prévisions pour une cible de 200.
- EF-07 : la PR #206 est déployée et ne corrige pas la réserve #205. La limite
  est l'agrégat horaire, pas la cadence de l'ETL.

tests/e2e/ reste vide, et le README dit maintenant pourquoi : le #47 a tranché
que la recette se joue contre la production et relève d'EC02, pas d'EC03.

Refs #47
docs: le scénario rejoué d'un seul tenant, et la vraie cause du décalage d'heure
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 22s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 43s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m1s
2207bc60c6
Passage enchaîné du 09/09, 12:10:58 -> 12:14:23 UTC : étapes 1 à 4, 7 et 8
dans l'ordre, sans incident, 3 min 25 s. La ligne entre au journal des
répétitions avec sa portée exacte — les étapes 5 et 6 n'ont pas été rejouées,
la session du navigateur avait expiré, et l'étape 9 est un document.

Ce que ce chiffre établit et ce qu'il n'établit pas est écrit sous le tableau :
l'exécution technique est le plancher, la répétition du 10 mesure autre chose,
le récit à cinq voix dans un budget de 20 minutes.

R1 est repris avec sa cause, désormais prouvée à la source :

    {"timestamp":"2026-09-09T14:13:07.790800", ...}   à 12:13:07 UTC

L'horloge de la source avance de deux heures et n'écrit aucun fuseau. Les deux
chemins d'ingestion en tirent deux conclusions différentes : les mesures
ramènent la valeur en UTC et sont justes, les alertes et le nommage des objets
bruts la rangent telle quelle et sont fausses de deux heures. Le défaut est
donc localisé, et le chemin des mesures porte déjà la conduite à recopier.

ENF-08 gagne une preuve : l'issue #243 est refermée par le relais au retour à
la normale. Le cycle de vie est tenu de bout en bout, pas seulement l'ouverture.

Refs #47
supervision: ce que l'ETL transforme, pas seulement qu'il tourne (#244)
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 46s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Workflows — lint et audit de sécurité (pull_request) Failing after 18s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m57s
4f0e4dd269
Depuis le #228, les trois passes de l'ETL publient leur etat : code de
retour, duree, derniere reussite. Elles ne publient pas ce qu'elles ont
fait. Une passe argent qui rend 0 sans rien ecrire ressemble, en
supervision, trait pour trait a une passe qui a traite 5 040 lignes, et
quand la chaine decroche le tableau de bord montre la zone or vieillir
sans dire lequel des trois maillons a lache. Le diagnostic se faisait en
tail sur silver.log.

Trois metriques de plus, etiquetees par zone du medaillon : le volume
ecrit, la journee de donnee traitee, la date du dernier refus. La
troisieme attrape le mode de panne le plus sournois de la chaine :
load-postgres peut reussir tous les quarts d'heure en rechargeant
indefiniment la meme journee d'hier, code 0 et derniere reussite fraiche,
pendant que la base gele.

Le job Python depose un resume avec --resume, le lanceur le publie. Le
repertoire textfile garde ainsi un seul ecrivain : node-exporter le
concatene, et une seule ligne mal formee y ferait echouer la collecte
entiere, donc emporterait aussi les metriques des six autres taches. Le
resume ne porte que des entiers, filtres une seconde fois par le lanceur,
et l'etiquette zone vient du lanceur, jamais du fichier.

Le refus de journee de la zone argent passe du code 1 au code 4, celui que
le contrat des codes de sortie reserve deja a ce cas et que la zone or
rendait seule. Un refus se corrige en regardant la source, une panne en
regardant le job : le code doit permettre de trancher sans ouvrir le
journal. L'ecart etait connu et documente comme un piege dans le manuel.

Sur un poste sans /var/log/enervision, rien n'est demande au job, rien
n'est publie, et les trois passes rendent le meme code qu'avant.
Merge pull request 'docs : le backlog dit l'état du 09 sur les tickets du Tech Lead' (#236) from lenaic/backlog-etat-du-09 into develop
Some checks failed
Intégration / Contrôles statiques du dépôt (push) Successful in 9s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 30s
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 45s
Intégration / Python — qualité, tests et dépendances (push) Has been cancelled
717c426c88
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/236
Reviewed-by: gabriel <gabriel@noreply.10.105.200.41>
Merge branch 'develop' into gabriel/244-supervision-etl
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 45s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Workflows — lint et audit de sécurité (pull_request) Failing after 23s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m9s
c708219ee8
Merge pull request 'supervision : ce que l'ETL transforme, pas seulement qu'il tourne (#244)' (#245) from gabriel/244-supervision-etl into develop
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (push) Waiting to run
Intégration / Contrôles statiques du dépôt (push) Waiting to run
Intégration / Workflows — lint et audit de sécurité (push) Waiting to run
Intégration / Python — qualité, tests et dépendances (push) Has been cancelled
7361775134
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/245
Reviewed-by: lenaic <lenaic@noreply.10.105.200.41>
Merge pull request 'ci : toute demande de fusion touchant Terraform est validée et auditée, sans Azure (#72)' (#242) from justine/72-garde-terraform into develop
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 45s
Intégration / Contrôles statiques du dépôt (push) Successful in 7s
Intégration / Terraform — format, validité et lint (push) Successful in 24s
Intégration / Checkov — audit de la configuration (push) Failing after 40s
Intégration / Workflows — lint et audit de sécurité (push) Failing after 23s
Intégration / Python — qualité, tests et dépendances (push) Successful in 5m58s
b5667d6cd4
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/242
Reviewed-by: olivier <olivier@noreply.10.105.200.41>
etl: la chaine passe aux dix minutes, l agregat continu suit (#246)
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 47s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 8s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 23s
Intégration / Checkov — audit de la configuration (pull_request) Failing after 41s
Intégration / Workflows — lint et audit de sécurité (pull_request) Failing after 17s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m42s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m43s
fb5a5b11a6
La serie au pas de la minute que le pave « etat des sites » affiche a
entre 28 et 87 minutes d age sur la machine : la crontab de `deploy`
porte encore la chaine horaire, et le chargement n ecrit les minutes de
l heure H qu a (H+1):27. Le #205 a ramene la chaine au quart d heure
pour la PREVISION, qui lit `mesure_horaire` ; il ne visait pas la serie,
que `dashboard/repository.py` lit dans `public.mesure`.

Les trois passes passent aux dix minutes — :00 :10 …, l or trois
minutes derriere, le chargement deux de plus — et la migration 0022
ramene `end_offset` et `schedule_interval` de `mesure_horaire` aux dix
minutes avec elles. L age de la derniere minute en base tombe de 28-87
a 6-15 minutes, et le seau horaire devient lisible a (H+1):10.

LES TROIS MINUTES DERRIERE L ARGENT NE BOUGENT PAS. C est la marge la
plus serree de la chaine — trois fois la duree d une passe de fin de
journee — et la seule que le verrou de la zone or ne couvre pas. Les
minutes gagnees ont donc ete prises sur l ecart or → chargement, que le
verrou couvre, ramene de cinq a deux.

Le banc de fraicheur ne code plus la cadence en dur : il la LIT dans
`taches_planifiees`, verifie qu elle est reguliere, et y confronte la
politique de la derniere migration qui en pose une. Le #205 avait ecrit
le quart d heure a trois endroits du banc, qu il fallait reecrire
ensemble ; une troisieme cadence n aura plus a y toucher. Le banc
nommait aussi `0020_fraicheur_zone_or.sql` en dur : il serait reste vert
sur une politique perimee.

TROIS ERREURS CORRIGEES AU PASSAGE.

La cadence etait ecrite a un SECOND endroit, et le rebasage sur le #244
ne pouvait pas le voir : les trois lanceurs passent leur intervalle a
`ops_publier_resultat`, qui publie `ev_ops_tache_cadence_secondes`, et
ils declaraient encore 900 s. Le tableau de bord `etl` divise l age de
la derniere reussite par cette valeur et l alerte du #228 se declenche
au dela de trois fois : une valeur perimee ne casse rien de visible,
elle DESSERRE l alerte en silence — 45 minutes de chaine arretee
tolerees au lieu de 30. Le banc controle desormais les SEPT taches,
crontab contre valeur declaree.

La borne d attente du verrou valait 900 s, posee pour une chaine horaire
et jamais suivie au quart d heure — elle depassait la cadence entiere,
soit exactement l empilement de processus qu elle empeche. Elle passe a
150 s, le quart de la cadence que son commentaire annonce.

Le commentaire de `taches_planifiees` affirmait que la passe de minuit
« sort sans rien ecrire, quatre fois par jour au quart d heure ».
Verifie dans `silver/job.py` et `silver/grille.py` : `_jour_demande` la
fait basculer sur la veille, qu elle clot, donc elle ecrit 1 440 minutes
et c est la passe la plus lourde du jour. Le garde-fou vise n est
atteint que par un `--jour` explicite, et une seule passe par jour tombe
a 00:00 quelle que soit la cadence.

Ce n est pas du temps reel, et le mot n est pas employe : c est une
chaine par lots dont le lot fait dix minutes. Dix minutes est le
plancher de cette forme — la passe argent relit la journee entiere a
chaque tour. Descendre a cinq demande de la rendre incrementale, et de
le mesurer d abord.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs: la démonstration est enregistrée, pas jouée en direct
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 26s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 44s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m3s
5a26dc49e8
La première version supposait une soutenance collective : répartition de la
parole entre les six, préparation de la salle, budget de 20 minutes à cinq
voix, journal de répétitions. L'oral est individuel et la démonstration passe
par des vidéos : tout cet appareil ne servait à rien.

DEMONSTRATION.md devient un compte rendu. Ce qui disparaît : le tableau
« qui parle », la préparation de la machine qui projette, la chorégraphie et
le chronomètre de parole. Ce qui reste, et qui portait la valeur : le fil d'une
seule mesure suivie jusqu'à la recommandation, et pour chaque étape ce qu'il
faut voir à l'image, écrit pour quelqu'un qui ne connaît pas le code.

Ce qui change de nature : chaque étape porte désormais ce qui a été observé en
la jouant le 09/09, la section 6 est le compte rendu du passage plutôt qu'un
journal de répétitions, et la préparation vise la prise de vue — l'autorité de
certification sur la machine qui filme, les alertes au repos, le modèle v6.

Les prises se font séparément et se remontent : une étape ratée se refait sans
conséquence sur les autres, ce qui retire au chronomètre l'essentiel de son
intérêt.

RECETTE.md est réaligné sur les deux renvois qui parlaient de répétition.

Refs #47
ci: shellcheck rougit sur un faux positif, et il masque deux etapes (#246)
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 48s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 8s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 24s
Intégration / Checkov — audit de la configuration (pull_request) Failing after 42s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 17s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m22s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m48s
4c08c290cb
`shellcheck tests/ci/*.sh` remonte SC2034 sur test-supervision.sh:495,
« OPS_TEXTFILE_DIR appears unused ». La variable est en fait lue par
`ops_ecrire`, dans bin/_metriques-ops.sh charge juste au-dessus (lignes
52-53 et 65-66) ; shellcheck ne peut pas le voir puisque la source est
dynamique, d ou le SC1090 deja pose sur la ligne precedente. Le controle
qui suit le prouve : sans cette affectation, `ev_etl_argent.prom` ne
serait pas ecrit dans le bac et l assertion echouerait.

CE N EST PAS UN ROUGE ISOLE, ET C EST LA VRAIE RAISON DE CE COMMIT. Dans
le job « Workflows », les etapes sont sequentielles sous `sh -e` :
l echec de shellcheck laissait yamllint ET zizmor a l etat « ignoree ».
Deux audits ne tournaient plus, et le rapport ne le disait pas en ces
termes — on lisait « seul shellcheck a un reproche ». Les deux sont
joues ici, a l outillage epingle de .forgejo/requirements-meta.txt
(yamllint 1.38.0, zizmor 1.30.0), avec les memes arguments que la
chaine : code 0 tous les deux, « No findings to report » pour zizmor.

Une directive plutot qu un `export` : la fonction tourne dans CE shell,
elle n a pas besoin de l environnement, et exporter donnerait a croire
qu un sous-processus la lit.

Le constat vient du #244 (PR #245) et il est present sur `develop` : ce
n est pas ce lot qui l a introduit, mais il bloque la fusion de toute
demande, dont celle-ci.

Le second rouge de `develop`, « Checkov », n est PAS traite ici : ce ne
sont pas des faux positifs mais huit constats de securite reels sur
`azurerm_storage_account.archive`, qui demandent un arbitrage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs: le PRD et les manuels rattrapent ce que la recette a troué
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 29s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 43s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m38s
46418fda9e
Retire docs/DEMONSTRATION.md. Les vidéos sont faites et l'oral est individuel :
un scénario de passage n'a plus d'objet. Ce qui comptait — le fil d'une mesure
suivie jusqu'à la recommandation, et le compte rendu du passage joué — vit dans
RECETTE.md, qui reste le livrable du #47.

PRD en 1.3. Les statuts ne sont plus relus, ils sont joués : neuf changent, et
pas tous vers le haut.

- ENF-02 : « Fait » -> « Non tenu ». Le fichier cité comme preuve dit dans sa
  propre documentation que tout utilisateur authentifié voit les sept sites.
- EF-08 : « En cours » -> « Fait ». Le mécanisme a tiré en production, et on
  sait maintenant pourquoi il ne tirait pas avant : seuil à 100 % de capacité,
  maximum historique du parc à 94,7 %.
- ENF-04 : « À publier » -> publié, 85 ms et 23 ms de p95.
- EF-05, ENF-06, ENF-07, ENF-12 gagnent une réserve qu'ils n'avaient pas.
- ENF-08 et ENF-16 gagnent une preuve obtenue en conditions réelles.
- §2 ne prétend plus que les sites autorisés sont portés par une table ; §3
  compte dix alertes et non cinq ; la table des risques est réalignée, trois
  risques sont levés et celui de la promotion est reformulé sur sa vraie cause.

Manuels d'exploitation, là où la recette les contredit :

- reentrainement.md : deux encarts. La règle de promotion ne regarde jamais le
  modèle en place, avec le tableau des trois versions qui le démontre et la
  conduite à tenir tant qu'elle n'a pas changé. Et le retour arrière ne prend
  pas effet à la seconde — le cron de :35 a gagné la course de 26 s.
- mlflow.md : la lecture des deux métriques avant de bouger l'alias n'est pas
  une politesse, c'est le seul garde-fou ; l'étape 2 est la comparaison que le
  code ne fait pas.
- acces-serveur.md : un troisième cas. ssh refuse pendant que nc et ssh-keyscan
  passent sur le même port, et les trois noms HTTPS tombent ensemble — ça
  décrit le lien du poste, pas le serveur. uptime tranche en dix secondes.

Refs #47
Merge branch 'develop' into gabriel/47-recette-demonstration
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 49s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 28s
Intégration / Checkov — audit de la configuration (pull_request) Failing after 40s
Intégration / Workflows — lint et audit de sécurité (pull_request) Failing after 19s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m6s
db5ef5db74
retours de Justine sur la #239 (#70)
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 42s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 8s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 18s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m14s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m2s
37677d2ad0
- secrets.md : la variable de coffre est posee, plus au futur ; section
  « Ou il vit » au present ; exemple de jeton a la forme reelle (si=, spr=,
  ni sp ni se) ; recette au passe, pointe le commentaire du #70
- README.md : la ligne d'index disait encore age/SOPS
- securite.md : la note « SOPS nulle part » devient la decision, prise ici
- stockage-secours.md : lien vers secrets.md depuis la ligne du client de depot
- note du nom archive vs daily-archive dans le tableau d'entete

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9GaXoZ17B3o9kxsb3P973
Merge branch 'develop' into marvin/70-sas-archive
Some checks failed
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 28s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 45s
Intégration / Workflows — lint et audit de sécurité (pull_request) Failing after 20s
Intégration / Checkov — audit de la configuration (pull_request) Failing after 41s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m37s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m46s
3f96c447b3
Merge pull request 'etl : la chaîne passe aux dix minutes, l'agrégat continu suit (#246)' (#248) from olivier/246-cadence-dix-minutes into develop
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 44s
Intégration / Contrôles statiques du dépôt (push) Successful in 7s
Intégration / Terraform — format, validité et lint (push) Successful in 28s
Intégration / Checkov — audit de la configuration (push) Failing after 40s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 18s
Infra Ansible / Playbooks Ansible valides (push) Successful in 2m20s
Intégration / Python — qualité, tests et dépendances (push) Successful in 5m56s
6ba6a5db7b
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/248
Reviewed-by: gabriel <gabriel@noreply.10.105.200.41>
Merge branch 'develop' into gabriel/47-recette-demonstration
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 49s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 25s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 19s
Intégration / Checkov — audit de la configuration (pull_request) Failing after 47s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m6s
fe5ab296b6
Le lot #246 (chaîne aux dix minutes) est arrivé sur develop après la fusion
précédente et reprend la parole sur EF-07 et EF-08 : conflit sur docs/PRD.md.
Les deux côtés ont écrit sur les mêmes exigences le même jour, depuis deux
observations différentes.

EF-08 : la recette du 09/09 a vu le mécanisme tirer en production — deux
dépassements écrits, propagés jusqu'à l'écran. Le « En cours » de develop
disait l'inverse sur la foi de zéro dépassement observé la veille. La preuve
mesurée l'emporte, avec sa provenance.

EF-07 : develop apporte le #246, que la recette ne connaissait pas, mais le
donne comme non déployé au même titre que le #206. Le #206 est dans `main`
depuis la mise en production du 09/09 et la recette l'a mesuré à l'œuvre — il
ne corrige simplement pas la réserve, dont la cause est l'agrégat horaire. La
cellule garde le relevé du 09/09 et gagne le #246, dont la migration 0022
écrit elle-même que le plancher de deux seaux ne bouge pas : le lot ne lève
pas la réserve non plus.

§6 corrigé dans la foulée : la ligne venue de develop annonçait « deux
cadences de retard », ce qui contredisait EF-07 à cinquante lignes d'écart.
Une seule cadence manque à la production, celle des dix minutes.
Merge branch 'develop' into marvin/70-sas-archive
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 47s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 8s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 24s
Intégration / Checkov — audit de la configuration (pull_request) Failing after 47s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 18s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 2m2s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m0s
52453a5b51
Merge pull request 'docs : critères de recette et scénario de démonstration (#47)' (#247) from gabriel/47-recette-demonstration into develop
Some checks failed
Intégration / Contrôles statiques du dépôt (push) Waiting to run
Intégration / Terraform — format, validité et lint (push) Waiting to run
Intégration / Checkov — audit de la configuration (push) Waiting to run
Intégration / Workflows — lint et audit de sécurité (push) Waiting to run
Intégration / Python — qualité, tests et dépendances (push) Has been cancelled
Intégration / Tableau de bord — dépendances, tests et construction (push) Has been cancelled
8147df2e83
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/247
Reviewed-by: lenaic <lenaic@noreply.10.105.200.41>
CE QUI N'ALLAIT PAS. Les tâches « terraform » et « checkov » vivent dans ci.yml,
et non dans un workflow filtré par `on.paths`, pour que le joker
« Intégration / * » les rende vraiment bloquantes -- un workflow filtré ne
démarre pas, n'écrit aucun statut, et laisserait un contrôle requis « en
attente » pour toujours. Mais elles tournaient EN ENTIER sur chaque demande de
fusion : une correction de CSS téléchargeait Terraform, TFLint et checkov, et
rougissait sur une faute d'infrastructure arrivée par quelqu'un d'autre. Un
contrôle qui accuse une personne du travail d'une autre se fait désarmer dans la
semaine.

LA MOITIÉ MANQUANTE. .forgejo/scripts/terraform-touche.sh. La tâche démarre
toujours -- le statut est écrit, le joker satisfait -- et ce sont ses ÉTAPES qui
se désistent quand la demande ne touche à rien de Terraform. C'est la doctrine du
reste de ci.yml ; seule la question posée change : non plus « ce dépôt
contient-il du Terraform » mais « CETTE DEMANDE y touche-t-elle ».

Réveillent les deux tâches : infra/terraform/, les fixtures fautives, les deux
bancs, le script lui-même et ci.yml.

IL RÉPOND « OUI » DÈS QU'IL DOUTE. Un « oui » de trop coûte deux minutes
d'exécuteur ; un « non » de trop laisse fusionner une faute d'infrastructure sous
une tâche VERTE, que personne ne verra puisqu'il n'y a rien à voir. Événement
hors demande de fusion, GITHUB_BASE_REF vide, git diff en erreur : tout vaut
« on contrôle tout ». Le seul « non » possible est celui d'un diff réellement
obtenu et réellement vide.

D'où le `fetch-depth: 0` sur les deux checkouts concernés : sans historique
complet, « origin/<base> » n'existe pas localement et la comparaison échoue. Le
retirer rend la chaîne lente, pas aveugle -- mais lente pour rien. 586 commits,
4,5 Mo.

tests/ci/test-terraform-touche.sh tient les deux moitiés, en douze cas d'essai
sur un dépôt jetable : poussée hors demande, base vide, base introuvable, chacun
des chemins concernés, un fichier concerné noyé parmi d'autres, et le seul cas
qui doit rendre « non ». Joué par la tâche « repo ».

Mesuré : les douze cas passent, les treize autres bancs sont inchangés,
actionlint, shellcheck, yamllint et zizmor sont propres, et le banc d'hygiène
compte trente-sept noms de contrôle qui correspondent tous.

Manuel d'exploitation : docs/runbooks/ci.md, section « Terraform et sécurité
IaC », nouvelle sous-section « Le désistement ».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cc9tidxhXLh4pyLxVar8es
Merge pull request 'infra : bundle Git quotidien du dépôt, hors serveur' (#240) from lenaic/74-bundle-depot into develop
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 49s
Intégration / Contrôles statiques du dépôt (push) Successful in 8s
Intégration / Terraform — format, validité et lint (push) Successful in 30s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 18s
Intégration / Checkov — audit de la configuration (push) Failing after 42s
Infra Ansible / Playbooks Ansible valides (push) Successful in 2m21s
Intégration / Python — qualité, tests et dépendances (push) Successful in 5m45s
1815fff35f
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/240
Reviewed-by: gabriel <gabriel@noreply.10.105.200.41>
terraform : les neuf constats de checkov portent leur motif, écrit sur la ressource (#72)
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 48s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 23s
Intégration / Checkov — audit de la configuration (pull_request) Successful in 48s
Intégration / Workflows — lint et audit de sécurité (pull_request) Failing after 25s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m59s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m58s
bdd2d80b31
La chaîne était rouge sur huit constats visant `azurerm_storage_account.archive`,
arrivé avec le #69 après que le #72 eut posé l'audit. Aucun n'était un défaut de
la chaîne : checkov faisait exactement ce qu'on lui demande.

SIX SONT DES REFUS DE L'ABONNEMENT OU DU RÔLE, et le fichier les développait
déjà, en toutes lettres, sans que checkov puisse le lire :

  CKV_AZURE_59  / CKV2_AZURE_33  point de terminaison privé impossible ; mettre
                                 public_network_access_enabled à false couperait
                                 le terraform init qui lit l'état DANS ce compte
  CKV2_AZURE_40                  shared_access_key_enabled reste true, le #70 en
                                 a besoin
  CKV_AZURE_206                  Standard_LRS est ce que bootstrap.sh a créé et
                                 ce que ce fichier adopte ; le géo-redondant
                                 doublerait le coût contre le plafond de l'ADR 0012
  CKV_AZURE_33                   aucun service Queue n'est utilisé
  CKV2_AZURE_1                   le chiffrement par clé gérée exige un Key Vault,
                                 hors du rôle Devops-cours-projet-eadl

DEUX SONT DES DÉCISIONS OUVERTES, marquées comme telles et non comme des refus :
CKV2_AZURE_38 (suppression réversible) et CKV2_AZURE_41 (expiration des jetons
SAS, qui appartient au #70). Elles se retireront dans le même geste que la
propriété qui les remplacera. Rien n'est décidé ici à la place de ces tickets :
ce commit ne change aucune propriété Azure, seulement des commentaires — le plan
est vide.

SUR LA RESSOURCE, PAS DANS .checkov.yml. Une exception globale éteint la règle
pour tout le dépôt, y compris pour la ressource que quelqu'un ajoutera demain
sans savoir qu'elle est éteinte. Le fichier global reste réservé à ce qui est
inapplicable partout — et il ne contient plus rien du tout.

CAR L'EXCEPTION GLOBALE DU #72 AVAIT EXPIRÉ. CKV2_AZURE_21 y était écartée au
motif que « ce dépôt ne gère aucun compte de stockage ». Le #69 a adopté le
compte : le motif est devenu faux, alors que la règle lève toujours, sur le
conteneur d'archive. Une exception dont la justification a expiré est le pire des
deux mondes — elle éteint largement, et son motif n'apprend rien. Déplacée sur le
conteneur dans archive.tf, avec un motif à jour et vérifiable : l'espace Log
Analytics qu'elle réclame n'est pas créable avec le rôle disponible. Le « À
REVOIR le jour où un compte de stockage est réellement déclaré ici » qu'elle
portait aura servi exactement à cela.

CE QUE CELA N'A PAS ÉTEINT, et c'est le contrôle à faire en relecture : une
exception posée sur une ressource ne vaut que pour elle. CKV_AZURE_59 est écartée
sur ce compte-là et reste vivante partout ailleurs — tests/ci/test-garde-
terraform.sh la rejoue sur le fichier fautif et passe toujours. Si quelqu'un
avait glissé ces huit lignes dans .checkov.yml, ce banc serait rouge.

Mesuré : checkov rend « 9 réussis, 0 en échec, 9 sautés », code 0, rapport JUnit
produit ; terraform fmt et tflint passent ; les treize autres bancs sont
inchangés. Ce commit n'ajoute aucun argument Terraform, seulement des
commentaires : fmt et tflint, qui analysent le HCL en entier, en sont la preuve.

Manuel d'exploitation : docs/runbooks/ci.md, « Que faire quand une tâche
bloque », état des exceptions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cc9tidxhXLh4pyLxVar8es
Merge pull request 'recommandations : un moteur de regles charge sans redeploiement (#116)' (#241) from olivier/116-moteur-regles-generique into develop
Some checks failed
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 47s
Intégration / Contrôles statiques du dépôt (push) Successful in 8s
Intégration / Terraform — format, validité et lint (push) Successful in 37s
Intégration / Checkov — audit de la configuration (push) Failing after 44s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 21s
Infra Ansible / Playbooks Ansible valides (push) Successful in 1m19s
Intégration / Python — qualité, tests et dépendances (push) Successful in 6m3s
e71d9f6bda
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/241
Reviewed-by: gabriel <gabriel@noreply.10.105.200.41>
ci : shellcheck ne bloque plus la chaîne sur une variable qui fonctionne
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 48s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 25s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 18s
Intégration / Checkov — audit de la configuration (pull_request) Successful in 47s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m7s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m48s
b61a95a17d
L'étape « shellcheck sur nos propres scripts » de la tâche « meta » était rouge
sur develop, donc la chaîne l'était pour tout le monde :

  tests/ci/test-supervision.sh:495
  SC2034 (warning): OPS_TEXTFILE_DIR appears unused.

L'avertissement est un faux positif, mais un faux positif explicable et non un
bruit à faire taire. La variable EST lue : `ops_publier_volumetrie` vient d'être
sourcée depuis bin/_metriques-ops.sh, qui la pose à la ligne 38 et la relit à
chaque écriture. shellcheck, lui, ne suit pas un source dynamique -- SC1090 est
désactivé deux lignes plus haut, précisément pour cela -- si bien qu'il ne voit
qu'une affectation jamais relue dans ce fichier.

Le remède est celui que shellcheck propose lui-même dans son message, « or
export if used externally » : un `export`, qui ne change rien au comportement et
dit ce qui se passe. Préféré à un `# shellcheck disable=SC2034`, qui aurait
masqué la classe entière dans ce fichier pour une seule ligne.

Mesuré : `shellcheck .forgejo/scripts/*.sh tests/ci/*.sh` -- la commande exacte
de l'étape -- rend 0. Le banc se comporte à l'identique, et ses quatre
assertions sur la volumétrie passent, dont « un résumé bien formé devient une
métrique », qui lit le fichier écrit dans le bac jetable et prouve donc que la
variable est bien prise en compte.

Sans rapport avec le #72, corrigé au passage parce qu'aucune demande de fusion
ne pouvait devenir verte tant que cette étape échouait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cc9tidxhXLh4pyLxVar8es
Merge remote-tracking branch 'origin/develop' into justine/72-garde-terraform-desistement
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 11s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 46s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 24s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 19s
Intégration / Checkov — audit de la configuration (pull_request) Successful in 50s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m57s
2a28099021
# Conflicts:
#	tests/ci/test-supervision.sh
Merge pull request '[72] Correction ci' (#251) from justine/72-garde-terraform-desistement into develop
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 47s
Intégration / Contrôles statiques du dépôt (push) Successful in 9s
Intégration / Terraform — format, validité et lint (push) Successful in 27s
Intégration / Checkov — audit de la configuration (push) Successful in 48s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 19s
Intégration / Python — qualité, tests et dépendances (push) Successful in 6m9s
f5da7052c8
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/251
Reviewed-by: gabriel <gabriel@noreply.10.105.200.41>
Merge branch 'develop' into marvin/70-sas-archive
All checks were successful
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 47s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 7s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 13s
Intégration / Checkov — audit de la configuration (pull_request) Successful in 15s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 19s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m34s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m48s
0aa15f4e83
Merge pull request 'runbook + coffre : le jeton SAS de l'archive de secours (#70)' (#239) from marvin/70-sas-archive into develop
All checks were successful
Intégration / Contrôles statiques du dépôt (push) Successful in 22s
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 49s
Intégration / Terraform — format, validité et lint (push) Successful in 26s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 19s
Intégration / Checkov — audit de la configuration (push) Successful in 50s
Infra Ansible / Playbooks Ansible valides (push) Successful in 1m40s
Intégration / Python — qualité, tests et dépendances (push) Successful in 5m53s
0dc919c9ea
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/239
Reviewed-by: justine <justine@noreply.10.105.200.41>
Un seul conflit, sur `rules.yaml`. Les deux côtés ajoutaient au même endroit :
cette branche une sixième alerte au groupe unique du #42, `develop` une
restructuration en deux groupes de cinq (#228).

La structure de `develop` est retenue, et la règle de dérive prend place dans
le groupe « Exploitation » plutôt que d'ouvrir un troisième groupe : une
dérive de modèle relève du même registre que les cinq du #42, un service qui
se dégrade sans tomber. Le pavé d'en-tête est recompté — onze alertes, non dix
— et dit d'où vient la onzième.

Rien d'autre n'a été touché. Les onze identifiants sont uniques, le fichier se
relit en YAML, et les deux groupes portent bien six et cinq règles.
outillage : le banc de supervision comptait les règles d'un seul préfixe (#117)
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 10s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 16s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 45s
Intégration / Checkov — audit de la configuration (pull_request) Successful in 16s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 17s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 6m4s
0fb36cf039
Le contrôle « chaque règle nomme un destinataire » comparait deux nombres qui
ne portaient pas sur le même ensemble. Le dénominateur ne retenait que les
identifiants `ev_ops_*`, le numérateur comptait tous les labels
`destinataire`. Une règle nommée autrement était donc invisible au premier et
visible au second.

C'est arrivé dès la première : le #117 ajoute `ev_ia_derive_modele`, une
alerte de modèle et non d'exploitation, nommée en conséquence. Le banc
annonçait « 11 label(s) destinataire pour 10 règle(s) » et faisait rougir la
tâche « Contrôles statiques » sur un fichier parfaitement correct.

Deux compteurs désormais, chacun sur son ensemble. `n_ops` garde le plancher
de dix règles d'exploitation promis par le #42 et le #228 — c'est lui qui
empêche une disparition silencieuse. `n_regles` compte toutes les règles quel
que soit leur préfixe, et c'est lui qui se compare aux destinataires.

Renommer la règle en `ev_ops_ia_derive` aurait fait taire le banc sans le
corriger : la prochaine famille d'alertes aurait rouvert le même défaut. Le
banc doit décrire la règle du dépôt — toute règle nomme un destinataire — pas
une convention de nommage qu'il est seul à connaître.

Les quatorze bancs de `tests/ci/` passent.
ia : la surveillance de dérive cesse d'annoncer « stable » quand elle ne mesure plus (#117)
All checks were successful
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 19s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 44s
Intégration / Checkov — audit de la configuration (pull_request) Successful in 15s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 18s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m51s
1911b2323f
Répond au point bloquant de la relecture. `publication.publier` n'était atteint
que sur le chemin heureux : les deux sorties anticipées de `surveiller_derive`
— référence absente, exception — rendaient `None` sans rien écrire. Le fichier
`.prom` restait alors sur le disque avec ses dernières valeurs, Grafana
affichait « stable » indéfiniment, l'alerte ne se déclenchait pas non plus
puisque son `noDataState` vaut `OK`, et seul un `warning` de journal en portait
la trace. Une surveillance qui annonce stable après avoir cessé de mesurer est
pire que pas de surveillance : elle fait cesser de regarder ailleurs.

Le motif retenu est celui que `bin/_metriques-ops.sh` a écrit au #228, cité
dans la relecture, et il est repris tel quel.

- `ev_ia_derive.prom` se réécrit à CHAQUE tentative, y compris celles qui
  n'ont rien mesuré. Il porte alors `derniere_tentative_timestamp_seconds` et
  RIEN D'AUTRE : pas de verdict. Un zéro y dirait « stable » là où le sens est
  « on ne sait pas ». La série disparaît, ce que `noDataState: OK` traite
  correctement.
- `ev_ia_derive_mesure.prom` ne s'écrit QUE sur une mesure réussie. Il vieillit
  donc dès que la surveillance s'arrête, et c'est ce vieillissement qui alerte,
  jamais un code de retour.
- La règle porte les deux causes par un `or`. Après `max()` les deux membres
  ont la même étiquette vide : `or` rend le gauche quand il existe, le droit
  sinon. Tant qu'on mesure, seul le verdict décide ; dès qu'on cesse, le
  verdict n'est plus là et l'âge prend le relais, au-delà de trois heures.

UN DÉFAUT TROUVÉ EN ÉCRIVANT LE CAS D'ESSAI, et il annulait le correctif.
`{valeur:.6g}` convient aux rapports, il détruit un horodatage : un epoch tient
sur dix chiffres et sortait en « 1.7574e+09 ». Toutes les dates d'une même
tranche de mille secondes devenaient la même, et l'arrondi pouvait aller vers
le HAUT — la métrique aurait paru plus fraîche qu'elle ne l'était, exactement
le mensonge que ces deux séries existent pour empêcher. Les secondes epoch se
rendent désormais en entier.

Les deux remarques non bloquantes sont traitées aussi.

Le commentaire du SQL de recopie disait que `p.horodatage < %s` écartait les
heures en cours. C'est faux, `horodatage` est le début du seau : à 14 h 35 le
seau de 14 h 00 passe la borne sans être terminé. Ce qui protège est que
`mesure_horaire` ne matérialise que des seaux complets. Le commentaire le dit
maintenant, et dit aussi ce qui casse si quelqu'un passe l'agrégat en
`materialized_only = false` pour gagner la fraîcheur qu'on vient de resserrer
au #205.

Le fichier temporaire sortait en `.prom.a8f3c2` via `NamedTemporaryFile`. Le
purgeur d'orphelins ne balaie que `*.prom.[0-9]*`, convention héritée du `$$`
du shell. Il est nommé par le pid, donc nettoyable, et supprimé si l'écriture
échoue.

Éprouvé : quatre cas d'essai de plus, 241 cas d'inférence et de modèle au vert,
quatorze bancs de chaîne au vert, ruff propre.
Merge pull request 'ia : détection de dérive du modèle de prévision (#117)' (#238) from florian/117-derive-modele into develop
All checks were successful
Intégration / Contrôles statiques du dépôt (push) Successful in 9s
Intégration / Terraform — format, validité et lint (push) Successful in 31s
Intégration / Tableau de bord — dépendances, tests et construction (push) Successful in 42s
Intégration / Workflows — lint et audit de sécurité (push) Successful in 19s
Intégration / Checkov — audit de la configuration (push) Successful in 44s
Intégration / Python — qualité, tests et dépendances (push) Successful in 5m54s
Intégration / Contrôles statiques du dépôt (pull_request) Successful in 9s
Intégration / Terraform — format, validité et lint (pull_request) Successful in 29s
Intégration / Tableau de bord — dépendances, tests et construction (pull_request) Successful in 42s
Intégration / Workflows — lint et audit de sécurité (pull_request) Successful in 18s
Intégration / Checkov — audit de la configuration (pull_request) Successful in 47s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m1s
Intégration / Python — qualité, tests et dépendances (pull_request) Successful in 5m56s
7344d4486c
Reviewed-on: https://10.105.200.41/g2/enervision/pulls/238
gabriel requested review from lenaic 2026-09-09 17:39:10 +00:00
lenaic approved these changes 2026-09-09 17:42:11 +00:00
lenaic merged commit b70983e56d into main 2026-09-09 17:42:14 +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!252
No description provided.