12 Commits

Author SHA1 Message Date
Patrick Schurig
909cfc5c1e docs(§10): 8.2 et 8.4 tranchés, 8.5 posée à l'agent app
Quatre des cinq points du §10 sont désormais tranchés. 8.2 : le plafond §14a
porte les deux formes — par phase et au total — sans jamais convertir. 8.4 : la
cible de confort reste un comparateur, le dosage attendant le modèle thermique du
§11. Confirmations, rien de rouvert.

8.5 reste, et il ne se tranche pas ici : c'est l'ordre de service APPARENT. Un
plancher éco de rang 4 passe devant un confort de rang 1 — voulu, lexicographique
sur (niveau, rang), et contre-intuitif pour l'installateur qui vient de poser son
ordre. La question est POSÉE à l'agent app dans le brief §9, pas répondue.

Trois choses lui sont données pour décider : sur quoi la liste est ordonnée — par
rang, et l'ordre affiché n'est plus l'ordre de service, ce qu'il faut dire ; ou
par ordre de service, et le rang cesse d'être lisible. Une entrée par passe ou
une entrée à deux états. Et ce qu'on voit d'une charge dont l'éco est servi et le
confort non — le cas le plus fréquent, et le seul qui n'ait AUCUN équivalent
aujourd'hui : la télémétrie publie un motif et une allocation, un seul de chaque.

Ce qui lui est demandé est de dire quelle FORME DE PUBLICATION rendrait l'écran
possible, comme la maquette des mécanismes l'a fait pour les six écrans — pas de
câbler quoi que ce soit, le §10 n'étant pas implémenté. Deux garde-fous joints
pour que la réponse ne se ferme pas sur un détail : une charge reçoit une SEULE
commande par cycle quelles que soient les passes (LM-1003), et le rang reste UN —
pas de prioritéEco/prioritéConfort, rejet explicite de LM-1002.

Et la conséquence est écrite des deux côtés : le §10 ne se code pas avant cette
réponse. Commencer par le moteur reviendrait à inventer une charge utile puis à
demander à l'écran de s'en arranger, c'est-à-dire l'ordre inverse de celui qui a
marché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-29 12:50:01 +02:00
Patrick Schurig
2c46f8e798 fix(3g-2): une entrée de configuration supprimée est rattrapée au cycle suivant
LM-1209-e. Le provisionnement vivait dans la branche « l'adaptateur vient d'être
créé » de syncAdapters(). Or l'adaptateur est construit depuis le THING : il
survit à la suppression de l'entrée de configuration. Une entrée supprimée depuis
l'app n'était donc jamais recréée — mesuré sur .75, rien en 140 s.

Le trou de 3g-2 rouvert par le côté client : la borne restait ARBITRÉE mais
ABSENTE de GetLoadConfig, à un rang de queue non réglable. Elle consommait du
budget sans que personne puisse la classer, et rien ne le disait.

La condition devient un ÉTAT et non un ÉVÉNEMENT — « cette borne n'a pas
d'entrée » au lieu de « l'adaptateur vient de naître ». C'est la leçon qui dépasse
le défaut : un rattrapage dont la condition est un événement ne rattrape rien,
puisqu'il ne se rejoue pas. Même distinction qu'entre ECS-411, qui relit l'état
réel des relais, et l'hypothèse de départ qu'il a remplacée.

L'entrée recréée revient en rankOrigin "auto" — personne ne l'a classée, son rang
est de nouveau un défaut — et le rang posé par l'installateur est PERDU avec
l'entrée. Supprimer n'est donc pas un moyen de retirer une borne de l'arbitrage :
elle revient, déclassée, en queue et avec son badge. C'est exactement pourquoi la
marque doit se voir. enabled: false reste la voie et conserve le rang.

Le refus de provisionnement n'est annoncé qu'une fois par borne
(m_provisionRefuse) : le rattrapage étant cyclique, le répéter produirait un
avertissement par minute et pour toujours — la règle est déjà posée pour les
charges sans décision dans buildTelemetry(). Le drapeau tombe au premier succès.

testDeletedEvChargerConfigComesBack couvre les deux moitiés : le rattrapage, et
le cas négatif — il ne se rejoue PAS quand l'entrée existe, sans quoi il
réécrirait la configuration à chaque cycle, donc un changed(), donc un rebuild
d'adaptateurs, donc des verrous réarmés en boucle (ECS-412). Vérifié échouant
avec l'ancien comportement.

