32 blocs générés (§3, §5.1-§5.3, §6, §7, §8) ; annexe G (contrat, validateur,
conformité). Écarts prose/code corrigés, le contrat suivant le code : refus de
token par réponse error, optimizerAlive inchangé sur abstention, exemples du
§5 sans les champs jamais émis (provider, perField détaillé, weather, comfort,
defaults, group, nominalPowerW, volumeL) et avec ceux qui le sont (available,
currentPowerW), instants avec décalage, engine réel en annexe A ; §5.2 aligné
sur la décision du 2026-10-16 (l'optimiseur ne consomme que le contexte).
Contrat : listes régénérées depuis le code (ordre des clés de x-etm-reason-params).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Question 4 de T7 : le masquage à la réception remplace TOUTE occurrence du
token dans ce que l'optimiseur renvoie ; un token court, présent par hasard
dans un identifiant de charge, corromprait le plan reçu (unknown-load).
- SetOptimizerToken (writeToken) refuse un token non vide de moins de
16 caractères (points de code UTF-8) : EnergyErrorInvalidParameter, fault
`token-refused` comme les autres refus ; rien n'est modifié ; le motif est
journalisé, jamais la valeur. Vide = retrait, inchangé.
- Le client relit le fichier au début de chaque tentative ; plus court que
16 caractères, il ne se connecte PAS (ni avec ce token, ni sans token : les
mesures du site ne partent pas vers un optimiseur que rien n'authentifie).
lastErrorCode `token-too-short`, dit une fois par série, nouvelle tentative
au backoff jusqu'au remplacement du fichier.
INTERFACE.md (SetOptimizerToken, lastErrorCode) et protocole §3 (E-T2-8,
forme du token). Tests : 15 refusé, 16 accepté, 15 « é » refusés (caractères,
pas octets), vide = retrait, fichier court sans connexion puis token conforme
envoyé ; tokens des suites portés à 16 au moins. Contre-épreuves M4a (refus
retiré) et M4b (fichier court envoyé) : mordent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Décision du 2026-10-17 : l'exemple GPL annonce `powersync-optimizer` dans le
`name` du handshake ; `etm-optimizer` reste le nom de l'optimiseur ETM,
propriétaire, hors de ce dépôt. Le fichier et son répertoire gardent le
préfixe des artefacts (`tools/etm-powersync-ref-optimizer/`).
README de l'outil, annexe E et tools/README.md le disent en une phrase ;
refoptimizer vérifie le `name` publié par GetOptimizerStatus (1.1 et 1.0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/ref-optimizer → tools/etm-powersync-ref-optimizer ; ce qu'il reproduit (passe confort)
et ce que le contexte 1.1 ne lui permet pas de reproduire ; codes émissibles seulement.
tools/README.md : renvoi vers son README, règle de comparaison d'une action state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Annexe D : défaut désactivé, nom des fichiers, rotation locale, plafond, appariement
requête → réponse (la ligne N attend la réponse à la requête N), no-response, jamais le token ;
le rejeu est fait par tools/etm-powersync-trace-replay (et non plus « hems-sim »).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Décision de Patrick du 2026-10-17 : un seul vocabulaire. Plan::strategy du rule-based,
GetPlan.strategy et la doc passent de « rule-based » à « rules » (INTERFACE.md, protocole §6).
Les libellés de journal (« repli rule-based ») ne sont pas des valeurs et restent.
Référence decisions régénérée. Preuve AVANT régénération, moteur de e592618 + ce seul
renommage, contre l'ancienne référence : comparateur --strategie (nouvelle option, qui ne tolère
que plan.strategy rule-based → rules) IDENTIQUE HORS STRATÉGIE — 23 792 lignes, 23 728
identiques à l'octet, 64 qui ne diffèrent que par .plan.strategy (décompte indépendant des
chemins JSON : 64 × .plan.strategy "rule-based" → "rules", rien d'autre). 0 rejeu. Le même
build, relu contre la nouvelle référence : IDENTIQUE ; 6 suites vertes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- INTERFACE.md : GetOptimizerStatus, SetOptimizerEndpoint, SetOptimizerToken, GetPlan,
OptimizerStatusChanged, permissions, UpdateLearned ignoré ; ce que nymea journalise des
paramètres d'un appel ; comment distinguer « pas d'optimiseur » de « pas de plan ce cycle ».
- OPTIMIZER_PROTOCOL.md : tier retiré (E23), « deux requêtes consécutives » (E17), §8 état 1.1.
- AGENTS.md invariant 6 : tier retiré.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`solarinverter` étend `smartmeterproducer` dans libnymea. Un compteur de production posé sur un
onduleur non communicant n'est que `smartmeterproducer` — c'est le PV du banc (sdm120Producer) :
le filtre précédent y rendait `pv.currentW: null` alors que l'EnergyManager additionnait bien
sa production. Même critère que la présence d'un producteur côté apprentissage.
- OPTIMIZER_PROTOCOL.md §5.1 : `pv.currentW` documenté ; un seul producteur déclaré par source
PV (nymea additionne les producteurs).
- EnergyManagerMock::setCurrentPowerProduction() (test) : la production du mock valait toujours 0.
- Test testPvIsReadFromAnyProducer : sans producteur → null ; smartmeterproducer seul → 2400.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Design §32 : grille, mesure à la minute, profil, qualité, tarif mural,
règle du créneau à cheval (moyenne pondérée) et ce qui reste pour e1.
Protocole 1.1, correction avant publication : price.source hphc → tou
(jamais émis), baseload.quality optionnel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Décision de Patrick du 2026-10-13. Le `corrections` du site reste
l'intersection (+ `ac-limit` si plafond) ; le détail se lit désormais en
donnée : `perField`, une entrée par plan sommé, `{id, corrections}`,
triée par id, présente seulement avec une courbe. Pas de booléen
« divergent ». Le journal garde son message.
- EnergyArbitrator::forecastSnapshot() : paramètre de sortie optionnel
`parPlan` (ForecastPlanLu) ;
- handler : `o:perField` déclaré et servi ;
- mock : classe `pvPlane` (état forecastCurveTransport) + addPvPlane() ;
premier test qui fait passer une COURBE par le RPC
(testTheSnapshotPublishesEachPlanesCorrections : divergence,
concordance, plafond, refus). Contre-épreuve : parPlan non rempli →
le test échoue (0 entrée au lieu de 2) ;
- OPTIMIZER_PROTOCOL §5.2, INTERFACE.md, design §30.2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Décision de Patrick du 2026-10-13. `fallback` avait été écrit sans
définition (protocole §5.2, design §28.5) et aucun émetteur ne l'a jamais
produit : correction avant publication, notée au changelog 1.1 du
protocole. Une prévision dégradée (température absente) se lit dans
`corrections`, pas dans `conversion`. Le champ reste pour des valeurs
futures.
- OPTIMIZER_PROTOCOL §5.2 et §0 (1.1) ;
- INTERFACE.md, GetForecastSnapshot ;
- design : notes datées en §28.5 et §29, nouveau §30.1 ;
- commentaires du handler (aucun changement de comportement).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remplacement verbatim par la v1.1 figée (aucune réécriture). Additif sur le
fil : prévisions explicites (forecast), groupes, domaine, besoins enrichis,
measurement {source, powerW}, validUntil, reasonCode/reasonParams, force
interdit sur le fil. Le kind `stage` et l'adaptateur `relay-stages`, jamais
livrés, sont retirés comme correction de brouillon.
Références vérifiées contre le dépôt ; les écarts (chemins inexacts,
porteurs absents) sont consignés dans le rapport du brief 1, pas corrigés
dans le texte figé.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ce document décrit le lien moteur ↔ optimiseur sur socket NDJSON. Il ne décrit pas la
frontière JSON-RPC de l'app, et un brief a pourtant envoyé l'agent app y chercher le
namespace de LoadConfig — qui n'y est pas et n'y sera pas. La confusion a coûté une
session : les deux protocoles portent du JSON-RPC, c'est leur seul point commun.
Note de périmètre en tête, avec le tableau des deux frontières et le renvoi vers
INTERFACE.md / nymeaenergyjsonhandler.cpp / JSONRPC.Introspect.
Relevé côté app, en corrigeant CLAUDE.md contre Introspect (plugin 1.15.2+etm15).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
writeRelay() jetait le ThingActionInfo* : aucun acquittement, aucun retour
arrière, available codé en dur à true, et m_currentStage mis à jour comme si tout
avait réussi — on annonçait une puissance non appliquée.
MODÈLE ASYNCHRONE. executeAction est asynchrone et update() ne doit jamais
attendre (AGENTS règle 5). « Attendre le résultat » ne veut donc pas dire bloquer
le cycle : l'écriture est émise, l'adaptateur retient combien d'acquittements il
attend (m_pending), le verdict tombe quand le compteur retombe à zéro, et la
conséquence est traitée au cycle suivant. Motif repris tel quel d'EvCharger
(evcharger.cpp:293-299), y compris `this` en contexte de connexion — si
l'adaptateur meurt, les callbacks sont coupés proprement.
INDÉTERMINATION. Pendant une transition, telemetry() annonce
max(m_stagePrev, m_stageTarget) : on ne sait pas ce qui est fermé, on annonce donc
la plus haute des deux puissances possibles. Même direction qu'ECS-411 (relais
injoignable supposé fermé) — ne jamais annoncer moins que ce qui peut être
appliqué. Sous-estimer fait sur-allouer les charges suivantes ; surestimer ne fait
que retarder une montée.
ÉCHELLE BORNÉE à trois barreaux, une tentative chacun, aucune boucle : cible →
retour arrière → arrêt total → défaut. Le retour arrière est asynchrone au même
titre et passe par le même compteur.
DÉFAUT COLLANT, pas clignotant. m_faulted est un verrou posé une seule fois, sans
délai ni expiration : available ne peut pas osciller d'un cycle à l'autre. Seul
NymeaEnergy.ClearLoadFault le lève — acte délibéré et journalisé de l'opérateur.
La reconstruction le lève aussi, mais par construction : un adaptateur neuf n'a
pas d'historique. À la levée, l'état matériel est RELU (ECS-411), pas supposé.
CANAL OUVERT. LoadContext n'avait AUCUN champ available : le publier aurait été
décoratif. Ajouté à LoadContextTelemetry, avec sa sémantique écrite noir sur
blanc — il gouverne l'allocation, PAS la comptabilité. Une charge en défaut ne
reçoit rien mais reste comptée : une puissance qu'on ne sait plus couper est de la
conso fixe, au même titre que la base de la maison. Le figeage est porté par
lockMin == lockMax == puissance crue engagée, jamais un plafond nul sous un
plancher non nul. Le même canal servira ECS-601.
OPTIMIZER_PROTOCOL.md mis à jour dans le MÊME lot, comme l'exige le §11 de la
spec : available, lockMinPowerW et lockMaxPowerW documentés avec leur sémantique.
clearFault() est PURE VIRTUELLE sur ILoadAdapter : les trois autres adaptateurs la
déclarent sans effet plutôt que d'hériter d'un défaut vide. Leur généralisation est
portée par ECS-414.
Test testEcsPartialFailure : cas nominal sans défaut, puis échelle complète via un
relais introuvable — défaut atteint, relais valide bien ramené à l'ouverture par la
tentative d'arrêt total, available faux, plancher == plafond == puissance comptée,
plus aucune commande une heure plus tard, puis levée délibérée.
Build amd64 0 erreur. Simulation : 16/16.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>