diff --git a/AGENTS.md b/AGENTS.md index ff79996..ac0d149 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1022,6 +1022,34 @@ aucune, la décision est une intention, et elle se périmera à la vitesse du co > test — **elle a trouvé un défaut**, le mock déréférençant le contrôleur de chaque Thing en > supposant que toute classe en possède un. L'homogénéité protégeait sa propre prémisse fausse. +> ### Deux masques empilés dans la même chaîne (2026-09-04, QUATRIÈME occurrence) +> +> Même famille, autre étage : **l'environnement de vérification était plus indulgent que la +> réalité**, et il l'a été deux fois de suite dans la même chaîne de build. +> +> 1. Le paquetage qui NOMME le paquet vivait dans l'arbre de travail, pas dans git. Le build +> passait parce qu'il partait de l'arbre. +> 2. Renommer les `.install.in` de `debian-qt5` a fait pendouiller les dix liens de +> `debian-qt6` qui pointaient dessus. Le build passait quand même : les `.install` +> d'un build PRÉCÉDENT traînaient encore, et make les trouvait à jour **sans jamais suivre +> le lien**. +> +> Un artefact périmé couvrait un lien cassé, et le build vert couvrait les deux. Aucune des +> deux erreurs n'était visible depuis le répertoire où l'on travaille — et c'est la propriété +> qui les rend chères : **un répertoire de travail accumule de l'état qui répond à la place du +> dépôt.** La question « est-ce que ça compile ? » n'a pas de valeur si l'on ne dit pas +> *depuis quoi*. +> +> **La seule vérification qui les révèle est la reconstruction dans un répertoire VIERGE, à +> partir de `git archive` — jamais depuis l'arbre de travail, même « propre » au sens de +> `git status`.** Un arbre sans modification en attente peut porter des fichiers générés que +> personne ne versionne, et ce sont eux qui mentent. +> +> **La bonne façon de fermer : le md5.** Le `.so` reconstruit depuis git seul s'est révélé +> identique bit à bit à celui posé sur la box le matin même. Ce n'est pas la même affirmation +> que « ça compile » : c'est la preuve que le build reproductible produit **exactement ce qui +> tourne**. Tant qu'on ne l'a pas comparé, on a deux artefacts dont on espère qu'ils coïncident. + > **Piège de build, 2026-09-01** : changer la DISPOSITION d'une structure dans un en-tête inclus > transitivement (ici un `QDateTime` ajouté à `EcoProgress`, atteint par `LoadAction`) peut > laisser des objets compilés contre l'ancienne. Le symptôme est un **plantage dans un diff --git a/docs/DESIGN_FUSEAU.md b/docs/DESIGN_FUSEAU.md new file mode 100644 index 0000000..9b4398c --- /dev/null +++ b/docs/DESIGN_FUSEAU.md @@ -0,0 +1,135 @@ +# Fuseau horaire — le silence d'une heure + +> Ouvert le 2026-09-04, après qu'une obligation éco déclarée à `"12:00"` se soit résolue +> à 13:00 sur l'horloge de l'atelier. Le moteur avait raison ; c'est la question qui +> n'avait jamais été posée. + +## 1. Le constat + +`.75` tourne en `Europe/London`. Une obligation `dailyDeadline: "12:00"` s'y résout donc à +12:00 heure de Londres, soit **13:00 en France**. Rien, nulle part, ne le dit. + +L'écart est d'une heure et ressemble trait pour trait à un défaut d'heure d'été dans la +résolution d'échéance — j'ai failli l'imputer au moteur avant de lire l'horloge de la box. +C'est le premier coût du silence : il déplace le soupçon. + +Le second coût est celui qui compte. Chez un client français, une box mal fuseau-horée décale +**toutes** ses fenêtres temporelles. Une obligation « avant 7 h » se déclenche à 8 h. Et +quand les heures creuses, Tempo et le marché de gros arriveront, elles se décaleront aussi — +sauf que là, personne ne le verra avant de compter sa facture. + +## 2. Fait établi — il n'y a qu'UN fuseau, et il vient du système + +Vérifié, pas supposé : + +- `EnergyArbitrator::update()` prend `QDateTime::currentDateTime()` + (`energyplugin/etm/energyarbitrator.cpp:329`), donc le fuseau du PROCESSUS `nymead`. +- `resolveDeadline()` construit l'échéance avec `now.timeZone()` + (`energyplugin/etm/types/ecotarget.cpp`). Elle n'introduit aucun fuseau à elle : elle + hérite. +- nymea n'a pas de fuseau propre. `System.GetTime` renvoie `QTimeZone::systemTimeZoneId()` + (`libnymea-core/jsonrpc/systemhandler.cpp`), et `System.SetTimeZone` écrit le fuseau + **système** via le `SystemController` de la plateforme. `Configuration.SetTimeZone` est + déprécié au profit du premier. + +**Conséquence : la box et le moteur ne peuvent pas diverger entre eux.** Il n'existe pas de +réglage nymea qui dirait autre chose que `timedatectl`. C'était la première question — elle +est fermée, et dans le sens rassurant. + +## 3. Fait mesuré — un processus vivant SUIT le changement + +Mesuré le 2026-09-04 dans le conteneur `build-cross-arm64`, avec un programme Qt6 qui lit +`QTimeZone::systemTimeZoneId()` et `QDateTime::currentDateTime()` en boucle pendant qu'on +bascule `/etc/localtime` sous lui : + +``` + systemTimeZoneId=Europe/London currentDateTime=06:48:50 BST zone(now)=Europe/London + --- /etc/localtime bascule vers Europe/Paris --- + systemTimeZoneId=Europe/Paris currentDateTime=07:48:53 CEST zone(now)=Europe/Paris +``` + +**Changer le fuseau ne demande donc PAS de redémarrer `nymead`.** Ce n'est pas une évidence : +la mise en cache du fuseau par processus est un comportement classique de la libc, et si Qt +l'avait fait, une correction de fuseau serait restée sans effet jusqu'au prochain +redéploiement — une correction qui ne corrige rien étant pire que pas de correction. + +Corollaire à ne pas perdre : une échéance en cours de résolution peut donc **changer d'heure +sous le moteur**, sans redémarrage et sans notification. Cf. §6, étape 3. + +## 4. La vraie divergence n'est pas là où on la cherchait + +Box et moteur s'accordent par construction. Ce qui peut diverger, c'est : + +**(a) L'installation et le client.** `dailyDeadline` est une chaîne `"HH:mm"` **sans fuseau**. +Le client saisit « 7:00 » dans l'app, sur un téléphone qui est dans SON fuseau, en pensant +7:00 chez lui. Le moteur la lit dans le fuseau de la box. Si les deux diffèrent, l'écart est +silencieux **des deux côtés** — l'app affiche ce qu'elle a saisi, le moteur exécute autre +chose, et les deux ont raison dans leur référentiel. + +Un fuseau implicite est une absence qui signifie « le défaut s'applique ». C'est exactement +ce qu'on s'interdit ailleurs (`measurement.source`, `rankOrigin`, `progress.regime`). + +**(b) L'installation et le CALENDRIER RÉGLEMENTÉ.** Et c'est le point neuf. + +## 5. La distinction qui décide : deux natures d'heure murale + +Elles ne prennent pas leur fuseau au même endroit, et les confondre est le piège. + +| | nature | fuseau qui fait foi | +|---|---|---| +| `dailyDeadline`, « avant 7 h » | **habitude du client** | celui de l'installation | +| heures creuses, Tempo, EPEX | **calendrier réglementé** | celui où le tarif est DÉFINI | + +Une plage d'heures creuses française est définie en `Europe/Paris` par le fournisseur, pas +« à l'heure locale de la box ». Une box française mal réglée ne décale pas seulement +l'affichage : elle **achète au mauvais moment**, et le tarif, lui, ne s'est pas décalé. + +Aujourd'hui les deux natures prendraient silencieusement le fuseau de la box. Pour la +première c'est correct **si** la box est bien réglée ; pour la seconde c'est faux dès qu'elle +ne l'est pas. Une grandeur réglementée ne doit donc pas hériter du fuseau système : elle doit +porter le sien. + +> Le marché de gros amont travaille en instants ABSOLUS (les tests construisent leurs +> horodatages via `EnergyTestBase::utcDateTime`), donc il est déjà à l'abri. Le risque naît +> le jour où l'on écrira une plage tarifaire en heures murales — c'est-à-dire au prochain lot +> tarifaire, pas plus tard. + +## 6. La décision : ni imposer, ni refuser — DÉCLARER + +**Ne pas imposer.** Le moteur ne connaît pas le fuseau « vrai » de l'installation ; il ne +connaît que celui de la box. Le forcer à `Europe/Paris` serait poser une affirmation qu'on ne +sait pas vérifier, et serait faux par construction pour toute box vendue hors de France. + +**Ne pas refuser de fonctionner.** Un HEMS qui s'arrête parce qu'un téléphone est en +itinérance laisse un chauffe-eau sans pilotage et une protection de disjoncteur sans arbitre. +**Les couches L0 à L4 ne doivent dépendre d'aucun fuseau** : elles raisonnent en durées, pas +en heures murales. Le refus est disproportionné, et il déplacerait un problème d'affichage +vers la sécurité. + +**Déclarer**, donc — la seule réponse conforme à la doctrine maison : *publier le régime, ne +jamais le laisser déduire.* Le moteur dit dans quel fuseau il résout les heures murales ; +c'est au client de rapprocher cela de l'heure de son utilisateur, et de l'avertir. + +### Les étapes, à comportement constant d'abord + +1. **Documenter le contrat** (aucun changement de code). `dailyDeadline` est une heure murale + du fuseau de la box, tel que publié par `System.GetTime.timeZone`. Écrit dans + `INTERFACE.md`, à côté du champ. +2. **Publier le fuseau avec la configuration**, en lecture seule, plutôt que d'exiger du + client qu'il croise deux espaces de noms pour interpréter un de NOS champs. Un client qui + doit appeler `System.GetTime` pour donner un sens à `dailyDeadline` finira par ne pas le + faire. +3. **Dire le changement.** Le fuseau peut bouger sous le moteur sans redémarrage (§3). Une + échéance résolue à 12:00 hier peut valoir 13:00 aujourd'hui sans qu'aucun champ n'ait + changé. À traiter comme tout élément runtime : un `Get*` ET un `*Changed`. +4. **Séparer les deux natures** au moment d'écrire la première plage tarifaire : une plage + réglementée porte le fuseau où elle est définie, elle n'hérite pas du système. +5. **Côté app** (lot séparé, à elle) : comparer le fuseau de la box à celui de l'appareil au + moment de la SAISIE d'une échéance, et avertir. C'est l'écran de saisie qui doit parler, + pas l'écran de résultat — au moment du résultat, l'heure est déjà passée. + +### Ce qui reste ouvert, et qu'on ne tranche pas ici + +Le fuseau de `.75` lui-même. `Europe/London` sur un banc français est une erreur de +configuration, mais la corriger déplace d'une heure la lecture de tout l'historique +d'énergie déjà enregistré. C'est une décision d'exploitation, pas de conception.