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
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
**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
@ -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
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 }`
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