docs(spec §10): LM-1009 — une échéance ne vaut que ce que son avancement se mesure

Question remontée par l'agent app. Note de conception, aucun code : elle
conditionne la forme du §10, elle ne la code pas.

Le constat de l'app est exact — vérifié sur les cinq classes evcharger du banc,
aucune ne déclare batteryLevel ni ne porte d'interface véhicule. Mais le code
dit quelque chose de plus troublant que « il manque une mesure » :

targetPercentage est comparé à carBatteryLevel, lu sur le Thing voiture. Et ce
batteryLevel, dans le seul cas où le mécanisme s'active — une voiture manuelle,
dont l'état est INSCRIPTIBLE — est écrit par le moteur lui-même :
updateManualSoCsWithMeter() intègre la puissance de la borne, applique un
facteur de pertes, divise par une capacité déclarée, et réinjecte le résultat
dans l'état du Thing. Le moteur compare donc sa cible à sa propre estimation,
bâtie sur trois grandeurs dont aucune n'est mesurée. La boucle se referme sur
elle-même. Et l'estimation sort par la frontière RPC indistinguable d'une
mesure — c'est l'app qui ment ensuite, de bonne foi.

Pendant ce temps, la seule grandeur réellement mesurée n'est lue par personne :
sessionEnergy est publiée par quatre des cinq classes, LoadContextTelemetry
porte déjà son emplacement (sessionWh), et le champ n'est rempli nulle part.

Réponse à la première question : OUI, et les trois pièces existent déjà —
sessionEnergy pour mesurer, sessionWh pour la transporter, needs.minEnergyWhPerDay
pour exprimer la cible. C'est la forme que le §10 doit retenir : une obligation
en ÉNERGIE, vérifiable de bout en bout sans communication véhicule. Avec quatre
réserves écrites, dont deux mordent tout de suite : sessionEnergy se remet à
zéro au branchement (donc inutilisable telle quelle pour un minEnergyWhPerDay),
et toutes les bornes ne la publient pas (donc la dégradation doit être VISIBLE).

Réponse à la seconde : OUI, et elle tient MÊME avec la première — parce que
targetPercentage ne disparaît pas. Deux objets, deux régimes de vérité : une
obligation en énergie peut annoncer son atteinte, une cible en pourcentage
publie une intention et un avancement inconnu ou estimé, jamais « tenue ». Même
exigence que BELOW_MIN_POWER contre SURPLUS_INSUFFICIENT : deux causes, deux
phrases, parce qu'elles n'appellent pas le même geste.

