docs+test(commande perdue §3): le recrédit est un prêt consenti contre l'obéissance

Mesuré AVANT d'implanter, et la mesure a corrigé le diagnostic.

Le constat disait « un budget de surplus qui ignore ces watts ». Ce n'est pas
ce qui se passe : quand une charge consomme et OBÉIT, la comptabilité est
juste — sa conso est recréditée puis réallouée à elle-même, et les suivantes
ne reçoivent que le surplus réel. Export 500 W, A à 4 400 W obéissante, B
reçoit 500 W. Corriger cela aurait été corriger un non-défaut.

Le vrai défaut est plus grave, et l'hypothèse est écrite dans le code :
« le budget est une mesure dont la conso de la charge a déjà été retranchée »,
c'est-à-dire « je peux rendre ces watts parce que je peux les reprendre ».
C'est un prêt, et rien ne vérifie que l'emprunteur rembourse.

Mesuré, le cas où la promesse est engagée : A commandée à 0 par son plancher,
ses 4 400 W redistribués à B. Si A obéit, tout est juste. Si A n'obéit pas —
commande perdue, mode manuel, véhicule qui limite — la maison tire 8 400 W
pour 4 900 de production : 3 500 W importés que personne n'a décidés.

Et l'identité surplus + recrédit − alloué == restant TIENT dans les deux cas.
C'est ce qui distingue un prêt d'une erreur : la comptabilité est juste,
c'est l'hypothèse sur laquelle elle repose qui n'est pas vérifiée — donc
aucun contrôle de cohérence ne pouvait l'attraper.

Le test épingle le comportement ACTUEL, correct SOUS HYPOTHÈSE, et dit à
quelle condition il devient faux : le jour où le recrédit sera conditionné à
l'obéissance constatée, c'est l'allocation de B qu'il faudra revoir, pas le
test. Écrit pour qu'on ne le « corrige » pas par réflexe.

Ce que le design proposait — retrancher la conso subie du budget — est SANS
OBJET : elle en est déjà retranchée par le compteur. La note est conservée
pour que personne ne la réimplante en la croyant oubliée.

Suite : simulation 60/60, loadmodel 22/22, charging 48/48 identique à la
référence, doxygen 0 avertissement.

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-01 11:58:06 +02:00
parent 770cef13f3
commit 3eb59e86f9
3 changed files with 120 additions and 1 deletions

View File