Le brief §6-C, qui promettait à l'app l'inverse de ce qui se passait sur le seul
geste client qui détruit de la configuration, est corrigé — et dit désormais ce
que la suppression coûte vraiment : le rang.

Passe aussi LM-1209-d (grouper à l'affichage, jamais à l'écriture) et
l'avertissement de sonde en tête d'INTERFACE.md.

amd64 0/0. simulation 32/32, charging 16/16, loadmodel 20/20, spotmarket 7/7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-29 11:08:08 +02:00
Patrick Schurig
87c97e407f docs(brief): les trois réponses à l'agent app, mesurées sur marque fraîche
Complète 1490183, dont l'édition du brief avait échoué sur une assertion sans
rien écrire — le commit ne portait donc que AGENTS.md, INTERFACE.md et la spec.

1. L'écho verbatim passe, et l'explication de nos relevés contradictoires : le
   premier essai suivait un déplacement qui avait fait tomber la marque.
   sansRankOriginAuto() peut partir ; relire avant de réécrire le remplace.

2. La correction de la promesse fausse sur la recréation d'une entrée supprimée,
   avec ce qu'elle produit — borne arbitrée mais absente de GetLoadConfig — et la
   consigne qui en découle : ne pas supprimer l'entrée d'une borne dont le Thing
   vit, enabled: false reste la voie.

3. Le regroupement par domaine, à l'affichage jamais à l'écriture (LM-1209-d), et
   la note d'entrelacement qui ne devrait pas être un avertissement.

Plus la ligne pour leur README des sondes : un refus arrive en status "success".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-29 10:49:30 +02:00
Patrick Schurig
c20d55d6f7 docs(banc): l'écho verbatim d'un « auto » passe — vérifié sur une vraie entrée
Une seconde wallbox appairée après +etm27 a donné la première entrée
rankOrigin="auto" en configuration. Insertion en queue vérifiée sur machine :
rang 5, aucun autre rang modifié, ligne de journal complète.

LE CAS QUI DÉCIDAIT LA DEMANDE DU BRIEF EST DÉJÀ TOLÉRÉ, et par conception :
reconcileRankOrigin() n'accepte "auto" que si le persisté le porte déjà — c'est
l'exception écrite à la règle 3, sans laquelle la neutralité Get→Set serait
rompue. Mesuré : écho verbatim intégral -> EnergyErrorNoError ; écho verbatim
AVEC un renommage ailleurs -> EnergyErrorNoError, marque survivante, rangs
inchangés. Un renommage n'est donc jamais bloqué par une borne
auto-provisionnée, et le filtre de l'app est inoffensif mais inutile.

CE QUI MORD VRAIMENT est ailleurs, et c'est mesuré aussi : l'ÉCHO PÉRIMÉ.
Déplacer l'entrée fait tomber la marque ; renvoyer ensuite un payload mis en
cache AVANT ce déplacement, qui porte encore "auto", est rejeté. C'est correct —
ce payload affirme quelque chose qui n'est plus vrai — et le remède est de relire
avant de réécrire. Le vrai piège est un cache de configuration qui survit à une
écriture, pas le champ.

COÛT DE L'ESSAI, consigné : le banc ne porte plus d'entrée "auto". Borner le
refus demandait de déplacer l'entrée puis de la remettre, ce qui a consommé la
marque — elle tombe sur un changement de rang, quelle qu'en soit la main. Pour en
retrouver une, retirer l'entrée et laisser l'auto-provisionnement la recréer : au
rang 6, pas 5. On ne peut pas avoir à la fois le rang choisi et la marque.

Aucun code : la tolérance demandée existait déjà.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-29 10:36:18 +02:00
Patrick Schurig
8927b0033c feat(§12): LM-1209 — insertion en queue, rang unique, et la création se dit
Les trois conditions, d'un bloc.

INSERTION EN QUEUE. provisionEvChargerConfig() posait rang min − 1, écrasé à 1 —
donc à égalité avec la charge que l'installateur avait posée là exprès. C'est
désormais 1 + max(rangs). Queue de LISTE et non de domaine : les domaines ne sont
pas contigus, donc « juste après le dernier ev » exigerait de pousser une charge
d'un autre domaine, ce que la condition 2 interdit. Et comme max(domaine) ≤
max(liste), la queue de liste est toujours au moins aussi reculée — elle coïncide
quand le domaine occupe la fin, et sinon place la nouvelle charge derrière tout
le monde plutôt que de l'intercaler devant une charge classée. Plus fidèle au
principe que la formulation d'origine.