Deux arbitrages sont laissés ouverts, dont un de lot : obligation par session
(faisable en lisant un champ déjà publié) ou obligation quotidienne (qui exige
l'historique du §11).

LM-1205 (§12) renvoie désormais vers cette note : la machinerie d'échéance
existe, son avancement ne se mesure pas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
This commit is contained in:
Patrick Schurig 2026-08-27 14:43:40 +02:00
parent 5a9b9a6786
commit d3013672a2

View File

@ -671,6 +671,106 @@ Le **dosage en Wh** viendra avec le modèle thermique, que l'historique du §11
précisément d'apprendre. **Une raison de plus de faire le §11 en premier** : il ne prépare pas précisément d'apprendre. **Une raison de plus de faire le §11 en premier** : il ne prépare pas
seulement les seuils, il prépare la seule chose qui rendra le dosage possible. seulement les seuils, il prépare la seule chose qui rendra le dosage possible.
**LM-1009 — Une échéance ne vaut que ce que son avancement se mesure.** *(Question remontée par
l'agent app, 2026-08-27. Note de conception : ne déclenche aucun travail, mais elle CONDITIONNE
la forme du §10.)*
**Le constat de l'app, et il est exact.** Sur `.75`, aucune classe `evcharger` ne déclare d'état
de charge et aucune ne porte d'interface véhicule — vérifié sur les cinq classes présentes
(`terraAcTcp`, `terraAcRtu`, `wallbox` ×2, `wallboxNoMeter`) : `batteryLevel` absent partout.
Les bornes publient `pluggedIn`, `charging`, `maxChargingCurrent`, `phaseCount` et, pour quatre
d'entre elles, `sessionEnergy`. Jamais un pourcentage.
**Ce que le code fait aujourd'hui, et c'est plus troublant que « il manque une mesure ».**
`targetPercentage` est comparé à `carBatteryLevel`, lu sur le Thing **voiture**
(`smartchargingmanager.cpp:628`). Et ce `batteryLevel`, quand la voiture est une voiture
*manuelle* — le seul cas où le mécanisme s'active, il exige un `batteryLevel` **inscriptible** —
est **écrit par le moteur lui-même** : `updateManualSoCsWithMeter()` intègre la puissance de la
BORNE, applique un facteur de pertes, divise par une `capacity` déclarée, et **réinjecte le
résultat dans l'état du Thing** par une Action.
Autrement dit : le moteur estime un pourcentage à partir de trois grandeurs dont **aucune n'est
mesurée** — le SoC de départ saisi par l'utilisateur, la capacité déclarée, un facteur de pertes
constant — puis compare la cible à sa propre estimation. **La boucle se referme sur elle-même**,
et rien, à aucun moment, ne lit l'état réel d'un véhicule.
> Le piège est là, et il touche l'app en premier : l'estimation est écrite dans `batteryLevel`,
> donc elle sort par la frontière RPC **indistinguable d'une mesure**. Un écran qui affiche
> « 62 % » affiche une intégration, pas une lecture. Si le §10 conserve ce chemin, ce champ doit
> être marqué comme ESTIMÉ là où il est publié — sans quoi l'app ment de bonne foi.
**Et pendant ce temps, la grandeur qui EST mesurée n'est lue par personne.** `sessionEnergy` —
l'énergie livrée depuis le branchement, comptée par la borne — est publiée par quatre des cinq
classes du banc. Le contrat porte déjà son emplacement : `LoadContextTelemetry::sessionWh`
(`surpluscontext.h:77`). **Il n'est rempli nulle part**, et `EvCharger` n'expose même pas
d'accesseur. Champ déclaré, jamais alimenté.
### Réponse à la première question : OUI, et le modèle a déjà la bonne forme
Un avancement utilisable se reconstruit sans communication véhicule, et les trois pièces
existent déjà :
| Ce qu'il faut | Ce qui existe | État |
|---|---|---|
| une grandeur mesurée | `sessionEnergy` de la borne | publiée, **jamais lue** |
| un emplacement au contrat | `LoadContextTelemetry::sessionWh` | déclaré, **jamais rempli** |
| une cible dans la même unité | `needs { minEnergyWhPerDay }` (LM-1001) | déclaré, **jamais lu par le scheduler** |
**C'est la forme que le §10 doit retenir** : une obligation exprimée en **énergie**, mesurée par
l'énergie livrée. Elle est vérifiable de bout en bout, et elle est déjà commune à toutes les
charges — un ballon et une borne portent le même `needs`, ce qui est exactement ce que LM-1001
annonçait.
**Quatre réserves, et chacune devient un défaut si on l'oublie :**
1. **Livré ≠ stocké.** `sessionEnergy` est comptée à la borne ; les pertes de charge font que le
véhicule en reçoit moins. Ce n'est pas un défaut de la mesure — c'est le bon côté du compteur
pour une **obligation d'énergie** : celui qu'on paie. Mais la promesse doit se formuler
« livrer X kWh », jamais « mettre X kWh dans la batterie ».
2. **`sessionEnergy` se remet à zéro au branchement.** Parfait pour une obligation *par session*,
**inutilisable tel quel** pour un `minEnergyWhPerDay` : débrancher et rebrancher relance le
compteur, et l'obligation du jour se croirait à zéro. L'accumulation entre sessions est un
mécanisme distinct, et sa place naturelle est l'historique du §11.
3. **Toutes les bornes ne la publient pas** — mesuré : `wallboxNoMeter` et l'une des deux classes
`wallbox` ne l'ont pas. La dégradation doit être **VISIBLE** : une échéance qui cesse
silencieusement d'être mesurable est pire qu'une échéance absente, parce qu'elle continue de
s'afficher. Même exigence que `hasPowerMeter()`, qui est déjà testé avant d'estimer un SoC.
4. **Elle ne répondra JAMAIS à « la voiture sera-t-elle pleine ? »** Aucune énergie livrée ne dit
où en est une batterie dont on ne connaît ni l'état de départ ni la capacité réelle. Une cible
en énergie est vérifiable ; une cible en pourcentage ne l'est pas. Ce sont **deux objets
différents**, et le §10 ne doit pas les confondre sous le mot « échéance ».
### Réponse à la seconde question : OUI, et elle tient MÊME avec la première
Répondre « oui » à la première ne dispense pas d'écrire la seconde, parce que `targetPercentage`
**ne disparaît pas** : c'est un champ amont, c'est ce que l'app affiche, et c'est ce que
l'utilisateur saisit. Deux objets, donc deux régimes de vérité, et le motif publié doit les
distinguer :
| Objet | Vérifiable ? | Ce que le motif a le droit de dire |
|---|---|---|
| obligation en **énergie** (`needs`) | **oui**, par `sessionEnergy` | « X kWh livrés sur les Y demandés » — et l'atteinte peut être annoncée |
| cible en **pourcentage** (`targetPercentage`) | **non**, sans communication véhicule | l'intention déclarée, et **l'avancement comme inconnu ou estimé** — jamais comme tenu |
**Règle à tenir dès l'écriture** : *un motif n'affirme pas ce que le moteur ne mesure pas.* Une
échéance qu'on ne peut pas vérifier ne doit jamais se présenter comme tenue, ni comme en voie de
l'être. C'est la même exigence que celle qui sépare `BELOW_MIN_POWER` de `SURPLUS_INSUFFICIENT`
(§12) et que LM-1006-1 pose pour la source d'un plafond : **deux causes, deux phrases**, parce
qu'elles n'appellent pas le même geste. Ici le geste diffère radicalement — « attends, ça
charge » contre « je ne peux pas savoir où tu en es ».
> **Ce que cette note NE tranche pas**, et qui demande une décision :
> — l'app garde-t-elle un écran en pourcentage, alimenté par l'estimation clairement étiquetée,
> ou bascule-t-elle en énergie livrée, qui est la seule chose que la box sait prouver ?
> — `minEnergyWhPerDay` exige-t-il l'accumulation inter-sessions du §11, ou le §10 se limite-t-il
> d'abord à une obligation **par session**, qui ne demande rien de plus que de lire un champ
> déjà publié ?
>
> La seconde est le vrai arbitrage de lot : l'obligation par session est faisable avec
> `sessionEnergy` seul, l'obligation quotidienne demande un historique.
---
**LM-1007 — Compatibilité descendante, et elle est gratuite.** Une charge qui ne déclare **LM-1007 — Compatibilité descendante, et elle est gratuite.** Une charge qui ne déclare
**aucun** besoin éco ne participe qu'à la passe 2 — soit exactement le comportement **aucun** besoin éco ne participe qu'à la passe 2 — soit exactement le comportement
d'aujourd'hui. Le modèle à deux niveaux **dégrade proprement** vers le modèle actuel, ce qui d'aujourd'hui. Le modèle à deux niveaux **dégrade proprement** vers le modèle actuel, ce qui
@ -866,6 +966,11 @@ l'émettre :
négociation de courant réelle, le délai d'établissement de la charge, et le comportement d'un négociation de courant réelle, le délai d'établissement de la charge, et le comportement d'un
véhicule qui refuse. véhicule qui refuse.
> ⚠️ **À lire avec LM-1009 (§10)** : la machinerie existe, mais son AVANCEMENT ne se mesure pas.
> `targetPercentage` est comparé à un pourcentage que le moteur a lui-même estimé, et la seule
> grandeur réellement mesurée — `sessionEnergy` — n'est lue nulle part. Une échéance en énergie
> est vérifiable ; une échéance en pourcentage ne l'est pas.
**LM-1205 — La machinerie d'échéance existe déjà.** `needs { dailyDeadline, minEnergyWhPerDay }` **LM-1205 — La machinerie d'échéance existe déjà.** `needs { dailyDeadline, minEnergyWhPerDay }`
est ce qui avait motivé le proxy — la crainte que le waterfall ne sache pas exprimer « cette est ce qui avait motivé le proxy — la crainte que le waterfall ne sache pas exprimer « cette
voiture doit être chargée à 7 h ». Il sait : c'est le même champ que le plancher éco du §10 voiture doit être chargée à 7 h ». Il sait : c'est le même champ que le plancher éco du §10