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
This commit is contained in:
Patrick Schurig 2026-09-08 17:02:46 +02:00
parent 0b8264ab04
commit 4a0dfa273f

View File

@ -381,3 +381,91 @@ faut que quelqu'un le vérifie à l'exécution, une fois, à l'entrée.
`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.