From 4a55d440dfd7dc2015b64f5c58be6a810f3a38ff Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sun, 9 Aug 2026 12:34:39 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec):=20LM-104=20=E2=80=94=20supposer=20c?= =?UTF-8?q?onservateur=20pour=20annoncer,=20tenter=20pour=20agir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le principe dégagé par les tests d'ECS-411 puis d'ECS-414 ne vit pas sous ECS-414 : il vaut pour tout mécanisme, présent et futur. Il est donc inscrit en LM-104 (spec_loadmodel.md §1), ECS-411 et ECS-414 n'en étant que deux applications. Les deux moitiés sont indissociables, et c'est l'omission de la seconde qui piège : supposer « contact fermé » puis en déduire « donc rien à écrire » transforme une hypothèse de prudence en masquage de panne. La prudence porte sur ce qu'on DIT de l'installation, jamais sur ce qu'on lui ENVOIE. Consigne aussi : le tableau ECS-110 (les deux Q_ASSERT de sgreadyadapter sont supprimés, l'échéance prospective est tombée), la charge utile sg-ready réellement implémentée en LM-300, et le statut de spec_loadmodel.md — §3 n'est plus une intention. Co-Authored-By: Claude Opus 5 (1M context) --- specs/spec_ecs.md | 28 +++++++++++++++----- specs/spec_loadmodel.md | 57 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/specs/spec_ecs.md b/specs/spec_ecs.md index 5ed2968..6c54d6e 100644 --- a/specs/spec_ecs.md +++ b/specs/spec_ecs.md @@ -464,6 +464,12 @@ attendre un quart d'heure pour sortir d'un état non voulu n'est pas défendable > Ce lot porte sa propre précondition — retirer `Q_ASSERT(m_stateRelays.contains(2))` > (`sgreadyadapter.cpp:36`) au profit d'un refus explicite, cf. ECS-110. +> **Le principe dégagé par ces deux exigences ne vit pas ici.** « Supposer +> conservateur pour annoncer, tenter systématiquement pour agir » vaut pour tout +> mécanisme, pas pour l'ECS : il est inscrit en **LM-104** +> (`specs/spec_loadmodel.md` §1). ECS-411 et ECS-414 en sont deux applications, +> pas la source. + **ECS-413 — Désactivation en état sûr.** Désactiver une charge (`enabled: false`) ou la retirer de la configuration DOIT laisser son matériel dans l'**état sûr défini par son adaptateur**, et non dans son dernier état commandé. L'état sûr est @@ -530,15 +536,21 @@ Quatre `Q_ASSERT` d'invariant de configuration existent aujourd'hui dans `etm/` |---|---|---| | `relayrouter.cpp:66` | `m_levels` non vide, `[0] == 0` | **Non** — le masque vide garantit la clé 0 par construction | | `etmvariableloadadapter.cpp:34` | `powerLevels[0] == 0` | **Non** — déjà refusé par `isValid()` (`loadconfig.cpp:114-115`) | -| `sgreadyadapter.cpp:35` | `states` non vide | **Non** — construction codée en dur | -| `sgreadyadapter.cpp:36` | état 2 présent = **repli sûr obligatoire** | **Non** aujourd'hui — construction codée en dur (`energypluginnymea.cpp:66-76`) | +| ~~`sgreadyadapter.cpp:35`~~ | `states` non vide | **SUPPRIMÉ (2026-08-09)** — remplacé par le refus `isValid()` | +| ~~`sgreadyadapter.cpp:36`~~ | état 2 présent = **repli sûr obligatoire** | **SUPPRIMÉ (2026-08-09)** — voir ci-dessous | Aucun ne masque donc de défaut à ce jour : ECS-110 est bien tenu par -`LoadConfig::isValid()`, pas par ces assertions. **Le risque est prospectif et il -est daté** : le jour où `SgReadyAdapter` bascule sur la configuration — travail -déjà inscrit au `DÉFÉRÉ` d'`AGENTS.md` — `sgreadyadapter.cpp:36` devient le seul -garde-fou sur la présence de l'état 2, l'état de repli sûr du mode dégradé L2, -et ce garde-fou sera absent du binaire livré. +`LoadConfig::isValid()`, pas par ces assertions. **Le risque était prospectif et daté ; l'échéance est +tombée le 2026-08-09**, quand `SgReadyAdapter` est passé à la configuration. Les +deux assertions ont donc été retirées **avant** ce basculement, dans cet ordre, +et remplacées par deux gardes réels : + +- `LoadConfig::isValid()` **refuse** toute configuration `sg-ready` dépourvue de + l'état 2, avec un message explicite. Une PAC inapte au repli sûr du mode + dégradé L2 n'est pas construite ; +- `SgReadyAdapter` conserve un drapeau `m_usable` : si on le construit malgré + tout hors du chemin de configuration, il **journalise en critique et refuse + toute commande** au lieu de disparaître sous `QT_NO_DEBUG`. Exigence : toute vérification portant sur des données venant de la configuration DOIT vivre dans `isValid()` ou dans la construction de l'adaptateur, avec un @@ -628,6 +640,7 @@ mécanique reste **L0**. Perte de sonde → mode sans sonde, pas arrêt d'urgenc | ECS-411 | simulation | `testEcsRestartRecovery` | | ECS-413 | simulation | `testEcsDisableLeavesSafeState` — + cas négatif : un rebuild sans désactivation ne coupe pas | | ECS-414 | simulation | `testSgReadyPartialFailure` — plancher = état 2, atomicité du repli, contact injoignable | +| ECS-110 (SG-Ready) / LM-300 | simulation | `testSgReadyFromConfig` — refus sans état 2, refus de charges utiles mélangées, round-trip, pilotage, désactivation → état 2 | | ECS-110, ECS-111 | unitaire | `testEcsConfigValidation` — DOIT s'exécuter aussi en build release (`QT_NO_DEBUG`), sinon il ne prouve rien du binaire livré | | ECS-501, ECS-502 | simulation | `testEcsStageFault` | | ECS-601, ECS-602 | simulation | `testEcsTemperatureTarget` | @@ -698,6 +711,7 @@ protocole dans le même lot.** | 2026-08-08 | **Étape 1 OUVERTE** — trois exigences : ECS-306, ECS-411, ECS-412 | | 2026-08-08 | ECS-412 : au démarrage à froid, le verrou est **ARMÉ** (défaut sûr), jamais purgé | | 2026-08-09 | ECS-412 précisé : cet armement est **TRANSITOIRE** et posé paresseusement au premier `now`. Défaut de blocage circulaire constaté **au banc**, corrigé, couvert par `testEcsColdStartLockExpires` | +| 2026-08-09 | **SG-Ready configurable (LM-300)** — union discriminée par mécanisme ; les deux `Q_ASSERT` de `sgreadyadapter.cpp` retirés au profit d'un refus `isValid()` + drapeau `m_usable` (ECS-110 tenu hors `Q_ASSERT`) ; principe général porté en LM-104, hors ECS | | 2026-08-09 | **ECS-414 FAIT** — échelle généralisée à SgReady et EtmVariableLoad ; plancher = état 2 pour la PAC ; repli soumis au même `transientHarm` que l'aller ; contact injoignable supposé fermé mais toujours commandé | | 2026-08-09 | **ECS-414 créé** — généralisation d'ECS-410 à SgReady et EtmVariableLoad, rattachée au lot SG-Ready en configuration | | 2026-08-09 | **ECS-413 créé** — `enabled: false` laissait les relais fermés (constat de banc, juste avant une intervention de câblage). État sûr propre à chaque adaptateur ; ne s'applique PAS à un redémarrage de `nymead`, qu'ECS-411 couvre | diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index dd7d924..83e5422 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -1,8 +1,16 @@ # SPEC — Modèle de charges (domaine × mécanisme) -Version : 0.1.1 +Version : 0.1.2 Dépôt : `etm-powersync-energy-plugin-etm` -Statut : **intention de conception figée. Aucune implémentation immédiate.** +Statut : **intention de conception figée.** §3 (schéma persisté) est **en cours +d'implémentation** depuis le 2026-08-09 : `relay-router`, `etmvariableload` et +`sg-ready` sont trois charges utiles discriminées par `adapter`. Le reste du +document reste une intention. + +> **0.1.2** — LM-104 ajouté (§1) : « supposer conservateur pour annoncer, tenter +> systématiquement pour agir ». Principe dégagé des tests d'ECS-411 et ECS-414, +> inscrit ici parce qu'il vaut pour tout mécanisme et non pour l'ECS seul. +> §3 : le mécanisme `sg-ready` a sa charge utile et cesse d'être une intention. > **0.1.1** — passe de vérification contre le code. Quatre précisions : > préséance des verrous (LM-303), portée de l'union discriminée (LM-302), @@ -35,6 +43,27 @@ domaine ne détermine pas le mécanisme et réciproquement. retire de l'allocation via `available` ; elle ne la négocie pas. Règles absolues 1 et 2 d'`AGENTS.md` inchangées. +**LM-104 — Supposer conservateur pour annoncer, tenter systématiquement pour agir.** +Un adaptateur qui ne peut pas LIRE l'état d'un organe DOIT le supposer dans l'état +le plus consommateur (contact fermé, palier engagé) quand il **annonce** — dans +`telemetry()`, `toLoadContext()` ou la déduction de son état courant au démarrage. +Le même adaptateur DOIT néanmoins **émettre l'écriture** vers cet organe quand il +agit, sans jamais la court-circuiter au motif que l'état supposé coïncide déjà avec +la cible. + +Les deux moitiés sont indissociables, et c'est l'omission de la seconde qui est +piégeuse : supposer « fermé » puis en déduire « donc rien à écrire » transforme une +hypothèse de prudence en **masquage de panne** — l'organe reste dans son état réel, +inconnu, et aucun verdict d'échec ne se déclenche. La prudence porte sur ce qu'on +DIT de l'installation, jamais sur ce qu'on lui ENVOIE. + +Portée : tout mécanisme, présent et futur. `Relay` (déduction de palier au +démarrage), `SgReady` (relevé du motif de contacts réellement fermés), et par +construction `ModbusSetpoint` dès qu'un registre devient illisible. Origine : +deux défauts symétriques trouvés par les tests d'ECS-411 puis d'ECS-414 — +d'abord l'hypothèse inverse (« injoignable donc ouvert »), puis son corollaire +(« supposé fermé donc jamais écrit »). + **Justification.** Un nommage par domaine seul (`EcsAdapter`, `HvacAdapter`) obligerait à écrire l'encodage SG-Ready deux fois (ECS et HVAC), la combinatoire de relais deux fois (ECS et EV en prise commandée), et le transport Modbus deux @@ -75,8 +104,30 @@ LoadConfig { } ``` +**État d'implémentation (2026-08-09).** Trois charges utiles existent, discriminées +par le champ `adapter` du schéma persisté — `relay-router`, `etmvariableload`, +`sg-ready`. Le domaine, le groupe et les facettes ne sont pas encore portés : ce +qui est figé, c'est la **forme**, celle qui coûte cher à rattraper. + +``` +adapter = "sg-ready" → sgReady { + states[] { state : 1..4, relays[] : thingId, estimatedPowerW } + minStateHoldS +} +``` + +`estimatedPowerW` porte volontairement ce nom : c'est une **estimation**, jamais un +engagement — une PAC ne consomme pas la même chose à −5 °C et à +12 °C. Le champ +sert à ordonner et à budgéter, la mesure reste la source de vérité (ECS-500). +L'état 2 est **obligatoire** : c'est le plancher de repli du mode dégradé L2 et de +la désactivation (ECS-413/414). Une configuration qui ne peut pas l'exprimer est +refusée, pas construite. + **LM-301** — Ajouter un mécanisme DOIT se limiter à un type de charge utile et -une branche de fabrique. Rien d'autre ne bouge. +une branche de fabrique. Rien d'autre ne bouge. **Vérifié à l'usage** : l'ajout de +`sg-ready` a touché exactement une charge utile, une branche de `isValid()`, une +branche de fabrique dans `rebuildLoadAdapters()`, et un champ optionnel au schéma +RPC. Aucun code d'arbitrage n'a bougé. **LM-302** — Chaque charge utile valide la sienne. Les états invalides doivent être inexprimables : pas de registre Modbus dans une configuration relais.