@ -70,7 +70,44 @@ au niveau de la charge, publié **seulement quand la comparaison est possible**
contre `none`, il n'y a rien à comparer et l'objet est **absent** — pas `divergent: false`, qui
affirmerait la concordance qu'on ne peut pas constater. Même règle que partout ailleurs ici.
**4.3 — Et la COMPTABILITÉ, qui est le vrai coût.**
**4.3 — La COMPTABILITÉ : le diagnostic était à corriger, et le vrai défaut est plus grave.**
> **MESURÉ le 2026-09-01, avant d'implanter.** Le constat disait « un budget de surplus qui
> ignore ces watts ». **Ce n'est pas ce qui se passe.** Quand une charge consomme et **obéit**,
> la comptabilité est juste : sa conso est recréditée puis réallouée à elle-même, et les
> suivantes ne reçoivent que le surplus réel (mesuré : export 500 W, A à 4 400 W obéissante → B
> reçoit 500 W). **Corriger cela aurait été corriger un non-défaut.**
**Le vrai défaut : le recrédit est un PRÊT consenti contre l'obéissance.** Son hypothèse est
écrite dans le code — *« le budget est une mesure dont la conso de la charge a déjà été
retranchée »* — c'est-à-dire **« je peux rendre ces watts parce que je peux les reprendre »**.
Rien ne vérifie que l'emprunteur rembourse.
Mesuré, le cas où la promesse est engagée : export 500 W, A consomme 4 400 W, plancher de A à
5 000 W. A est commandée à **0**, et ses 4 400 W sont **redistribués à B**.
| | si A obéit | si A n'obéit pas |
|---|---|---|
| A | tombe à 0 | reste à 4 400 W |
| B | 4 000 W réels | 4 000 W **fictifs** |
| maison | 4 000 W pour 4 900 de production | **8 400 W pour 4 900** → 3 500 W importés |
**Et l'identité de réconciliation TIENT dans les deux cas** — `surplus + recrédit − alloué ==
restant`. C'est ce qui distingue un prêt d'une erreur : la comptabilité est juste, c'est
l'**hypothèse sur laquelle elle repose** qui n'est pas vérifiée. Aucun contrôle de cohérence ne
pouvait donc l'attraper.
**Ce qui change maintenant : l'obéissance est CONSTATABLE.** `command.divergent` (étape 2) dit
si la charge a suivi au cycle précédent. Le recrédit peut donc être **conditionné** — c'est la
proposition, et elle demande une décision parce qu'elle a son propre risque : l'anti-clignotement
existe pour une raison, et le suspendre trop vite ferait osciller une charge saine sur un écart
transitoire. `since` est précisément ce qui permet de distinguer les deux.
**Ce que le §4.3 d'origine proposait — retrancher la conso subie du budget — est SANS OBJET** :
elle en est déjà retranchée, par le compteur. La note est conservée ici pour que personne ne la
réimplante en la croyant oubliée.
**4.3-bis — l'ancienne formulation, conservée pour mémoire.**
`draw.committedW` à 0 pendant que 4,4 kW sont tirés n'est pas un défaut d'affichage : le budget
de surplus est calculé sans ces watts, donc **le moteur alloue de l'énergie qui n'existe pas**.
Une charge que l'arbitre ne commande plus mais qui consomme est de la **consommation fixe**, au

View File

@ -7180,6 +7180,87 @@ void Simulation::testCommandDivergenceIsPublishedNotDiagnosed()
#endif
}
/*!
* \brief [Commande perdue, étape 3] Le recrédit est un PRÊT consenti contre l'obéissance.
*
* \par Ce que la mesure a corrigé dans le diagnostic
* Le constat de l'app disait « un budget de surplus qui ignore ces watts ». Vérification faite,
* **ce n'est pas ce qui se passe** : quand une charge consomme et obéit, la comptabilité est
* juste — sa conso est recréditée puis réallouée à elle-même, et les suivantes ne reçoivent que
* le surplus réel. Corriger cela aurait été corriger un non-défaut.
*
* \par Le vrai défaut, et il est plus grave
* Le recrédit anti-clignotement repose sur une hypothèse écrite dans le code : *« le budget est
* une mesure dont la conso de la charge a déjà été retranchée »* — autrement dit **« je peux
* rendre ces watts parce que je peux les reprendre »**. C'est un **prêt consenti contre
* l'obéissance**, et rien ne vérifie que l'emprunteur rembourse.
*
* Ce test épingle le cas où la promesse est engagée : A est commandée à **0** et ses 4 400 W
* sont redistribués. Si A obéit, tout est juste. **Si A n'obéit pas** — commande perdue, mode
* manuel, véhicule qui limite — la maison tire 8 400 W pour 4 900 W de production, soit
* **3 500 W importés que personne n'a décidés**.
*
* \warning **Ce test épingle le comportement ACTUEL, qui est correct SOUS HYPOTHÈSE.** Il
* devient faux le jour où le recrédit sera conditionné à l'obéissance constatée
* (`command.divergent`) — et ce jour-là, c'est l'allocation de B qu'il faudra revoir, pas le
* test. La distinction est écrite ici pour qu'on ne le « corrige » pas par réflexe.
*/
void Simulation::testRecreditIsALoanAgainstObedience()
{
#ifndef ETM_ARBITRATOR
QSKIP("nécessite ETM_ARBITRATOR.");
#else
cleanupTestCase();
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
initTestCase();
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
QVERIFY(arb);
RuleBasedScheduler sched(arb);
SurplusContext ctx;
ctx.timestamp = utcDateTime(QDate(2026, 9, 1), QTime(13, 0, 0));
ctx.meter.exportW = 500; // 500 W exportés SEULEMENT
LoadContext a;
a.id = "A-coupee"; a.label = "A"; a.adapter = "etmvariableload"; a.priority = 1;
a.declared.maxPowerW = 6000;
a.declared.minPowerW = 5000; // plancher AU-DESSUS du budget : A est coupée
a.telemetry.currentPowerW = 4400; // …mais elle consomme 4 400 W à cet instant
ctx.loads.append(a);
LoadContext b;
b.id = "B-suivante"; b.label = "B"; b.adapter = "etmvariableload"; b.priority = 2;
b.declared.maxPowerW = 4000;
ctx.loads.append(b);
const Plan plan = sched.getPlan(ctx);
QHash<QString, LoadAction> parId;
for (const LoadAction &act : plan.timeSlots.first().actions)
parId.insert(act.loadId, act);
// 1. A est coupée — son plancher n'est pas payable, et le budget PASSE à la suivante.
QCOMPARE(qRound(parId.value("A-coupee").estimatedPowerW), 0);
QCOMPARE(parId.value("A-coupee").reason.code, QString(DecisionCode::BelowMinPower));
// 2. B reçoit 4 000 W — c'est-à-dire la conso de A, PRÊTÉE d'avance sur sa coupure.
QCOMPARE(qRound(parId.value("B-suivante").estimatedPowerW), 4000);
// 3. LE PRÊT EST VISIBLE DANS LA COMPTABILITÉ, et c'est ce qui le rend vérifiable : 4 000 W
// alloués pour 500 W de surplus réel. L'écart est exactement le recrédit.
QCOMPARE(qRound(plan.budget.surplusW), 500);
QCOMPARE(qRound(plan.budget.recreditedW), 4400);
QCOMPARE(qRound(plan.budget.allocatedW), 4000);
QVERIFY2(plan.budget.allocatedW > plan.budget.surplusW,
"le recrédit prête d'avance ce que la coupure de A doit libérer");
// 4. L'identité de réconciliation TIENT malgré tout — surplus + recrédit − alloué == restant.
// C'est ce qui distingue un prêt d'une erreur : la comptabilité est juste, c'est
// l'hypothèse qu'elle repose dessus qui n'est pas vérifiée.
QCOMPARE(qRound(plan.budget.surplusW + plan.budget.recreditedW - plan.budget.allocatedW),
qRound(plan.budget.remainingW));
#endif
}
void Simulation::testCountsSumToTarget()
{
#ifndef ETM_ARBITRATOR

View File

@ -198,6 +198,7 @@ private slots:
void testEcoPassBuysAndSaysTheTrajectory();
void testEcoFloorMissedSaysWhatWasLostAndWhy();
void testCommandDivergenceIsPublishedNotDiagnosed();
void testRecreditIsALoanAgainstObedience();
void testARefusedStateIsDistinguishableFromAnApplied();
void testL4ShedsAPacAndSaysItForcedTheLock();