diff --git a/debian-qt5/changelog b/debian-qt5/changelog index 0325662..4745ee4 100644 --- a/debian-qt5/changelog +++ b/debian-qt5/changelog @@ -1,3 +1,20 @@ +powersync-energy-plugin-nymea (1.15.2+etm10) trixie; urgency=medium + + * ECS-309 — motif de décision fidèle au mécanisme. Lorsqu'un verrou, et non le budget, + produit le palier appliqué, le decisionReason nomme le verrou et la contrainte qu'il + impose. Le chemin minOff — celui de l'armement à froid d'ECS-412 — publiait + « Surplus insuffisant (7706 W) » alors que 7706 W étaient précisément disponibles : + la décision était juste, le motif accusait le budget. Il cite maintenant le budget + DISPONIBLE, pour couper court à l'explication budgétaire. + * Les trois causes sont distinguables à la lecture : budget réellement insuffisant, + verrou minOn maintenant un palier au-dessus du budget, verrou minOff empêchant + l'enclenchement. Les cas minOn et minStateHold (PAC) étaient déjà fidèles. + * La règle 7 d'AGENTS.md exigeait un motif non vide ; ECS-309 l'étend de la présence à + la fidélité, et exige que les tests portent sur le mécanisme annoncé plutôt que sur + la non-vacuité de la chaîne — un test « motif non vide » n'aurait rien vu. + + -- Patrick Schurig Tue, 11 Aug 2026 10:00:00 +0200 + powersync-energy-plugin-nymea (1.15.2+etm9) trixie; urgency=medium * Corrige le va-et-vient JSON-RPC cassé par l'union LM-300. GetLoadConfig sérialise diff --git a/specs/spec_ecs.md b/specs/spec_ecs.md index 06e52e3..b5052d8 100644 --- a/specs/spec_ecs.md +++ b/specs/spec_ecs.md @@ -229,6 +229,34 @@ correction DEVRAIT restaurer un signal de verrou vers le scheduler plutôt que s contenter de remonter le palier appliqué après coup : la première corrige dans le cycle, la seconde seulement au cycle suivant. +Le motif publié lorsque le clamp mord est régi par **ECS-309**. + +**ECS-309 — Motif de décision fidèle au mécanisme.** Lorsque le palier appliqué +résulte d'un **verrou** et non du budget, le `decisionReason` DOIT nommer le verrou +et la contrainte qu'il impose. Il NE DOIT PAS attribuer l'issue au budget. + +`AGENTS.md` règle 7 exige un motif non vide sur chaque action ; cette exigence +l'étend de la **présence** à la **fidélité**. Un motif faux est pire qu'un motif +absent : il envoie diagnostiquer le mauvais problème, et il le fait avec l'autorité +d'une explication. + +Trois cas DOIVENT être distinguables à la lecture du texte : + +| Cause réelle | Ce que le motif doit dire | +|---|---| +| budget réellement insuffisant | le budget, et son montant | +| verrou `minOn` maintenant un palier **supérieur** au budget | le verrou `minOn`, et le palier qu'il retient | +| verrou `minOff` empêchant l'enclenchement | le verrou `minOff`, et l'interdiction de monter | + +**Cas observé au banc (2026-08-09)**, pendant la fenêtre d'armement à froid +d'ECS-412 : palier plafonné à 0 avec **7706 W de surplus disponible**, et motif +publié « Surplus insuffisant (7706 W) ». La décision était juste, le motif disait +l'inverse de la cause. + +**Test.** Les tests de motif DOIVENT porter sur le **mécanisme annoncé**, pas sur +la non-vacuité de la chaîne. Un test qui vérifie « motif non vide » ne distingue +aucun des trois cas ci-dessus et n'aurait rien vu du défaut de banc. + **ECS-412 — Survie des horodatages de verrou au `rebuild`.** `rebuildLoadAdapters()` (`energyarbitrator.cpp:137-144`) détruit et reconstruit les adaptateurs à chaque `SetLoadConfig`, remettant `m_currentStage` **et** @@ -264,6 +292,10 @@ peut jamais s'enclencher, donc jamais commuter, donc jamais valider son horodatage. Passer `now` au constructeur n'est PAS la réponse : cela changerait le contrat d'`ILoadAdapter` pour un cas particulier. +Pendant la fenêtre d'armement à froid, le motif publié DOIT nommer l'armement +`minOff`, conformément à **ECS-309**. C'est sur ce chemin que le défaut de motif a +été observé ; `testEcsColdStartLockExpires` DOIT donc l'asserter. + **Symétrie exigée.** Les deux cas de démarrage à froid sont armés de la même façon et expirent de la même façon : @@ -662,6 +694,7 @@ mécanique reste **L0**. Perte de sonde → mode sans sonde, pas arrêt d'urgenc | Exigence | Type | Test | |---|---|---| | ECS-306 | simulation | `testEcsBudgetUnderLock` | +| ECS-309 | simulation | `testEcsColdStartLockExpires` — les trois causes distinguées par le TEXTE du motif (minOff / budget / minOn), vérifié échouant sans le correctif | | ECS-412 | simulation | `testEcsRebuildPreservesLock` + **`testEcsColdStartLockExpires`** (armement à froid transitoire) | | ECS-303 | banc | relevé de commutations à la frontière de recombinaison (§4.0) — **mesurer avant de coder** | | ECS-305 | unitaire | `testEcsSwitchCount` — compteur par relais | @@ -744,6 +777,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-11 | **ECS-309 créé et FAIT** — motif fidèle au mécanisme. Le chemin minOff attribuait l'issue au budget ; le motif nomme désormais le verrou et cite le budget DISPONIBLE pour couper court à l'explication budgétaire. Les cas minOn et `minStateHold` étaient déjà fidèles | | 2026-08-10 | **Va-et-vient RPC cassé par LM-300** — `GetLoadConfig` sérialise via le méta-objet, donc un `sgReady` VIDE sur toute charge non SG-Ready ; le schéma SET exigeait `states` dès la clé présente, et nymea rejetait avant le handler. Constaté au banc à la première tentative réelle de `SetLoadConfig` : aucun test ne rejouait le geste de l'app (lire, modifier, réécrire). Corrigé en `o:states` — c'est le mur documenté en LM-302, la validation réelle restant dans `isValid()` | | 2026-08-09 | **ECS-110-b créé et FAIT** — un Thing n'appartient qu'à une charge active. Trou trouvé en répondant à la question « le cas général est-il couvert ? » : il ne l'était pas du tout. `LoadConfigStore::validateSet()` ; doublons intra-charge également refusés | | 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 | diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index 3f35e96..726efa3 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -136,25 +136,26 @@ peut la vérifier, quelle que soit sa rigueur. Elle vit donc au niveau du store `LoadConfig::claimedThingIds()` — c'est le seul point à étendre, et l'oublier rend le mécanisme invisible à la vérification. Voir ECS-110-b. -**LM-302** — Chaque charge utile valide la sienne. Les états invalides doivent -être inexprimables : pas de registre Modbus dans une configuration relais. +**LM-302 — Charges utiles auto-validantes, et portée de l'« inexprimable ».** +Chaque charge utile valide la sienne. Les états invalides doivent être inexprimables +dans les **types C++** et le **schéma persisté** ; à la frontière **JSON-RPC**, la +validation est **à l'exécution**, par contrainte nymea. -**Portée exacte.** L'union discriminée s'applique aux **types C++** et au -**schéma persisté**. Elle ne s'applique **pas** au schéma JSON-RPC : nymea valide -les paramètres *avant* le handler, si bien que deux formes exclusives obligent à -marquer tous les champs spécifiques en optionnel (`"o:"`), faute de quoi l'une -des deux serait rejetée en amont. La contrainte est déjà documentée dans -`nymeaenergyjsonhandler.cpp:155-160` pour deux mécanismes ; elle s'aggrave à -quatre. **Le mur a été touché le 2026-08-10**, exactement où ce paragraphe l'annonçait, et d'une -façon qu'il n'avait pas anticipée : ce n'est pas seulement que deux formes exclusives -obligent à marquer les champs `"o:"`, c'est que `pack()` sérialise **toutes** -les propriétés déclarées — une charge `relay-router` sort donc de `GetLoadConfig` avec un -`sgReady` vide. Exiger une clé **à l'intérieur** d'une charge utile optionnelle casse -alors le va-et-vient de l'app sur les charges des **autres** mécanismes. Corollaire pour -tout mécanisme futur : les clés internes d'une charge utile se marquent `"o:"` elles -aussi. À la frontière RPC, la validation reste donc **à l'exécution** -(`LoadConfig::isValid()`). Ne pas tenter un schéma RPC strict : c'est un mur -connu. +**Corollaire vérifié à l'usage (2026-08-09).** `GetLoadConfig` sérialise via le +méta-objet, donc **toutes** les propriétés déclarées apparaissent dans la sortie — y +compris la charge utile **vide** d'un autre mécanisme. Un schéma qui exige une clé +interne dès que la charge utile est présente rend donc impossible l'aller-retour +lire-puis-réécrire, c'est-à-dire le geste ordinaire de l'application, et le rejet +survient **avant** le handler. + +**Règle.** Toutes les clés internes d'une charge utile optionnelle DOIVENT être +déclarées optionnelles (`o:`) au schéma RPC. L'exigence réelle vit dans `isValid()`, +jamais dans le schéma. Les clés internes d'une charge utile optionnelle contraignent +les charges des **autres** mécanismes — c'est le côté par lequel le mur se touche. + +**Obligation permanente.** Tout mécanisme ajouté DOIT être accompagné d'un test +d'aller-retour `GetLoadConfig` → `SetLoadConfig` **inchangé**, vérifié échouant sans +le correctif. Sans cette obligation, le prochain mécanisme rejouera la scène. **LM-303 — Verrous.** `typeAppareil` fournit des **défauts** (résistif ≈ 60 s, thermodynamique ≈ 300 s), l'installateur peut les modifier, et c'est la valeur