Préalable au reste, et il n'ajoute aucune fonction. La chaîne de sélection
du motif vivait au milieu de l'émission : il n'y avait qu'un motif possible
parce qu'il n'y avait qu'un endroit pour le former. Chaque passe devra
choisir SON motif sur SES grandeurs (R7 — le motif dit de quel niveau il
parle), donc la chaîne sort avant que la seconde passe n'arrive.
`MotifEntrees` regroupe exactement ce dont le choix dépend : ce qui n'y
figure pas ne peut pas influencer le motif, et c'est vérifiable à l'œil.
Comportement CONSTANT, et vérifié comme tel : la fonction extraite est
IDENTIQUE TOKEN POUR TOKEN à la chaîne d'origine, comparée par
normalisation. L'ordre des branches est ce qui porte les précédences
(LM-1011 : BELOW_MIN_POWER prime sur DRAW_CAP tant qu'il reste de
l'autorisation) — le réordonner changerait le motif publié sans qu'aucun
nombre ne bouge.
Deux erreurs attrapées en chemin, toutes deux par relecture :
- la substitution avait préfixé les CLÉS de charge utile ("budgetW" →
"e.budgetW"), soit un changement de contrat silencieux ;
- un `else if` transformé en `if` : sans effet ici puisque chaque branche
retourne, mais une divergence gratuite dans une fonction dont l'ordre
des branches est la spécification.
Et un piège de HARNAIS, sans rapport avec l'étape mais qui la bloquait :
testL4ShedsAPacAndSaysItForcedTheLock échouait 5 fois sur 5 alors qu'il
passait la veille, à code identique. Ni le code sous test, ni une fragilité
de temporisation — un cycle d'arbitrage tourne à l'horloge MURALE avant le
premier cycle simulé (sonde : action SG_NORMAL estampillée « 22:52:07 »).
L'armement paresseux d'ECS-412 fige m_lastSwitch sur cette heure, et un
scénario daté en dur se déroule « avant » son propre armement : elapsed
négatif, verrou jamais ouvert. Le test passait ou échouait SELON L'HEURE
QU'IL EST, ce qui est pire que les deux, et c'est aussi l'explication du
remainingS: 1288 observé plus tôt sans qu'on sache le lire.
Corrigé en ancrant l'origine du scénario sur l'horloge réelle : la
chronologie relative suffit, l'origine doit vivre dans la même horloge que
ce qui touche le système. Le commentaire dit à quelle condition ce choix
devient faux, conformément au troisième mode d'érosion.
Reste ouvert et consigné : pourquoi un cycle complet s'exécute à l'horloge
murale dans une suite compilée avec ENERGY_SIMULATION, où les minuteurs sont
explicitement exclus. Pas un défaut de production — une seule horloge y
existe — mais une faille de reproductibilité pour tout test manipulant un
Thing.
Suite complète : simulation 54/54, charging 48/48 identique à la référence
ligne à ligne, loadmodel 21/21, spotmarket 32/32, doxygen 0 avertissement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
11.1 étalement uniforme, avec les trois choses qui s'écrivent avec lui :
recalculé à chaque cycle (une cadence figée ne rattrape pas et manque
l'échéance en ayant l'air de travailler) ; il se durcit à l'approche —
2 kWh font 2 kW à une heure et 12 kW à dix minutes, donc la fenêtre d'achat
massif arrive d'un coup et c'est le MOTIF qui doit le dire ; et un point de
non-retour explicite, parce qu'un seuil nommé se discute là où une asymptote
se subit.
11.2 oui aux quatre champs, et la subtilité entre dans le CONTRAT : sous la
réserve batterie, draw.remainingW reste non nul pendant que budget.remainingW
est à zéro. LM-1203-b vue depuis la frontière — la réserve annule le surplus,
jamais l'autorisation de soutirage.
11.3 dissoute, 11.4 par champ explicite.
Et un troisième mode d'érosion dans AGENTS.md, symétrique des deux autres :
non pas une règle qui se périme faute d'être exécutable, mais une règle rendue
IMMORTELLE par sa propre garde. draw.authorisedW a été refusé pour un motif
juste, épinglé par un test, puis le motif est tombé — et la formulation a
survécu, protégée par un test qui ne savait pas que le monde avait changé.
Parade déjà pratiquée ici : quand un test fige une ABSENCE, son commentaire
écrit la condition de levée. « Absent tant que rien ne le mesure » se relit et
se lève ; « ne doit pas exister » se relit et s'obéit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Rangés pour ne pas se perdre — aucun ne passe devant le §10, et aucun ne se
déduit du code : les trois viennent de la machine.
1. Une commande perdue pendant une indisponibilité n%s est jamais réappliquée.
Coupure Modbus, la box restaure « false 6 A », la borne reste à 7 A : 4,4 kW
tirés, draw.committedW à 0, budget de 2 349 W qui les ignore. Une seconde de
coupure suffit. Sur 18 kVA invisible, sur 6 kVA c%s est le disjoncteur.
Même famille qu%sECS-111 et ECS-415, sous une troisième forme : après la
télémétrie et après la valeur de retour, voici l%sexécution.
2. chargingState figé à Idle pendant une charge réelle — pas de la péremption,
personne ne maintient le champ. Un champ sans mainteneur doit être ABSENT,
pas figé, sinon un écran dit « à l%sarrêt » pendant une charge.
3. ChargingModeNormal n%est pas un boost : il restaure des valeurs mémorisées
qui peuvent être false/6 A. Le nom et l%seffet ne coïncident pas.
Et un point ouvert qui se clôt : la V2C publie sessionEnergy. PIÈGE consigné
dans le design §10 étape C — l%saccumulateur porte un OFFSET constant (7,2044
au dix-millième sur deux échantillons), donc deliveredWh doit être une
différence depuis une base capturée. Une lecture directe ferait croire toute
obligation éco tenue de 7,2 kWh au premier cycle.
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
La protection de surcharge ne connaissait que les bornes VE : une PAC ou un
routeur pouvaient tenir un dépassement sans que rien ne les redescende. Elle
déleste désormais les charges du waterfall, de la dernière servie à la
première — le même ordre total que la cascade, lu à l'envers, pour que
délester ne puisse pas contredire l'ordre dans lequel on a servi.
Délester n'est pas toujours retirer des watts. Une PAC ne se module pas : on
la fait redescendre d'ÉTAT, vers 2 et jamais 1. C'est le cas que le bornage
de l'étape 3 ne pouvait pas traiter — il empêche d'allouer, il ne retire
rien à personne.
Le franchissement de verrou se CONSTATE : on tente d'abord sans forcer, et
`minOnBypassed` n'est marqué que si le verrou a réellement refusé. Le
déduire d'un verrou simplement ACTIF serait faux — un verrou peut être armé
et permettre malgré tout le geste demandé. Le marqueur annoncerait alors un
court-cycle qui n'a pas eu lieu, et le premier compresseur réellement
maltraité passerait inaperçu au milieu des fausses alertes. Ce constat
n'était pas possible avant ECS-415.
On ne force que pour un plafond SUBI. Comportement observé, non postulé : la
contre-épreuve « forçage interdit » — qui est le comportement d'un plafond
EcoSelfImposed — laisse la PAC en état 4.
Un seul commandeur par organe : les charges délestées sont revendiquées pour
le cycle et le dispatch les saute, sinon la consigne du plan rallumerait ce
que la sécurité vient de couper.
La fenêtre d'exécution est étroite — après syncAdapters(), avant le dispatch
— et les deux placements écartés sont consignés au design §6bis parce qu'ils
se représenteront.
Ce lot a été repris après un arrêt : deux versions antérieures du test
passaient sans rien prouver, leurs prémisses étant supposées. Les trois
cycles sont désormais MESURÉS puis assertés, l'armement à froid compris.
Suite complète : simulation 54/54, charging 48/48 identique à la référence
ligne à ligne, loadmodel 21/21, spotmarket 32/32, doxygen 0 avertissement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Deux ajouts qui sortent d'ECS-415, à côté de la règle des garde-fous.
Le versant CONTRAT : un contrat qu'aucun appelant n'exerce n'est pas un
contrat tenu, c'est un contrat non testé. Le garde-fou dit « une règle que
rien ne rejoue se périme » ; le contrat dit « une garantie que personne ne
consomme n'a jamais été vérifiée ». D'où la conduite : avant d'exploiter une
valeur de retour qu'aucun code n'exploitait, l'épingler d'abord par un test.
Le fait de méthode : la règle d'ECS-415 était déjà écrite dans le
commentaire de la branche VOISINE, à trois lignes de la branche fautive, et
elle ne s'était pas propagée. Une règle écrite à un endroit ne se propage
pas toute seule — c'est ce qui justifie de balayer les autres implantations
du même contrat plutôt que de corriger localement. Corollaire de relecture :
la proximité d'un bon commentaire n'est pas une preuve que le code voisin le
respecte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
SgReadyAdapter::applyAction() renvoyait, quand le verrou minStateHold
refusait la transition, l'action DEMANDÉE telle quelle. Un appelant qui
relisait `state` y retrouvait l'état qu'il venait de demander et concluait
au succès — alors que rien n'avait été écrit et que les contacts n'avaient
pas bougé.
Ce n'est pas un défaut du délestage, c'est un défaut de contrat. N'importe
quelle descente d'état pouvait échouer en silence, et `available` restait
vrai : la charge avait l'air saine pendant que la PAC tirait 3 kW. Même
famille qu'ECS-111 — une issue indiscernable de son contraire — transposée
au retour d'une commande au lieu de la télémétrie.
Toute sortie décrit désormais l'état RÉELLEMENT tenu, `estimatedPowerW`
compris. L'idempotence n'y échappe pas : demander « état 4, 0 W » à une PAC
qui en tire 3000 doit rendre 3000, pas le zéro de l'appelant — sinon il lit
son enveloppe, pas une réponse. Même correction sur la branche « motif
vide » du RelayRouter et de l'EtmVariableLoadAdapter.
Le défaut était LATENT : aucun appelant ne lisait ce retour, le dispatch
l'ignore. Le premier à s'y fier fut le délestage L4 en cours d'écriture, qui
a publié 3 kW rendus par une PAC restée en état 4. Un contrat qu'aucun
appelant n'exerce n'est pas un contrat tenu, c'est un contrat non testé.
Deux pistes écartées, mesurées et non supposées. m_currentState n'était pas
en cause : il est écrit synchroniquement, sans attendre d'acquittement.
ECS-411-b non plus : la relecture des contacts répond à une divergence entre
état interne et matériel, et il n'y en avait aucune — l'état interne était
juste, c'est le retour qui mentait.
Les trois pièges rencontrés à l'étape 4 sont consignés dans
DESIGN_DELESTAGE §6bis, les deux placements écartés compris. L'étape 4
elle-même reste non livrée.
Suite complète : simulation 53/53, charging 48/48, loadmodel 21/21,
spotmarket 32/32, doxygen 0 avertissement. Contre-épreuve : sur l'ancien
retour, refuse.state vaut 2 — l'état demandé — au lieu de 4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Vivre avec le pessimisme de la pire phase est le bon arbitrage pour une
couche de sécurité — il coûte du rendement, jamais de la sécurité. Mais
l'échéance se rapproche par les deux bouts, et l'écrire comme une position
de principe serait faux.
Côté matériel, l'information existe déjà pour une borne : la V2C publie
currentL1/L2/L3 depuis le 2026-08-28. Côté modèle, le §14a distinguera par
phase et ne pourra pas se contenter d'un nombre.
Le jour venu, houseDrawMargin() retiendra la phase de la charge au lieu de
la pire : un seul appel change, parce que l'étape 2 compare des MARGES et
non des plafonds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Le surplus dit ce qu'on peut dépenser sans acheter ; l'autorisation de
soutirage dit ce que le branchement supporte. Une charge doit tenir dans
les deux. Jusqu'ici seules les bornes VE étaient bornées par la seconde —
une charge de waterfall pouvait donc créer un dépassement que seule la
protection L4 rattrapait, après coup. C'est ce qui change.
`SurplusContext::drawMargin` est une valeur de CYCLE, résolue une fois par
`houseDrawMargin()` et jamais relue : deux lectures à deux instants
feraient décider sur deux mondes. Chaque charge servie l'entame.
Le motif porte les quatre grandeurs gravées au §6.3 : la marge (SIGNÉE),
l'ampleur du dépassement, la source, et la phase qui borne. La phase n'est
publiée que s'il y en a une — un plafond total n'en a pas, et en annoncer
une enverrait mesurer au hasard. Précédence LM-1011 appliquée telle quelle :
DRAW_CAP seulement à autorisation NULLE, sinon BELOW_MIN_POWER.
Les bornes VE ne sont pas écrêtées par la marge de la maison, et ce n'est
pas un oubli : `evPhaseAllowanceW()` connaît LES PHASES QU'ELLES OCCUPENT.
Leur appliquer en plus la pire phase les priverait d'une phase saine parce
qu'une autre sature. Entre deux bornes justes on garde la mieux informée,
pas la plus sévère. Elles décrémentent la marge comme les autres.
LIMITE DE MODÈLE assumée (LM-1210-a) : `LoadDeclared::phases` est un nombre,
pas un masque — le moteur ignore sur quelle phase une charge est câblée. La
pire phase est donc retenue. Deviner, dans une couche de sécurité, se paie
au disjoncteur. Le pessimisme est borné en pratique et coûte du rendement,
jamais de la sécurité ; le lever demande un masque déclaré, donc config,
interface et migration. Documenté dans la spec et le design, à trancher.
Suite complète : simulation 52/52, charging 48/48 identique à la référence
ligne à ligne — les bornes ne bougent pas, c'est l'invariant du lot —
loadmodel 21/21, spotmarket 32/32, doxygen 0 avertissement. Contre-épreuve :
bornage débranché, la charge est servie 1000 W alors que l'autorisation est
déjà dépassée de 460 W.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Les branches des phases B et C testaient `currentPhaseB`/`currentPhaseC`
— des ampères — puis lisaient `currentPowerPhaseB`/`currentPowerPhaseC`
divisés par 230 — des watts. La phase A, elle, lit bien l'état qu'elle
teste. L'asymétrie n'était pas cosmétique : un compteur publiant les
courants par phase SANS les puissances par phase entrait dans la première
branche, y lisait 0, et se voyait accorder une place libre égale au plafond
entier. La protection de surcharge était AVEUGLE sur B et C, sans rien
signaler.
Le correctif change le comportement, et dans le bon sens : des bornes
seront écrêtées qui ne l'étaient pas, parce que la protection redevient
effective. Sur une triphasée déséquilibrée, le disjoncteur était jusqu'ici
le seul rempart — L0 employé comme régulateur au lieu de dernier recours.
Aucun test de ce dépôt ne pouvait le trouver : le mock pose currentPhaseB =
currentPowerPhaseB / 230, donc les deux états y sont cohérents par
construction et lire l'un ou l'autre y donne le même nombre. Le test
désaccorde les états à la main, ce qu'aucun chemin normal du mock ne
permet ; sans le correctif il annonce 25 A de place libre sur une phase qui
en tire 20.
L'angle mort est nommé comme une CLASSE dans SAFETY.md, et non comme un
incident : l'installation de référence n'exerce pas le cas, une suite verte
ne dit alors rien sur lui, et `stateValue()` d'un état absent rend 0 —
valeur qui, dans un calcul de marge, se confond avec « rien ne consomme ».
Un défaut voisin de la même fonction est consigné sans être corrigé :
`voltage` est déclarée hors des trois blocs et mutée par celui de la phase
A. Il aura son propre correctif isolé.
Fait avant l'étape 3 délibérément : c'est la couche de sécurité, c'est le
chemin qu'on étend, et brancher la marge dessus l'aurait rendu plus
difficile à isoler.
Suite complète : simulation 51/51, charging 48/48 identique à la référence
ligne à ligne, loadmodel 21/21, spotmarket 32/32, doxygen 0 avertissement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
`phasePowerLimit` devient un `DrawCap{Breaker, perPhaseW}` et
`evPhaseAllowanceW()` traverse `resolveDrawMargin()` — la mécanique du
délestage est branchée, et rien ne bouge. C'est le but : à comportement
constant, tout écart observé ensuite est un défaut, pas une évolution.
L'égalité est obtenue par CONSTRUCTION. `phaseDrawW()` inverse
`calculateAllowanceAmpere()` phase par phase plutôt que de relire les états
du compteur : les lectures amont ne sont pas symétriques entre phases, et
les recopier ici en aurait fait deux versions vouées à diverger.
Deux déplacements sans changement de valeur : l'écrêtage `qMax(0, …)`
descend chez le consommateur (la résolution rend une marge SIGNÉE, dont
l'étape 4 a besoin pour dire de combien délester), et le facteur 230 remonte
dans le plafond, qui s'exprime en watts comme les autres.
Vérification : quatre suites diffées ligne à ligne contre une référence
capturée avant l'édition — charging 48/48 et simulation 50/50 identiques,
loadmodel 21/21, spotmarket 32/32. Contre-épreuve : à 1 V près la suite ne
discrimine pas (consignes en ampères entiers), donc cet essai ne prouve
rien ; à un facteur dix, 7 échecs. Le chemin est vivant.
Au passage, une `\note` du .h affirmait qu'on rend à la borne ce qu'elle
tire déjà. Le code ne l'a jamais fait, et c'est délibérément de sécurité :
sur une phase en dépassement la marge est négative, et c'est ce signe qui
ramène la borne à son minimum.
Décisions 6.1 à 6.3 gravées dans le design, et défaut amont consigné dans
SAFETY.md — non corrigé sciemment, il appartient à un [upstream-fix] isolé.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Trois membres de DrawCap n'étaient pas documentés (DoD §5) : source, bornePhase()
et borneTotal(). ci-quality les a signalés ; je les ai vus APRÈS avoir commité et
poussé, en ayant lancé le contrôle dans une commande où son verdict passait sous
la sortie du push.
Le contrôle a fait son travail, pas moi : je l'ai regardé trop tard. C'est
exactement l'écart que la section « ce qui tient et ce qui s'érode » décrit — la
machine rejouait bien la règle, c'est la lecture du résultat qui a manqué.
La documentation ajoutée dit aussi la règle du signe, qui n'était écrite que dans
l'invariant de la structure : une valeur négative signifie « ce plafond ne borne
pas de cette façon », jamais « zéro watt autorisé » — zéro est une valeur licite
et voudrait dire interdiction totale de soutirer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Étape 1 du design : le TYPE et sa résolution. Rien ne le consomme encore, et
c'est voulu — dans un lot de sécurité, ce qui coûte n'est pas d'écrire le
comportement, c'est de prouver qu'on n'a rien cassé en chemin.
DrawCap porte la source en PARAMÈTRE, jamais enfouie dans le nom d'une fonction :
Breaker (par phase, permanent), GridOperator (total, sur ordre — aucun émetteur,
le transport du §14a n'existe pas), EcoSelfImposed (total, le temps de la passe 1
— la passe n'existe pas non plus). Les deux sources sans émetteur sont déclarées
pour que la porte reste ouverte, pas parce qu'elle s'ouvre.
LE POINT TECHNIQUE : on ne compare pas les plafonds, on compare les MARGES. Un
plafond par phase donne sa marge sur la phase la plus chargée, un plafond total
la sienne sur le soutirage mesuré ; les deux sont des watts. Le plus petit mord,
et on retient sa source ET la phase qui borne — sans elle, un installateur devant
une maison triphasée déséquilibrée ne sait pas où mesurer.
La contre-épreuve de la conversion est éloquente : ramener un plafond par phase à
un « total équivalent » donne 9 300 W de marge là où il y en a 500. Dix-huit fois
trop, sur la couche qui décidera d'acheter. C'est exactement l'hypothèse
d'équilibre que LM-1006-1 interdit, et elle serait restée invisible jusqu'au jour
où elle coupe du courant.
Trois autres invariants, tous vérifiés échouant : aucun plafond déclaré rend une
marge SANS OBJET et non nulle — une marge nulle bornerait tout à 0 W et couperait
l'installation, c'est le « zéro forgé » appliqué à la sécurité ; une marge
NÉGATIVE conserve l'ampleur du dépassement, qui est ce que L4 doit délester ; et
le départage à marge égale est STABLE (Breaker > GridOperator > EcoSelfImposed),
sans quoi le motif publié changerait au gré de l'ordre du tableau, donc sans
qu'aucune décision n'ait changé.
Le design complet est dans docs/DESIGN_DELESTAGE.md, avec les trois décisions
qu'il demande — dont celle qui touche la sécurité : que fait le délestage d'une
charge sous verrou minOn, et la réponse diffère selon la source.
simulation 35/35, charging 17/17, loadmodel 21/21, spotmarket 7/7, amd64 0/0.
Comportement inchangé : aucune suite ne bouge d'une ligne.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Écrit avant tout code de liaison, comme le workflow l'exige pour une couche de
sécurité. Il dit sous quelle forme, dans quel ordre, et nomme trois points à
trancher.
L'écart mesuré : verifyOverloadProtection() parcourt m_evChargers et rien
d'autre ; le waterfall ne connaît le plafond que par evPhaseAllowanceW(), et
uniquement pour les bornes. Les charges pilotées et la PAC n'ont aucun plafond —
invisible tant que la cascade ne dépense que du surplus, puisqu'on ne peut pas
dépasser ce qu'on n'achète pas. Le §10 change cela.
LE POINT TECHNIQUE DU LOT, et il n'est pas évident : on ne compare pas les
plafonds — ils n'ont pas la même forme — on compare LES MARGES qu'ils laissent.
Un plafond par phase donne sa marge sur la phase la plus chargée, un plafond
total la sienne sur le soutirage mesuré ; les deux sont des watts, donc
comparables. Le plus petit mord, et on retient sa source. Aucune conversion, donc
aucune hypothèse d'équilibre entre phases — et la marge est exactement ce que la
cascade doit connaître pour ne pas dépasser : le même nombre borne et explique.
Deux endroits, deux rôles, et ce n'est pas une redondance : la cascade empêche de
CRÉER le dépassement, la protection L4 rattrape ce qu'on n'a pas décidé — une
plaque de cuisson, un sèche-linge. L4 garde le dernier mot.
Quatre étapes dont DEUX À COMPORTEMENT CONSTANT (le type, puis la source Breaker
branchée à résultat identique au watt près). Dans un lot de sécurité, ce qui coûte
n'est pas d'écrire le comportement, c'est de prouver qu'on n'a rien cassé.
Trois décisions demandées, avec recommandation : la marge tient-elle compte du
commandé non encore mesuré (recommandé NON — une hypothèse fausse y coupe du
courant) ; que fait le délestage d'une charge sous minOn (recommandé : bypass pour
Breaker et GridOperator, PAS pour EcoSelfImposed — un plafond qu'on s'impose ne
justifie pas d'abîmer un compresseur, et c'est la première conséquence concrète
du fait que la source soit un paramètre) ; et ce que porte le motif DRAW_CAP.
Et ce que le lot ne fait pas : aucun transport §14a, aucun changement du repli L2
— le plafond auto-imposé est respecté par construction dès lors que le repli ne
dépense pas, et l'exigence dure appartient au §14a, hors du moteur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
R7 — decision.level sur le motif hérité, présent exactement quand levels[] l'est.
Sans lui chaque client réinvente sa règle de fusion et deux écrans affichent deux
motifs pour le même cycle. Un seul niveau aujourd'hui, donc la question ne se pose
pas : c'est la raison d'ajouter le champ maintenant plutôt qu'avec le §10 — même
argument que R2 sur le coût de déplacement.
R1 — l'absence et le zéro épinglés dans les deux sens. Niveau absent = passe non
parcourue ; niveau à targetW 0 = passe parcourue, rien à prendre. Publier l'un
sans l'autre les rendrait indistinguables. Aujourd'hui la passe unique est
toujours parcourue dès qu'une charge est arbitrée, donc c'est le MODE DÉGRADÉ qui
porte le cas « aucune passe » — levels[] absent, planification suspendue.
Vérifié échouant dans les deux sens : un niveau à zéro omis, et un decision.level
publié sans levels[].
LM-1014 — un client teste la PRÉSENCE d'un champ, jamais une version. La règle
vient de l'agent app, qui n'a pas retiré son exception EV_GRID_START mais l'a
déplacée sur la bonne frontière : elle survit là où la charge utile n'a pas de
counts. Ce qui rend une capacité détectable est la charge utile, pas un numéro —
une box se rétrograde, un paquet se reconstruit, une branche se déploie hors
séquence. Portée aussi en tête d'INTERFACE.md, avec sa conséquence : les versions
y datent les changements, elles ne sont pas des conditions à tester.
R5, R6, R8 NE SONT PAS REPOUSSÉES — elles sont sans objet, et je l'ai vérifié
plutôt que supposé : ECO_FLOOR_MET, ECO_FLOOR_GRID et DRAW_CAP n'existent pas
dans le catalogue. Écrire la garde de R6 aujourd'hui poserait un invariant sur un
code que rien n'émet, c'est-à-dire un test toujours vert.
simulation 35/35, charging 17/17, loadmodel 20/20, spotmarket 7/7, amd64 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Il ne peut pas produire ce régime sur le banc : pluggedIn est en lecture seule,
aucune action RPC ne l'écrit, et sans assignedCarId la borne reste hors loads[].
Le partage est une propriété du CODE, pas du matériel — le banc n'ajouterait que
la confirmation que le binaire arm64 fait pareil.
La charge utile de testEvGridStartIsAnnouncedAndBounded est donc reproduite telle
quelle dans le brief, avec son critère vérifiable ligne à ligne :
params.budgetW 900 → counts["budget.allocatedW"] 900 → budget.allocatedW 900
params.gridW 480 → counts["draw.committedW"] 480 → draw.committedW 480
Σ counts = 1380 == targetW == allocatedW
Les deux moitiés sont dans les deux compteurs, et les deux compteurs sont de même
nature. budget.evReservedW vaut 0 : il ne reçoit plus de tranche d'allocation, il
est redevenu la correction de latence qu'il a toujours été.
La seconde borne de la trace montre l'autre moitié de la règle — targetW: 0 avec
counts PRÉSENT et sa destination à zéro. C'est le cas le plus fréquent, celui
qu'un counts vide aurait perdu.
+etm32 est posé sur .75 et l'identité s'y vérifie sur des données réelles : Σ
counts[budget.allocatedW] = 1700 = budget.allocatedW. Le régime EV_GRID_START n'y
est pas reproductible et ne sera pas fabriqué — counts en serait la seule preuve,
et il l'est déjà dans la trace.
L'instrumentation qui a produit la trace était temporaire et a été retirée ;
l'arbre est identique à avant. Elle n'aurait rien affiché sans
QT_LOGGING_RULES="*.debug=true" — les règles par défaut suppriment qDebug sans
catégorie, ce qui a d'abord donné une sortie vide et un test qui passait.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
counts est publié par niveau : { destination → watts }, Σ counts == targetW
toujours. Clés opaques et jamais recyclées, publié même à une seule destination
et même à zéro — un counts vide satisferait l'identité en perdant la destination,
sur le cas le plus fréquent.
L'identité ne passe plus par funding : elle somme des COMPTEURS, plus des
origines. EV_GRID_START y entre sans exception. funding et counts ne se dérivent
pas l'un de l'autre — l'un dit d'où viennent les watts, l'autre où ils sont
comptés.
LA GARANTIE EST UNE MACHINE. Sept sites construisent une LoadAction ; vérifier
qu'ils renseignent tous counts en RELISANT le code se referait à chaque site
ajouté. testCountsSumToTarget relit la charge utile — toutes les charges, trois
régimes dont l'import où tout vaut 0 — et constate le résultat.
ET LA CONTRE-ÉPREUVE A TROUVÉ UN TROU DANS LE TEST LUI-MÊME. Amputer le partage
d'EV_GRID_START le laissait PASSER : la borne, auto-provisionnée en queue par
LM-1209, ne voyait jamais de budget, et le régime n'était pas exercé. Corrigé —
rang 1, et EV_GRID_START en PREMIER régime, parce qu'une fois la borne en charge
son recrédit porte le budget bien au-dessus du plancher et le démarrage sous
tolérance ne peut plus se produire. Plus une assertion qui EXIGE que le régime
ait été exercé, sinon le test ne prouve rien du partage.
Deux fois dans ce lot, la contre-épreuve a corrigé le test plutôt que le code :
la première fois elle avait échoué au MONTAGE (NYMEA_PLUGINS_PATH omis) et ne
prouvait rien non plus. Un garde-fou qu'on n'a pas vu échouer POUR LA BONNE
RAISON ne vaut pas mieux qu'un garde-fou qu'on n'a pas vu échouer.
GetChargingInfos filtre sur evChargerId. Le paramètre était déclaré au schéma,
documenté « for all or a single EV charger », et Q_UNUSED : accepté, sans effet,
sans erreur. Des trois issues c'était la pire — un paramètre absent se voit, un
paramètre refusé se voit, un paramètre ignoré ne se voit pas. Identifiant inconnu
→ liste vide, jamais une erreur, et la liste vide est non ambiguë puisque toute
borne configurée a une entrée.
Balayage du namespace : sur 20 méthodes, 9 déclarent des paramètres et
GetChargingInfos était la seule à en ignorer un. Mon premier balayage avait
conclu « aucune méthode ne déclare de paramètre » — regex fausse ; refait en
croisant, pour chaque méthode, les params.insert de sa déclaration avec les
params.value de son corps.
Le garde-fou doxygen de ci-quality a par ailleurs attrapé ma propre insertion :
le bloc CountKey s'était glissé entre le commentaire de LoadAction et la
structure, qui perdait sa documentation.
simulation 34/34, charging 17/17, loadmodel 20/20, spotmarket 7/7, amd64 0/0,
doxygen 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
CARTOGRAPHIE DES REMOTES CORRIGÉE, et elle était fausse sur deux points dont un
bloquait tout push. `gitea-lan` = 192.168.1.113 est la SEULE route SSH — le port
22 de git.etm-powersync.fr est filtré — et le même Gitea sert les deux noms :
origin (HTTPS, qui n'authentifie pas) et origin-ssh désignent LE MÊME dépôt.
Surtout : « gitea-lan = miroir public GPL, ne jamais y pousser » était faux. Le
remote etm-public pointe sur powersync-energy-plugin-etm.git — sans le préfixe
etm- — et ce dépôt N'EXISTE PAS : Gitea répond « Cannot find repository ». Un
push y aurait échoué, pas publié. La prudence était donc fondée sur une
cartographie inexacte, et elle interdisait la seule route disponible.
Reste vrai : la publication du miroir public est un geste manuel de Patrick. Et
la vérification d'ascendance avant push est consignée comme obligatoire — c'est
ce qui distingue une avance rapide d'un écrasement.
61 commits poussés vers origin-ssh, ascendance vérifiée avant et relecture après :
plus rien en local seulement.
ET LA LEÇON DE LA SEMAINE, en section propre : la différence entre ce qui tient
et ce qui s'érode n'a jamais été la qualité de l'argument, c'est de savoir s'il
existe une machine qui le rejoue. Six cas de la semaine en regard — la table
d'état qui déclarait absents trois mécanismes testés, la table de traçabilité qui
nommait huit tests inexistants, la promesse de recréation jamais exercée,
ECS-111 décrit et jamais corrigé, l'absence de draw.authorisedW défendue en prose
à trois endroits — contre ce qu'un générateur, un test ou un refus RPC a tenu.
Avec son corollaire, éprouvé lui aussi : un garde-fou qu'on n'a jamais vu échouer
ne protège rien. Tous les tests d'invariant de la semaine ont été vérifiés
échouant avant d'être déclarés prêts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
« C'est le genre de champ qu'on ajoute pour compléter, six mois plus tard, sans
se rappeler pourquoi il manquait. » — et un contrat ne fait échouer aucune
compilation. L'absence était défendue en prose dans INTERFACE.md, dans le
changelog et dans un commentaire de code : trois endroits qui n'arrêtent
personne.
testLoadTelemetryRpc épingle désormais la FORME de `draw` : exactement une clé,
`committedW`. Les quatre du §14a — authorisedW, remainingW, binding,
perPhaseBound — sont absents, et leur ajout casse le test avec la raison.
VÉRIFIÉ EN L'AJOUTANT : `draw.insert("authorisedW", 0.0)` « pour compléter
l'objet » fait échouer le test — 2 clés au lieu d'1. C'est la contre-épreuve qui
compte ici : un test d'absence qui ne casserait pas sur l'ajout ne protégerait
rien.
Le commentaire dit quoi faire quand il casse, parce que le premier réflexe devant
un test rouge est de corriger le test : ce n'est pas lui qu'il faut changer,
c'est le champ ajouté qui doit arriver AVEC son transport §14a et une valeur
vraie — jamais à zéro. Un `authorisedW: 0` assorti d'un `binding` par défaut
afficherait un plafond de soutirage qui n'existe pas, avec sa source.
Épinglé aussi en mode dégradé : `draw` y est absent comme `budget`, pour la même
raison — sans plan, il n'y a pas de soutirage DÉCIDÉ, et un `committedW: 0` s'y
lirait « ce cycle n'a rien acheté » alors qu'il n'y a pas eu de cycle.
Aucun changement de comportement. simulation 33/33.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
L'app a validé le préalable sans réserve, et sa formulation de pourquoi counts
n'a pas été livré mérite d'être gardée : une identité dépendante du temps est
pire que pas d'identité. Vraie au premier cycle après un changement, fausse après
convergence, elle passe tous les tests — qui portent sur des cycles isolés — et
certifie un total faux chez les clients, avec le sceau d'une identité publiée.
draw.committedW est publié dans la MÊME TRAME que budget : les deux décrivent le
même cycle, et les lire à deux instants ferait comparer deux mondes.
SEUL, et c'est la deuxième demande, celle qui compte. authorisedW, remainingW,
binding, perPhaseBound sont ABSENTS, jamais à 0 — un authorisedW: 0 assorti d'un
binding par défaut afficherait un plafond de soutirage qui n'existe pas, avec sa
source. Un chiffre crédible et faux, c'est-à-dire la faute même que l'objet doit
rendre impossible.
budget.evReservedW n'a plus qu'UNE nature. Il recevait la part réseau
d'EV_GRID_START — une vraie tranche d'allocation — en plus de la correction A.
Déménagée dans draw.committedW. Sans ce déplacement, aucune définition du champ
n'aurait été vraie pour ses deux termes, et l'app aurait renommé son libellé sur
une définition à moitié fausse.
Sa définition est écrite, puisqu'elle affichait « Réservé au véhicule » : c'est
une CORRECTION du budget de surplus, pas une réservation ; elle tend vers 0 à
mesure que la mesure rattrape la commande, sans qu'aucune décision n'ait changé.
Le contrat dit : ne l'additionner à rien, il ne se réconcilie jamais. Le libellé
affirme deux choses fausses — que c'est réservé, et que ça concerne le véhicule
plutôt que la latence de sa mesure.
EV_GRID_START cesse d'être une exception : ses deux moitiés tombent dans des
compteurs de même nature. Le partage demeure — c'est un fait, une part est
achetée — mais il ne s'énonce plus comme une entorse.
Publication seule, aucune arithmétique de budget ne change : vérifié avant
d'écrire que la ligne alimentant evReservedW depuis la cascade n'avait aucun
retour dans le calcul.
simulation 33/33, charging 16/16, loadmodel 20/20, amd64 0/0, doxygen 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
LM-1013 — « grid » l'emporte dès qu'un watt est acheté, écrit comme RÈGLE et non
comme approximation. L'argument de l'agent app emporte la décision : tant qu'on
la décrit comme approximative, quelqu'un voudra la rendre exacte — part
dominante, seuil de majorité — et sur 900/480 le libellé basculerait d'un cycle à
l'autre sans qu'aucune décision n'ait changé. Déterministe, indépendante des
proportions, alignée sur le fait actionnable, et vraie sans décision nouvelle pour
le plancher éco du §10.
Pas de « mixed » publié, et c'est l'app qui l'a prouvé par son propre code : tout
client qui teste funding == "grid" a une branche par défaut, et cette branche est
« surplus ». Une troisième valeur y tomberait SANS ERREUR et rangerait au soleil
un niveau dont une part est achetée.
LM-1012 — on publie ce qui ne se dérive pas, on dérive ce qui se dérive. La ligne
est de l'app et elle vaut au-delà du cas : une valeur dérivée LOCALE est légitime
PARCE QU'elle est dérivable ; publiée, elle est illégitime POUR LA MÊME RAISON.
Elle explique après coup trois refus pris séparément — l'index d'ordre de
service, le « mixed », et stagesW qui est publié précisément parce que la
combinatoire n'est PAS dérivable côté client. Symétrique : rankOrigin est publié
parce que rien dans un rang ne dit qui l'a posé.
Ses trois demandes : clés opaques jamais réutilisées (engagement pris, même règle
que les numéros de règle retirés) ; counts publié même à zéro avec sa destination
(confirmé) ; et counts fait foi pour la COMPTABILITÉ, decision.params pour la
PHRASE du motif (sa lecture est la bonne).
MAIS R3 NE PEUT PAS ÊTRE ÉCRITE TELLE QUELLE, et c'est le résultat de la journée.
budget.evReservedW n'est pas un registre d'allocation : c'est un terme de
correction du budget, et il mélange la correction A — commandé non encore mesuré,
calculé AVANT la cascade — avec la part réseau d'EV_GRID_START, qui est une vraie
tranche d'allocation. Une borne servie au réseau qui tire déjà ce qu'on lui a
commandé y contribue 0 alors que son targetW vaut 7 000 W : Σ counts == targetW
serait vrai au premier cycle et faux ensuite. Ça passerait les tests et
dériverait en production.
Ce que R3 a mis au jour : les watts achetés n'ont AUCUN registre aujourd'hui.
draw.committedW devient un préalable à R3, pas une suite — petit, et sans effet
sur l'arithmétique, la ligne 355 étant de la publication seule. Ce qui change est
le SENS d'evReservedW, qui redevient une correction. Proposé à l'app, en attente :
c'est son identité de réconciliation.
Aucun code : counts n'est pas livré sur une base fausse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
AGENTS.md — « Sauvegarder l'historique quand la forge est injoignable », avec les
trois commandes et la seule qui compte : cloner À BLANC depuis la machine de
destination. Un bundle copié ailleurs n'est pas une sauvegarde ; un bundle cloné
à blanc depuis ailleurs en est une, et l'étape 3 est celle qu'on saute — dix
secondes, et c'est la seule qui distingue « restaurable depuis une autre
machine » de « présent sur une autre machine ». Avec ce qu'elle ne remplace pas :
un bundle est une photo, pas un remote.
R3 envoyée à l'app, avec la réponse à la question de forme : counts sera publié
TOUJOURS, même à une seule destination. L'omettre obligerait l'app à connaître la
destination par défaut, donc à la déduire, et lui donnerait deux chemins de
lecture dont un exercé sur presque toutes les charges. Surtout, l'identité
deviendrait conditionnelle — une identité à branche est une identité qu'on peut
se tromper à écrire, en silence.
C'est la règle qui a payé toute la semaine, et elle est citée comme telle : une
absence ne doit jamais vouloir dire « le défaut s'applique ». measurement.source
"none" est publié plutôt que la clé retirée ; rankOrigin absent n'affirme rien ;
levels[] absent dit « box antérieure », pas « un niveau implicite ». Le silence
ne porte que l'ignorance.
ET UNE QUESTION QUE counts FAIT APPARAÎTRE : R2 a déplacé le problème d'un cran
sans l'éliminer pour EV_GRID_START, dont le niveau est financé en partie au
surplus et en partie au réseau. funding: "grid" y est déjà une approximation — au
niveau, comme il l'était à la charge. Les deux champs ne dérivent pas l'un de
l'autre (origine contre registre), garder les deux est justifié À CONDITION QUE
CE SOIT DIT, sinon quelqu'un supprimera un jour celui qui a l'air redondant.
L'arbitrage sur le cas mixte est renvoyé à l'app, avec notre penchant motivé.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
TROIS choses, plus une sauvegarde.
1. LM-1011 — la précédence BELOW_MIN_POWER / DRAW_CAP est dans la SPEC. Je
l'avais écrite hier dans DESIGN_10.md : une note de conception se périme, la
spec fait autorité, et l'index ne voyait donc pas la règle. DESIGN_10 n'en
garde qu'un renvoi.
Autorisation résiduelle non nulle mais sous le plus petit palier →
BELOW_MIN_POWER ; DRAW_CAP réservé au résiduel nul. Ce n'est pas une règle
nouvelle : c'est celle de +etm19 transposée à une seconde ressource — le
surplus a déjà SURPLUS_INSUFFICIENT (« il n'y a rien ») contre BELOW_MIN_POWER
(« il y en a, mais pas au bon format »), et il aurait été absurde qu'une
ressource se lise autrement que l'autre. Ce qui décide n'est pas la véracité
mais le GESTE appelé : revoir le plafond, ou lire la fiche technique.
Conséquence de l'agent app retenue : le point de rupture de l'écran 2 est
ancré sur le DRAW_CAP, donc sur l'épuisement réel. Une charge refusée APRÈS
lui pour cause de palier ne le déplace pas — sans quoi deux charges
successives produiraient deux points de rupture concurrents dans un cycle.
2. LES TROIS NOMBRES au contrat — décidé, appliqué, mesuré. Ma correction sur
allocatedW a fait apparaître un troisième nombre que ni la maquette ni nous
n'avions nommé. INTERFACE.md porte le tableau ET ce que chaque écart signifie,
parce que c'est là qu'un écran se trompe de diagnostic : décidé ≠ appliqué est
une affaire de FORMAT, appliqué ≠ mesuré une affaire d'OBÉISSANCE — et cette
seconde lecture dépend encore de measurement.source.
3. R3 proposée à l'app avant d'être écrite : funding dit d'où viennent les watts,
il ne dit pas dans quel compteur ils tombent. Un niveau publierait sa
ventilation (counts), Σ counts == targetW toujours, et l'identité devient
calculable SANS exception — y compris quand draw.committedW arrivera.
Et la PRÉCONDITION du §10 est consignée : il ne part pas sans l'extension du
délestage au waterfall (LM-1006 couplage 1). La protection de surcharge ne
déleste aujourd'hui que les bornes ; livrer l'éco sans elle donnerait un plancher
capable de faire disjoncter une installation.
Aucun code. ci-quality 3/3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
funding était par CHARGE. Au §10 la même charge sera financée au réseau pour son
plancher éco et au surplus pour son confort DANS LE MÊME CYCLE : un financement
par charge y devient indécidable, et c'est lui qui porte l'identité de
réconciliation de l'app. Laissé à la charge, cette identité cesserait d'être
vérifiable sans que rien ne le signale.
Déplacé maintenant bien que le §10 ne soit pas écrit : seule des huit exigences
de la maquette à être un changement de FORME d'un champ existant, donc seule à se
renchérir à chaque écran qui s'y appuie.
UN SEUL NIVEAU aujourd'hui, et c'est « comfort ». La passe unique d'aujourd'hui
est celle qui, au §10, servira le confort depuis le surplus — la passe éco est la
nouveauté du lot, pas la passe existante rebaptisée. C'est ce qui rend le
déplacement faisable avant le lot : la forme est juste dès maintenant.
funding N'EST PAS conservé en résumé sur la charge, et c'est la première des deux
questions posées. Le cas mixte est justement celui qui le rendrait indécidable :
« mixte » serait un troisième code que personne n'a demandé, « grid si l'un
l'est » une convention que chaque client redériverait autrement. Deux endroits
pour un fait divergent.
Le motif `decision` reste, lui, au niveau de la charge — parce qu'on saura dire
de quel niveau il parle (R7). L'asymétrie est la règle : on conserve un champ
hérité QUAND ON PEUT EN DIRE QUELQUE CHOSE DE VRAI, jamais par symétrie.
L'IDENTITÉ DE RÉCONCILIATION est écrite dans INTERFACE.md, seconde question
posée. Somme sur les charges, somme sur les niveaux dont funding == "surplus", de
niveau.targetW == budget.allocatedW. Un client qui la reconstruirait depuis
l'ancienne forme divergerait au premier cas mixte.
Et son unique exception est publiée plutôt que sue : EV_GRID_START partage une
allocation entre deux compteurs, sa contribution au surplus est
decision.params.budgetW et non targetW. C'est ce que R3 généralisera ; d'ici là
l'exception vit dans le contrat, pas dans la tête du lecteur.
levels[] absent = box antérieure OU mode dégradé — le repli de l'app couvre les
deux. Rupture assumée pour un client lisant entry.funding : même arbitrage que
measuredW → measurement.
simulation 33/33, charging 16/16, loadmodel 20/20, amd64 0/0, doxygen 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
L'agent app a répondu au §9. Décisions inscrites en LM-1010, précédence des
motifs et réponse à R4 en DESIGN_10.md, réponse à l'app en brief §10.
SON ARGUMENT SUR LA LISTE EST MEILLEUR QUE LE NÔTRE, et il est repris tel quel :
nous disions « le rang doit rester lisible », il dit que l'ORDRE DE SERVICE N'EST
PAS UNE PROPRIÉTÉ DE LA CONFIGURATION — il change d'un cycle à l'autre selon les
planchers atteints. Trier par lui ne rendrait pas le rang illisible, ça rendrait
la LISTE instable, ce qui est pire. Une liste de configuration ne peut pas avoir
une colonne vertébrale qui bouge sous le doigt qui vient d'y glisser une charge.
Les deux autres réponses : une entrée par charge et deux lignes dans la carte —
on dédouble là où l'objet affiché est la SÉQUENCE, jamais là où il est la CHARGE ;
et éco servi / confort non en deux ÉTATS NOMMÉS, jamais une fraction, sans quoi
le régime nominal se lirait comme une dégradation permanente.
PRÉCÉDENCE DRAW_CAP ↔ BELOW_MIN_POWER, tranchée et écrite ICI plutôt que de
chaque côté de la frontière. Autorisation résiduelle non nulle mais sous le plus
petit palier : BELOW_MIN_POWER. DRAW_CAP dit « il n'y a plus d'autorisation », or
il en reste — pas au bon format. C'est la distinction de +etm19 entre
SURPLUS_INSUFFICIENT et BELOW_MIN_POWER, transposée telle quelle au soutirage :
pas une règle nouvelle, la même appliquée à une seconde ressource. Ce qui la
décide est le GESTE qu'elle appelle — revoir le plafond, ou lire la fiche
technique.
RÉPONSE À R4 : les compteurs de budget comptent le DÉCIDÉ. Le budget est le
registre de la cascade, et ce qui détermine ce que reçoit la charge suivante est
la valeur décidée. L'arrondi vit après, dans l'adaptateur, propre à chaque
mécanisme — compter le commandé obligerait à rouvrir le budget après le dispatch,
c'est-à-dire à décider deux fois.
ET UNE PRÉMISSE DE R4 EST FAUSSE, mieux vaut le dire maintenant : la maquette
pose que allocatedW publie « ce qui a été commandé ». buildTelemetry() publie le
PLAN, et applyActionsToAdapters() reçoit le slot const — rien n'est réécrit après
le dispatch. Mesurable : testUnpluggedChargerTakesNoBudget attend allocatedW ==
3000 sur une borne dont le courant commandé donne 2 990 W. Donc Σ targetW ==
allocatedW tiendra exactement, et l'écart que l'écran veut nommer est entre
allocatedW (décidé) et la charge utile de MÉCANISME (appliqué) — déjà publiée.
L'app a les deux nombres ; leurs noms étaient inversés.
PROCHAINE ACTION DU §10 : R2, funding descend au niveau. Seule des huit à être un
changement de forme d'un champ existant ; les sept autres attendent sans se
renchérir.
Aucun code. ci-quality 3/3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
ECS-111 défini et corrigé — le premier défaut réel exhumé par l'index des règles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Le comportement était « non défini » depuis l'écriture de la spec. telemetry()
SAUTAIT un relais qui ne résout plus : ou bien plus aucun relais actif ne
mesurait et l'on retombait sur le nominal du PALIER ENTIER — part du fantôme
comprise — ou bien des survivants mesuraient et l'on publiait une somme
sous-évaluée.
Et ce n'était pas de l'affichage : telemetry().currentPowerW alimente le recrédit
anti-clignotement, donc le BUDGET. L'arbitre se trompait sur le surplus
disponible pendant qu'available restait vrai. Un mensonge qui décide, pas un
mensonge qui s'affiche.
Option (a), la seule cohérente avec la famille : available faux, faultCode
THING_MISSING, traitement ECS-410 — sortie de l'ARBITRAGE, maintien dans la
COMPTABILITÉ, figée à la puissance crue engagée. La puissance est donc RETIRÉE du
budget des suivantes au lieu de leur être recréditée. Aucune somme partielle.
(b) — compter 0 W et servir le reste — est écartée : un relais absent du registre
est un Thing supprimé ou un plugin qui ne le voit plus, on ne sait pas dans quel
état est le contact, et servir le reste reviendrait à supposer.
Jugé sur TOUS les relais déclarés, jamais sur ceux du palier courant — sinon la
disponibilité basculerait au rythme du thermostat. Même règle que
deviceMeasurement(), qui traitait DÉJÀ le cas correctement : aligner l'ancien sur
le neuf, pas inventer une politique.
THING_MISSING prime sur WRITE_FAILED : un relais absent EXPLIQUE les écritures
qui échouent, et annoncer WRITE_FAILED enverrait l'opérateur au bus quand la
cause est dans la configuration. D'où le corollaire, écrit dans la doc de
clearFault : ClearLoadFault lève le défaut d'ÉCRITURE, il ne fait pas exister un
Thing.
testEcsPartialFailure a dû être ajusté et c'est instructif : il utilisait un
relais JAMAIS CONFIGURÉ comme raccourci pour forcer un échec d'écriture. Le
raccourci a maintenant un sens propre, et le test distingue les deux causes — le
défaut collant se lève, le câblage manquant non.
Trouvé ni par un test ni par le banc, mais par la LECTURE d'une règle que l'index
a rendue visible. Consigné comme tel dans spec_ecs.md et AGENTS.md : c'est le
premier défaut réel exhumé par le chantier de documentation.
simulation 33/33, charging 16/16, loadmodel 20/20, amd64 0/0, doxygen 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Sept étaient des trous de classification ou de citation, sept sont réelles.
TROIS CITATIONS AJOUTÉES, portées par du code depuis longtemps :
LM-400 une machine physique est UNE charge → c'est l'exclusivité d'un Thing
dans validateSet() qui l'empêche d'être déclarée deux fois
LM-301-b « une charge utile, une branche de fabrique » vaut pour CONSTRUIRE,
pas pour ALIMENTER en actions → le repli L2 lit supportedKinds
LM-600 les codes d'adapter portent le nom du MÉCANISME, jamais du domaine
QUATRE PASSÉES EN CADRAGE : ECS-004/005 (invariant tenu par ABSENCE — aucun
identifiant de relais ne franchit la frontière, il n'y a rien à pointer), LM-404
(hors périmètre déclaré), LM-501 (justification de NE PAS bâtir les groupes tant
que la liste plate suffit).
RESTENT SEPT, et ce sont de vraies attentes : ECS-111, ECS-302, ECS-305, ECS-307
(les trois dernières déjà listées en « ce qui reste à éprouver », §9 de
spec_ecs.md), LM-401/LM-402 (facettes, couche non bâtie), LM-502 (plafond de
groupe, conditionnel).
ECS-111 mérite d'être remonté : ce n'est pas une attente, c'est un défaut vivant,
et sa conséquence dépasse ce que la spec en dit. Détail dans le rendu de session.
Bilan de l'index après cette passe : 34 « spécifiée seule » (dont 27 en
territoire non construit), 26 cadrage, 24 citées par le code, 17 par un test, 12
vérifiées sur machine. Le nombre de départ était 70.
Aucun changement de comportement. ci-quality 3/3, build 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Le premier usage de l'index avait donné 70 règles « spécifiée seule », un nombre
qui ne voulait rien dire : il mélangeait « pas encore construite » avec « rien ne
pourra jamais la citer ». Une règle NÉGATIVE — « un thermostat n'est PAS une
charge », « aucune ThingClass n'est exposée » — s'applique par ABSENCE de code.
Il n'existe aucune ligne à pointer, et la compter comme un trou de couverture
invite à boucher un trou qui n'existe pas.
tools/rules-noncode.txt déclare 22 règles de cadrage, chacune avec son motif :
constats résolus, interdictions, structure du modèle, hors-périmètre, conventions
de nommage, justifications, et un contrat d'affichage client. §10, §14, ECS-5xx
et ECS-6xx n'y sont PAS : ils attendent d'être construits, et c'est ce que
« spécifiée seule » doit continuer de dire.
Résultat : 41 « spécifiée seule » au lieu de 70, dont 27 en territoire non
construit. Restent ~14 règles en territoire livré que rien ne cite — un nombre
qu'on peut lire une par une.
LE GARDE-FOU, éprouvé et non supposé : le générateur CASSE si une règle de
cadrage acquiert une citation de code ou de test. Vérifié en ajoutant une
citation de LM-202 dans un en-tête — RC=1, la règle nommée, le fichier et la
ligne donnés. Même mécanique que le refus de redéfinir un numéro retiré.
Et la réparation est TOUJOURS la même : retirer la ligne. Deux cas la
déclenchent — la règle a été implémentée depuis, ou elle est citée pour expliquer
une absence — et dans les deux « citée par le code » est plus vrai que
« cadrage ». La liste ne peut donc que RÉTRÉCIR : elle ne sait pas mentir dans le
sens qui compte. C'est ce qui rend une liste tenue à la main acceptable ici.
ci-quality 3/3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
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
Premier usage réel de l'index, et il donne un résultat inattendu : sur les 70
règles « spécifiée seule », une part notable n'est PAS un trou d'implémentation
mais un trou de CITATION. Vérifié en cherchant le CONTENU dans le code plutôt que
l'identifiant :
ECS-110-b un Thing n'appartient qu'à une charge active → validateSet()
LM-302-b l'exclusivité ne se valide pas dans une charge utile → idem
LM-1209-a-bis écarter et refuser ne sont pas la même fonction → validateRanks()
ECS-410-b l'issue de ClearLoadFault est journalisée par l'arbitre
LM-303 les verrous appartiennent au mécanisme, pas au domaine
LM-1208-c sans voiture, le nombre de phases vient de la borne
ECS-308 MaxRelays TRONQUE au lieu de refuser
Toutes portées par du code, aucune citée. L'index les comptait donc comme
spécifiées seules — il SOUS-DÉCLARE la couverture, exactement comme il peut la
surdéclarer. C'est la même limite que celle écrite en tête de la page générée
(« cité par n'est pas couvert par »), prise par l'autre bout : un identifiant
absent d'un commentaire ne dit rien de ce que le code fait.
Citations ajoutées avec \rule{}, l'alias posé hier — donc navigables du
commentaire vers la règle. Aucun changement de comportement : ce sont des
commentaires. 63 « spécifiée seule » au lieu de 70, 21 citées par le code au lieu
de 14.
ci-quality 3/3, build 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Étape 4 du chantier de documentation. tools/ci-quality.sh porte les trois
vérifications qui se posent au même endroit — elles partagent la même sortie
(doc-generated/) et la même commande de réparation, les séparer ferait trois jobs
qui régénèrent trois fois la même chose :
1. l'index des règles est VRAI (code de retour de gen-rules.py) ;
2. la doc Doxygen est complète — 0 avertissement, DoD §5 ;
3. l'index est REPRODUCTIBLE : deux passes, un seul résultat.
PAS EN SERVICE, et l'extension le garantit : ci/gitea-workflow.yml.disabled ne
déclenche rien tant qu'il n'est pas déplacé dans .gitea/workflows/. La raison est
datée dans le fichier — forge muette, plus de trente commits en attente ici et
plus de soixante côté app ; un job activé à l'aveugle échouerait au moment précis
où tout le monde pousse, et le premier réflexe serait de le désactiver.
CHAQUE ÉCHEC DONNE LA COMMANDE À LANCER. Un job qui annonce « l'index est
périmé » sans dire quoi faire se contourne en le désactivant : c'est le chemin de
moindre effort, et il gagne toujours.
VÉRIFIÉ AVANT D'ÊTRE DÉCLARÉ PRÊT, et ça ne passait pas du premier coup.
— Les 26 avertissements Doxygen de ce matin sont CORRIGÉS, pas contournés par un
cliquet sur un compte de référence : c'était de la dette DoD-5 (retours et
paramètres non documentés dans relayrouter.h, energyarbitrator.h, loadconfig.h,
les trois clearFault(), ILoadAdapter::updateSoftConfig), plus deux que j'avais
introduits — un \rule{} dans un titre \par, et un \param energyLogs qui ne
correspond à aucun argument. Le seuil est donc ZÉRO, le seul qui ne rote pas.
— Et le script lui-même échouait sur un dépôt SAIN : `grep -c ... || echo 0`
affiche 0 PUIS sort en 1 quand il ne trouve rien, si bien que le « || » empile
un second zéro et que le test entier casse. Corrigé ici et dans gen-doc.sh, qui
portait le même piège. Une commande de mesure dont l'échec produit une lecture
fausse — le motif de la semaine, appliqué à l'outil.
CE QUI N'EST PAS VÉRIFIÉ, et pourquoi : « régénérer et refuser si ça diffère de
ce qui est commité » n'a rien à comparer, doc-generated/ étant dans .gitignore.
Le risque qu'un tel diff attraperait — un artefact commité qui dérive de sa
source — a été SUPPRIMÉ en ne le commitant pas, pas déplacé. Ce qui reste à
garantir est que l'index soit vrai (contrôle 1) et déterministe (contrôle 3).
État au 2026-08-29 : ./tools/ci-quality.sh → RC=0, 113 règles, 0 avertissement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Le rattrapage cyclique du provisionnement, et la correction de ce que le brief
promettait à l'app sur la suppression d'une entrée.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
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
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
VERDICT SUR L'ÉCHO, sur une marque FRAÎCHE et sans rien déplacer avant : écho
verbatim inchangé -> EnergyErrorNoError, configuration identique, marque
survivante ; écho verbatim + un renommage ailleurs -> EnergyErrorNoError. Nos
deux relevés contradictoires s'expliquent : le premier essai suivait le
déplacement qui avait fait tomber la marque, l'app écrivait donc un "auto" que le
persisté ne portait plus — cas de l'écho périmé, refus correct.
sansRankOriginAuto() peut partir ; relire avant de réécrire le remplace.
CORRECTION LOURDE — le brief promettait à l'app l'inverse de la vérité.
« Supprimer l'entrée d'une borne dont le Thing existe encore la fait recréer au
cycle suivant » est FAUX : mesuré sur .75, entrée retirée, Thing vivant, rien
recréé en 140 s. syncAdapters() n'appelle provisionEvChargerConfig() que dans la
branche !m_adapters.contains(id), et l'adaptateur est construit depuis le Thing —
il survit à la suppression de l'entrée.
Ce que ça produit est le trou de 3g-2 rouvert par le côté client : la borne reste
ARBITRÉE mais ABSENTE de GetLoadConfig, à un rang de queue non réglable
(evPriority() → 1000). Il ne se referme qu'au redémarrage de nymead. Un correctif
est proposé — reprovisionner toute borne sans entrée, pas seulement à la
naissance de l'adaptateur — et NON écrit : c'est un changement de comportement,
la décision appartient à Patrick.
LM-1209-d — le rang est un ordre TOTAL, les domaines ne le partitionnent pas.
L'insertion en queue entrelace les domaines ; la contiguïté n'a jamais été un
invariant et rien dans le moteur n'en a besoin. L'exiger serait CONTRADICTOIRE
avec la raison même de LM-1209 : insérer une charge heating en respectant la
contiguïté décalerait toutes les ev suivantes, soit une détection qui réordonne
une installation qu'un humain a classée. Le même raisonnement vaut côté client :
grouper à l'affichage oui, à l'écriture non — et la note d'entrelacement ne
devrait pas être un avertissement, une liste entrelacée n'ayant rien
d'incohérent.
UN REFUS ARRIVE EN status: "success" — le verdict est dans params.energyError.
Porté en tête d'INTERFACE.md avec l'extrait qui va bien : une sonde qui teste
status annonce « accepté » sur une écriture rejetée, et une sonde qui se trompe
de champ produit un relevé qui a l'air d'un résultat. Même famille que le journal
lu par un sudo qui échoue en silence.
Banc rendu intact : 5 charges, rangs 1-5, marque "auto" en place au rang 5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
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
Un `sudo journalctl` par ssh ÉCHOUE — pas de TTY pour le mot de passe — et
l'échec part sur stderr : stdout est vide, le grep en aval ne trouve rien, la
commande rend 0. Un relevé conduit ainsi ne dit rien et en a l'air.
C'est arrivé sur mes propres relevés des 28 et 29 : deux recherches « la ligne X
apparaît-elle ? » n'avaient RIEN LU. Et l'explication que j'avais donnée à
l'absence — « cette ligne est en qCDebug, le niveau n'est pas actif » — était
fausse : les lignes `D | NymeaEnergy:` sortent bel et bien sur .75. Une cause
plausible avancée pour un silence qu'on n'a pas mesuré, c'est le corollaire
LM-104 appliqué à l'instrument de mesure.
L'accès qui marche est écrit : `journalctl -u nymead`, SANS sudo, l'utilisateur
etm étant dans le groupe adm. Vérifié sur .75.
Et la règle qui va avec : une absence ne s'affirme que dans un flux dont on a
prouvé qu'il coule — citer une ligne qu'on a LUE dans la même fenêtre, puis dire
ce qui n'y est pas. RELEVE_LOTD.md est donné en modèle : il cite la ligne
« 0 créée(s), 0 mise(s) à jour, 2 INCHANGÉE(S) » qu'il a lue, PUIS conclut à
l'absence d'ECS-413 et de commutation. L'absence y porte parce que la présence
l'accompagne.
AUDIT DES RELEVÉS. Repris sur ce critère : aucun ne repose sur un journal vide.
RELEVE_LOTBbis §267 parle d'un dry-run reprepro, pas du journal. Le seul défaut
était dans la PROCÉDURE — TEST_TERRAIN.md prescrivait `sudo journalctl` dans son
helper logs(), celui qu'un opérateur lance pendant un essai, alors que sa ligne
51 était déjà juste. Le document se contredisait ; corrigé.
Consigné aussi : +etm27 posé sur .75 et ce qui y a été vérifié — avec les deux
vérifications qui n'y sont PAS observables et qu'il ne faut pas fabriquer. La
config du banc porte les rangs 1-2-3-4, réparée à la main hier soir ; la
migration se rencontrera sur une box cliente non migrée.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Étapes 1 et 3 du chantier de documentation.
DIX NUMÉROS RETIRÉS — ECS-100/101/102/300/301/304/400/402/403/404. Ils
n'existaient QUE comme une étiquette de trois mots dans une table de statut :
aucun énoncé, nulle part. Leur reconstruire un texte depuis l'étiquette et le
code aurait été inventer une exigence en croyant la restituer, et une règle
inventée qui porte un numéro fait autorité pour toujours. Si le texte d'origine
n'existe nulle part, la règle n'existe pas ; ce que le code fait reste vrai, il
n'a simplement plus de règle à citer.
Retirés, pas effacés : tools/rules-retired.txt les porte, l'index les publie avec
leur date, leur motif et les endroits qui les citent encore. Un identifiant
croisé dans un vieux relevé renvoie donc à quelque chose — RELEVE_ECS306.md cite
toujours ECS-404, et c'est bien. Un numéro retiré n'est JAMAIS réattribué : le
générateur refuse une définition qui en reprendrait un, une même référence ne
doit pas désigner deux exigences selon la date de lecture.
Les citations VIVANTES de ces numéros sont retirées (spec_ecs §2/§4/§5,
AGENTS.md) — les phrases tiennent sans elles. Les citations HISTORIQUES restent.
DEUX TABLES SUPPRIMÉES, pas corrigées. §1 déclarait ECS-410/411/412 « absents »
alors que les trois étaient implémentées et testées. §9 « Traçabilité » nommait
HUIT tests qui n'ont jamais été écrits. Son défaut n'était pas d'être périmée
mais de mélanger un CONSTAT et une INTENTION sous la même colonne — « ECS-306 →
testEcsBudgetUnderLock » est un fait, « ECS-303 → banc » est un projet ; réunis,
le second garantit que l'ensemble devient faux. Les deux sont séparés : le
constat est généré, l'intention devient « Ce qui reste à éprouver », avec pour
chaque exigence POURQUOI elle n'est pas éprouvée.
DOXYGEN (étape 3). doc-generated/RULES.md entre en INPUT — ses ancres {#LM-1105}
rendent \rule{} résoluble — et l'alias \rule{1} est défini. Les specs restent
HORS périmètre : les y ajouter ferait entrer deux fois le même énoncé et
doublerait les ancres. Les 250 citations existantes ne sont pas converties : le
générateur les voit déjà, et \ref sert l'autre sens — du commentaire vers la
règle. Éprouvé sur un cas réel, \rule{LM-1209-c} résout sans avertissement.
Piège d'ordonnancement corrigé au passage : gen-rules.py écrit DANS $SORTIE, donc
l'appeler avant `rm -rf "$SORTIE"` l'effaçait aussitôt — doxygen signalait
« RULES.md is not a readable file ». L'index se produit après le nettoyage. Son
code de retour ne stoppe pas la génération (un index incomplet reste utile à
lire) mais est répercuté à la fin.
Avertissements doxygen : 30 → 26, dont 3 étaient les miens — rankOrigin(),
setRankOrigin() et knownRankOrigins() n'étaient pas documentés (DoD 5).
L'index sort désormais RC=0 : 111 règles, aucun orphelin, aucun domicile ambigu.
69 spécifiées seules, 16 citées par un test, 14 par le code, 12 vérifiées sur
machine. Build 0/0, loadmodel 20/20.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Étape 2 du chantier de documentation. tools/gen-rules.py produit
doc-generated/RULES.md : chaque règle, son énoncé d'une ligne, son domicile, son
état de preuve DÉRIVÉ, et la liste de ce qui la cite. Sort en erreur sur un
identifiant cité et jamais défini, sur un identifiant qui n'existe qu'en ligne de
tableau, et sur un domicile ambigu.
L'état de preuve ne demande AUCUNE convention nouvelle : il se lit dans l'endroit
où l'identifiant apparaît — energyplugin/ pour le code, tests/ pour les tests,
docs/RELEVE_* pour la machine. Citer les règles en commentaire est déjà
l'habitude de la maison, et c'est cette habitude qui devient l'index.
« cité par », jamais « ✅ » : cet index prouve la CITATION, pas la couverture. Un
✅ automatique serait le même mensonge que celui qu'on corrige, en plus rapide.
C'est écrit en tête de la page générée, pas seulement dans un commit.
DEUX MESURES PRÉALABLES ÉTAIENT FAUSSES, et le générateur le montre. Un
identifiant admet des suffixes ALPHABÉTIQUES et NUMÉRIQUES — LM-1006-1,
LM-1105-b, LM-1209-a-bis. Un tiret ordinaire ne peut donc pas séparer
l'identifiant de l'énoncé : « **LM-1006-1** (…) », une citation, se lisait comme
une définition concurrente de LM-1006. D'où le séparateur cadratin, et d'où la
disparition des « 49 orphelins » et des « 20 domiciles ambigus » annoncés :
il en reste DIX, et ce sont les mêmes dans les deux catégories.
Et la première sortie a trouvé une faiblesse dans les citations écrites hier :
LES SOUS-RÈGLES N'HÉRITENT DE RIEN. Citer LM-1105 ne marque pas LM-1105-b, qui
sortait « spécifiée seule » alors qu'elle est implémentée et testée. Corrigé aux
quatre endroits concernés — la règle de convention est désormais : citer
l'identifiant EXACT, jamais le parent.
Aucun changement de comportement : les quatre citations touchées sont des
commentaires. Build 0/0, testMeasurementSourceIsPublished 3/3.
doc-generated/ reste ignoré par git, comme le reste de la sortie de gen-doc.sh :
versionner une page générée ferait un diff à chaque édition de spec. La garantie
qu'elle est à jour appartient au contrôle CI (étape 4).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
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
Décision Patrick : insertion en queue, jamais en tête. Le rang est un choix de
l'installateur ; une détection ne réordonne pas ce qu'un humain a décidé. Une
borne détectée un mois après la mise en service passerait sinon devant l'ECS que
quelqu'un a réglé exprès.
La règle est écrite GÉNÉRALE — elle porte sur toute création d'entrée par le
moteur, pas sur les seules bornes.
Elle lève la tension apparente avec l'ordre par défaut « le VE d'abord » : ce
défaut décrit une MISE EN SERVICE INITIALE, où personne n'a encore rien classé.
Un ajout ultérieur est une autre situation — il existe un classement et il fait
autorité. C'est la distinction qui manquait à la question.
« En queue de son domaine » est précisé en `1 + max(rangs existants)`, parce que
les domaines ne sont pas des blocs contigus : insérer juste après le dernier `ev`
exigerait de pousser une charge d'un autre domaine, ce que LM-1209-b interdit. Et
la queue de LISTE est ici plus fidèle au principe — max(domaine) ≤ max(liste)
toujours, donc elle est toujours au moins aussi reculée, elle coïncide quand le
domaine occupe la fin, et quand elle diverge elle place la nouvelle charge
derrière tout le monde au lieu de l'intercaler devant une charge classée.
LM-1209-a — l'unicité de rang, garantie plutôt que supposée. Avec une mise en
garde qui corrige une lecture périmée : l'égalité ne coûte PAS l'indéterminisme.
Depuis 3g-2 le waterfall trie sur (rang, identifiant), ordre total et
reproductible ajouté précisément pour cela. Trois charges au rang 1 sont servies
dans un ordre STABLE mais arbitraire et non réglable — il obéit à des
identifiants, c'est-à-dire à personne. Exigence de gouvernabilité, pas de
déterminisme ; s'y tromper ferait chercher le défaut au mauvais endroit.
LM-1209-b — insérer ne renumérote personne, pendant exact de la suppression côté
app : un geste de configuration ne doit pas devenir un geste d'arbitrage.
LM-1209-c — la création doit se dire, et le journal ne suffit pas : l'app ne le
lit pas, et c'est elle qui montre le classement. Même exigence que C2 de LM-1009
et LM-1106 — le régime se publie, il ne se déduit pas. La FORME du champ est
laissée ouverte : à figer avant écriture.
Aucun code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Ma note de déploiement présentait les trois vérifications non observables comme
« masquées », ce qui se lit comme un angle mort à ouvrir. C'en est l'inverse :
le chauffe-eau et la PAC ont leurs compteurs dédiés depuis le 27, `meter` qui
l'emporte EST le comportement voulu, et les deux chemins masqués sont déjà
éprouvés en simulation et vérifiés échouant.
Le risque de la formulation d'origine était concret : qu'un prochain passage
retire ces meterThingId pour « mieux voir », échangeant une preuve contre une
observation et laissant la box dans un état que personne n'a voulu. La consigne
est donc explicite.
Corollaire de « à guetter au banc, jamais à fabriquer », posé à côté : le banc
sert les cas qu'on ne sait pas fabriquer, pas à rejouer ce qu'on a démontré.
Et une note de méthode, parce que l'erreur se refait : les trois attentes étaient
fausses pour avoir raisonné PAR MÉCANISME là où la CONFIGURATION décide — la même
erreur que LM-1106 corrige côté client, et elle se commet aussi bien en lisant un
banc qu'en écrivant une app.
Docs seuls, pas de release.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Distinction remontée par l'agent app, qui a dû l'ajouter parce que le contrat ne
la portait pas — et confirmée sur le banc le jour même.
`none` est PUBLIÉ, donc il affirme « cette charge n'est pas mesurable ».
L'absence n'affirme rien : elle dit que la charge n'a pas participé à l'arbitrage
du cycle — mode manuel, aucun véhicule branché — ce qui est sans rapport avec sa
mesurabilité. Sans la distinction, une borne débranchée se verrait annoncer « le
compteur dédié est la seule voie » alors qu'elle publiera la sienne au prochain
cycle.
Même famille que LM-1009 D (un champ absent se voit, un champ qui s'efface se lit
comme un zéro), appliquée un cran plus haut : ici c'est l'ENTRÉE entière qui
s'efface, et la lire comme un `none` transformerait un silence en affirmation.
La garantie qui rend la règle utilisable est posée explicitement : `measurement`
est présent pour toute charge de loads[], et pour elle seule.
Constaté tel quel sur .75 en +etm26 : deux bornes pluggedIn:false absentes de
loads[], dont la V2C qui porte currentPower.
AGENTS.md consigne aussi l'état du déploiement et les trois vérifications de banc
masquées par la configuration de la box — chauffe-eau et pac-terrain portant un
meterThingId réel, elles publient `meter` et le chemin `device` n'est pas atteint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
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
La mesure par charge lue sur l'appareil piloté, et measuredW remplacé par
measurement {source, o:powerW}. Contrat déplacé — l'app a un écran à reprendre.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p