UNICITÉ. validateRanks() refuse une écriture où deux charges portent le même
rang. Avec la mise en garde écrite noir sur blanc : l'égalité ne coûte pas le
déterminisme — le tri est total sur (rang, identifiant) depuis 3g-2 — elle coûte
la gouvernabilité. S'y tromper ferait chercher le défaut dans le tri.

ET LA RÈGLE GÉNÉRALE QUI EN SORT (LM-1209-a-bis) : une vérification qui ÉCARTE et
une vérification qui REFUSE ne peuvent pas être la même fonction. validateSet()
nomme ce qui rend l'ensemble dangereux et load() s'en sert pour écarter ; y
ajouter l'unicité de rang aurait fait perdre des charges au démarrage sur une
configuration héritée — disparues de la configuration ET de l'arbitrage, sans
applySafeState, soit ECS-413 rouvert par une autre porte. Les deux sont
séparées ; validateRanks() n'est appelée que par setConfigs(). Au chargement, un
doublon hérité est signalé bruyamment et CONSERVÉ.

o:rankOrigin. Énumération fermée "auto" | "user", absente par défaut. Une
énumération et non un booléen parce que le troisième état est le point :
`rankIsDefault: false` affirmerait que le rang a été choisi, alors que c'est ce
qu'on ignore d'une configuration héritée. L'absence doit porter le « on ne sait
pas » ; un booléen ne sait pas se taire. Même motif que measurement.source, et la
spec le nomme désormais comme un motif de conception plutôt qu'une décision au
cas par cas.

Et la précision demandée en écrivant : l'effacement porte sur le RANG, jamais sur
la réception d'une écriture. SetLoadConfig remplace en bloc, donc réordonner une
AUTRE charge renvoie forcément l'entrée de celle-ci — l'effacer à cette occasion
perdrait une marque encore vraie. L'omission ne lève rien ; pour lever sans
déplacer, un client envoie "user". Un "auto" fabriqué est refusé, l'aller-retour
verbatim reste licite.

Trois tests. testAutoProvisionedRankGoesToTheQueue vérifié échouant avec
l'insertion en tête — et il échoue en disant que la borne n'entre PAS en
configuration, les deux mécanismes se répondant : le rang libre par construction
et le refus d'écriture en filet. testRankOriginFollowsTheRankNotTheWrite couvre
le cas qui se serait perdu, un client naïf qui omet la clé en déplaçant une autre
charge. testRankUniquenessRefusesWriteButNeverDiscards couvre la moitié qui ne se
voit pas : qu'un chargement conserve ce qu'il aurait pu écarter.

amd64 0 erreur / 0 avertissement. simulation 31/31, charging 16/16, loadmodel
20/20, spotmarket 7/7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-28 15:56:46 +02:00
Patrick Schurig
b1394bb681 docs(§11): la règle d'affichage du compteur se lit sur source, jamais sur adapter
Signalé en relayant +etm26 : « masqué dès que la charge mesure, sauf SG-Ready »
formule la règle PAR MÉCANISME. La conclusion est bonne, la clé est fausse, et le
contrat ne l'interdisait pas explicitement.

L'exception SG-Ready est déjà DANS LA DONNÉE : un sg-ready publie `none`, donc
« masquer quand source == device » ne le masque pas, sans qu'aucun client ait à
connaître le mécanisme.

Et brancher sur `adapter` se trompe sur un cas qui n'a rien de théorique : un
relay-router à topologie MIXTE publie lui aussi `none` (LM-1105-a). Un client qui
déciderait au mécanisme masquerait le champ sur un ECS qui ne peut être mesuré
QUE par un compteur dédié. Un mécanisme est un mauvais indice de mesurabilité ;
la source EST la mesurabilité.

C'est LM-1106 pris par l'autre bout : le régime est publié POUR qu'il ne soit pas
déduit. Le redéduire du mécanisme côté client remettrait dans l'app la déduction
qu'on vient de retirer du moteur — et il faudrait la maintenir à chaque mécanisme
ajouté.

