docs(design §10): réconcilié avec le délestage livré, et plan en quatre étapes

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
This commit is contained in:
Patrick Schurig 2026-08-30 22:34:44 +02:00
parent b7a7f22ca3
commit 2d615e6bcc

View File

@ -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.