Réponse de l'app aux trois questions du brief plugin docs/BRIEF_depuis_plugin.md §9
(= DESIGN_10.md 8.5, LM-1002 / LM-1003 / LM-1006 couplage 2 / LM-1009). Rien n'est câblé côté app :
cette maquette est le brief, et la charge utile demandée est en bas de page.
Elle vaut comme contrat, pas comme feuille de route. Le §10 n'est pas implémenté, le transport du
§14a n'existe pas, draw non plus : aucun de ces trois écrans ne se construit avant que
levels[] soit en ligne. Ce qu'elle fixe, c'est la forme de ce qui sera publié — pour
que le moteur n'ait pas à inventer une charge utile que l'écran devra ensuite arranger.
1 · La liste reste ordonnée par RANG — parce que c'est la surface d'édition du rang, et parce que l'ordre de service n'est pas une propriété de la configuration : il change à chaque cycle selon les planchers déjà atteints. Une liste qui se réordonne toute seule sous le doigt est inutilisable.
2 · Une seule entrée par charge dans la liste, deux lignes DANS la carte — la commande est le parent, les deux niveaux sont sa décomposition. On ne dédouble que là où l'objet affiché est la séquence, jamais là où l'objet est la charge.
3 · L'ordre de service devient un second écran, en lecture seule — c'est lui qui « le dit ». Il montre le rang 4 éco avant le rang 1 confort, avec les rangs visibles, donc il montre en même temps que votre ordre est respecté dans chaque passe. C'est l'écran qui désamorce « l'ordre n'est pas respecté ».
priority à plat,
et il n'existe qu'un rang. La maquette est un rejet appliqué de prioritéEco/prioritéConfort.
Σ targetW ↔ allocatedW :
ces deux-là sont égaux par construction, tous deux du décidé. Un écran bâti sur la prémisse
inverse comparerait deux valeurs toujours égales — il ne montrerait jamais rien, et sans
erreur. L'appliqué se lit dans la charge utile de mécanisme (stageW,
setpointW, currentA), qui existe déjà aujourd'hui.dailyDeadline est un "HH:MM" sans fuseau, interprété dans celui de
la box. Mesuré le 2026-09-04 — .75 est en Europe/London, donc
« 12:00 » vaut 13:00 sur une horloge française. Le fuseau se lit par
System.GetTime : l'écran l'affiche à côté du champ et signale la divergence,
plutôt que de choisir une horloge à la place de l'installateur.unmeasurable, le plancher ne peut pas se refermer — mesuré le
2026-09-01 sur quatre cycles : remainingWh figé à 4 000, remainingS qui
descend, gridW qui monte 755 → 767. Ce n'est pas un cas limite, c'est la
trajectoire garantie du régime : l'écran doit montrer reste et temps, pas
seulement les watts de l'instant, sans quoi « 767 W achetés » ne laisse pas deviner le mur.budget.evReservedW est un terme de correction : la latence entre l'ordre et sa
lecture au compteur. Il est affiché parce que la box le publie, jamais sommé. Son ancien
libellé, « Réservé au véhicule », affirmait deux faussetés — que c'est réservé, et que ça regarde le
véhicule plutôt que la latence de sa mesure ; réécrit le 2026-08-30 sur la définition livrée par le
moteur. Les watts achetés, eux, n'ont de registre qu'avec draw.committedW.(niveau, priority) : ne publiez pas d'index d'ordre, il serait une
seconde source de vérité pour une donnée déjà déductible.
DRAW_CAP de la première charge refusée, avec sa source.ECO_FLOOR_*ECO_FLOOR_METDRAW_CAPDRAW_CAP ne sort que si
l'autorisation résiduelle est nulle. S'il en reste mais sous le plus petit palier, le motif
est BELOW_MIN_POWER — il y en a, pas au bon format. Deux gestes : revoir le plafond,
ou lire la fiche technique.regimeSURPLUS_SETPOINTSURPLUS_INSUFFICIENTCOMFORT_SKIPPEDallocatedW — ce que l'arbitre a planifié, et ce que les compteurs de
budget comptent.mechanism.* — ce que l'adaptateur a su produire.measurement.powerW — ce que la charge tire, quand c'est mesurable.
measurement.source) ; décidé ↔ mesuré = la somme des deux, à ne jamais lire
seule — elle mélange une cause bénigne et une cause qui ne l'est pas.Σ targetW et allocatedW : ils sont égaux par
construction. Cherché là, l'arrondi est invisible et l'écran se tait sans se tromper.counts, il n'y en a plus une seule.
Un écart qui subsiste est un défaut, et il se dit.GetLoadTelemetryAucun de ces trois écrans n'est constructible sur la charge utile d'aujourd'hui : un allocatedW et
un decision par charge, un seul de chaque. Voici la forme minimale qui les rend possibles — elle
ajoute un tableau et ne retire rien.
{
"loadId": "…", "available": true, "measurement": { … }, // inchangés
"allocatedW": 1400, // INCHANGÉ — le DÉCIDÉ du cycle. Σ targetW == allocatedW, exactement.
// L'appliqué vit dans "mechanism" ; le mesuré dans "measurement".
"levels": [ // AJOUT — un élément par passe RÉELLEMENT parcourue
{ "level": "eco", // RELEVÉ sur `.75` le 2026-09-01, +etm43
"targetW": 766.813994355397, // le décidé, NON arrondi
"funding": "grid",
"counts": { "draw.committedW": 766.813994355397 },
"progress": { "regime": "unmeasurable", "targetWh": 4000 },
// deliveredWh ABSENT sous unmeasurable — jamais 0
"decision": { "code": "ECO_FLOOR_GRID", "params": {
"gridW": 767, "remainingWh": 4000, "targetWh": 4000,
"remainingS": 18779, "regime": "unmeasurable", "atMaximum": false } } },
{ "level": "comfort",
"targetW": 0,
"decision": { "code": "SURPLUS_INSUFFICIENT", "params": { "requiredW": 900, "availableW": 0 } } }
],
"decision": { "level": "eco", "code": "ECO_FLOOR_GRID", "params": { … } } // CONSERVÉ + le niveau reflété
}
// et, une fois par cycle, à côté de "budget" :
"draw": { "authorisedW": 4000, "committedW": 4000, "remainingW": 0,
"binding": "gridOperator", "perPhaseBound": false }
// ⚠ committedW part SEUL, et AVANT counts (2026-08-30) : c'est le registre des watts
// achetés, et il n'en existe aucun aujourd'hui. draw: {"committedW": 1400} tout court
// est la bonne première livraison — les quatre autres champs ABSENTS, jamais à 0 :
// un authorisedW forgé afficherait un plafond de soutirage qui n'existe pas.
Une charge sans obligation éco porte une seule entrée, comfort — pas une entrée
eco à zéro. L'absence est alors l'information (« ne participe qu'à la passe 2 », LM-1007), et un
zéro forgé la détruirait. levels[] entièrement absent = box antérieure : l'app retombe sur
allocatedW/decision et n'affiche ni bandeau ◆ ni écran 2.
Aujourd'hui il est par charge. Au §10, la même charge peut être financée au réseau pour son éco
et au surplus pour son confort dans le même cycle : un funding par charge devient
indécidable. Or c'est lui qui porte notre identité de réconciliation (« somme des allocations financées au
surplus == budget.allocatedW »). S'il reste à la charge, cette identité cesse d'être vérifiable
sans que rien ne le signale.
Si vous hiérarchisez, prenez celle-ci en premier : c'est la seule des huit qui soit un
changement de forme d'un champ existant et non un ajout. Chaque écran qui s'appuiera d'ici là sur
un funding par charge en augmentera le coût de déplacement — les sept autres attendent sans
se renchérir.
Parti le 2026-08-29 — l'argument de coût a emporté la priorisation. C'est la première des huit à être écrite.
La forme du moteur est prise telle quelle (2026-08-30) : à côté de funding, un
counts qui nomme les registres, avec Σ counts == targetW toujours, publié
même à une seule destination. L'identité devient calculable sans exception et par niveau :
l'exception EV_GRID_START, qui partageait une allocation entre deux compteurs, disparaît —
remplacée par une règle vérifiable qui vaudra telle quelle quand draw.committedW arrivera.
Les deux champs ne se dérivent pas l'un de l'autre, et c'est ce qui empêchera qu'on en supprime un :
funding dit l'origine (« ces watts sont achetés » — voisin du motif), counts
dit le registre (« ils sont comptés ici » — une comptabilité).
Sur un niveau mixte, un code unique : "grid" — et la règle demandée n'est pas « une
approximation » mais « grid l'emporte dès qu'un watt est acheté ». Déterministe, indépendante des
proportions (un libellé qui bascule sur 900/480 serait pire que faux), et alignée sur ce qu'un humain doit
savoir : l'achat est le fait actionnable. Elle vaudra sans nouvelle décision pour le plancher éco du §10.
Ni omission (notre isSurplusFunded range alors le niveau au soleil, en silence)
ni "mixed" publié : une troisième valeur tomberait dans la branche par défaut de tout
client — « surplus » — donc reproduirait la panne de l'omission, et elle serait dérivable de
counts, donc supprimée un jour à juste titre.
Et il a un préalable, trouvé en le câblant (2026-08-30) : draw.committedW.
budget.evReservedW n'est pas un registre d'allocation mais un terme de correction du
budget — une borne servie au réseau qui tire déjà ce qu'on lui a commandé y contribue 0 pour un
targetW de 7 000 W. Σ counts == targetW y serait vraie au premier cycle après un
changement et fausse ensuite : une identité dépendante du temps, qui passe tous les tests et
certifie un total faux. Les watts achetés n'ont aucun registre aujourd'hui — d'où une seule identité
vérifiable, du côté surplus, et rien de l'autre côté tant que committedW n'est pas publié.
Ce que l'app promet en échange : les clés de counts sont traitées comme des
identifiants opaques, jamais comme des chemins. Une clé inconnue d'une version ancienne n'est pas
perdue : l'identité Σ counts == targetW reste vérifiable sans connaître le sens des clés, et
l'écran dit « destination inconnue de cette version », jamais zéro. Corollaire pour le moteur : ne
jamais réutiliser une clé existante pour un sens nouveau — c'est le seul cas qu'aucun client ne peut
détecter.
Corrigé le 2026-08-29 : notre prémisse était fausse. allocatedW publie le
décidé, pas le commandé — buildTelemetry() publie le plan, applyActionsToAdapters()
reçoit le slot en const, et un test attend allocatedW == 3000 sur une borne dont
le courant commandé donne 2 990 W. Les niveaux et allocatedW sont donc la même monnaie,
et leur somme est une identité exacte — c'est ce qui rend levels[] vérifiable contre un
champ qui existe déjà. L'app traitera toute violation comme une incohérence de la box, pas comme un
arrondi.
Ce qui reste demandé : ne pas attribuer l'appliqué à un niveau. L'écart vit entre
allocatedW et la charge utile de mécanisme (stageW, setpointW,
currentA) ; l'adaptateur arrondit une consigne unique et ne sait pas de quel besoin elle
venait. Toute répartition serait une fiction — le principe n'a pas bougé, seulement l'endroit où l'écart
se trouve.
Et les compteurs de budget comptent le décidé, parce que c'est la valeur qui détermine ce que reçoit la charge suivante. Conséquence d'écran, assumée : le budget d'un cycle peut annoncer 1 400 W pendant que l'installation en tire 1 000 — d'où le bloc « trois nombres » de l'écran 3, qui empêche de lire cet écart comme une erreur de comptage.
Vérifié à la sonde le 2026-09-01 : progress et decision descendent au
niveau, et deliveredWh est bien absent sous unmeasurable.
Et le motif publie remainingWh, pas deliveredWh — accepté. Ce n'est pas
« une source par fait » qui tranche (on aurait pu retirer deliveredWh de progress)
mais ceci : remainingWh n'est pas dérivable sous unmeasurable, et c'est
exactement le régime où l'on achète. Un champ qui n'existe que là où on peut le calculer ferait taire
l'écran dans le cas qui coûte de l'argent. Sous unmeasurable,
remainingWh == targetWh : « 4 000 sur 4 000 » se lit « rien de mesurable », jamais
« rien livré ».
progress.regime ∈ measured | estimated | unmeasurable, et deliveredWh
absent sous unmeasurable — jamais 0. C'est la même règle que
measurement.source = "none", qui nous a déjà servis : un régime publié sépare « pas mesurable »
de « rien reçu », et c'est lui qui autorise la barre hachurée plutôt qu'une barre vide.
Une obligation non mesurable peut être servie, jamais annoncée tenue (LM-1009 C1). C'est une
garde testable côté moteur — et côté app, la combinaison sera traitée comme une incohérence de la box,
affichée comme telle, pas rendue. Le cas est réel dès aujourd'hui : la V2C Trydan ne publie pas
sessionEnergy.
Ajoutez decision.level. Sans lui, chaque client réinvente sa propre règle de fusion (« le
motif du dernier niveau servi ? du premier refusé ? ») et deux écrans affichent deux motifs différents pour le
même cycle. Une clé de plus, et la règle cesse d'être devinée.
Déjà prévu au DESIGN_10.md §6, on le confirme comme condition d'écran : c'est la source
qui décide du texte, et les trois textes appellent trois gestes différents. Le point de rupture de l'écran 2
est lu sur ce motif, jamais déduit d'un solde tombé à zéro.
servedIndex, order…) : la séquence de
l'écran 2 se dérive entièrement d'un tri lexicographique sur (niveau, priority). Un index
publié serait une seconde source de vérité pour une donnée déductible — et diverger un jour.services/telemetry_text.dart.Autorisation résiduelle non nulle mais inférieure au plus petit palier du mécanisme :
BELOW_MIN_POWER. Les deux motifs sont vrais, mais pas au même niveau —
DRAW_CAP dit « il n'y a plus d'autorisation », or ici il en reste, simplement
pas au bon format. C'est exactement la distinction posée en +etm19 entre
SURPLUS_INSUFFICIENT (« il n'y a rien ») et BELOW_MIN_POWER (« il y en a,
mais pas au bon format »), transposée telle quelle au soutirage. Le geste utile est de lire la fiche
technique du mécanisme, pas de revoir le plafond.
DRAW_CAP reste réservé à l'autorisation résiduelle nulle. Le point de rupture de
l'écran 2 (« autorisation épuisée ici ») est donc bien un DRAW_CAP ; une charge refusée
après lui pour cause de palier porterait BELOW_MIN_POWER et ne déplacerait pas le
point de rupture. La règle est écrite par le moteur, pas établie de chaque côté.