From 1e80a83df927a1e73cfd7ed41ca136703ee1c28f Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sat, 8 Aug 2026 11:56:53 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec):=20spec=5Fecs=200.5.0=20=E2=80=94=20?= =?UTF-8?q?=C2=A713-1/=C2=A713-2=20tranch=C3=A9s,=20=C3=A9tape=201=20ouver?= =?UTF-8?q?te,=20c=C3=A2blage=20confirm=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- specs/spec_ecs.md | 154 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 115 insertions(+), 39 deletions(-) diff --git a/specs/spec_ecs.md b/specs/spec_ecs.md index 5ceb79e..3286997 100644 --- a/specs/spec_ecs.md +++ b/specs/spec_ecs.md @@ -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