docs(spec): spec_ecs 0.5.0 — §13-1/§13-2 tranchés, étape 1 ouverte, câblage confirmé

Précondition de la consigne d'étape 1 : les deux arbitrages étaient RELEVÉS au
journal, pas TRANCHÉS. Ils le sont maintenant, datés, dans les termes reçus.

§13-1 CLOS — le schéma « ARCHITECTURE CIBLE » d'AGENTS.md a été redessiné par le
lot de consolidation (f6a4be5) : RelayRouter et EtmVariableLoadAdapter y
figurent, le kind Stage en est retiré, SocketScheduler et BatteryAdapter sont
marqués non écrits.

§13-2 CLOS, lecture (b) — ECS-412 réduit à m_lastSwitch et redirigé vers sa cause
racine (rebuild incrémental : ne reconstruire que ce qui a changé) ; ECS-411
remonté en étape 1. L'étape 1 porte donc TROIS exigences : ECS-306, ECS-411,
ECS-412. Ajouté explicitement : au démarrage à froid, m_lastSwitch étant
irrécupérable, le verrou DOIT être ARMÉ et non purgé — l'implémentation naturelle
(QDateTime nul = verrou inactif) fait l'inverse et court-circuiterait la
protection compresseur sur une boucle de redémarrage.

Câblage du banc confirmé 500/1000/2000 (pondération binaire, 8 paliers de 0 à
3500 W au pas de 500, aucune redondance). Trois conséquences consignées :
  - ECS-302 n'est PAS exercé par ce matériel — un seul encodage par palier. La
    restructuration de m_relayMapping perd sa justification immédiate et cesse
    d'être un préalable structurel de l'étape 3.
  - ECS-305 monte en importance : sur un balayage complet R500 commute 7 fois,
    R1000 3 fois, R2000 une seule. C'est la seule mesure d'usure utile.
  - ECS-303 CRÉÉ — point dur 1500→2000 W : les trois relais basculent d'un coup,
    donc trou de puissance à chaque franchissement avec l'ordre
    coupure-avant-fermeture. La parade est un seuil d'hystérésis élargi à cette
    frontière, pas un réordonnancement. À MESURER au banc avant de coder.

Aucun code touché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-08-08 11:56:53 +02:00
parent 41d3a329e4
commit 1e80a83df9

View File

