appendConfig() ne borne que l'entrée ajoutée, mais validateSet() (THING_SHARED) et
validateRanks() (RANK_DUPLICATE) citaient en entier les libellés des deux entrées en
conflit — dont une héritée, sans borne. provisionEvChargerConfig() journalise cette phrase.
Les libellés y passent par libellePourMotif() : 64 caractères au plus (leftChars, jamais au
milieu d'une paire), suivis de « … ». Le motif publié (faultParams) ne change pas.
Test : testInheritedLabelIsCutInSetRefusals — héritée de 300 caractères ; l'ajout d'une borne
refusé sur THING_SHARED puis sur RANK_DUPLICATE : motif exact inchangé, phrase avec les 64
premiers caractères et sans le libellé entier.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Même défaut que I-1, dans les avertissements de chargement ajoutés par Q7 : l'identifiant hors
règle et l'échéance illisible étaient cités en noquote(). Avant Q7, un client pouvait les
persister avec un saut de ligne (ni l'id ni l'échéance n'étaient bornés), et la lecture du
fichier fabriquait alors une fausse ligne de journal.
Ces valeurs passent par citee() : entre guillemets, échappées par QDebug (même rendu
qu'avant pour une valeur ordinaire). L'avertissement LIBELLÉ TROP LONG, qui cite l'identifiant
dans sa phrase, perd son noquote().
Test : testInheritedValuesAreEscapedInTheJournal charge un fichier dont l'id et l'échéance
valent "x\n[NymeaEnergy] FAUX" : les trois avertissements partent, citent la valeur échappée,
et aucun message n'ajoute de ligne commençant par [NymeaEnergy].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le refus écrivait, en noquote(), la phrase détaillée du refus : elle cite mode, domaine,
rankOrigin, adaptateur, relays[].thingId, échéance et libellé tels que saisis. Un saut de ligne
dans l'un d'eux fabriquait une fausse ligne de journal (etm72 l'échappait par `<< why`).
Le journal ne porte plus que la phrase rendue depuis le motif (renderFr), comme SetTariff :
cause et endroit, un identifiant seulement s'il est bien formé. Échapper le détail aurait
suffi contre l'injection, pas contre le volume : relays[].thingId n'est pas borné.
La cause système d'un WRITE_FAILED, que seul le détail portait, est désormais écrite par
LoadConfigStore::commit() dans une ligne à part (chemin et erreur du fichier, aucune saisie).
Test : testSetLoadConfigRefusalIsPublished envoie dix refus dont un champ vaut
"x\n[NymeaEnergy] FAUX" et vérifie, message par message (JournalCapture::messages()), que le
refus est journalisé et qu'aucun message n'ajoute de ligne commençant par [NymeaEnergy].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les 69 fonctions existantes rendent un enregistrement identique, octet pour
octet, à la référence précédente (inchangée depuis fae7101). Les seules
lignes ajoutées sont celles de testSetLoadConfigRefusalIsPublished et de
testAutoProvisionedLabelIsCutNeverRefused. Après régénération : check
IDENTIQUE (71 fonctions, 23 728 cycles), traces activées comprises.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Avec les traces actives, la ligne d'un cycle recopiait la requête GetPlan
même quand le client ne l'avait pas écrite (ligne de plus de 1 Mio,
request-too-long) : environ 1 Mio par cycle jusqu'au plafond journalier,
pour une requête que personne n'a reçue, et que le rejeu aurait renvoyée.
- La ligne porte alors `requestBytes` (taille de la ligne, octets) à la
place de `request` ; sa réponse reste l'erreur request-too-long. Même
règle pour la requête qui SERAIT partie sans optimiseur.
- La décision « part ou ne part pas » est celle du client :
OptimizerClient::getPlanLine(), extraite de requestPlan() sans changer
son comportement, est lue par la trace — pas une seconde estimation.
- Le commentaire « la requête RÉELLEMENT envoyée » est corrigé, la doc de
la ligne de trace (optimizertrace.h) aussi.
Preuve : testARequestNotSentIsTracedBySizeOnly (avec et sans optimiseur ;
contre-épreuve : la requête envoyée est recopiée).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
testInheritedOutOfRuleConfigWhatGetPlanSends : une configuration écrite sur
disque avant la règle (identifiant « ballon test », échéances « 07:00 » et
« 7:00 ») est chargée par un store neuf et signalée. Lu sur le faux
optimiseur : l'identifiant hors règle part tel quel ; l'échéance lisible part
avec ses energyTargets (contre-épreuve) ; l'échéance illisible ne part ni
brute ni résolue, minEnergyWhPerDay part encore. La requête est jugée
conforme au contrat embarqué (LoadNeeds.dailyDeadline impose
^[0-9]{2}:[0-9]{2}$, que « 7:00 » violait).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Écart « important » de la relecture d'etm72. provisionEvChargerConfig()
prenait le nom du Thing pour libellé et réécrivait l'ensemble complet : un
nom de plus de 256 caractères, ou un libellé hérité trop long sur une AUTRE
entrée, refusait l'ajout, et la borne restait arbitrée sans rang réglable.
- Le libellé est le nom coupé à 256 points de code (LoadConfigStore::
leftChars), jamais au milieu d'une paire de substitution.
- L'ajout passe par appendConfig() : les bornes de saisie ne portent que
sur l'entrée apportée. L'écriture cliente, elle, reste refusée tant que
l'hérité n'est pas raccourci (règle d'etm72 inchangée).
- Le journal nomme la borne par ce libellé coupé.
Preuve : testAutoProvisionedLabelIsCutNeverRefused (nom de 300 caractères,
emoji en 256e position, hérité de 300 caractères sur une autre entrée).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
testSetLoadConfigRefusalIsPublished : l'introspection déclare o:fault et
o:faultParams à côté d'energyError ; un succès rend toujours energyError
seul ; chacune des vingt causes atteignables rend EnergyErrorInvalidParameter,
fault LOAD_CONFIG_REFUSED et les faultParams attendus, sans rien changer à
GetLoadConfig ni au fichier, octet pour octet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le refus garde EnergyErrorInvalidParameter et gagne, comme SetTariff, un
`fault` LOAD_CONFIG_REFUSED et des `faultParams` : une cause par motif
existant (ID, LABEL, DEADLINE, MODE, DOMAIN, RANK_ORIGIN, RANK_ORIGIN_AUTO,
ADAPTER, FIELD_*, SG_READY_STATE_2, ID_DUPLICATE, THING_SHARED,
RANK_DUPLICATE, WRITE_FAILED, STORE_UNAVAILABLE) et l'endroit (index, id
quand il est bien formé, field, state, indexes/ids, rank, length/max,
position). Jamais le libellé. Les deux champs sont déclarés optionnels dans
le schéma ; un succès rend exactement ce qu'il rendait.
- id : 1 à 128 caractères de [A-Za-z0-9._{}-], refusé à l'écriture ;
hérité hors règle, chargé tel quel et signalé.
- needs.dailyDeadline : vide ou HH:MM (00:00-23:59), refusé à l'écriture ;
hérité illisible, l'entrée est chargée, l'échéance retirée de la mémoire
(ni l'arbitre ni l'optimiseur ne la reçoivent) et signalée.
- Les bornes de saisie passent avant la réconciliation de rankOrigin, dont le
motif cite le libellé.
- appendConfig() : l'ajout par le moteur ne borne que l'entrée ajoutée.
- leftChars() coupe en caractères sans couper une paire de substitution ;
l'id d'un motif n'est plus coupé par QString::left().
- Au chargement, chaque libellé trop long est signalé, pas seulement le
premier ; loadconfigstore.h dit « n'écarte rien au chargement ».
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
testTheSessionMatchesTheContractOffUtc : le moteur sous Europe/Berlin,
cadencé en heure locale comme en production, du 2026-10-25 02:58 CEST au
retour à 02:00 CET. Aucun échec de requête, et la session passe
tools/etm-powersync-schema-check --semantic contre le contrat embarqué.
Contre-épreuve, correctif retiré : 8 rpc-error (le constat VM), et le
validateur seul refuse chaque params.timestamp sur son motif `pattern`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Constat VM du 2026-10-21 (Europe/Berlin) : l'optimiseur conforme refusait
chaque GetPlan, -32602 sur params.timestamp. `instant()` écrivait
toString(Qt::ISODate), qui n'émet aucun décalage pour une heure locale —
celle de currentDateTime(), donc de ctx.timestamp en production. Les
goldens, construits en UTC ou à décalage fixe, ne pouvaient pas le voir.
Le décalage de l'instant reçu est désormais rendu explicite : une heure
locale garde l'heure du site (+02:00), une heure UTC reste en Z. Il est lu
sur l'instant, donc 02:30 la nuit du retour à l'heure d'hiver sort +02:00
puis +01:00. Goldens inchangés.
Tests (loadmodel) : cinq fuseaux dont un négatif et un à la demi-heure,
été et hiver, tous les instants du contexte ; la nuit des deux changements
d'heure, minute par minute. Vérifiés échouant sans le correctif.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le libellé part tel quel dans chaque GetPlan. Rien ne le bornait : une saisie de plus de
1 Mio suffisait à faire dépasser la limite de ligne du protocole. La borne arrête la
saisie en amont ; le refus d'émission du client (commit précédent) reste le filet.
- LoadConfigStore::validateLabels(), appelée en tête de setConfigs() : un libellé de plus
de 256 points de code Unicode → refus en bloc, rien n'est modifié ; SetLoadConfig rend
EnergyErrorInvalidParameter et journalise le motif en français, qui nomme la charge par
son id et donne la longueur, jamais le libellé. Placée AVANT isValid() parce que les
motifs suivants citent le libellé.
- Même partage que les rangs (LM-1209-a-bis) : load() ne l'applique pas. Un libellé plus
long déjà persisté est chargé tel quel, ni tronqué ni écarté, et signalé ; la première
écriture devra le raccourcir.
- Test : testLabelIsBoundedOnWriteNeverOnLoad (256 accepté, 257 refusé sans modification
en mémoire ni sur disque, configuration héritée conservée entière).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>