4 Commits

Author SHA1 Message Date
Patrick Schurig
4a0dfa273f docs(prévision PV): la clé de révision — déduplication, et receivedAt s'appelle receivedAt
Retenu : déduplication sur le contenu, pas de requête meta.json. Mais le coût
n'est pas un décalage de cycle — c'est un HORIZON FAUX DE PLUSIEURS HEURES.

Entre l'initialisation d'un run et sa mise à disposition il s'écoule la latence
de publication du modèle : des heures pour un modèle global. Donc
targetAt − receivedAt n'est pas l'horizon, il est trop court de toute cette
latence, systématiquement et dans le même sens. Une prévision qu'on croirait à
H−1 peut être le run de H−5.

Ce que la déduplication sauve : les BORNES des révisions — combien, et laquelle
s'applique. Ce qu'elle ne sauve pas : leur ÂGE. « Quelle prévision » est juste,
« quel horizon » est faux. Sans effet sur ce lot qui AFFICHE ; déterminant pour
l'apprentissage, dont l'objet est justement de corréler l'erreur à l'horizon.

Trois conditions :

1. La colonne s'appelle receivedAt, JAMAIS issuedAt. Un nom qui promet l'heure
   du run fabrique le calcul faux tout seul : quelqu'un écrira
   targetAt - issuedAt et obtiendra un nombre plausible. Et il faut l'écrire :
   aucun horizon ne se calcule depuis receivedAt.

2. La déduplication compare les points COMMUNS, pas la charge utile. Si la
   fenêtre glisse, un == naïf verrait une révision toutes les heures —
   exactement ce que la déduplication existe pour éviter. Je code la comparaison
   sur l'intersection dans les deux cas : elle est correcte pour les deux et ne
   dépend pas d'un comportement du voisin qui pourrait changer sans préavis.

3. La dette meta.json se rouvre AVEC son consommateur. La décision reposait sur
   « aucun consommateur ne le demande » et elle RESTE juste : un affichage n'a
   pas besoin de l'âge du run. Le jour où quelque chose corrèle l'erreur à
   l'horizon, meta.json devient nécessaire — et la rouvrir maintenant au nom
   d'un besoin supposé serait payer une requête par cycle pour un calcul que
   personne ne fait.

Volume confirmé : 24-48 points, 4-8 révisions/jour ⇒ ~200-400 lignes/jour au
lieu des 1150 estimées sur révision horaire.

Et la dette forecastModel : la colonne model reste NULLE tant que la correction
n'est pas livrée. Un NULL qui dit « on ne sait pas » est honnête ; une valeur
qui dit « ce qu'on avait demandé » est fausse d'une façon parfaitement crédible.
Aucun remplissage rétroactif.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-09-08 17:02:46 +02:00
Patrick Schurig
002ecc639b docs(prévision PV): le canal — trois grandeurs mesurées, et le précédent ne couvre pas ce lot
Découpage tranché : ems n'appelle pas, l'invariant 10 reste. La distinction
retenue est plus fine que « zéro réseau » — une intégration qui lit une API perd
UNE ENTRÉE quand le lien tombe ; un experience-plugin qui appelle met LA LOGIQUE
DE DÉCISION sous dépendance distante. L'attribution CC BY 4.0 disparaît avec le
découpage au lieu d'être gérée.

LE PRÉCÉDENT CITÉ NE COUVRE PAS CE LOT. awattar/tempo/spothinta sont des
précédents d'intégrations qui APPELLENT. Le plugin awattar amont n'expose que
des SCALAIRES — currentMarketPrice, average, lowest, highest, rank, validUntil.
La série ne traverse jamais : l'amont a résolu le problème en appelant DEUX
FOIS. Il n'existe donc aucun canal éprouvé pour une série, parce que personne
n'en a construit.

ET LE MOTEUR APPELLE DÉJÀ : spotmarketdataproviderawattar.cpp, dans CE dépôt,
sort en HTTPS vers api.awattar.at et alimente planSpotMarketCharging() — une
DÉCISION. C'est ce que l'invariant 10 interdit, en service, hérité et jamais
choisi. Un invariant qui a une exception non déclarée n'est plus un invariant,
c'est une habitude. À trancher séparément.

Les trois grandeurs, sur le code et non la doc :
- taille : plafond dur à 1 MiB (jsonrpcserverimplementation.cpp:554, connexion
  larguée au-delà). Une courbe 48 h fait 2-5 ko — la taille n'est pas le sujet ;
- rediffusion : PAR RÉVISION, pas par cycle. thing.cpp dédoublonne
  (if (oldValue == newValue) return). La prémisse surestimait d'un facteur 60 ;
- nommage : forecastPvCurve, et le mot dit que c'est un canal.

Les deux vrais dangers ne sont pas la taille : « cached » réécrirait things.conf
toutes les heures sur une carte SD (cached: false obligatoire), et la
JOURNALISATION D'ÉTAT créerait une base de données par une case à cochée dans
l'app — chaque révision copiant le JSON entier dans energylogs.sqlite, sans que
personne l'ait décidé.

