From 2d615e6bcc5f75ff57a9bea220fe681e42b2e225 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sun, 30 Aug 2026 22:34:44 +0200 Subject: [PATCH] =?UTF-8?q?docs(design=20=C2=A710):=20r=C3=A9concili=C3=A9?= =?UTF-8?q?=20avec=20le=20d=C3=A9lestage=20livr=C3=A9,=20et=20plan=20en=20?= =?UTF-8?q?quatre=20=C3=A9tapes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ce design précédait le lot délestage. Trois de ses sections décrivaient du code désormais livré (le plafond comme objet, le délestage, DRAW_CAP), et une proposait une forme depuis refusée. Le lire tel quel faisait reconstruire l'existant. La correction qui compte : PlanDraw n'a qu'un champ. L'absence d'authorisedW/remainingW a été défendue en prose puis épinglée par un test, au motif qu'un authorisedW forgé afficherait un plafond qui n'existe pas. Ce motif est TOMBÉ — le plafond existe maintenant, et le champ serait une lecture. C'est une décision à reprendre, pas une règle à appliquer : une décision juste dont le motif a disparu et dont seule la formulation survit. Plan de travail réécrit sur la stratégie qui a marché au délestage : trois étapes à comportement constant, une seule qui change. A — le split décider/émettre, sans ajouter de fonction. Aujourd'hui buildSetpointAction() décide, écrête, comptabilise et motive dans le même geste, et décrémente le budget en émettant : une seconde passe y est littéralement inexprimable. Piège nommé : l'arrondi et l'écrêtage de verrou doivent migrer SANS changer d'ordre relatif, ECS-306 et ECS-309 en dépendent — les déplacer ferait publier des motifs faux sans qu'aucun nombre ne bouge. B — draw complété (binding, perPhaseBound, authorisedW, remainingW), publication seule. C — l'avancement branché EN LECTURE SEULE : sessionWh alimenté, progress publié avec son régime. C'est là que R5 devient vraie et que R6 devient testable AVANT d'être nécessaire — on sépare publier le régime de décider avec, comme measurement l'a été de l'arbitrage. D — la passe éco, seule étape qui change le comportement. Quatre décisions posées, dont la centrale : comment une obligation en ÉNERGIE devient une cible en WATTS. minEnergyWhPerDay est transporté et lu par aucune décision ; sessionWh est déclaré et rempli nulle part. Les deux moitiés de l'obligation éco sont de la plomberie inerte. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p --- docs/DESIGN_10.md | 147 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 136 insertions(+), 11 deletions(-) diff --git a/docs/DESIGN_10.md b/docs/DESIGN_10.md index 6aa8671..b9efb5d 100644 --- a/docs/DESIGN_10.md +++ b/docs/DESIGN_10.md @@ -19,6 +19,35 @@ deux régimes d'échéance). --- +## 0. ⚠️ CE QUI A CHANGÉ DEPUIS L'ÉCRITURE DE CE DESIGN (2026-08-30) + +Ce document a été écrit **avant** le lot délestage. Trois de ses sections décrivent désormais du +code livré, et une propose une forme qui a depuis été **refusée**. Lire les sections suivantes +sans cette mise à jour ferait reconstruire l'existant, ou pire, ressusciter un champ qu'un test +interdit. + +| Section | État | +|---|---| +| **§2 — le plafond de soutirage devient un OBJET** | **LIVRÉ.** `DrawCap` / `DrawMargin` / `resolveDrawMargin()`, avec les trois sources et le départage par rang de contrainte. Voir `DESIGN_DELESTAGE.md`. | +| **§5 — le délestage, ce qu'il coupe et dans quel ordre** | **LIVRÉ** (`LM-1211`) : ordre inverse du rang, franchissement de verrou **constaté** et non présumé, réservé aux plafonds subis. | +| **§6 — les motifs** | `DRAW_CAP` **existe**, avec sa source, son ampleur et sa phase. `ECO_FLOOR_MET` / `ECO_FLOOR_GRID` restent à naître. | +| **§3 — la forme de `PlanDraw`** | **PARTIELLEMENT REFUSÉE**, voir ci-dessous. | + +### La correction à faire sur §3 : `PlanDraw` n'a qu'un champ + +Le §3 propose `PlanDraw { authorisedW, committedW, remainingW }`. Seul **`committedW`** a été +livré, et l'absence des deux autres a été **défendue en prose à trois endroits** puis **épinglée +par un test** (`testLoadTelemetryRpc` casse si on ajoute le champ). Le motif d'alors : *« un +`authorisedW` forgé afficherait un plafond de soutirage qui n'existe pas »*. + +> **Mais ce motif est tombé.** Le plafond existe désormais — `ctx.drawMargin`, résolu une fois +> par cycle, avec sa source et sa phase. `authorisedW` ne serait plus une forge : il serait une +> lecture. **C'est donc une décision à reprendre, pas une règle à appliquer** (voir §11.2), et +> c'est un bon exemple de ce qu'`AGENTS.md` appelle une décision qui se périme : elle était +> juste, son motif a disparu, et seule sa formulation a survécu. + +--- + ## 1. Pourquoi le délestage part AVEC, et pas après C'est la précondition écrite en §13 de la spec, et elle tient pour une raison arithmétique @@ -364,17 +393,113 @@ mécanismes l'a fait pour les six écrans. La charge utile suivra. --- -## 10. Ordre de travail proposé +## 10. ORDRE DE TRAVAIL — trois étapes à comportement CONSTANT, puis une seule qui change -| Étape | Contenu | Éprouvable | +*(Réécrit le 2026-08-30, après le lot délestage. La question de forme du §9 est tranchée : la +maquette `deux_passes_eco_confort_mockup.html` **fait contrat**, et `levels[]`, `funding` et +`counts` sont déjà livrés.)* + +La stratégie du délestage a marché et se réemploie telle quelle : **ce qui coûte dans un lot de +ce genre n'est pas d'écrire le comportement, c'est de prouver qu'on n'a rien cassé en chemin.** +Trois étapes ne changent rien et rendent la quatrième petite. + +### Étape A — le SPLIT décider / émettre, à comportement constant + +C'est le gros du travail, et il n'ajoute aucune fonction. `getPlan()` se scinde en deux temps : +une phase qui calcule une **cible par charge**, une phase qui **émet une commande et une seule**. +Aujourd'hui `buildSetpointAction()` décide, écrête, comptabilise et motive dans le même geste, et +décrémente le budget en même temps qu'il émet — ce qui rend une seconde passe littéralement +inexprimable. + +Tant qu'il n'y a qu'une passe, la cible confort est la seule, et la sortie doit être **identique +au watt près**. C'est ce qui rend l'étape vérifiable : `simulation` et `charging` ne doivent pas +bouger d'une ligne. Le filet est déjà là — les tests de motif portent sur le **texte publié**, +donc ils survivront au remaniement ou le condamneront. + +> **Le piège à surveiller.** L'arrondi au mécanisme et l'écrêtage de verrou doivent migrer vers +> la phase d'émission **sans changer d'ordre relatif**. `ECS-306` (clamp lock-aware AVANT de +> décrémenter le budget) et `ECS-309` (le motif nomme le mécanisme réel, via `vouluW`) reposent +> tous deux sur cet ordre. Les déplacer d'un cran ferait publier des motifs faux sans qu'aucun +> nombre ne change — le genre de régression qu'une somme correcte masque. + +### Étape B — `draw` complété, publication SEULE + +`draw` ne publie que `committedW`. Le plafond existe maintenant : `binding` (la source qui +borne), `perPhaseBound`, `authorisedW` et `remainingW` deviennent des **lectures** et non des +forges. Aucune décision ne change ; c'est un ajout de charge utile, et il ferme la moitié de R8 +qui restait ouverte au niveau du cycle. + +Voir la décision **§11.2** — l'ajout suppose de revenir sur un refus dont le motif est tombé, et +de mettre à jour le test qui l'épingle. + +### Étape C — l'AVANCEMENT mesuré, publié sans rien décider + +`LoadContextTelemetry::sessionWh` est **déclaré et rempli nulle part** ; `EvCharger` n'expose +même pas d'accesseur vers `sessionEnergy`, que quatre des cinq classes du banc publient. +`minEnergyWhPerDay` est transporté par la config et le RPC, et **lu par aucune décision**. Les +deux moitiés de l'obligation éco sont donc de la plomberie inerte. + +Cette étape les branche **en lecture seule** : `sessionWh` alimenté, `progress` publié dans +`levels[]` avec son `regime` ∈ `measured | estimated | unmeasurable`, `deliveredWh` **absent** +quand le régime est `unmeasurable` — jamais à zéro, sans quoi une dégradation se lirait comme un +avancement nul, ce que `LM-1009 C1` interdit nommément. + +> **C'est ici que R5 devient vraie, et R6 devient TESTABLE avant d'être nécessaire.** Le régime +> se publie sans qu'aucune décision ne s'y appuie : l'app peut afficher « non mesurable » sur la +> V2C dès cette étape, et la garde de R6 — `ECO_FLOOR_MET` interdit sous `unmeasurable` — aura +> son régime à interroger le jour où le motif naît. On sépare **publier le régime** de **décider +> avec**, exactement comme `measurement` l'a été de l'arbitrage. + +### Étape D — la PASSE ÉCO naît, et c'est la seule étape qui change le comportement + +Deux passes sur la même liste triée, `ECO_FLOOR_MET` et `ECO_FLOOR_GRID` au catalogue, R6 armée. +La passe 1 est bornée par l'**autorisation de soutirage**, la passe 2 par le **reliquat de +surplus**. Le délestage la rattrape si tous les planchers se servent au réseau le même matin — +c'est la précondition qui vient d'être levée. + +--- + +## 11. CE QUI DEMANDE UNE DÉCISION AVANT L'ÉTAPE D + +**11.1 — Comment une obligation en ÉNERGIE devient-elle une cible en WATTS pour ce cycle ?** +C'est la question centrale, et le design ne l'a jamais tranchée. `needs` porte +`minEnergyWhPerDay`, une `deadline` et un `targetSocPercent` — jamais un plancher en watts. La +passe éco doit donc convertir « il reste 900 Wh à livrer avant 7 h » en « prends 1400 W +maintenant ». + +Trois formes possibles, et elles ne se valent pas : + +| Forme | Ce qu'elle donne | Ce qu'elle coûte | |---|---|---| -| **10-a** | `DrawCap` / `DrawBudget`, calculés dans `buildContext()`, publiés, **sans effet sur les décisions** | oui — télémétrie, aucun changement de comportement | -| **10-b** | Délestage : le plafond borne le plan ; motif `DRAW_CAP` avec sa source ; l'ordre de coupe est l'inverse du rang | oui — simulation + banc | -| **10-c** | Refonte décider/émettre (§4) : deux passes, une commande, recrédit unique — **à comportement CONSTANT**, passe 1 vide | oui — les 16 scénarios `run` doivent rester identiques au bit près | -| **10-d** | Passe 1 réellement servie : obligation par session, régimes de LM-1009, motifs éco | oui — banc | -| **10-e** | Passe 2 confort : comparateur de sonde | oui — banc, avec la sonde du §11 | +| **Tout ou rien** | la charge prend son plancher dès que l'obligation n'est pas tenue | achète au pire moment, et fait disjoncter à plusieurs | +| **Étalement uniforme** | `reste / temps restant` | simple, vérifiable, mais ignore le tarif et le soleil à venir | +| **Au plus tard** | ne rien acheter tant que le surplus peut encore suffire | optimal, mais suppose une **prévision** — hors modèle | + +**Recommandation : l'étalement uniforme**, pour la même raison que `LM-1008` a fait de la cible +de confort un **comparateur** plutôt qu'un dosage : ce qui manque n'est pas la formule, c'est le +modèle qui la rendrait honnête. Un étalement se lit, se teste, et ne prétend rien prévoir. Le +« au plus tard » viendra avec la prévision, pas avant. + +**11.2 — Publie-t-on `draw.authorisedW` et `remainingW` maintenant ?** +Le refus tenait à ce qu'ils n'existaient pas. Ils existent. Mais `remainingW` a une subtilité : +sous la réserve batterie, `LM-1203-b` annule le budget de surplus **sans annuler l'autorisation +de soutirage** — donc `draw.remainingW` reste non nul quand `budget.remainingW` est à zéro, et +c'est **juste**. Il faut le dire, sinon un client conclura à une incohérence. +**Recommandation : oui aux quatre**, et le test qui les interdit se met à jour en même temps — +c'est un changement de contrat assumé, pas une régression. + +**11.3 — La passe éco achète-t-elle au réseau par DÉFAUT ?** +`ECO_FLOOR_GRID` dit « ce plancher est servi en achetant ». Mais faut-il acheter dès que le +surplus manque, ou seulement quand l'échéance est menacée ? Avec l'étalement uniforme la question +se dissout : on achète ce que l'étalement réclame **et rien de plus**, donc jamais « par défaut » +— toujours parce que le temps restant l'exige. **Recommandation : pas de décision séparée**, elle +est absorbée par 11.1. + +**11.4 — Que devient le recrédit anti-clignotement avec deux passes ?** +`LM-1004` fixe qu'il est attribué **une seule fois**. Reste à dire **où** : à la première passe +où la charge apparaît. Une charge à plancher éco le reçoit donc en passe 1, une charge sans +obligation en passe 2. **Recommandation : un champ explicite dans la cible** (`recreditAttribue`) +plutôt qu'une discipline — la discipline ne se teste pas, le champ si. + +--- -**10-a et 10-c sont les deux étapes qui ne changent rien**, et ce sont les plus importantes : -elles rendent le reste petit. 10-c en particulier doit être prouvée par l'**invariance** des -scénarios enregistrés — si la refonte décider/émettre change un seul point de contrôle, c'est -qu'elle a changé une décision, et il faut savoir laquelle avant d'aller plus loin.