docs(spec): LM-104 — supposer conservateur pour annoncer, tenter pour agir

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) <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-08-09 12:34:39 +02:00
parent 28eddab0f5
commit 42d95a2670
2 changed files with 75 additions and 10 deletions

View File

@ -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))` > 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. > (`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`) **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 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 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 | | `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`) | | `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:35`~~ | `states` non vide | **SUPPRIMÉ (2026-08-09)** — remplacé par le refus `isValid()` |
| `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: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 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 `LoadConfig::isValid()`, pas par ces assertions. **Le risque était prospectif et daté ; l'échéance est
est daté** : le jour où `SgReadyAdapter` bascule sur la configuration — travail tombée le 2026-08-09**, quand `SgReadyAdapter` est passé à la configuration. Les
déjà inscrit au `DÉFÉRÉ` d'`AGENTS.md` — `sgreadyadapter.cpp:36` devient le seul deux assertions ont donc été retirées **avant** ce basculement, dans cet ordre,
garde-fou sur la présence de l'état 2, l'état de repli sûr du mode dégradé L2, et remplacées par deux gardes réels :
et ce garde-fou sera absent du binaire livré.
- `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 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 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-411 | simulation | `testEcsRestartRecovery` |
| ECS-413 | simulation | `testEcsDisableLeavesSafeState` — + cas négatif : un rebuild sans désactivation ne coupe pas | | 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-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-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-501, ECS-502 | simulation | `testEcsStageFault` |
| ECS-601, ECS-602 | simulation | `testEcsTemperatureTarget` | | 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 | **É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-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 | 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 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-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 | | 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 |

View File

@ -1,8 +1,16 @@
# SPEC — Modèle de charges (domaine × mécanisme) # SPEC — Modèle de charges (domaine × mécanisme)
Version : 0.1.1 Version : 0.1.2
Dépôt : `etm-powersync-energy-plugin-etm` 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 : > **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), > 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 retire de l'allocation via `available` ; elle ne la négocie pas. Règles absolues
1 et 2 d'`AGENTS.md` inchangées. 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`) **Justification.** Un nommage par domaine seul (`EcsAdapter`, `HvacAdapter`)
obligerait à écrire l'encodage SG-Ready deux fois (ECS et HVAC), la combinatoire 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 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 **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 **LM-302** — Chaque charge utile valide la sienne. Les états invalides doivent
être inexprimables : pas de registre Modbus dans une configuration relais. être inexprimables : pas de registre Modbus dans une configuration relais.