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 7f204ebf3a
commit e7a5c7fbab

View File

@ -1,6 +1,6 @@
# SPEC — ECS multi-palier # SPEC — ECS multi-palier
Version : 0.4.3 Version : 0.5.0
Dépôt : `etm-powersync-energy-plugin-etm` (experience-plugin ems) Dépôt : `etm-powersync-energy-plugin-etm` (experience-plugin ems)
Branche : `feature/beta-rulebased` Branche : `feature/beta-rulebased`
Contrats faisant autorité : `docs/OPTIMIZER_PROTOCOL.md`, `docs/SAFETY.md`, `AGENTS.md` 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`, > **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`. > 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, 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. 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 > **Tranché le 2026-08-08 (§13-2, lecture (b)) — l'étape 1 est OUVERTE.** Elle
> réduire à `m_lastSwitch` et remonter ECS-411 ici. Ne pas ouvrir l'étape 1 sans > porte **trois** exigences : ECS-306, **ECS-411** (remonté ici) et ECS-412
> l'avoir tranché. > **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`).** **Étape 2 — extraction du noyau de calcul (type pur, sans `ThingManager`).**
Sortir `m_levels`, `stageForPower` et `lockWindow`. Débloque ECS-304 en vrai 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`. **Étape 3 — ECS-302 / ECS-305.** Changement de structure de `m_relayMapping`.
Doit précéder tout ce qui s'appuiera dessus. Doit précéder tout ce qui s'appuiera dessus.
**Étape 4 — ECS-411 puis ECS-410**, même chemin `applyRelayStage` / `available`, **Étape 4 — ECS-410** (échec d'écriture). ECS-411 a été **remonté en étape 1**
à faire ensemble. ECS-411 peut remonter en étape 1 selon l'arbitrage §13-2. (§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.** **É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 contenter de remonter le palier appliqué après coup : la première corrige dans le
cycle, la seconde seulement au cycle suivant. cycle, la seconde seulement au cycle suivant.
**ECS-412 — Survie au `rebuild`.** `rebuildLoadAdapters()` **ECS-412 — Survie des horodatages de verrou au `rebuild`.**
(`energyarbitrator.cpp:137-144`) détruit et reconstruit les adaptateurs à chaque `rebuildLoadAdapters()` (`energyarbitrator.cpp:137-144`) détruit et reconstruit
`SetLoadConfig`, remettant `m_currentStage` **et** `m_lastSwitch` à zéro. Le les adaptateurs à chaque `SetLoadConfig`, remettant `m_currentStage` **et**
palier courant et les horodatages de verrou DOIVENT survivre à un `rebuild` `m_lastSwitch` à zéro.
lorsque la charge concernée n'a pas changé de câblage.
**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**. 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 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`) ## §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 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 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é. à 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 porter deux combinaisons. ECS-302 est donc un changement de structure, pas
l'ajout d'un critère. l'ajout d'un critère.
**ECS-305 — Compteurs de commutations.** Chaque relais DOIT porter un compteur **ECS-305 — Compteurs de commutations. IMPORTANCE RELEVÉE** par le câblage
de commutations, exposé en télémétrie. Le relais de plus faible puissance est le binaire confirmé. Chaque relais DOIT porter un compteur de commutations, exposé
bit de poids faible de la combinatoire et s'usera le premier ; c'est la grandeur en télémétrie.
qui décide de la durée de vie des contacteurs.
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 **ECS-307 — Grain des temporisations.** `minOn`/`minOff` sont aujourd'hui **par
charge** (un unique `m_lastSwitch`, `relayrouter.h:107`), ce qui satisfait 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-306 | simulation | `testEcsBudgetUnderLock` |
| ECS-412 | simulation | `testEcsRebuildPreservesLock` | | 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-304 | unitaire | `testEcsLevelsPure` |
| ECS-410 | simulation | `testEcsPartialFailure` | | ECS-410 | simulation | `testEcsPartialFailure` |
| ECS-411 | simulation | `testEcsRestartRecovery` | | 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 | 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 | É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 | 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 ## §13 — Points ouverts
1. **Gouvernance** — redessiner le schéma « ARCHITECTURE CIBLE » d'`AGENTS.md` 1. **ECS-307** — grain des temporisations par relais : nécessaire, ou le compteur
(`EcsRelayAdapter` et le kind `Stage` y figurent encore ; cf. §0). Le document de commutations suffit-il ? Le câblage binaire rend la question plus concrète
normatif décrit des classes supprimées tant que ce n'est pas fait. (R500 commute 7× plus que R2000), sans la trancher.
2. **Ordonnancement ECS-411 ↔ ECS-412** — bloque l'ouverture de l'étape 1. Deux 2. **ECS-303 / ECS-605** — largeur de l'hystérésis à la frontière 1500/2000 W,
lectures : et asymétrie montée/descente. **À mesurer au banc avant de coder** : c'est le
- **(a) garder l'ordre du §2** : ECS-412 traite `m_currentStage` et relevé de commutations qui donne le chiffre.
`m_lastSwitch` dès l'étape 1 ; ECS-411 en réécrira la moitié en étape 4. 3. **Conflit d'écriture** — `connectIO()` est le mécanisme natif si l'on veut un
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
jour exposer un relais ECS au pilotage manuel de l'utilisateur. Ce serait jour exposer un relais ECS au pilotage manuel de l'utilisateur. Ce serait
alors un conflit d'écriture à arbitrer, pas une aide. 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 **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 (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 état-à-état avec pour seule transformation un booléen `inverted` — il ne peut