@ -1,6 +1,6 @@
# SPEC — ECS multi-palier
Version : 0.4.3
Version : 0.5.0
Dépôt : `etm-powersync-energy-plugin-etm` (experience-plugin ems)
Branche : `feature/beta-rulebased`
Contrats faisant autorité : `docs/OPTIMIZER_PROTOCOL.md`, `docs/SAFETY.md`, `AGENTS.md`
@ -30,6 +30,11 @@ remonte. Il ne tranche pas seul.
>
> **0.4.3** — ECS-110 étendu : la validation ne doit pas reposer sur `Q_ASSERT`,
> absent du binaire release. Constat issu de la consolidation d'`AGENTS.md`.
>
> **0.5.0** — §13-1 et §13-2 **tranchés et consignés** ; l'étape 1 est débloquée.
> ECS-412 réduit à `m_lastSwitch` (lecture (b)) et redirigé vers sa cause racine.
> Câblage du banc confirmé **500/1000/2000** : ECS-302 n'est plus exercé par ce
> matériel, ECS-305 monte, ECS-303 créé pour le point dur 1500→2000 W.
---
@ -140,9 +145,10 @@ L'ordre est normatif : chaque étape conditionne la suivante.
défauts qui produisent un mauvais comportement sur du code qui tourne au banc,
pas des fonctionnalités manquantes. Ils passent devant tout le reste.
> Périmètre d'ECS-412 en étape 1 : **suspendu à l'arbitrage §13-2**, qui peut le
> réduire à `m_lastSwitch` et remonter ECS-411 ici. Ne pas ouvrir l'étape 1 sans
> l'avoir tranché.
> **Tranché le 2026-08-08 (§13-2, lecture (b)) — l'étape 1 est OUVERTE.** Elle
> porte **trois** exigences : ECS-306, **ECS-411** (remonté ici) et ECS-412
> **réduit à `m_lastSwitch`**. `m_currentStage` se redéduit des Things une fois
> ECS-411 en place : il ne demande aucun code dédié.
**Étape 2 — extraction du noyau de calcul (type pur, sans `ThingManager`).**
Sortir `m_levels`, `stageForPower` et `lockWindow`. Débloque ECS-304 en vrai
@ -156,8 +162,10 @@ unitaire et conditionne l'étape 3.
**Étape 3 — ECS-302 / ECS-305.** Changement de structure de `m_relayMapping`.
Doit précéder tout ce qui s'appuiera dessus.
**Étape 4 — ECS-411 puis ECS-410**, même chemin `applyRelayStage` / `available`,
à faire ensemble. ECS-411 peut remonter en étape 1 selon l'arbitrage §13-2.
**Étape 4 — ECS-410** (échec d'écriture). ECS-411 a été **remonté en étape 1**
(§13-2). ECS-410 reste ici : il touche le même chemin `applyRelayStage` /
`available`, mais il demande d'attendre le résultat de `executeAction`, ce qui
est un changement de nature — pas une lecture d'état au démarrage.
**Étape 5 — ECS-110 (complément) et ECS-111.**
@ -197,11 +205,31 @@ 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.
**ECS-412 — Survie au `rebuild`.** `rebuildLoadAdapters()`
(`energyarbitrator.cpp:137-144`) détruit et reconstruit les adaptateurs à chaque
`SetLoadConfig`, remettant `m_currentStage` **et** `m_lastSwitch` à zéro. Le
palier courant et les horodatages de verrou DOIVENT survivre à un `rebuild`
lorsque la charge concernée n'a pas changé de câblage.
**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**
`m_lastSwitch` à zéro.
**Périmètre réduit à `m_lastSwitch`** (§13-2, lecture (b)) : `m_currentStage` se
redéduit de l'état réel des Things une fois ECS-411 en place, donc il ne demande
aucun code dédié ici. `m_lastSwitch` est l'irréductible — l'horodatage d'un
verrou n'existe nulle part dans le matériel — et c'est la part qui protège le
compresseur.
**Corriger la cause racine, pas le symptôme.** Ne PAS transporter `m_lastSwitch`
à travers la reconstruction : `rebuildLoadAdapters()` DOIT ne reconstruire **que
ce qui a changé**. Une charge dont le câblage et les verrous sont identiques ne
doit pas être détruite parce qu'une AUTRE charge a changé de priorité. Cela règle
`m_lastSwitch`, évite des réécritures de relais inutiles, et vaut pour toutes les
charges — y compris `EtmVariableLoadAdapter`.
**Démarrage à froid.** Après un redémarrage de `nymead`, `m_lastSwitch` est
irrécupérable. Le défaut sûr est alors d'**ARMER** le verrou — traiter la
dernière commutation comme venant d'avoir lieu — et non de le purger. Une boucle
de redémarrage court-circuiterait sinon la protection compresseur exactement
quand elle est la plus nécessaire. **L'implémentation naturelle fait l'inverse
(`QDateTime` nul = verrou inactif) : ce point DOIT être écrit explicitement dans
le code et couvert par un test.**
Il ne s'agit pas de confort : les verrous sont de la **protection matérielle**.
Avec un `minOn` de 300 à 600 s sur un ballon thermodynamique, un client qui
@ -222,7 +250,44 @@ fenêtre de verrou active ».
## §4 — Combinatoire (`RelayRouter`)
**ECS-302 — Encodages équivalents.** Un palier DOIT pouvoir porter **plusieurs**
> **Câblage du banc CONFIRMÉ le 2026-08-08 : 500 / 1000 / 2000 W.** Pondération
> binaire — huit valeurs distinctes de 0 à 3500 W au pas de 500 W, **aucune
> redondance**, un seul encodage par palier.
>
> | Palier | Relais ON | W |
> |---|---|---|
> | 0 | — | 0 |
> | 1 | 500 | 500 |
> | 2 | 1000 | 1000 |
> | 3 | 500+1000 | 1500 |
> | 4 | 2000 | 2000 |
> | 5 | 500+2000 | 2500 |
> | 6 | 1000+2000 | 3000 |
> | 7 | 500+1000+2000 | 3500 |
>
> Ceci clôt le point ouvert « 500/1000/1500 ou 500/1000/2000 » et déplace trois
> exigences — voir ECS-302, ECS-305 et ECS-303 ci-dessous.
**ECS-302 — Encodages équivalents. NON EXERCÉ PAR CE MATÉRIEL.** Avec une
pondération binaire il n'y a **rien à arbitrer** : un seul encodage par palier.
L'exigence reste valable pour un câblage 1-2-3 ailleurs (deux encodages pour
1500 W), mais **le changement de structure de `m_relayMapping` perd sa
justification immédiate** : il redescend dans l'ordre de l'étape 3 et **cesse
d'être un préalable structurel**. Ne plus le traiter comme tel.
**ECS-303 — Point dur 1500 → 2000 W.** Cette transition bascule **les trois
relais d'un coup** (R500 et R1000 s'ouvrent, R2000 se ferme). Avec l'ordre
coupure-avant-fermeture d'ECS-402, elle produit un **trou de puissance à chaque
franchissement**, en plein milieu de la plage de surplus utile.
La parade n'est PAS dans la combinatoire — aucun réordonnancement ne supprime le
trou, puisque aucun sous-ensemble intermédiaire ne vaut ni 1500 ni 2000 W. Elle
est dans un **seuil d'hystérésis élargi à cette frontière précise**. Entrée
commune avec ECS-605 (hystérésis paramétrable par charge). **À MESURER au banc
avant de coder quoi que ce soit** : c'est le relevé de commutations qui dira
quelle largeur donner au seuil.
**ECS-302 (suite) — Encodages équivalents.** Un palier DOIT pouvoir porter **plusieurs**
combinaisons de relais, et la combinaison retenue DOIT être celle dont le coût de
transition depuis l'état courant est le plus faible : d'abord le nombre de relais
à basculer, puis le relais dont le compteur de commutations est le moins entamé.
@ -236,10 +301,15 @@ rencontrée gagne, c'est-à-dire le masque le plus bas. Sur un câblage
porter deux combinaisons. ECS-302 est donc un changement de structure, pas
l'ajout d'un critère.
**ECS-305 — Compteurs de commutations.** Chaque relais DOIT porter un compteur
de commutations, exposé en télémétrie. Le relais de plus faible puissance est le
bit de poids faible de la combinatoire et s'usera le premier ; c'est la grandeur
qui décide de la durée de vie des contacteurs.
**ECS-305 — Compteurs de commutations. IMPORTANCE RELEVÉE** par le câblage
binaire confirmé. Chaque relais DOIT porter un compteur de commutations, exposé
en télémétrie.
Sur un balayage complet des huit paliers, **R500 commute sept fois, R1000 trois
fois, R2000 une seule** : le bit de poids faible s'use sept fois plus vite que le
bit de poids fort. Le comptage par relais devient **la seule façon de savoir
quand le contacteur 500 W arrivera en fin de vie** — la puissance commutée, elle,
ne le dit pas.
**ECS-307 — Grain des temporisations.** `minOn`/`minOff` sont aujourd'hui **par
charge** (un unique `m_lastSwitch`, `relayrouter.h:107`), ce qui satisfait
@ -376,7 +446,9 @@ mécanique reste **L0**. Perte de sonde → mode sans sonde, pas arrêt d'urgenc
|---|---|---|
| ECS-306 | simulation | `testEcsBudgetUnderLock` |
| ECS-412 | simulation | `testEcsRebuildPreservesLock` |
| ECS-302, ECS-305 | unitaire | `testEcsSwitchCost` |
| ECS-303 | banc, puis simulation | relevé de commutations à la frontière 1500/2000 — **mesurer avant de coder** |
| ECS-305 | unitaire | `testEcsSwitchCount` — compteur par relais |
| ECS-302 | unitaire | `testEcsSwitchCost`**non exerçable sur le câblage 500/1000/2000** (encodage unique) |
| ECS-304 | unitaire | `testEcsLevelsPure` |
| ECS-410 | simulation | `testEcsPartialFailure` |
| ECS-411 | simulation | `testEcsRestartRecovery` |
@ -445,36 +517,40 @@ protocole dans le même lot.**
| 2026-08-08 | Recouvrement ECS-411 ↔ ECS-412 relevé → §13-2, bloque l'ouverture de l'étape 1 |
| 2026-08-08 | Étape 2 : « type domaine » → « noyau de calcul » (collision avec `spec_loadmodel.md` LM-100) |
| 2026-08-08 | ECS-110 : validation interdite de reposer sur `Q_ASSERT` (absent en release) |
| 2026-08-08 | **§13-1 CLOS** — schéma « ARCHITECTURE CIBLE » d'`AGENTS.md` redessiné (`f6a4be5`) |
| 2026-08-08 | **§13-2 CLOS, lecture (b)** — ECS-412 réduit à `m_lastSwitch` + cause racine (rebuild incrémental) ; ECS-411 remonté en étape 1 |
| 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 | Câblage banc confirmé **500/1000/2000** — ECS-302 non exercé, ECS-305 relevé, **ECS-303 créé** (point dur 1500→2000) |
---
## §13 — Points ouverts
1. **Gouvernance** — redessiner le schéma « ARCHITECTURE CIBLE » d'`AGENTS.md`
(`EcsRelayAdapter` et le kind `Stage` y figurent encore ; cf. §0). Le document
normatif décrit des classes supprimées tant que ce n'est pas fait.
2. **Ordonnancement ECS-411 ↔ ECS-412** — bloque l'ouverture de l'étape 1. Deux
lectures :
- **(a) garder l'ordre du §2** : ECS-412 traite `m_currentStage` et
`m_lastSwitch` dès l'étape 1 ; ECS-411 en réécrira la moitié en étape 4.
Fidèle au principe « les défauts de comportement passent devant », au prix
d'un aller-retour.
- **(b) réduire ECS-412 à `m_lastSwitch`** et remonter ECS-411 en étape 1 avec
lui : ils partagent le chemin de reconstruction, et `m_currentStage` devient
gratuit. Évite l'aller-retour, mais fait entrer en étape 1 une exigence que
le §2 justifiait autrement.
Dans les deux cas, `testEcsRebuildPreservesLock` DOIT couvrir « `SetLoadConfig`
pendant une fenêtre de verrou active » : c'est l'irréductible.
3. **ECS-307** — grain des temporisations par relais : nécessaire, ou le compteur
de commutations suffit-il ?
4. **ECS-605** — asymétrie de l'hystérésis, à régler à l'usage.
5. Câblage réel du banc : 500/1000/1500 ou 500/1000/2000. Ne bloque pas le code,
mais le premier est le seul qui exerce ECS-302.
6. **Conflit d'écriture**`connectIO()` est le mécanisme natif si l'on veut un
1. **ECS-307** — grain des temporisations par relais : nécessaire, ou le compteur
de commutations suffit-il ? Le câblage binaire rend la question plus concrète
(R500 commute 7× plus que R2000), sans la trancher.
2. **ECS-303 / ECS-605** — largeur de l'hystérésis à la frontière 1500/2000 W,
et asymétrie montée/descente. **À mesurer au banc avant de coder** : c'est le
relevé de commutations qui donne le chiffre.
3. **Conflit d'écriture**`connectIO()` est le mécanisme natif si l'on veut un
jour exposer un relais ECS au pilotage manuel de l'utilisateur. Ce serait
alors un conflit d'écriture à arbitrer, pas une aide.
**CLOS le 2026-08-08 :**
- **§13-1 gouvernance** — le schéma « ARCHITECTURE CIBLE » d'`AGENTS.md` a été
redessiné (lot de consolidation douze zones, commit `f6a4be5`) : `RelayRouter`
et `EtmVariableLoadAdapter` y figurent, le kind `Stage` en est retiré,
`SocketScheduler` et `BatteryAdapter` sont marqués non écrits.
- **§13-2 ordonnancement** — **lecture (b) retenue**. ECS-412 est réduit à
`m_lastSwitch` et redirigé vers sa cause racine (rebuild incrémental) ;
ECS-411 remonte en étape 1. L'étape 1 porte donc trois exigences.
`testEcsRebuildPreservesLock` couvre obligatoirement « `SetLoadConfig` pendant
une fenêtre de verrou active ».
- **Câblage du banc** — confirmé **500/1000/2000 W** (cf. §4). ECS-302 n'est pas
exercé par ce matériel ; ECS-305 monte en importance ; ECS-303 créé.
**CLOS par l'audit** : le waterfall ne choisit pas entre encodages équivalents
(il n'en voit qu'un) ; `ioConnections()`/`connectIO()` est un lien 1↔1
état-à-état avec pour seule transformation un booléen `inverted` — il ne peut