Ferme l'écart Q7 trouvé au lot protocole 1.2. Une requête GetPlan dont la ligne
dépasserait maxLineBytes n'est plus écrite sur le socket : OptimizerClient::requestPlan()
sérialise d'abord, rend l'identifiant, puis émet planFailed(id, "request-too-long") au
tour de boucle suivant. L'échec compte (règle des échecs consécutifs, §7), la connexion
est conservée, et le journal le dit une fois par connexion, avec la taille, jamais le
contenu. Avant : la ligne partait, l'optimiseur la jetait, et la requête finissait en
délai dépassé sans que la cause se voie.
Le handshake n'est pas contrôlé : sa ligne est bornée par construction (token de 4 096
octets au plus, 24 Kio une fois échappé).
- knownErrorCodes() : + request-too-long ; LastErrorCode du contrat régénéré
(--enums-from), blocs générés de la prose régénérés.
- OPTIMIZER_PROTOCOL.md §0/§2 et x-etm-transport : l'écart connu devient la règle tenue.
Pas de changement de version.
- Tests : testARequestOverTheLineLimitIsNeverSent remplace le XFAIL (aucun octet émis,
limite exacte éprouvée des deux côtés), testARequestTooLongIsLoggedOncePerConnection,
testARequestTooLongCountsAsAFailure (socketscheduler).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/OPTIMIZER_PROTOCOL.md passe en v1.2 (mineure) et écrit les décisions du
2026-10-21 à leur place :
- §2 : maxLineBytes (1 Mio, '\n' non compté) dans les deux sens ; lots
interdits dans les deux sens ; /run/powersync appartient au paquet de
l'optimiseur, le moteur ne le crée ni ne le suppose. Écart connu noté :
le moteur ne borne pas encore ce qu'il émet.
- §3 : la majeure se compare des deux côtés (-32602 puis fermeture) ; -32001
normé ; un token non exigé est accepté ; un handshake refusé est fermé des
deux côtés ; GetPlan avant handshake → -32600, connexion gardée.
- §7 : contexte GetPlan invalide → -32602 avec chemins et natures, jamais les
valeurs, compté comme un échec ; codes JSON-RPC 2.0 standard sinon ; table
des erreurs GÉNÉRÉE depuis le contrat.
- §8 : UpdateLearned à sens unique, -32601 avec id ; forme non normée en 1.2.
- §9, §0 (changelog, compatibilité 1.1), annexes A, E, G.
Contrat OpenRPC : methods[].errors complétés (x-etm-connection),
x-etm-errors-engine, x-etm-message-errors (nouveau), x-etm-transport
(maxLineBytesScope, batches, runtimeDirectory), règle handshake-refused.
La version (info.version, ProtocolVersion) régénérée depuis le code par
--enums-from.
Moteur : OptimizerProtocol::ProtocolVersion et OptimizerClient::kProtocolVersion
passent à "1.2" ; testHandshakeAndPlan vérifie aussi que les deux constantes
sont égales. Aucun autre comportement ne change.
Tests suivis : version attendue dans testoptimizerclient et
testsocketscheduler, handshake de la session de schema-check-test, et un
test du bloc « errors » dans gen-protocol-doc-test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quatre fonctions, une connexion de SONDE sur l'optimiseur (sans le moteur),
pour ce que le client du moteur n'envoie jamais :
- testGetPlanBeforeTheHandshakeIsRefused (Q3) : -32600 à son id, puis le
handshake sur la même connexion est accepté (connexion gardée) ;
- testARefusedHandshakeIsClosedByTheOptimizer (Q3, Q4) : autre majeure →
-32602 puis l'optimiseur ferme ; avec un token attendu, token faux →
-32001, fermé pareil ;
- testABatchIsRefused (Q8) : un lot → une erreur -32600 à id null, son
contenu non traité, connexion gardée ;
- testALineOverTheLimitIsRefused (Q7) : maxLineBytes + 1 → -32600 à id null,
une ligne d'exactement maxLineBytes est lue.
Le traitement d'un message se constate par une réponse normée sans contexte :
UpdateLearned avec id → -32601 (Q6). Réponses lues brutes : un lot renvoyé
se verrait. README : tableau, boucle de lancement, ce qui n'est pas vérifié.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Il s'annonce 1.2 et s'aligne sur les règles écrites par ce lot :
- Q3 : GetPlan avant un handshake réussi → -32600 (était -32002, hors norme),
connexion gardée ; Handshake d'une autre majeure → -32602 puis fermeture
(il ne comparait pas la majeure du moteur) ; token refusé → -32001, inchangé.
- Q2 : un timestamp illisible ou sans décalage n'est plus une abstention mais
-32602, error.data.problems = [{path, problem}], jamais la valeur reçue.
- Q7 : une ligne de plus de 1 Mio ('\n' non compté) reçoit -32600 à id null.
Correctif au passage : readline(LIGNE_MAX + 2) rendait le reste d'une
ligne trop longue comme des lignes suivantes, et comptait le '\n' (une
ligne d'exactement 1 Mio était refusée). La ligne est désormais lue jusqu'à
sa fin par morceaux, sans être gardée.
- Q8 : un lot reçoit -32600 à id null, aucun de ses messages n'est traité
(il était ignoré en silence).
- -32700 à id null pour une ligne non JSON (ignorée en silence avant).
- Q5 et Q6 étaient déjà tenus : token accepté sans --token-file,
UpdateLearned en notification ignorée, avec id → -32601.
README : tableau des erreurs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preuves, côté moteur, des points Q4, Q7 et Q8 du protocole 1.2. Aucun code du
moteur n'est modifié.
- testARefusedHandshakeIsClosedByTheEngine (Q4) : -32001 ou -32602 au
handshake, faux optimiseur qui garde son socket ouvert → c'est le moteur qui
ferme (handshake-refused), nouvelle tentative au backoff seulement.
- testAReceivedBatchIsNeverUnpacked (Q8, reçu) : un lot portant la réponse
attendue est une ligne écartée (invalid-json), connexion conservée, réponse
non lue ; la requête finit en délai dépassé.
- testEachLineSentIsOneObject (Q8, émis) : lu sur les octets, chaque ligne
émise est UN objet JSON, une requête remplacée donne une ligne de plus.
- testAContextOverTheLineLimitIsStillSent (Q7, émis) : ÉCART constaté, non
corrigé dans ce lot — requestPlan() ne borne pas ce qu'il émet ; un GetPlan
de plus de 1 Mio part tel quel. QEXPECT_FAIL : le test passera en XPASS le
jour où le moteur bornera l'émission.
Q7 en réception était déjà éprouvé par testLineLengthLimit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
etm-powersync-gen-protocol-doc sait rendre un bloc `errors` : une table des
methods[].errors puis de x-etm-message-errors (code, méthode, message,
connexion après l'erreur, quand), suivie de ce que le moteur fait d'une
réponse error (x-etm-errors-engine). Aucune prose n'utilise encore le bloc :
--check reste vert sur l'état commité.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moteur de cd7cd1a contre l'ancienne référence (61db6cf), comparateur
--sous-palier, lignes du nouveau test retirées : IDENTIQUE HORS MOTIFS ET HORS
MOTIF ÉCO SOUS PALIER — 23 792 lignes, 23 784 identiques à l'octet, 8 qui ne
diffèrent que par ECO_FLOOR_GRID → ECO_FLOOR_BELOW_STAGE (niveau éco, et
decision quand l'éco est en tête). purchasedWh identique, sans --achat.
Aucune consigne ne change. S'y ajoutent les 4 lignes de
testEcoBelowTheFirstStageBuysNothing : 69 fonctions, 23 796 lignes.
check : IDENTIQUE, 0 rejeu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retire le motif des niveaux « eco » portant l'un des deux codes et les
confronte deux à deux : tout autre passage, ou un ECO_FLOOR_GRID aux
paramètres modifiés, est un ÉCART. targetW, counts, progress du niveau restent
comparés. Les changements sont listés par scénario et par charge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prose §6 seulement : la liste blanche et le contrat OpenRPC ne changent pas.
L'optimiseur ne reçoit pas les paliers (powerLevels non émis, §5.3) ; seul le
moteur sait qu'une consigne tombe sous le premier palier.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sur une charge à paliers (powerLevels déclarés : relay-router, etmvariableload
fixed), une cadence éco sous le premier palier laisse le mécanisme au palier 0.
Le niveau éco publiait pourtant ECO_FLOOR_GRID « 83 W achetés au réseau ».
Il porte désormais ECO_FLOOR_BELOW_STAGE {targetW, stageW, gridW: 0,
o:remainingWh, o:remainingS}. Le premier palier est declared.powerLevels[1],
la source de BELOW_MIN_POWER en passe confort. Une charge continue garde
ECO_FLOOR_GRID.
Aucune consigne ne change : même targetW, mêmes counts, même draw, même
palier, même purchasedWh. Seul le motif du niveau change, et avec lui (R7,
inchangée) decision, decision.level et les lignes [Arbitre]/[RelayRouter].
Hors liste blanche optimiseur : l'optimiseur ne reçoit pas les paliers (E9).
Tests : testEcoBelowTheFirstStageBuysNothing (neuf, échoue sur 61db6cf),
testTheDecisionDescribesTheMergedAction mis à jour (il exigeait l'achat sur
un relais de 1000 W à 83 W), rendu FR et refus optimiseur en loadmodel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
testNymeaEnergyPermissionScopes compare le permissionScope PUBLIÉ par JSONRPC.Introspect (la
table que nymea consulte) à une table écrite dans le test : méthode ajoutée, retirée ou droit
changé → échec, avec tous les écarts listés.
testNymeaEnergyPermissionsEnforced crée de vrais comptes sur le MockTcpServer du harnais, dont
l'authentification est active (Hello : authenticationRequired vérifié) :
- sans droit : GetLoadTelemetry, GetTariff, GetBatteryLevelConsideration répondent ;
SetTariff {zones: []} et SetLoadDerogation → « Permission denied. », tarif inchangé ;
- ControlThings : SetLoadDerogation posée puis levée ; SetLoadConfig refusé, config inchangée ;
- ConfigureThings : SetTariff passe ; SetPhasePowerLimit et GetOptimizerTrace refusés.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chaque méthode déclare son droit au registerMethod ; plus aucune ne tombe en Admin par la
valeur par défaut de libnymea.
- None (tout compte authentifié) : tous les Get* sauf GetOptimizerTrace, dont
GetBatteryLevelConsideration (Admin par oubli jusqu'ici). GetOptimizerStatus ne porte ni
l'endpoint ni le token.
- ControlThings : SetChargingInfo, SetLoadDerogation, ClearLoadDerogation, ClearLoadFault,
SetLockOnUnplug.
- ConfigureThings : SetLoadConfig, SetTariff, SetForecastSite, SetAcquisitionTolerance,
SetSpotMarketConfiguration.
- Admin, désormais écrit : SetPhasePowerLimit, SetBatteryLevelConsideration,
SetOptimizerEndpoint, SetOptimizerToken, GetOptimizerTrace, SetOptimizerTrace.
Un registerMethod privé SANS valeur par défaut masque celui de JsonHandler : oublier le droit
ne compile plus. C'est la seule garde possible pour une méthode Admin, le défaut étant Admin.
Changement de contrat RPC (JSONRPC.Introspect publie permissionScope). Les droits ne mordent
que sur un transport avec authentification.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
S'abstenir est une réponse permise (§7). Avec ETM_POWERSYNC_CONFORMANCE_EXPECT=abstain,
testEveryPlanIsAccepted exige une abstention à chaque cycle (rules, fallbackCode abstain,
verdict abstention/abstain, aucun échec, optimizerAlive inchangé) ; testTheSafetyLoopStaysOnTop
vérifie L4 sous le rule-based (shed exigé) puis se termine en QSKIP, rien à écrêter. Mode `plan`
(défaut) inchangé. Le mode retenu est affiché en tête de chaque fonction.
Pas de déduction depuis le handshake : `planning` est informatif (multi-créneaux ou non) et
`kinds` absent veut dire « tout admis » ; la référence en mode abstain annonce des kinds, le mode
protocol-1.0 (planning false, sans capabilities) planifie. README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les 16 lignes de testTracesReplayWithZeroDivergence : chaque requête, et
chaque réponse de l'optimiseur rejugée sur sa requête (--trace --semantic).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lance l'optimiseur de $ETM_POWERSYNC_OPTIMIZER_CMD (défaut : la référence,
mode normal) et y branche le vrai moteur : handshake direct accepté, GetPlan
répondus sous 10 s, session entière conforme au contrat embarqué (règles
sémantiques comprises), chaque plan appliqué sans fallbackCode, consigne hors
bornes écrêtée et surcharge délestée par L4. Relais transparent pour capturer
la session. README : la lancer contre un autre optimiseur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Tableaux de messages (champs, types, obligatoire, valeurs, sens), règles
sémantiques et codes de motif, entre balises BEGIN/END GENERATED de
OPTIMIZER_PROTOCOL.md. --check (code 1 si la prose ou la forme canonique du
contrat n'est pas à jour) branché en 5/5 de tools/ci-quality.sh. --enums-from
pose les listes fermées du contrat depuis le dump du code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
optimizerclient : chaque Handshake envoyé ; socketscheduler : tout ce que le
moteur envoie au faux optimiseur, plus testTheEngineSpeaksTheContractItServes
(GetOptimizerProtocol == embarqué == docs, requêtes jugées avec le contrat
servi) ; refoptimizer : session capturée par la référence, requêtes toujours,
session entière et règles sémantiques en normal et en 1.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contrat embarqué identique à docs/ ; listes x-etm-generated égales au code
(ancre côté test, dump pour régénérer) ; chaque code de verdict, d'erreur et
de repli lu dans les SOURCES figure dans les listes ; règles sémantiques ==
verdicts du validateur, même ordre ; sérialiseur GetPlan jugé conforme sur
toutes ses branches par le validateur publié.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le contrat OpenRPC est une ressource Qt du plugin (jamais lu sur disque à
l'exécution) ; GetOptimizerProtocol rend { protocolVersion, schema }, lecture,
PermissionScopeControlThings, aucun secret, sans *Changed (statique pour un
binaire). Documenté dans INTERFACE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OptimizerPlan::kinds() (désormais appliquée par convertirAction) et
verdictCodes(), dans l'ordre des contrôles ; SocketScheduler::knownTriggers()
(appliquée par rangDeclencheur), knownFallbackCodes(), knownLastErrorCodes() ;
OptimizerClient::knownErrorCodes(). Le contrat OpenRPC en régénère ses
énumérations. Aucun comportement changé : mêmes listes, mêmes ordres.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python, bibliothèque standard seule. Sous-ensemble de JSON Schema déclaré ;
un mot-clé non géré est une erreur de schéma, jamais ignoré. Valide un
message, une session NDJSON ou des traces ; --semantic applique
x-etm-semantic-rules dans l'ordre du validateur du moteur (une règle sans
implantation, ou l'inverse, est une erreur).
Test : références du sérialiseur et traces réelles du moteur conformes ;
optimiseur de référence mode par mode, et qui rejette chaque mode mauvais.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--capture écrit chaque message reçu et envoyé (NDJSON), token remplacé par
autant d'astérisques. wrong-type rend un planId numérique, que le moteur
accepte et que le contrat refuse : contre-épreuve de la conformité.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/etm-powersync-optimizer-protocol.openrpc.json décrit Handshake, GetPlan
(SurplusContext complet : en-tête, état, forecast, loads[] par mécanisme) et
UpdateLearned, relus champ par champ sur le sérialiseur, le client et le
validateur du moteur. Requêtes du moteur : additionalProperties false.
Réponses d'optimiseur : ouvertes (§9). Transport, sens, heartbeat, machine §7,
règles sémantiques du validateur et tolérances : extensions x-etm-…
Les listes fermées marquées x-etm-generated seront régénérées depuis le code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Doxygen : 1 avertissement introduit par 5768c36 (paramètre non documenté) ;
0 après. Commentaire seul.
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>
Constat `fallbackCode` de T7 (E-T7-6) : une abstention, un plan expiré ou
refusé à la réception vidaient le cache, et le cycle suivant repliait avec
`no-plan` — l'app ne distinguait pas « l'optimiseur s'abstient » de « rien
reçu ».
Le code du verdict de réception d'une réponse non appliquée est gardé comme
code de repli jusqu'à l'issue de la requête suivante ; un plan valide
l'efface, un échec de requête ou une connexion perdue le ramènent à
`no-plan`, qui ne dit plus que « rien reçu ». Publication seule : aucune
décision ne change.
INTERFACE.md : liste fermée des valeurs de fallbackCode et leur lecture
(« pas d'optimiseur », « pas encore de plan », « l'optimiseur s'abstient »,
« plan refusé »). Tests : refoptimizer lit GetPlan.fallbackCode après chaque
cycle de chaque mode (abstain, expired, invalid, unknown-load, missing-load…,
puis retour normal sans fallbackCode) ; socketscheduler vérifie le code publié
pour chaque refus (les tests qui épinglaient `no-plan` sont mis à jour).
Contre-épreuve M3 (toujours `no-plan`) : mord.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Constat `drop` de T7 : SocketScheduler notait ConnexionPerdue à CHAQUE état
non connecté du client (Connecting, Waiting d'une tentative), réécrivait
lastErrorCode avec celui du client et émettait etatChange() à chaque
tentative au backoff (une par minute au plafond).
La perte se note désormais à la transition : sortie de Ready ou de
Handshaking (fermeture, handshake refusé), ou premier échec d'un client qui
n'a jamais été connecté (sinon un optimiseur absent dès le départ
n'aurait jamais de lastErrorCode). Les tentatives suivantes d'une panne déjà
notée ne touchent ni au dernier code, ni aux échecs, ni à l'état publié.
Publication seule : aucune décision ne change.
Test socketscheduler::testAReconnectionAttemptIsNotANewLoss : optimiseur
disparu, 3 tentatives au backoff → 1 seul etatChange, 1 seule notification,
lastErrorCode `connection-lost` (le client en est à `connect-failed`) ; au
retour, les transitions du §7 et rien d'autre. Ligne `drop` de refoptimizer :
le dernier code reste `disconnected`. Contre-épreuve M2 (ancien comportement) :
mord sur les deux.
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>
La contre-épreuve E4 (L2 ne suspend plus la planification) ne mordait que par la ligne de
trace : le plan en cache valait déjà 0 avant L2. Le test ramène l'optimiseur à 2000 / 3000 W
et PAC forcée juste avant la coupure du compteur ; E4 mord désormais sur les consignes de repli.
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>
Chaque test lance tools/etm-powersync-ref-optimizer en sous-processus (démarrage borné,
échec en clair, jamais bloqué).
- identité Q6 (banc restreint, passe confort seule) sur unix:// et tcp:// : ce que les mocks
reçoivent au cycle i sous l'optimiseur == au cycle i-1 sous le rule-based seul, 36 cycles ;
- chaque mode de défaut → la ligne du tableau §7, sur le banc complet, et le retour ;
notifications une par transition ; slow : update() ≤ 1 ms ;
- optimiseur 1.0 accepté ; traces rejouées sur un nouvel optimiseur : 0 divergence, puis
mode deviate : divergence, code 1 ; L4 et L2 gagnent ; token recopié : nulle part ;
- catalogue des codes émissibles identique à celui du moteur.
8 fonctions, 33 / 0, 54 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le plan du rule-based tracé porte la puissance ESTIMÉE d'une action state ; un optimiseur ne
la donne pas (§6 ne la définit pas). Le rejeu d'une trace rule-based contre l'optimiseur de
référence voyait donc une divergence sur chaque PAC. Seuls l'état (state) et la puissance
d'une consigne (setpoint) sont comparés. Test unitaire ajouté (7 / 0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python 3, bibliothèque standard seule, GPL-3.0. Socket unix par défaut
(unix:///run/powersync/optimizer.sock, 0660), tcp:// accepté. Handshake (majeure, capacités,
token lu dans un fichier et relu à chaque handshake, -32001 puis fermeture s'il diffère),
GetPlan, abstention, plusieurs clients à la fois (un fil par connexion, plan fonction pure de
la requête).
Plan à un créneau qui reproduit la passe confort du waterfall rule-based depuis le seul
contexte GetPlan : tri (rang, id), surplus net signé, recrédit, réserve de stockage, plancher,
plafond, verrous, mapping SG-Ready avec hystérésis. Codes du catalogue émissible seulement,
clés documentées seulement (--list-codes, confronté au moteur par le test).
Modes d'injection : normal, abstain, slow, invalid, expired, wrong-major, drop, unknown-load,
forbidden-kind, missing-load, echo-token, protocol-1.0, deviate ; --mode-file pour changer de
mode sans reconnexion. README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Constat de T6 (question 2) : la trace masquait un token recopié dans reason ou planId, mais
les lignes [Optimiseur] (planId, texte, détail des refus, nom du handshake) et
GetOptimizerStatus.name le portaient en clair.
- OptimizerSecret : UNE fonction de masquage (texte brut et échappé JSON, objet JSON à toute
profondeur, clés comprises ; ligne de trace sérialisée), partagée par le client et les traces.
- OptimizerClient masque chaque message reçu dès son analyse, avec le token envoyé sur cette
connexion : rien en aval (journal, traces, app) ne voit le secret ; dit une fois.
- Test testAnEchoedTokenLeaksNowhere : secret avec " et \ recopié partout ; aucune ligne de
journal (tous niveaux), aucune trace, aucune réponse RPC. 4 contre-épreuves mordent.
decisions sans endpoint : IDENTIQUE (23 792 lignes), 0 rejeu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contre-épreuve T4 : sans le contrôle d'identifiant du traceur, aucun test ne cassait — le
SocketScheduler écarte déjà les réponses obsolètes en amont. Ce test unitaire éprouve la
seconde garde seule (autre requête, aucune requête, réponse en double). socketscheduler :
34 fonctions, 117 / 0.
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>
Python 3, bibliothèque standard seule. unix:// par défaut (unix:///run/powersync/optimizer.sock),
tcp:// accepté ; token lu dans un fichier (--token-file), jamais en argument ni affiché.
Réponses écrites à côté de la trace, jamais d'écrasement ; résumé : requêtes, taux
d'abstention, computeMs médian et max, cycles non comparables, cycles dont les actions du
créneau courant diffèrent du plan enregistré. --fail-on-divergence : code 1.
Test (unittest) contre un serveur Unix minimal qui renvoie les réponses enregistrées (ce n'est
pas l'optimiseur de référence) : trace synthétique et trois traces réelles du moteur → zéro
divergence ; réponse modifiée → divergence vue, code 1 ; tcp://, refus d'écraser, endpoint
absent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Réglage optimizerTrace (energy.conf Optimizer/trace, …RetentionDays, …MaxBytesPerDay), défaut
DÉSACTIVÉ : ni objet, ni fil, ni fichier. NymeaEnergy.GetOptimizerTrace / SetOptimizerTrace,
PermissionScopeAdmin comme SetOptimizerEndpoint, type contrôlé par le moteur, appliqué à chaud.
Une ligne par cycle — rule-based, optimiseur, et L2 (strategy none, sans requête) : t (heure
locale), request (params GetPlan envoyés, ou qui l'auraient été), response (ce qui a répondu
à CETTE requête : result brut, error.code, no-response ; sinon le plan du rule-based en codes),
executed (appliqué, clamped, shed), strategy, latencyMs. La ligne N attend la réponse à la
requête N (elle sert au cycle N+1).
Toutes les E/S sur un fil dédié (OptimizerTraceWriter) : rotation à minuit heure locale, purge
au-delà de la rétention (14 j), plafond par fichier journalier (64 Mio, mesuré : 5 Ko/ligne
rule-based, 39 Ko sous un optimiseur à 48 créneaux, 1 cycle/min), file bornée à 256 lignes.
Échec d'écriture et plafond dits une fois. update() ne fait que poster. Le token n'est jamais
dans une ligne ; recopié par une réponse d'optimiseur, il est masqué.
Preuves : decisions IDENTIQUE traces désactivées ET activées (21 132 lignes de trace écrites,
ETM_DECISIONS_TRACES=1). socketscheduler 33 fonctions, 114/0 (+10 fonctions T6).
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>
Sous Qt 6, le validateur nymea ne contrôle pas le type des paramètres de base : la
déclaration Variant ne protégeait de rien (contre-épreuve inerte), le contrôle du handler
est la seule garde. Retour à String, plus lisible à l'introspection ; INTERFACE.md corrigé
(seule une clé EN TROP fait recopier les paramètres au journal par nymea).
testGetPlanShowsTheAppliedPowerUnderALock : sous le rule-based, un verrou minOn tient le
relais ; GetPlan publie l'appliqué. Avec l'écrêtage amont retiré, c'est la relecture de
l'appliqué qui protège seule (double mutation, elle mord).
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>
- OptimizerStatus::Vivacite : optimizerAlive, échecs comptés PAR REQUÊTE (E17), deux
consécutifs = mort, socket fermé = mort, retour au premier plan valide ; une abstention
rompt la série sans ressusciter. strategieActive() : none sous L2, prioritaire sur tout ;
rules dès qu'un optimiseur n'est plus vivant, sans attendre le cycle suivant.
- SocketScheduler : branchement de la vivacité, cache abandonné à la mort, UpdateLearned
journalisé une fois et ignoré.
- NymeaEnergy.GetOptimizerStatus + OptimizerStatusChanged (une notification par
transition, charge utile déclarée une fois) ; champs installateur absents s'ils sont
inconnus ; jamais le token, tokenConfigured seulement. Pas de tier (Q5).
- SetOptimizerEndpoint (persisté, appliqué à chaud) et SetOptimizerToken (écriture seule,
fichier 0600, Q2), tous deux PermissionScopeAdmin ; token déclaré Variant pour qu'un
mauvais type ne soit pas recopié au journal par le validateur nymea.
- NymeaEnergy.GetPlan : plan en vigueur (appliqué au créneau courant, demandé ensuite),
codes et paramètres filtrés seulement, inForce toujours présent, fallbackCode quand un
optimiseur configuré n'a pas commandé.
- Tests : tableau §7 ligne par ligne, transitions notifiées une fois, RPC, token absent de
tout journal du moteur et de toute réponse.
Sans endpoint : decisions IDENTIQUE (23 792 lignes), suites inchangées, journal >= info
inchangé (8 739 lignes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moteur de 6481515 avec les scénarios de ff7c801, contre l'ancienne référence :
IDENTIQUE HORS MOTIFS ET HORS ACHAT (comparateur --achat) — 23 772 lignes, 23 765 identiques à
l'octet, 7 qui ne diffèrent que par progress.purchasedWh, toutes « 0 → absent » (régime
unmeasurable). Aucune consigne ne change. S'y ajoutent les 20 lignes du nouveau
testPurchasedIsCountedOnTheMeasuredEnergy : 68 fonctions, 23 792 lignes. check : IDENTIQUE, 0 rejeu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le correctif c change une VALEUR PUBLIÉE (progress.purchasedWh), pas une consigne. --achat
retire en plus ce champ de chaque niveau et dit, par scénario et charge, l'ancienne et la
nouvelle valeur (ou « absent ») ; --lignes écrit chaque ligne qui change. Sans option, le
comportement d'etm70 est inchangé.
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>