Schéma et clé de forecastLearning, règles NULL, révision « en vigueur à X », tableau détectable / non détectable de l'écrêtage, neige, écriture et base abîmée, limites. Inventaire du mock : pvPlane (paramètres) et solarInverter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
231 lines
14 KiB
Markdown
231 lines
14 KiB
Markdown
# INVENTAIRE À FROID DU MOCK — ce qu'il rend structurellement introuvable
|
||
|
||
*(Fait le 2026-09-01, après trois occurrences en trois jours d'un cas réel impossible à
|
||
reproduire. Ce n'est plus un incident, c'est une propriété du banc d'essai.)*
|
||
|
||
## Pourquoi cet inventaire
|
||
|
||
Trois fois cette semaine, un défaut réel s'est révélé **inatteignable par la suite** :
|
||
|
||
| Occurrence | Ce qui était introuvable | Comment on l'a su |
|
||
|---|---|---|
|
||
| Correctif amont `calculateAllowanceAmpere` | un compteur publiant les courants **sans** les puissances par phase | le mock pose `currentPhaseB = currentPowerPhaseB / 230`, cohérent **par construction** |
|
||
| Test `command` sous `source: none` | une charge dont rien ne mesure la puissance | **toutes** les classes pilotables portent `currentPower` |
|
||
| Test `ECO_FLOOR_*` en régime `unmeasurable` | une borne sans `sessionEnergy` | il a fallu **ajouter** l'état à une seule classe pour avoir les deux cas |
|
||
|
||
Le point commun : **le mock est plus homogène que la réalité.** Il a été construit pour faire
|
||
marcher des scénarios, pas pour représenter la diversité du parc — et une suite verte ne dit alors
|
||
rien sur les cas qu'il ne sait pas produire.
|
||
|
||
## Les classes, et ce qu'elles portent
|
||
|
||
| Classe | `currentPower` | `totalEnergyConsumed` | `sessionEnergy` |
|
||
|---|---|---|---|
|
||
| `meter` | ✅ | ✅ | — |
|
||
| `charger` | ✅ | ✅ | ❌ |
|
||
| `chargerPhaseSwitching` | ✅ | ✅ | ✅ *(ajouté le 2026-08-31)* |
|
||
| `simpleCharger` | ❌ | ❌ | ❌ |
|
||
| `powerSwitch` | ✅ | ❌ | — |
|
||
| `etmVariableLoad` | ✅ *(`currentPowerW`)* | ❌ | — |
|
||
| `energyStorage` | ✅ | ❌ | — |
|
||
| `car`, `notification` | ❌ | ❌ | — |
|
||
|
||
**Ce que la table donne, et c'est la bonne nouvelle** : `sessionEnergy` et `totalEnergyConsumed`
|
||
sont désormais **discriminants** — il existe des classes avec et sans. Les régimes `measured` /
|
||
`unmeasurable` et les causes `noSessionEnergy` / `meterWithoutEnergy` sont donc exerçables.
|
||
|
||
## Ce qui reste STRUCTURELLEMENT introuvable
|
||
|
||
**1. ~~`measurement.source: none` pour une charge saine.~~ — OUVERT le 2026-09-02.** La classe
|
||
`dryRelay` a été ajoutée : elle commute, elle ne mesure pas. Une charge **saine et non mesurée**
|
||
est désormais fabricable, et `testHealthyButUnmeasuredDiffersFromFaulty` vérifie ce que rien ne
|
||
pouvait vérifier avant — que le moteur **distingue** « rien ne mesure cette charge » de « cette
|
||
charge est en panne », deux affirmations que le seul chemin disponible (un Thing manquant)
|
||
mélangeait.
|
||
|
||
> **Ce que l'ajout a révélé au passage** : `executeAction()` déréférençait le contrôleur HTTP de
|
||
> **chaque** Thing sans vérifier — le mock supposait que toute classe en possède un. Un relais sec
|
||
> n'en a pas besoin, n'ayant rien à mesurer. Le plantage était immédiat et net ; l'hypothèse, elle,
|
||
> était invisible tant qu'aucune classe ne la contredisait. **Le mock avait sa propre prémisse
|
||
> fausse**, protégée par son homogénéité.
|
||
|
||
**2. ~~Un compteur par phase incohérent.~~ — OUVERT le 2026-09-03.** `voltagePhaseX` et
|
||
`currentPhaseX` sont désormais réglables **indépendamment** des puissances, par des paramètres
|
||
OPTIONNELS — sans eux, le comportement dérivé d'origine est conservé et aucun scénario existant
|
||
ne bouge.
|
||
|
||
> **Et l'ouverture a corrigé une erreur de RAISONNEMENT, pas seulement comblé un trou.** Le
|
||
> premier test de `LM-1210-a` attendait qu'une phase en export donne une marge *supérieure* au
|
||
> plafond — c'est ce que le design affirmait. Il a échoué : `calculateAllowanceAmpere()` part du
|
||
> plafond et ne fait que des `qMin`, donc la marge y est **écrêtée**. Physiquement juste — l'export
|
||
> ne permet pas de tirer plus que le disjoncteur — mais la phrase du design était fausse. **La
|
||
> conclusion tenait, la raison non**, et rien ne pouvait le révéler tant que le déséquilibre était
|
||
> infabricable.
|
||
|
||
**3. ~~`temperature`.~~ — OUVERT le 2026-09-05.** La classe `thermalProbe` porte l'état, et
|
||
`testThermalProbeFeedsTelemetry` exerce enfin la branche « sonde présente » de
|
||
`attachMeasurements()`, écrite depuis toujours et **jamais parcourue**. C'était la seule entrée du
|
||
volet thermique, et rien ne l'exerçait.
|
||
|
||
> **Ce qu'il reste à dire, et qui n'est pas corrigé ici** : une sonde DÉSIGNÉE mais incapable —
|
||
> un Thing sans état `temperature` — produit exactement la même charge utile qu'aucune sonde du
|
||
> tout (`temperatureC` absent). Le geste de réparation, lui, diffère : désigner une sonde, ou en
|
||
> changer. C'est un silence de la famille `progressUnmeasurableCause`, relevé par le test et
|
||
> laissé ouvert : le fermer demande un catalogue de causes thermiques qui n'existe pas encore.
|
||
|
||
**4. ~~Une triphasée déséquilibrée.~~ — OUVERT le 2026-09-03.**
|
||
`testWorstPhaseBindsWhenPhasesAreUnbalanced` exerce enfin `LM-1210-a` : phase B saturée à 30 A
|
||
sous un plafond de 25 pendant que A et C exportent, et la maison **exporte globalement**. La
|
||
marge retenue est celle de B — négative — et non celle du total, qui rassurerait à tort.
|
||
|
||
Ce manque était nommé **deux fois par deux chemins indépendants** — cet inventaire et le balayage
|
||
des contrôles — avant d'être comblé. C'est ce qui lui a donné sa priorité.
|
||
|
||
## La règle qui en sort
|
||
|
||
> **Avant d'écrire un test qui exerce une ABSENCE, vérifier que le mock peut la produire.**
|
||
> Une assertion sur un cas inatteignable ne passe pas : elle **ne s'exécute pas**, ce qui est
|
||
> pire — elle compte comme verte.
|
||
>
|
||
> Le symptôme se reconnaît : **la contre-épreuve ne mord pas**. C'est le corollaire fin
|
||
> d'`AGENTS.md` appliqué au banc d'essai plutôt qu'au code.
|
||
|
||
**Et la parade tient en une ligne de test** : quand on itère pour trouver des cas, **compter
|
||
combien on en a examinés**, et échouer si le compte est nul. C'est ce qui a rattrapé le test de
|
||
`command`, et ce qui garde celui de la mesurabilité.
|
||
|
||
**5. ~~Un compteur d'énergie FIGÉ.~~ — OUVERT le 2026-09-04.** `totalEnergyConsumed` n'était
|
||
écrit par personne : il valait sa valeur par défaut, indéfiniment. **Un compteur figé est un
|
||
compteur qui n'existe pas pour tout ce qui l'intègre** — `deliveredWh` restait à 0, `remainingWh`
|
||
à 4 000, et l'étalement adaptatif n'était **jamais exercé**. Le comportement se confondait avec
|
||
`unmeasurable` pour une raison entièrement différente et bénigne.
|
||
|
||
Le mock intègre désormais `currentPower` dans le cumul, une fois par seconde. **En export il ne
|
||
recule pas — il ne bouge pas** : un compteur de consommation est monotone, et le faire reculer
|
||
produirait un cas que le terrain ne produit pas, que la garde de sémantique traiterait comme une
|
||
anomalie.
|
||
|
||
**Et `LM-1014-b` s'y applique, question posée et répondue** : `totalEnergyConsumed` est déclaré
|
||
`cached`, donc il **continue** au redémarrage du mock. Repartir de zéro ferait reculer un cumul
|
||
monotone — le mock fabriquerait l'anomalie que le moteur est censé détecter.
|
||
|
||
**Quatrième occurrence.** Après les six états cohérents du `rootmeter`, les classes portant toutes
|
||
une puissance, et la triphasée équilibrée.
|
||
|
||
**6. ~~Le compteur sans énergie et la sonde thermique.~~ — OUVERTS le 2026-09-05**, et les deux
|
||
derniers de la liste. `meterNoEnergy` remplace le `powerSwitch` détourné en instrument de mesure —
|
||
un montage que personne ne fait, et qui faisait tenir DEUX rôles à la même Thing dans le même
|
||
scénario. `thermalProbe` ouvre le volet thermique.
|
||
|
||
> **Et l'ouverture a révélé un défaut, comme les trois précédentes.** `setupThing()` du mock
|
||
> n'avait **aucune sortie par défaut** : une classe sans branche ne rencontrait jamais de
|
||
> `info->finish()`. Le setup ne échouait pas — **il ne se terminait jamais**. Le symptôme est un
|
||
> Thing éternellement « en cours de configuration » et un test qui expire ailleurs, très loin de
|
||
> la cause.
|
||
>
|
||
> C'est le même mécanisme que le contrôleur déréférencé pour toute classe : **une hypothèse du
|
||
> mock que son homogénéité protégeait.** Tant que chaque classe avait sa branche, l'absence de
|
||
> sortie par défaut ne pouvait pas se voir. Corrigé : une classe oubliée se dit maintenant en
|
||
> `qCWarning` et rend `ThingErrorSetupFailed`.
|
||
>
|
||
> **Et une garde a mordu au passage** : la première version du test thermique lisait `loads[]`
|
||
> sans compteur racine — donc une liste vide, et trois assertions qui ne se seraient jamais
|
||
> exécutées. `QCOMPARE(parId.count(), 3)` l'a attrapé. C'est la parade écrite plus haut dans ce
|
||
> document, appliquée le jour même à son auteur.
|
||
|
||
## Ce qu'il faudrait ajouter, par ordre d'utilité
|
||
|
||
1. **Une classe pilotable SANS aucune puissance** — un relais sec. Débloque `source: none` sur
|
||
une charge saine, et sépare enfin « non mesurée » de « en panne ».
|
||
2. ~~**Un compteur sans `totalEnergyConsumed`** en tant que classe déclarée.~~ — **FAIT le
|
||
2026-09-05** : `meterNoEnergy`. Elle ne déclare PAS l'interface `energymeter`, qui exige
|
||
`totalEnergyConsumed` — promettre une interface qu'on ne tient pas ferait mentir le mock là où
|
||
il doit être littéral.
|
||
3. **Une tension par phase réglable** indépendamment des puissances — pour exercer le correctif
|
||
amont ailleurs qu'en inversant la fonction.
|
||
|
||
> Aucune de ces trois n'est urgente. Ce qui l'est, c'est de **savoir qu'elles manquent** : sans
|
||
> cette liste, chaque lot repaiera le même temps à découvrir qu'un cas est introuvable.
|
||
|
||
---
|
||
|
||
## 7. L'homogénéité qui FABRIQUE un cas — 2026-09-05
|
||
|
||
Les six premières entrées décrivent des cas que le banc ne savait pas **produire**. Celle-ci est
|
||
l'inverse : un cas que le banc produit **en trop**, et qui n'existe pas ailleurs.
|
||
|
||
`ECS-Meter` publie 1500 W en permanence, `PAC-Meter` 800 W. Ce sont des simulateurs à valeur
|
||
fixe : ils ne suivent aucune consigne. Le journal de divergence a donc compté **14 épisodes en
|
||
24 h** qui ne sont pas des désobéissances, mais la distance entre une consigne variable et un
|
||
compteur constant.
|
||
|
||
**Un cas masqué se signale par un test qui ne mord pas. Un cas fabriqué se signale par des données
|
||
qui rassurent** — et rien, dans un compte juste, n'annonce de quelle question il est la réponse.
|
||
|
||
Le critère de tri est écrit avant les données : `docs/DESIGN_COMMANDE_PERDUE.md` §6.
|
||
|
||
**Ce qu'il faudrait au mock, et qui manque** : un compteur dont la valeur SUIT la commande de la
|
||
charge qu'il mesure. Aucune classe ne le fait aujourd'hui — les compteurs sont des sources
|
||
indépendantes qu'on pilote à la main. Tant que c'est le cas, aucun test ne peut exercer une charge
|
||
qui obéit, donc aucun ne peut distinguer l'obéissance de la désobéissance.
|
||
|
||
> **Nommé DEUX FOIS par deux chemins indépendants, le 2026-09-05 et le 2026-09-06** — et c'est ce
|
||
> qui lui donne sa priorité, comme au manque n° 4 avant lui.
|
||
>
|
||
> - Par les **traces de divergence** : quatorze épisodes qui mesurent l'écart entre une consigne
|
||
> variable et un compteur constant, et non une désobéissance.
|
||
> - Par la **pose d'une obligation éco** : sur un banc dont le compteur ne suit pas la commande,
|
||
> on ne peut pas avoir à la fois une cible modeste et un chemin d'achat longuement exercé. Ce
|
||
> qui borne n'est pas la cible, c'est la constance du compteur.
|
||
>
|
||
> Deux lots différents, deux symptômes sans rapport apparent, une seule cause. Quand un manque se
|
||
> fait nommer deux fois sans qu'on l'ait cherché, il ne s'agit plus d'un confort de test : il
|
||
> décide de ce que plusieurs lots pourront prouver.
|
||
|
||
---
|
||
|
||
## 8. Le sixième manque sous sa forme réelle — mesuré le 2026-09-08
|
||
|
||
Le §7 disait « des compteurs figés fabriquent de faux cas ». La fenêtre d'observation du
|
||
2026-09-08 (12:10 → 18:00, 292 points par charge) donne le chiffre, et il est plus net que la
|
||
formulation :
|
||
|
||
| charge | σ sur la fenêtre | min → max |
|
||
|---|---|---|
|
||
| **V2C Trydan** | **1 879 W** | 0 → 5 871 |
|
||
| chauffe-eau | **0,0 W** | 0 → 0 |
|
||
| pac-terrain | **0,0 W** | 0 → 0 |
|
||
| Simulated wallbox | **0,0 W** | 0 → 0 |
|
||
|
||
> **Aucun compteur du banc ne suit la commande de la charge qu'il mesure — sauf un.**
|
||
|
||
Ce n'est pas « des instruments imprécis ». C'est **un seul instrument valide sur quatre**, et il
|
||
faut le dire ainsi parce que la conséquence change de nature : toute conclusion tirée de ce banc
|
||
sur l'**obéissance** des charges est une conclusion **sur cette borne**, jamais sur le moteur.
|
||
|
||
**Le critère de tri du §6 de `DESIGN_COMMANDE_PERDUE.md` rend le manque visible ; il ne le
|
||
comble pas.** Il écarte correctement les épisodes des trois compteurs constants — sans nommer
|
||
personne, sur la seule propriété observable de la donnée. Mais un tri qui garde un instrument sur
|
||
quatre a **trié**, il n'a pas **mesuré**.
|
||
|
||
### Ce qui débloque, et c'est daté
|
||
|
||
De **vrais SDM120** sur la PAC et l'ECS, prévus en fin de semaine. Deux instruments de plus, et
|
||
le critère aurait enfin quelque chose à trier.
|
||
|
||
**C'est la condition d'existence de l'étape 4**, pas un confort : décider de rejouer une commande
|
||
demande de savoir si des commandes se perdent, et cette question n'est pas posable sur un banc
|
||
qui ne mesure qu'une charge.
|
||
|
||
## 9. Prévision PV — `pvPlane` et `solarInverter` (2026-10-13)
|
||
|
||
| Classe | Porte | Ce qu'elle ouvre |
|
||
|---|---|---|
|
||
| `pvPlane` | état `forecastCurveTransport` ; paramètres `kwp`, `performanceRatio`, `tempCoeffPctPerK`, `noctC` *(paramètres ajoutés au brief 3 T2)* | une courbe de prévision par le RPC (D2) ; la lecture des paramètres de conversion **sur le Thing**, par leur nom |
|
||
| `solarInverter` | interface `smartmeterproducer` (`currentPower`, `totalEnergyProduced`) *(ajoutée au brief 3 T2)* | le seul producteur du mock : sans lui, « le site a-t-il un producteur ? » valait toujours non, et la production mesurée de l'enregistrement pour l'apprentissage n'était jamais publiée |
|
||
|
||
**Ce qui reste introuvable ici** : un Thing **site** portant `snowfall` (le compteur fait office de
|
||
site, il ne l'a pas — `snow` n'est éprouvé qu'en unitaire), et un plan **sans** les paramètres v2
|
||
(la classe les porte toujours — le cas « openmeteo d'avant le brief 2 » n'est éprouvé qu'en
|
||
unitaire, par `lirePlan()`).
|