# Prévision de production PV — une série, et elle est révisée > ## ✅ TRANCHÉ le 2026-09-08 — `ems` n'appelle pas. Voir §12. > > *L'encadré ci-dessous est conservé comme historique de la décision.* > > ## ⛔ (résolu) — le §1 contredisait un invariant du dépôt > > **Ce document propose que `ems` appelle lui-même une API distante. C'est interdit ici**, et > l'interdiction est explicite à quatre endroits : > > - `AGENTS.md` invariant **10** : *« ZÉRO cloud — aucun appel réseau sortant vers un service > distant […] Le moteur expose l'état, il ne contacte personne. »* La seule exception nommée > est le client d'optimiseur sur socket **local/LAN**, jamais un service externe ; > - `docs/SAFETY.md` §« Signalisation locale (zéro cloud) », puis deux rappels : *« Aucun canal > sortant réseau »*, *« Le système fonctionne SANS internet »*. > > **Ce n'est pas une objection de style** : « fonctionne sans internet » est un argument produit > (autoconsommation, local-first), et le mode dégradé L2 est construit sur l'idée que rien > d'essentiel ne dépend d'un lien externe. > > **Et l'attribution CC BY 4.0 suit l'APPELANT.** Si `ems` appelle, l'obligation d'attribution > d'Open-Meteo atterrit dans un dépôt où elle n'est écrite nulle part — donc elle serait > **perdue**, en silence, dans un dépôt GPL publié. *(Point soulevé hors de ce fil ; noté ici > parce que c'est ce dépôt qui la porterait.)* > > **Aucun code ne s'écrit 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 > dans ce dépôt ; soit le **découpage change** et l'appel reste chez le plugin météo, qui a > déjà le droit de sortir. > > Tout ce qui suit reste valable **sous réserve de cet arbitrage** : le §2 (table bitemporelle) > et les §3 à §5 ne dépendent pas de qui appelle. > Ouvert le 2026-09-08. **Design avant code, rien d'implémenté.** > Cadrage donné par Patrick. Les constats météo viennent de l'agent openmeteo et sont > **repris tels quels** : ils sont mesurés, pas supposés. ## 1. La frontière, et pourquoi elle tombe là | | responsabilité | |---|---| | plugin **openmeteo** | **météo pure** — irradiance, température, couverture. Rien de photovoltaïque | | **ems** (ici) | les **plans PV** (crête, orientation, inclinaison), la **conversion**, l'**appel API**, et l'**historique** | Le plugin météo ne connaît pas l'installation ; l'installation ne se déclare pas dans un plugin météo. Et c'est ici que vivent déjà la configuration persistée, le compteur de production et la frontière RPC — donc ici que la série peut être servie à l'app sans traverser une troisième maison. ### Pourquoi ce n'est pas un état nymea **Le besoin n'est pas de décider, c'est d'afficher** : la courbe prévue d'aujourd'hui et de J+1, superposée à la production réelle. - **Aucun état nymea ne porte une série.** Un état est une valeur à un instant. - **`suggestLogging` ne construit que du passé.** Il enregistre ce qu'un état a valu. **Une prévision porte sur des heures qui ne sont pas arrivées** — il n'y a rien à enregistrer, il y a quelque chose à **écrire d'avance**. C'est le même geste que le logging natif pour les compteurs, à ceci près qu'il regarde devant. ## 2. LE POINT DE CONCEPTION — la table est BITEMPORELLE **Une prévision est révisée.** Celle de 8 h pour 15 h n'est pas celle de 14 h pour 15 h. La table n'est donc **pas** « valeur par heure ». Elle est **« valeur par heure, telle que prévue à tel moment »** : ```sql CREATE TABLE forecastPv ( targetAt INTEGER NOT NULL, -- epoch UTC de l'heure VISÉE issuedAt INTEGER NOT NULL, -- epoch UTC de l'ÉMISSION de cette prévision forecastWh REAL NOT NULL, model TEXT, PRIMARY KEY (targetAt, issuedAt) -- la CLÉ EST LA PAIRE ); ``` **La clé est la paire, et c'est tout le lot.** Avec `targetAt` seul, chaque rafraîchissement écrase le précédent : on perd les révisions, donc la comparaison prévu/réalisé — c'est-à-dire exactement ce qu'on veut montrer, et ce dont Héos aura besoin. > C'est la même distinction que `measuredSince` : **quand on a mesuré**, et **ce qu'on a mesuré**. > Deux dates, deux questions, et les confondre en perd une. ### Conséquence de contrat : « prévu » ne veut rien dire sans HORIZON Dès que les révisions coexistent, « la prévision pour 15 h » est ambiguë. Toute lecture doit nommer son horizon, et l'écran aussi : | lecture | requête | ce qu'elle répond | |---|---|---| | **courante** | pour chaque `targetAt`, la ligne de `issuedAt` maximal ≤ maintenant | « ce qu'on prévoit en ce moment » | | **veille** | `issuedAt` le plus proche de J−1 à heure fixe | « la prévision de la veille valait-elle quelque chose ? » | | **à H−1** | `issuedAt` le plus proche de `targetAt − 1 h` | « la meilleure estimation possible » | Les trois répondent à des questions différentes. Publier « prévu » sans dire laquelle laisse le client en choisir une au hasard, et deux écrans montrent deux courbes pour le même jour. ## 3. Ce que la conversion IGNORE — cinq facteurs, tous optimistes Repris de l'agent openmeteo, mesuré : 1. l'**ombrage**, 2. l'**encrassement**, 3. la **température de module**, 4. l'**écrêtage onduleur**, 5. les **salissures**. **Les cinq vont dans le même sens.** Ce n'est donc pas un bruit qui se compense : c'est un **biais systématique et signé**. La conversion ne produit pas une estimation, elle produit un **plafond**. > **À écrire dans le contrat, pas seulement ici.** Une courbe présentée comme « production > prévue » et systématiquement au-dessus du réel se lit comme une installation qui sous-performe. > L'utilisateur cherchera une panne qui n'existe pas — et il aura raison de chercher, puisque > l'écran lui aura affirmé une chose fausse. ## 4. La dispersion d'ensemble ne QUALIFIE PAS la production Le réflexe serait de publier la dispersion des membres d'ensemble comme un indice de confiance. **C'est exactement l'inverse qu'il faut faire.** Elle est **anticorrélée au biais** : un ciel clair et stable donne la **dispersion la plus basse** au moment précis où la **perte thermique est la plus forte** — donc où l'erreur de conversion est la plus grande. > **Un indicateur de confiance anticorrélé à l'erreur est pire que pas d'indicateur.** Il est le > plus rassurant exactement quand il se trompe le plus. Il ne s'agit pas d'un indicateur > imparfait qu'on affinerait : il pointe dans la mauvaise direction, et l'affiner l'empire. Elle peut être **stockée** (elle décrit le ciel, et Héos en fera peut-être quelque chose). Elle ne doit **jamais être publiée comme une qualité de la prévision de production**. *(Famille des modes d'érosion d'`AGENTS.md` : l'instrument dont le silence ressemble à un résultat. Ici, c'est l'instrument dont la confiance ressemble à une garantie.)* ## 5. Ce qui est HORS GPL — et ce que le GPL doit publier pour que l'autre existe **Comparer prévu à mesuré pour en tirer une correction est un apprentissage**, donc hors GPL. Mais la conséquence est constructive, et c'est elle qui justifie la table bitemporelle : > **Le GPL n'apprend pas ; il rend l'apprentissage POSSIBLE.** La table `(targetAt, issuedAt)` > est l'interface entre les deux. Sans elle, Héos n'aurait pas la matière — et l'écraser à chaque > révision détruirait cette matière pour toujours, sans que rien ne le signale. Le GPL publie donc la série et le réalisé côte à côte, **sans les rapprocher**. Le rapprochement est le produit. ## 6. Cadence — une heure, et le raisonnement est celui du polling d'une borne Les modèles se rafraîchissent **au mieux toutes les heures**. Interroger plus souvent brûle du quota sans rien apporter : ce n'est pas une donnée neuve, c'est la même donnée relue. Le quota (10 000 appels/jour/IP) est très loin — **et ce n'est pas l'argument**. L'argument est le même que pour la borne : *on ne polle pas plus vite que la chose n'a de nouveau à dire*. Une cadence justifiée par « on a le droit » est une cadence qu'on ne sait pas défendre. ## 7. Le NOM porte la protection **Préfixe `forecast` partout**, sans exception : `forecastProductionW`, `forecastIssuedAt`, `forecastHorizonS`. Jamais `productionW`, jamais une valeur de `measurement.source`, jamais un champ qu'une somme puisse mêler à du mesuré. > **C'est la règle qui a coûté le plus cher ces trois semaines.** Un nombre qui ne dit pas d'où > il vient finit toujours par être lu comme une mesure — et une prévision lue comme une mesure > est un mensonge que personne n'a écrit. --- ## 8. Ce que j'ajoute au cadrage ### 8.1 Une heure n'est pas une heure — le piège du 6 septembre, à nouveau `targetAt` doit être un **instant absolu en UTC**, jamais un libellé d'heure locale. Un `2026-10-25 02:00` local existe **deux fois** et un `2026-03-29 02:00` n'existe **pas** : indexer par heure murale corrompt silencieusement la série deux fois par an, et l'erreur se voit un jour où personne ne regarde. Concrètement : demander `timezone=UTC` à l'API, stocker des epochs, **convertir au rendu seulement**. C'est la distinction déjà tranchée le 2026-09-07 — *un réglage est une chaîne qu'on interprète, un instant est absolu* — appliquée à une série entière au lieu d'un champ. **Et la vérification qui mord** : deux jours de 23 h et 25 h dans les données de test, avec la somme quotidienne comparée au nombre de lignes. Une série indexée en heure murale y perd ou double une heure, et rien d'autre ne le montre. ### 8.2 Rétention — la table grandit en heures × révisions 48 h d'horizon × 24 révisions/jour ≈ **1 150 lignes par jour**, ~46 ko/jour, ~17 Mo/an. Tenable sur la cible, **mais pas indéfiniment**, et une table qui ne dit pas quand elle purge finit par le décider toute seule un jour de disque plein. Proposition : **7 jours de révisions complètes**, puis effondrement à **deux lignes par heure visée** — la **première** (horizon le plus long) et la **dernière avant l'heure** (meilleure estimation). Garder les deux bouts conserve les deux seules questions qui se posent : *« la veille valait-elle quelque chose ? »* et *« que pouvait-on savoir de mieux ? »*. N'en garder qu'une détruit la révision, c'est-à-dire l'objet du lot. ### 8.3 Où vit le stockage : chez nous, pas dans `energylogs.sqlite` `energylogs.sqlite` appartient à l'amont, qui en migre le schéma. Y ajouter une table nous exposerait à une migration qui ne nous connaît pas. Un fichier à nous, à côté de `energy-load-configuration.json`, avec **notre** numéro de schéma. ### 8.4 L'absence n'est pas un zéro Pas de réseau, API en erreur, plan PV non déclaré : **aucune ligne**. Jamais `0 Wh`, qui se lirait « on prévoit une nuit ». La courbe publiée porte des **trous**, et l'écran doit savoir les afficher comme tels. Même règle que partout ici, et c'est la plus souvent oubliée quand la donnée est une série : un trou dans une courbe demande un effort de rendu, un zéro n'en demande aucun — et c'est pour ça qu'on écrit des zéros. ### 8.5 Les plans PV sont une CONFIGURATION, donc ils se versionnent Crête, orientation, inclinaison sont saisis par l'installateur, comme les rangs de charge. Même famille que `LoadConfigStore` : store persisté, RPC de lecture/écriture, validation au **refus** et non à la correction silencieuse. **Et un plan qui change périme la conversion, pas la météo.** L'historique doit donc retenir *sous quel plan* une prévision a été convertie — sinon un recâblage rend toute la série antérieure incomparable **sans que rien ne le dise**. C'est la même erreur que le plancher de borne qu'on aurait stocké au lieu de le dériver, déplacée dans le temps. --- ## 9. Ce qu'il me manque pour écrire le code 1. **La convention d'indexation, avec ses PARAMÈTRES.** Elle est rétrograde et mesurée — je la reprends. Mais une convention d'indexation dépend de la requête qui l'a produite (variables demandées, `timezone`, horaire vs quart-horaire). Reprendre la conclusion sans ses prémisses, c'est hériter d'un fait vrai sous des conditions qu'on ne saura pas reproduire. **L'énoncé exact et les paramètres de l'appel mesuré**, et je m'aligne dessus. 2. **Où se déclarent les plans PV** : un nouveau store à nous (ma proposition, §8.5), ou des champs sur le Thing onduleur ? 3. **La position du banc** — latitude/longitude. Elles conditionnent l'appel, et une prévision convertie sur la mauvaise position est fausse d'une façon parfaitement crédible. ## 10. Étapes proposées, à comportement constant d'abord **A** — le store des plans PV et son RPC. Rien n'appelle l'API ; la configuration existe et ne décide de rien. **B** — l'appel Open-Meteo et la table bitemporelle, **sans publication**. La série se remplit et s'observe au banc, y compris en travers d'un changement d'heure. **C** — la publication RPC de la courbe, avec son horizon nommé et ses trous. **D** — la superposition au réalisé, **côte à côte et sans rapprochement**. Le pas **D** est celui qui touche à la frontière avec le produit, et il arrive quand la série est déjà vérifiable. --- ## 11. Décisions rendues le 2026-09-08 **Question 1 — qui appelle l'API.** *Non tranchée, remontée.* Voir l'encadré en tête. Les quatre décisions ci-dessous ne s'appliquent qu'une fois celle-là réglée. **Nouvelle table, pas `energylogs.sqlite`.** Retenu, et la raison retenue est plus forte que celle du §8.3 : ce n'est pas seulement que l'amont migre ce schéma, c'est que **la clé change de nature**. `energylogs` est indexée par un instant de mesure ; la table de prévision est indexée par une **paire**. Une prévision révisée n'a rien à faire dans la base du mesuré. **Un `Get*`, sans `*Changed` — amendement à l'invariant 11.** L'invariant (*« tout élément runtime a un `Get*` ET un `*Changed` »*) vise **l'état de l'arbitre**, qui bouge à chaque cycle et dont un client déconnecté se retrouverait en retard sans pouvoir se resynchroniser. **Une courbe historisée n'a pas ce problème** : elle se lit sur demande, comme `GetPowerBalanceLogs`. Pas de notification. > À noter comme précédent : l'invariant 11 porte sur **l'état vivant**, pas sur **l'historique**. > La distinction n'y était pas écrite, et 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 lot est un **affichage**. La prévision n'entre pas dans le plan, ne pondère pas l'étalement éco, ne déplace aucune échéance. > Le jour où l'étalement éco lira la prévision, ce sera **un lot à part avec sa propre > décision** — et le **§10** est aujourd'hui la seule place où elle mordrait : c'est la cadence > d'obligation qui achèterait moins ce matin en pariant sur cet après-midi. Un pari, sur une > courbe dont le §3 dit qu'elle est un **plafond** systématique. À ne pas glisser dans ce lot-ci. --- ## 12. Le découpage tranché, et le canal — 2026-09-08 **`ems` n'appelle pas l'API. L'invariant 10 reste tel quel**, et la raison est plus fine que « zéro réseau » : > Un **plugin d'intégration** qui lit une API distante perd **une entrée** quand le lien tombe, > comme un capteur perd la sienne. Un **experience-plugin** qui appelle le réseau, c'est **la > logique de décision** qui se met à dépendre d'un service distant. L'invariant est écrit dans ce > dépôt-ci pour cette raison, et il est juste là où il est. L'attribution CC BY 4.0 reste donc chez openmeteo, où elle est déjà écrite : **le problème disparaît avec le découpage au lieu d'être géré.** ### 12.1 Le précédent cité ne couvre PAS ce lot — vérifié dans l'arbre amont `awattar`, `tempo`, `spothinta` sont des précédents d'**intégrations qui appellent une API**. Ils ne sont **pas** des précédents de **série transmise au moteur**. Le plugin `awattar` de l'amont n'expose que des **scalaires** : ``` currentMarketPrice · averagePrice · lowestPrice · highestPrice · rank · validUntil ``` **La série ne traverse jamais.** L'amont a résolu le problème en **appelant deux fois** : notre propre `energyplugin/spotmarket/spotmarketdataproviderawattar.cpp` refait la requête vers `https://api.awattar.at/v1/marketdata`. > **Il n'existe donc aucun canal éprouvé pour une série, parce que personne n'en a construit.** > Ce lot en ouvre un ; il n'en reprend pas un. ### 12.2 Conséquence à ne pas laisser sans suite : le moteur appelle DÉJÀ `SpotMarketDataProviderAwattar` vit dans **ce dépôt**, sort en HTTPS vers `api.awattar.at` / `api.awattar.de`, et son résultat alimente `planSpotMarketCharging()` — **une décision**, pas un affichage. C'est exactement ce que l'invariant 10 interdit, selon le raisonnement qui vient de le confirmer, et **c'est en service**. Hérité de l'amont, jamais choisi — mais un invariant qui a une exception non déclarée n'est plus un invariant, c'est une habitude. **À trancher séparément** : soit l'exception est écrite avec sa raison, soit le fournisseur spot descend chez une intégration. ### 12.3 Le canal : un état portant du JSON — les trois grandeurs, mesurées | question | réponse | source | |---|---|---| | **taille supportable** | **1 MiB**, plafond dur du transport | `jsonrpcserverimplementation.cpp:554` — au-delà, la connexion est **larguée** | | **coût de rediffusion** | **par RÉVISION, pas par cycle** | `thing.cpp` : `if (oldValue == newValue) return;` — nymea **dédoublonne** | | **nommage** | préfixe `forecast`, et le mot dit que c'est un canal | §7 | Une courbe de 48 h fait **2 à 5 ko** : trois ordres de grandeur sous le plafond. **La taille n'est pas le sujet.** Et réécrire la même valeur à chaque poll n'émet **rien** — le coût est d'une diffusion par heure, pas de soixante. > La prémisse « rediffusion à chaque cycle » surestime d'un facteur 60. Ce qui compte n'est pas > la cadence du moteur, c'est celle des **révisions**. ### 12.4 Les deux vrais dangers, et aucun n'est la taille **a. `cached` — la réécriture du fichier de configuration.** Un état `cached` est **écrit sur disque** à chaque changement (`thingmanagerimplementation.cpp` → `storeThingStates()`). Un état `cached` portant plusieurs kilo-octets de JSON réécrit `things.conf` **toutes les heures**, sur une carte SD. **`"cached": false` est obligatoire**, comme l'état `connected` d'`awattar`. **b. La journalisation d'état — une base de données créée par une case à cocher.** Si quelqu'un active le logging sur cet état (`Integrations.SetStateLogging`, exposé dans l'app), **chaque révision écrit une copie complète du JSON dans `energylogs.sqlite`**. Personne ne l'aura décidé, rien ne le signalera, et la base grossira jusqu'à ce que le disque le dise. > C'est le mode d'érosion de l'instrument dont le silence ressemble à un résultat, retourné : > **un mécanisme qu'un tiers arme sans savoir ce qu'il arme.** Le nom doit l'en dissuader, et le > contrat doit dire que cet état ne se journalise pas. ### 12.5 Ce que je vois de plus : un JSON qui traverse n'a plus de schéma Au moment où la courbe devient une chaîne, **elle n'est plus typée**. Si openmeteo change la forme du contenu, `ems` casse **à l'exécution**, sans qu'aucune compilation ni aucun contrat ne l'ait signalé — et le symptôme sera « la courbe est vide », c'est-à-dire indistinguable d'une nuit. **Un numéro de version DANS la charge utile, et le refus plutôt que l'interprétation au mieux.** Une version inconnue produit **aucune courbe** et un avertissement nommé, jamais une lecture partielle qui aurait l'air de marcher. C'est le versant charge utile de la règle déjà écrite au contrat : *un contrat qu'aucun appelant n'exerce n'est pas un contrat*. Ici, c'est un contrat qu'aucun compilateur ne vérifie — donc il faut que quelqu'un le vérifie à l'exécution, une fois, à l'entrée. ### 12.6 Nommage proposé | | | |---|---| | état | `forecastPvCurve` — **`cached: false`**, non journalisable | | charge utile | `{ "v": 1, "issuedAt": , "points": [{ "t": , "wh": }] }` | `v` d'abord, `issuedAt` ensuite : les deux choses à lire **avant** de croire le reste. Et `issuedAt` est **dans la charge utile**, pas déduit de l'instant de réception — sinon un redémarrage de `ems` réétiquette toute la série avec l'heure du redémarrage, et la table bitemporelle perd exactement ce qu'elle existe pour retenir. --- ## 13. La clé de révision — tranché le 2026-09-08 **Retenu : la déduplication sur le contenu, et la colonne s'appelle `receivedAt`.** Pas de requête `meta.json`. Mais la décision n'est bonne qu'avec les trois conditions ci-dessous, et la première est celle qui la rend acceptable. ### 13.1 Le coût réel n'est pas un décalage de cycle — c'est un horizon FAUX de plusieurs heures `receivedAt` n'est pas « `issuedAt` à une minute près ». Entre l'initialisation d'un run et sa mise à disposition, il s'écoule la **latence de publication du modèle** — des **heures**, pas des minutes, pour un modèle global. Conséquence directe : `targetAt − receivedAt` **n'est pas l'horizon**. Il est trop court de toute la latence de publication, **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, et ce qu'elle ne sauve pas.** Elle donne les **bornes** des > révisions exactement : *combien* de révisions, et *laquelle* s'applique. Elle ne donne pas leur > **âge**. « Quelle prévision » est juste ; « quel horizon » est faux. > > C'est sans effet sur ce lot, qui **affiche**. C'est déterminant pour l'apprentissage, dont > l'objet même est de corréler l'erreur à l'horizon. ### 13.2 Condition 1 — la colonne s'appelle `receivedAt`, jamais `issuedAt` Un nom qui promet l'heure du run alors qu'il porte l'heure de réception fabrique le calcul faux tout seul : quelqu'un écrira `targetAt - issuedAt` et obtiendra un nombre **plausible**. C'est la classe qu'on paie le plus cher ici, et le remède est le même que pour `forecast` — **le nom porte la protection.** Et il faut l'écrire, pas seulement le nommer : **aucun horizon ne se calcule depuis `receivedAt`.** ### 13.3 Condition 2 — la déduplication compare les points COMMUNS, pas la charge utile « Courbe identique à la précédente » ne suffit pas si la fenêtre **glisse** : si openmeteo ne publie que les heures à venir, la charge utile de 11 h a un point de moins que celle de 10 h, et un `==` sur la charge utile verrait une révision **toutes les heures** — exactement ce que la déduplication existe pour éviter. La règle : *deux charges utiles sont la même révision si, sur leurs `targetAt` COMMUNS, toutes les valeurs coïncident.* > **Question à trancher avec openmeteo, et elle décide de l'implémentation** : la fenêtre > publiée couvre-t-elle les journées **entières** (aujourd'hui 00 h → J+1 23 h, passé compris), > ou seulement **les heures à venir** ? Dans le premier cas le jeu de points est stable dans la > journée et un `==` naïf suffirait — sauf au passage de minuit. Dans le seesecond, il faut la > comparaison sur l'intersection. **Je code la seconde dans les deux cas** : elle est correcte > pour les deux, et elle ne dépend pas d'un comportement qu'un changement chez le voisin pourrait > retirer sans nous prévenir. ### 13.4 Condition 3 — la dette `meta.json` se rouvre AVEC son consommateur, pas avant La décision de ne pas appeler `meta.json` reposait sur *« aucun consommateur ne le demande »*. Elle **reste juste** : le consommateur d'aujourd'hui est un **affichage**, et un affichage n'a pas besoin de l'âge du run — il a besoin de savoir *ce qu'on prévoyait*, ce que la déduplication donne exactement. Le consommateur qui en aura besoin est **l'apprentissage**, et il n'existe pas encore. La dette est donc notée, avec son déclencheur : > **Le jour où quelque chose corrèle l'erreur à l'horizon, `meta.json` devient nécessaire** — et > la décision se reprend avec un consommateur réel, ce qui est le raisonnement qui l'a rendue > juste la première fois. Ce qui serait fautif, c'est de la rouvrir **maintenant** au nom d'un besoin supposé : une requête par cycle payée aujourd'hui pour un calcul que personne ne fait. ### 13.5 Volume, confirmé 48 h d'horizon, **24 à 48 points** selon l'heure. À 4–8 révisions par jour après déduplication : **~200 à 400 lignes par jour**, contre les 1 150 estimées au §8.2 sur une révision horaire. La rétention du §8.2 devient largement confortable, et l'effondrement aux deux bouts peut attendre bien plus que 7 jours. ### 13.6 La dette `forecastModel` — je ne m'appuie pas dessus, et je n'écris rien de plausible `forecastModel` publie le **modèle configuré**, pas celui qui a répondu. Le stocker comme provenance enregistrerait une **intention**, et une intention rangée dans une colonne de provenance devient une provenance au premier lecteur. Donc : la colonne `model` existe et 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. **Et aucun remplissage rétroactif** : les lignes écrites avant la correction resteront nulles, parce qu'elles le sont.