Aucun code, aucune sémantique déplacée : le contrat dit désormais ce qu'il
laissait deviner. Pas de release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-28 14:54:40 +02:00
Patrick Schurig
e392c1c7b8 feat(§11): la mesure d'une charge se lit sur l'appareil, et publie sa source
LM-1105/LM-1106 implantés. measuredW disparaît, sans alias.

ILoadAdapter::deviceMeasurement() — PURE VIRTUELLE, comme runtimeView() et pour
la même raison : un mécanisme ajouté qui hériterait d'un « non mesurable » par
défaut sortirait de la mesure sans bruit, et son écran afficherait « pas
mesurable » avec l'autorité d'un constat. Le compilateur pose la question.

Le type porte une CAPACITÉ, pas une valeur nulle : `available` répond à « cette
charge est-elle mesurable », question posée à la ThingClass, jamais à la valeur
du moment. Un routeur au palier 0 mesure 0 W et reste mesurable ; sans ce
booléen les deux seraient indistinguables.

attachMeasurements() résout meter → device → none. Le compteur explicite garde la
priorité, et pas d'abord parce qu'il mesure mieux : un réglage sans effet est
pire que pas de réglage. La priorité porte sur la SOURCE, pas sur la valeur —
compteur déclaré injoignable, on retombe sur l'appareil, parce que se taire
perdrait une mesure qu'on a et que le silence se lirait « non mesurable ».

Sous `none`, powerW est OMIS. La puissance nominale reste lisible dans
mechanism.*, qui dit d'où elle vient ; la faire passer pour une mesure produirait
un écart commandé↔mesuré identiquement nul à tous les cycles — un « tout
concorde » permanent, corollaire LM-104.

DEUX AFFINEMENTS IMPOSÉS PAR L'IMPLANTATION, tous deux de modèle :

LM-1105-a — une mesure PARTIELLE n'est pas une mesure. Là où plusieurs Things
portent la charge, il faut qu'ils la portent TOUS : trois relais dont un seul
mesure annonceraient 500 W là où 1 500 W coulent, et l'écart enverrait chercher
une panne inexistante. À ne pas confondre avec LoadTelemetry::currentPowerW, qui
accepte « au moins un » et retombe sur le nominal — celui-là est le juge runtime
du budget et doit rendre un nombre à chaque cycle. Les fusionner rouvrirait la
correction B.

LM-1105-b — la règle présuppose que le Thing piloté est TRAVERSÉ par la
puissance. Un relais de relay-router commute la charge, donc sa puissance EST
celle de la charge ; un contact SG-Ready est un contact de SIGNAL — la PAC est
alimentée ailleurs et un contact qui mesurerait quelque chose mesurerait sa
propre bobine. sg-ready est donc `none` par CONSTRUCTION, même sur des relais qui
mesurent — c'est le cas du banc — et c'est le seul mécanisme pour lequel
meterThingId reste la seule voie.

RelayRouter : la capacité se juge sur la ThingClass de TOUS les relais déclarés,
la valeur se lit sur les relais FERMÉS. Jugée sur les relais actifs, la source
vaudrait `device` au palier 1 et `none` au palier 0 — un bloc de mesure qui
apparaît et disparaît au rythme du thermostat. VÉRIFIÉ ÉCHOUANT ainsi.

testMeasurementSourceIsPublished : les trois sources sans une ligne de config de
compteur, le palier 0 qui ne fait pas basculer la source, la PAC en `none` alors
que ses contacts mesurent, la disparition de measuredW, puis les deux règles de
priorité — le compteur explicite l'emporte (1 234 contre 2 900), un compteur
injoignable fait retomber sur l'appareil.

Ne touche PAS ce que l'arbitre lit : LM-1101 tient, LoadTelemetry::currentPowerW
est inchangé, et le recrédit anti-clignotement n'a jamais transité par
meterThingId.

amd64 0 erreur / 0 avertissement. Suites, un processus par test : simulation
30/30, charging 16/16, loadmodel 18/18, spotmarket 7/7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-28 14:49:28 +02:00
Patrick Schurig
fcba98159f docs(spec §11): la mesure d'une charge se lit sur l'appareil, et dit sa source
Constat d'usage de l'agent app, retenu : pour afficher « mesuré » à côté de
« alloué », il fallait désigner une borne comme SON PROPRE compteur dans
meterThingId. Ce n'est devinable par personne, et une note d'écran qui explique
un réglage dit que le défaut est dans le modèle. LM-1105 : quand le Thing piloté
porte lui-même l'état de puissance, le moteur le lit sans configuration.

