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:
parent
770cef13f3
commit
3eb59e86f9
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -198,6 +198,7 @@ private slots:
|
||||
void testEcoPassBuysAndSaysTheTrajectory();
|
||||
void testEcoFloorMissedSaysWhatWasLostAndWhy();
|
||||
void testCommandDivergenceIsPublishedNotDiagnosed();
|
||||
void testRecreditIsALoanAgainstObedience();
|
||||
void testARefusedStateIsDistinguishableFromAnApplied();
|
||||
void testL4ShedsAPacAndSaysItForcedTheLock();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user