Et ce que je vois de plus : un JSON qui traverse n'a plus de schéma. Un numéro
de version DANS la charge utile, avec refus plutôt qu'interprétation au mieux —
sinon le symptôme d'un changement de forme est « courbe vide », indistinguable
d'une nuit. issuedAt vient aussi de la charge utile, jamais de l'instant de
réception : sinon un redémarrage réétiquette la série et la table bitemporelle
perd ce qu'elle existe pour retenir.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-09-08 16:49:22 +02:00
Patrick Schurig
91250f081f docs(prévision PV): BLOQUÉ — le découpage contredit l'invariant « ZÉRO cloud »
Le §1 de ce design fait appeler une API distante par ems. C'est interdit ici, et
l'interdiction est explicite à quatre endroits : AGENTS.md invariant 10 (« aucun
appel réseau sortant vers un service distant […] le moteur expose l'état, il ne
contacte personne », dont la seule exception nommée est un socket local/LAN) et
docs/SAFETY.md, trois fois.

Ce n'est pas une objection de style : « fonctionne sans internet » est un
argument produit, et le mode dégradé L2 est bâti sur l'idée que rien
d'essentiel ne dépend d'un lien externe.

Je n'avais PAS vu ce conflit en écrivant le design — il a été soulevé hors de ce
fil, avec le point d'attribution CC BY 4.0 qui va avec : l'obligation suit
l'appelant, donc si ems appelle, elle atterrit dans un dépôt où elle n'est
écrite nulle part et serait perdue en silence, dans un dépôt GPL publié.

Aucun code avant arbitrage. Deux issues, et ce n'est pas à l'agent de trancher :
soit l'invariant 10 est amendé avec sa raison écrite et l'attribution posée ici,
soit le découpage change et l'appel reste chez le plugin météo.

Trois décisions rendues, consignées :
- nouvelle table, et la raison est plus forte que la mienne — la CLÉ CHANGE DE
  NATURE, une prévision révisée n'a rien à faire dans la base du mesuré ;
- un Get* sans *Changed : l'invariant 11 vise l'ÉTAT VIVANT, pas l'historique.
  La distinction n'y était pas écrite ; l'appliquer mécaniquement aurait produit
  une notification par heure sur une donnée que personne n'attend en direct ;
- aucun effet sur l'arbitrage. Le jour où l'étalement éco lira la prévision, ce
  sera un lot à part — et le §10 est la seule place où elle mordrait, en pariant
  sur une courbe dont le §3 dit qu'elle est un plafond systématique.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-09-08 16:35:11 +02:00
Patrick Schurig
5316a6f723 docs(design): la prévision PV est une série RÉVISÉE — table bitemporelle
Nouveau lot cadré par Patrick. Le plugin openmeteo reste météo pure ; les plans
PV, la conversion, l'appel API et l'historique vivent ici.

LE POINT DE CONCEPTION : la table n'est pas « valeur par heure » mais « valeur
par heure, telle que prévue à tel moment ». La clé est la PAIRE
(targetAt, issuedAt). Avec targetAt seul, chaque rafraîchissement écrase le
précédent : on perd les révisions, donc la comparaison prévu/réalisé — ce qu'on
veut précisément montrer. Même distinction que measuredSince.

Conséquence de contrat que le cadrage n'énonçait pas : « prévu » ne veut rien
dire sans HORIZON. Trois lectures (courante, veille, H−1) répondent à trois
questions ; publier « prévu » sans dire laquelle laisse deux écrans montrer deux
courbes pour le même jour.

Repris de l'agent openmeteo, mesuré et non supposé : indexation rétrograde ;
cinq facteurs ignorés, TOUS optimistes — donc un biais signé, pas un bruit : la
conversion produit un PLAFOND, pas une estimation ; la dispersion d'ensemble est
ANTICORRÉLÉE au biais, donc un indicateur de confiance qui rassure le plus quand
il se trompe le plus — pire que pas d'indicateur, et l'affiner l'empire ; et la
correction prévu↔mesuré est un apprentissage, hors GPL.

Le GPL n'apprend pas, il rend l'apprentissage POSSIBLE : la table bitemporelle
EST l'interface avec Héos, et l'écraser à chaque révision détruirait la matière
pour toujours sans que rien ne le signale.

Cinq ajouts au cadrage : targetAt en epoch UTC jamais en heure murale (le piège
du 6 septembre appliqué à une série entière — vérification sur des jours de 23 h
et 25 h) ; rétention 7 jours pleins puis effondrement aux DEUX bouts, parce que
n'en garder qu'un détruit la révision ; stockage chez nous et non dans
energylogs.sqlite, que l'amont migre ; l'absence est un TROU, jamais un zéro ;
et l'historique doit retenir SOUS QUEL PLAN une prévision a été convertie, sinon
un recâblage rend la série antérieure incomparable en silence.

Trois choses manquent pour coder, dont la convention d'indexation AVEC ses
paramètres : reprendre une conclusion sans ses prémisses, c'est hériter d'un
fait vrai sous des conditions qu'on ne saura pas reproduire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-09-08 16:30:25 +02:00