La règle porte sur le THING, pas sur le mécanisme — certains powerswitch portent
currentPower et RelayRouter sait déjà les sommer ; écrire la règle par mécanisme
y ajouterait la table à maintenir à la main que LM-600 refuse. Et le test se pose
sur la CLASSE, sur TOUS les Things déclarés : interroger les relais actifs ferait
osciller la réponse au palier 0, sur une installation dont rien n'a changé.

Le compteur explicite reste prioritaire — répondu oui, et la raison principale
n'est pas la précision mais qu'un réglage sans effet est pire que pas de réglage.
La priorité porte sur la SOURCE, pas sur la valeur : compteur déclaré injoignable
→ on retombe sur l'appareil, et la charge utile dit lequel a servi.

Le second point de l'agent app décide de la forme, et la vérification l'a rendu
plus tranchant qu'il n'était posé : le moteur porte DÉJÀ trois régimes sous un
seul nom et jette la distinction. evcharger rend une mesure ; relay-router rend
une mesure OU le palier nominal — le booléen `metered` est calculé puis jeté ;
sg-ready rend toujours une puissance DÉCLARÉE, jamais mesurée, délibérément.
Publier ces quatre lignes sous un même measuredW serait faux pour deux d'entre
elles, et un measuredW auto-rempli détruirait au passage la seule information que
le champ portait : sa présence signifiait « un compteur a été posé exprès ».

D'où LM-1106 : le régime se PUBLIE, il ne se déduit pas d'une valeur — C2 de
LM-1009 mot pour mot. measurement {source: meter|device|none, o:powerW}, powerW
absent sous none. `none` est un état POSITIF — C1 de LM-1009 : une capacité
manquante se dit — et c'est lui qui distingue « pas mesurable » de « compteur en
panne ». Sous none, la puissance nominale reste lisible dans mechanism.* : la
faire passer pour une mesure produirait un écart identiquement nul à tous les
cycles, un « tout concorde » permanent, corollaire LM-104.

Ce que la source change au diagnostic, et qui la rend utile plutôt que
décorative : device dit « la charge n'obéit pas » (commande et mesure sortent de
la même boîte) ; meter ouvre trois hypothèses — charge, compteur, câblage — dont
il ne tranche aucune ; none n'autorise rien. L'asymétrie va plus loin : sous
device une CONCORDANCE ne prouve rien, plusieurs wallbox renvoyant leur consigne
en guise de puissance, alors que sous meter elle est une preuve. device détecte
la désobéissance, meter est le seul à pouvoir détecter un mensonge — et c'est ce
qui donne à LM-1104 sa raison d'être : meterThingId reste AUTORISÉ sur une charge
qui publie déjà, à masquer par défaut, pas à interdire.

Ne touche PAS ce que l'arbitre lit : LM-1101 tient, et le recrédit
anti-clignotement continue de lire LoadContext::telemetry.currentPowerW, qui
vient de l'adaptateur et n'a jamais transité par meterThingId. Ce lot déplace ce
qui est PUBLIÉ, pas ce qui décide.

Aucun code. Le contrat bouge (measuredW → measurement), et le brief le dit à
l'app avec son coût : un écran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-28 14:28:46 +02:00
Patrick Schurig
3b63eec251 fix(3g-2): une borne sans voiture assignée reste arbitrée
LM-1208-b appliqué : `assignedCarId` sort du filtre d'entrée du waterfall.
Restent deux exclusions, et elles ne disent pas la même chose — le mode manuel
est une INTENTION (l'utilisateur pilote), l'absence de véhicule branché un
CONSTAT (rien à consommer de ce qu'on allouerait).

Le critère venait du proxy, où il a un sens : sans voiture, pas de SoC, pas de
capacité, pas d'échéance à planifier. Le waterfall n'a besoin d'aucune des trois
pour allouer du surplus. Il contredisait LM-1206 — le rang et le plancher
appartiennent à la borne, pas au véhicule — il n'était pas vérifiable, et il
excluait la borne d'un invité au moment précis où elle consomme. Ce qu'on perd
sans voiture est une FONCTION, échéance et cible d'énergie, pas l'arbitrage.

LM-1208-c, trouvé en implantant, et c'est lui qui aurait mordu : retirer le
critère ne suffit pas. Le proxy ne PRÉPARE pas une borne sans voiture —
prepareInformation() la filtre en entrée — donc elle entre dans le waterfall
sans ChargingProcessInfo. Or internalProcessInfo() rend un objet par DÉFAUT, et
ses valeurs sont plausibles : une phase, pas de bascule. Une borne triphasée
aurait été annoncée monophasée et le budget divisé par trois — plafond 3 680 W
au lieu de 11 040, 16 A commandés au lieu de 9, 11 kW tirés d'une enveloppe qui
en comptait 3,7. Le défaut de 3g-1 pris à l'envers, et silencieux.

D'où internalHasProcessInfo() : la question « cette borne a-t-elle été préparée
ce cycle » devient POSABLE au lieu d'être déduite d'une valeur crédible. Sans
ChargingProcessInfo on ne bascule pas — basculer suppose de savoir ce que la
voiture accepte — et on retient le phaseCount de la BORNE, hypothèse
conservatrice au sens de LM-104 : elle surestime la consommation.

Éprouvé par testChargerWithoutCarIsArbitrated, et VÉRIFIÉ ÉCHOUANT sans le
garde-fou : 3 680 W au lieu de 6 000 W alloués. Le test porte les deux moitiés —
la borne sans voiture est servie ET le mode manuel reste une exclusion, sans quoi
il passerait aussi si toute borne était arbitrée sans condition.

amd64 0 erreur / 0 avertissement. Suites, un processus par test : simulation
29/29 (dont run 16/16), charging 16/16, loadmodel 18/18, spotmarket 7/7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-28 12:46:44 +02:00
Patrick Schurig
05310cf561 docs(spec): LM-1208 — sortir de la décision n'est pas sortir de la comptabilité
Quatre constats de l'agent app, vérifiés un par un sur le banc.

A — la CONCLUSION est retenue, le MÉCANISME ne l'est pas.

« Pas de voiture assignée » cesse d'être un critère d'exclusion (LM-1208-b).
Les trois arguments tiennent, et le troisième est décisif : le critère
contredit LM-1206, qui donne le rang et le plancher à la borne et non au
véhicule. S'y ajoutent le cas de l'invité — la borne cesserait d'être arbitrée
au moment précis où elle consomme — et l'invérifiabilité d'une assignation
déclarée à la main.

Mais le budget n'était pas faussé, et c'est le journal du banc qui le montre :
pendant que la V2C tirait, le compteur est passé de −2 915 W (export) à
+3 531 W (import) et l'arbitre a publié « Surplus insuffisant (−3 531 W) —
chauffe-eau à 0 W ». L'ECS n'a pas été servi par-dessus. Le budget part de la
mesure BRUTE (invariant 8) : la consommation d'une charge exclue y est déjà
déduite. L'exclusion est conservatrice.

La règle générale est néanmoins écrite, parce qu'elle a un porteur étroit et
réel : une charge exclue du contexte mais commandée par le PROXY ne reçoit
aucun evReservedW. Transitoire, sans effet aujourd'hui — mais elle mordra au
§10, où la passe éco décide d'acheter.

Et un écart de MESURE, distinct et non tranché, est signalé : le compteur
racine lit ~1,5 kW de moins que la somme des charges mesurées. Même famille que
son aveuglement documenté aux résistances ECS. Ce n'est pas un problème
d'arbitrage, mais un budget juste suppose un compteur qui voit tout.

B — même cause, mais le mode manuel n'est pas en cause : il reste une exclusion
délibérée. Le vrai point est que le mode par défaut d'une borne détectée EST
ChargingModeNormal — une borne neuve n'est donc jamais arbitrée tant que
personne n'a choisi un mode éco. Correct, mais invisible à la mise en service.

C — l'entrée résiduelle se supprime déjà depuis l'app : SetLoadConfig remplace
l'ensemble, et l'auto-provisionnement ne recrée que les bornes dont le Thing
existe. Le geste demandé existe ; la limite à connaître est qu'une entrée dont
le Thing vit encore sera recréée — pour celle-là, c'est enabled: false.

D — instruction reçue, aucun cas particulier V2C dans le moteur, C1 maintenue.
Leur distinction est plus fine que ce que le brief anticipait : un champ absent
se voit, un champ qui s'efface se lit comme un zéro. Troisième régime, et c'est
pourquoi C2 exige que le régime soit publié plutôt que déduit d'une valeur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
2026-08-28 11:55:32 +02:00
Patrick Schurig
d4ad01324f docs(spec §10): LM-1009 — les trois arbitrages tranchés, et deux conditions de livraison
Décisions de Patrick, écrites dans la spec et portées à l'agent app qui avait
remonté la question.

D1 — l'avancement s'affiche en ÉNERGIE LIVRÉE, jamais en pourcentage. « 8,4 kWh
livrés depuis le branchement » est vérifiable ; « 62 % » suppose une capacité
que personne ne connaît et se lit comme une lecture du véhicule. Le pourcentage
reste dans la saisie de l'intention, il sort de l'affichage de l'avancement.

D2 — obligation par SESSION d'abord. Elle ne demande que de lire un champ déjà
publié, c'est l'usage réel (on branche le soir pour partir le matin), et surtout
la remise à zéro de sessionEnergy au branchement CESSE d'être une réserve : pour
une obligation de session, c'est la bonne sémantique. Le texte est réécrit en
conséquence — le problème disparaît au lieu d'être contourné. L'obligation
quotidienne devient un lot à part, dépendant de l'historique du §11.

D3 — tant que le chemin carBatteryLevel survit, il est étiqueté à la
publication. Une valeur que le moteur a lui-même intégrée ne franchit pas la
frontière RPC sous le même nom qu'une mesure. Précédent de nommage déjà dans ce
document (estimatedPowerW), et même refus que le currentPowerStorage à zéro du
relevé 3g-1 : mieux vaut « estimé », ou rien, qu'un chiffre crédible et faux.

Les deux réserves deviennent des CONDITIONS DE LIVRAISON, pas des notes — ce
sont les deux façons dont le §10 peut mentir sans erreur visible :

  C1 — une borne sans sessionEnergy dégrade VISIBLEMENT. Mesuré : wallboxNoMeter
  et l'une des deux classes wallbox ne l'ont pas. Une échéance qui cesse
  silencieusement d'être mesurable est pire qu'une échéance absente, parce
  qu'elle continue de s'afficher. Et « pas mesurable » n'est pas « rien livré ».
  C2 — on doit pouvoir dire LEQUEL des deux régimes s'applique, dans le motif
  comme à l'écran. Un avancement mesuré et un avancement estimé ne se
  distinguent par aucune de leurs valeurs : les deux sont un nombre qui monte.

La distinction des deux régimes est conservée telle quelle : une obligation en
énergie peut annoncer son atteinte, une cible en pourcentage publie une
intention et un avancement estimé — jamais « tenue ».

Le brief app gagne un cinquième point : c'est celui qui change un écran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
2026-08-27 14:49:55 +02:00
Patrick Schurig
d57a0184f3 docs(brief): ce que 3g-2 change pour l'app
Quatre points, émis vers l'agent app.

Le deuxième est le plus utile pour lui : la mise en garde « pour une borne,
allocatedW est la décision de l'arbitre et non l'état de la borne » TOMBE.
L'arbitre commande seul depuis +etm23 — 9 commandes sur la fenêtre du banc, 9
par lui, 0 par le proxy. allocationIsCommand rend true partout, les branches
conditionnelles peuvent disparaître.

Avec une réserve qu'il faut lui laisser : l'allocation reste ce qui a été
COMMANDÉ, pas ce que la borne TIRE. Ce qu'il peut enfin affirmer, c'est que
personne d'autre ne commande. L'écart commande/réalité se lit dans la charge
utile `mechanism`, pas dans allocatedW.

Les trois autres : loads[] ⊆ GetLoadConfig rétabli (2 → 4 entrées sur le banc,
avec la forme d'une entrée evcharger et la neutralité de l'aller-retour) ;
EV_GRID_START {budgetW, floorW, gridW} avec sa comptabilité partagée — clé à
prévoir, mais PAS ENCORE VUE sur machine, donc pas d'écran à bâtir autour ;
et le rang de borne, qui se classe au glisser-déposer sur une seule liste.

La réserve sur le rang par défaut est signalée explicitement parce qu'elle le
concerne : avant tout réglage, l'ordre affiché est un artefact, pas un choix.
À ne pas présenter comme un réglage tant que le point n'est pas tranché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
2026-08-27 13:15:10 +02:00