From a510202ac7778dfea91b6031eb8715cd6ae33afe Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Thu, 3 Sep 2026 07:21:13 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20la=20contradiction=20progressMeasurable/?= =?UTF-8?q?regime,=20et=20la=20garde=20qui=20la=20rend=20s=C3=BBre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le premier dit ce que le MATÉRIEL permet, le second ce que le MOTEUR fait. Seul l'EvAdapter remplissait sessionMeasurable, donc sur toute charge non-VE le premier disait oui et le second non — et l'avertissement de saisie de l'app, qui se déclenche sur progressMeasurable, restait muet exactement sur les charges achetées à l'aveugle. sessionWh est alimenté depuis le compteur désigné. L'avertissement de l'app est arrivé AVANT que j'écrive la garde, et il était décisif. Un compteur qui disparaît peut publier 0 au lieu de se taire : la V2C a forgé un zéro sur currentPower et ses booléens, mais laissé sessionEnergy à sa dernière valeur. La politique dépend du plugin — donc inutilisable comme garde. Et le point à vérifier avait une réponse nette : il n'existe AUCUN signal de joignabilité fiable. setupComplete parle du setup et non du lien ; connected est maintenu par le plugin, la V2C basculant après trois échecs quand un compteur générique peut ne jamais basculer. La garde ne pouvait pas reposer là-dessus. Elle porte donc sur la SÉMANTIQUE du compteur, seule chose que le moteur sache sans parier : sur un compteur de session un recul est une session neuve, sur un compteur de vie c'est une anomalie — on gèle, on ne recapture jamais. Sans elle, le scénario chiffré sort tel quel : base reprise à 0 pendant l'absence, retour avec 10 kWh réels, deliveredWh à 10 000 en un cycle, obligation déclarée tenue sans rien de livré. Et R6 ne l'attrape pas : elle n'interdit ECO_FLOOR_MET que sous unmeasurable, alors que ce chemin est MESURÉ — c'est le seul par lequel un faux « tenue » peut sortir, et c'est celui qu'ouvrait l'alimentation depuis un compteur. audit-controles.py dit maintenant en tête qu'il ne tranche pas et ne doit jamais trancher : neuf des dix identités relevées contrôlaient réellement, et un verdict automatique aurait supprimé neuf contrôles utiles. AGENTS.md : la diversité du mock n'est pas un confort de test, c'est une borne supérieure sur ce que la suite peut prouver — elle se fixe en écrivant les classes, pas les tests. Quatrième occurrence du piège d'insertion doxygen, rattrapée par le même contrôle. Comme annoncé. Suite : simulation 63/63, loadmodel 22/22, charging 48/48 identique à la référence, spotmarket 32/32, doxygen 0 avertissement. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p --- debian-qt5/changelog | 36 +++++++++++ energyplugin/etm/energyarbitrator.cpp | 59 +++++++++++++++++- energyplugin/etm/energyarbitrator.h | 17 +++++ energyplugin/etm/types/surpluscontext.h | 28 +++++++++ specs/spec_loadmodel.md | 25 ++++++++ tests/auto/simulation/simulation.cpp | 83 +++++++++++++++++++++++++ tests/auto/simulation/simulation.h | 1 + tools/audit-controles.py | 10 +++ 8 files changed, 257 insertions(+), 2 deletions(-) diff --git a/debian-qt5/changelog b/debian-qt5/changelog index c415865..5083573 100644 --- a/debian-qt5/changelog +++ b/debian-qt5/changelog @@ -1,3 +1,39 @@ +powersync-energy-plugin-nymea (1.15.2+etm51) trixie; urgency=medium + + * URGENT — CONTRADICTION SUPPRIMÉE entre `progressMeasurable` et `regime`. Le premier dit ce + que le MATÉRIEL permet, le second ce que le MOTEUR fait ; seul l'EvAdapter remplissait + `sessionMeasurable`, donc sur toute charge non-VE le premier disait oui et le second non. + L'avertissement de saisie de l'app se déclenchant sur `progressMeasurable == false`, il + restait MUET exactement sur les charges achetées à l'aveugle. `sessionWh` est désormais + alimenté depuis le compteur DÉSIGNÉ pour les charges non-VE. + * LM-1012-d — UN ACCUMULATEUR NE PARIE JAMAIS SUR CE QU'UN PLUGIN FAIT DE SES ÉTATS EN TOMBANT. + Avertissement de l'app intégré AVANT l'écriture, et il était décisif : un compteur qui + disparaît peut publier 0 au lieu de se taire. Mesuré sur la V2C — `currentPower` 4 396 → 0 et + les booléens à false, mais `sessionEnergy` resté à sa dernière valeur. La politique dépend du + plugin, ce qui la rend inutilisable comme garde. + * ET IL N'EXISTE AUCUN SIGNAL DE JOIGNABILITÉ FIABLE — vérifié : `setupComplete` parle du setup + et non du lien, `connected` est maintenu par le plugin (V2C après trois échecs, un compteur + générique peut ne jamais basculer). La garde ne pouvait donc pas reposer là-dessus. + * LA GARDE PORTE SUR LA SÉMANTIQUE DU COMPTEUR, seule chose que le moteur sache sans parier : + un recul sur compteur de SESSION est une session neuve (recapture) ; sur compteur de VIE, + c'est une anomalie (GEL, jamais de recapture). Sans elle : base reprise à 0 pendant + l'absence, retour du compteur avec 10 kWh réels, `deliveredWh` saute à 10 000 en un cycle et + l'obligation se déclare TENUE sans que rien n'ait été livré. R6 ne l'attrape pas — elle + n'interdit ECO_FLOOR_MET que sous `unmeasurable`, et ce chemin-là est MESURÉ. Contre-épreuve : + garde retirée, le régime reste « measured » pendant la panne. + * `tools/audit-controles.py` — écrit en tête qu'il NE TRANCHE PAS et ne doit jamais trancher : + au balayage, neuf des dix identités relevées contrôlaient réellement. Un outil qui aurait + rendu un verdict aurait supprimé neuf contrôles utiles. + * AGENTS.md — la diversité du mock n'est pas un confort de test, c'est une BORNE SUPÉRIEURE sur + ce que la suite peut prouver, et elle se fixe en écrivant les classes, pas les tests. + Troisième occurrence documentée. + * Quatrième occurrence du piège d'insertion doxygen, rattrapée par le même contrôle — comme + annoncé. + * Suite : simulation 63/63, loadmodel 22/22, charging 48/48 identique à la référence, + spotmarket 32/32, doxygen 0 avertissement. + + -- Patrick Schurig Thu, 03 Sep 2026 15:00:00 +0200 + powersync-energy-plugin-nymea (1.15.2+etm50) trixie; urgency=medium * BALAYAGE — « qu'est-ce que ce contrôle aurait attrapé s'il avait échoué ? » passée à toute la diff --git a/energyplugin/etm/energyarbitrator.cpp b/energyplugin/etm/energyarbitrator.cpp index 43d5f5d..63f5ddd 100644 --- a/energyplugin/etm/energyarbitrator.cpp +++ b/energyplugin/etm/energyarbitrator.cpp @@ -585,7 +585,25 @@ EcoProgress EnergyArbitrator::ecoProgress(const LoadContext &lc, const QDateTime // réelle, et l'écran ne pourrait plus dire ce qui reste dû. if (!m_ecoMesureDepuis.contains(lc.id)) m_ecoMesureDepuis.insert(lc.id, now); - } else if (sessionWh < m_ecoBaseWh.value(lc.id) - BruitAvancementWh) { + } else if (lc.telemetry.sessionCumulative + && sessionWh < m_ecoBaseWh.value(lc.id) - BruitAvancementWh) { + // COMPTEUR DE VIE QUI RECULE — impossible physiquement, donc la valeur ne vaut rien : + // zéro forgé à la perte du lien, ou compteur remplacé. On NE RECAPTURE PAS, et on gèle + // l'avancement le temps de l'anomalie, comme si la mesure avait disparu. + // + // Recapturer ici serait le chemin du faux « tenue » : base reprise à 0 pendant + // l'absence, retour du compteur avec son cumul réel, et l'obligation se déclarerait + // tenue sans que rien n'ait été livré. R6 ne l'attraperait pas — elle n'interdit + // ECO_FLOOR_MET que sous `unmeasurable`, et ce chemin-là est MESURÉ. + qCInfo(dcNymeaEnergy()) << "[Arbitre] Avancement éco" << lc.id + << "— compteur de VIE en recul (" + << m_ecoBaseWh.value(lc.id) << "→" << sessionWh + << " Wh) : valeur ignorée, avancement gelé."; + p.regime = QString::fromLatin1(ProgressRegime::Unmeasurable); + p.purchasedWh = m_ecoAchatWh.value(lc.id, 0.0); + return p; // le tronçon reste OUVERT sur sa dernière valeur saine + } else if (!lc.telemetry.sessionCumulative + && sessionWh < m_ecoBaseWh.value(lc.id) - BruitAvancementWh) { // RECUL FRANC de l'accumulateur : le plugin l'a remis à la valeur brute, donc une // nouvelle session s'est ouverte. On recapture, sinon la différence devient négative. // Ce n'est PAS la détection de remise à zéro de la borne — celle-là vit dans le plugin, @@ -1030,6 +1048,40 @@ bool EnergyArbitrator::evSurplusPlannedByWaterfall(EvCharger *evCharger) const return evParticipatesInWaterfall(evCharger); } +void EnergyArbitrator::alimenterAvancement(LoadContext &lc) const +{ + // CONTRADICTION CORRIGÉE (2026-09-03, remontée par l'app). `progressMeasurable` dit ce que + // le MATÉRIEL permet ; `regime` dit ce que le MOTEUR fait. Seul l'EvAdapter remplissait + // `sessionMeasurable`, si bien que toute charge non-VE était `unmeasurable` alors que sa + // configuration annonçait l'inverse. + // + // Ce n'est pas qu'une incohérence d'affichage : l'avertissement de saisie de l'app se + // déclenche sur `progressMeasurable == false`, donc il restait MUET exactement sur les + // charges achetées à l'aveugle — celles dont l'obligation ne pouvait pas être suivie. + if (lc.telemetry.sessionMeasurable || !m_tm || !m_loadConfigStore) + return; // déjà renseigné par l'adaptateur (VE), ou rien pour le résoudre + + for (const LoadConfig &c : m_loadConfigStore->configs()) { + if (c.id() != lc.id || c.meterThingId().isEmpty()) + continue; + Thing *compteur = m_tm->findConfiguredThing(ThingId(c.meterThingId())); + // Il faut un CUMUL, pas une puissance : `currentPower` ne dit rien de ce qui a été + // livré. C'est la même exigence que celle publiée en `progressUnmeasurableCause`, et + // elle doit être la MÊME des deux côtés — sinon la contradiction se déplace au lieu de + // disparaître. + if (compteur && compteur->thingClass().hasStateType("totalEnergyConsumed")) { + lc.telemetry.sessionMeasurable = true; + // Compteur de VIE : un recul y est une ANOMALIE, jamais une nouvelle session. + lc.telemetry.sessionCumulative = true; + // Compteur de VIE, pas de session — et c'est sans importance : l'avancement est une + // DIFFÉRENCE depuis une base capturée à l'ouverture de l'obligation (LM-1012-b). + // Un cumul monotone est même plus sûr qu'un compteur de session, qui recule. + lc.telemetry.sessionWh = compteur->stateValue("totalEnergyConsumed").toDouble() * 1000.0; + } + return; + } +} + void EnergyArbitrator::qualifierRefus(LoadContext &lc, const QDateTime &now) const { const QDateTime depuis = m_divergenceDepuis.value(lc.id); @@ -1142,6 +1194,7 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const continue; } LoadContext lcq = it.value()->toLoadContext(now); + alimenterAvancement(lcq); qualifierRefus(lcq, now); ctx.loads.append(lcq); } @@ -1171,7 +1224,8 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const lc.telemetry.lockMinPowerW = 0; // rien d'engagé : rien à geler au budget lc.telemetry.lockMaxPowerW = -1; LoadContext lcq = lc; - qualifierRefus(lcq, now); + alimenterAvancement(lcq); + qualifierRefus(lcq, now); ctx.loads.append(lcq); } } @@ -1180,6 +1234,7 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const // sg-ready) — une seule table, toutes construites depuis la configuration persistée. --- for (auto it = m_loadAdapters.constBegin(); it != m_loadAdapters.constEnd(); ++it) { LoadContext lcq = it.value()->toLoadContext(now); + alimenterAvancement(lcq); qualifierRefus(lcq, now); ctx.loads.append(lcq); } diff --git a/energyplugin/etm/energyarbitrator.h b/energyplugin/etm/energyarbitrator.h index 28aaf4a..82891a2 100644 --- a/energyplugin/etm/energyarbitrator.h +++ b/energyplugin/etm/energyarbitrator.h @@ -705,6 +705,23 @@ private: */ void qualifierRefus(LoadContext &lc, const QDateTime &now) const; + /*! + * \brief Alimente l'avancement d'une charge NON-VE depuis son compteur désigné. + * \param[in,out] lc Charge du cycle. + * + * \par La contradiction que cette méthode supprime + * \c progressMeasurable dit ce que le MATÉRIEL permet ; \c regime dit ce que le MOTEUR fait. + * Seul l'\c EvAdapter remplissait \c sessionMeasurable — donc sur toute charge non-VE le + * premier disait oui et le second non. L'avertissement de saisie de l'app se déclenchant sur + * \c progressMeasurable, il restait **muet exactement sur les charges achetées à l'aveugle**. + * + * \note Il faut un CUMUL (\c totalEnergyConsumed), pas une puissance : \c currentPower ne dit + * rien de ce qui a été livré. Même exigence que \c progressUnmeasurableCause, et elle doit + * être la même des deux côtés, sinon la contradiction se déplace au lieu de disparaître. + */ + void alimenterAvancement(LoadContext &lc) const; + + /*! * \brief Publie l'écart entre ce qu'on a COMMANDÉ et ce qui est MESURÉ. diff --git a/energyplugin/etm/types/surpluscontext.h b/energyplugin/etm/types/surpluscontext.h index 76d17e6..1592dd4 100644 --- a/energyplugin/etm/types/surpluscontext.h +++ b/energyplugin/etm/types/surpluscontext.h @@ -111,6 +111,34 @@ struct LoadContextTelemetry { */ bool sessionMeasurable = false; + /*! + * \brief La source d'avancement est-elle un compteur de VIE (monotone) ou de SESSION ? + * + * \par Pourquoi cette distinction est la garde, et pas la joignabilité + * Un recul ne veut pas dire la même chose selon la source, et **c'est la seule chose que le + * moteur sache sans parier sur le plugin** : + * + * | Source | Un recul signifie | Geste | + * |---|---|---| + * | session (\c sessionEnergy) | une **nouvelle session** s'est ouverte | recapturer la base | + * | vie (\c totalEnergyConsumed) | la valeur n'est **pas fiable** — zéro forgé, compteur perdu | **geler**, ne jamais recapturer | + * + * **Il n'existe pas de signal de joignabilité sur lequel s'appuyer** : `setupComplete` parle + * du setup et non du lien, et `connected` est maintenu par le plugin — la V2C bascule après + * trois échecs, un compteur Modbus peut avoir un autre seuil, un compteur générique peut ne + * jamais basculer. **Un accumulateur ne doit pas parier sur ce que chaque plugin fait de ses + * états en tombant** : mesuré le 2026-08-30, la V2C a forgé un zéro sur \c currentPower et + * ses booléens, mais a laissé \c sessionEnergy à sa dernière valeur. La politique dépend du + * plugin, ce qui la rend inutilisable comme garde. + * + * \note Ce que ce champ évite, chiffré : base reprise à 0 pendant l'absence, retour du + * compteur avec ses 10 kWh réels, et \c deliveredWh saute à 10 000 en un cycle — + * l'obligation se déclare **tenue sans que rien n'ait été livré**. Et `R6` ne s'y oppose + * pas : elle n'interdit \c ECO_FLOOR_MET que sous \c unmeasurable. Le chemin **mesuré** + * est donc le seul par lequel un faux « tenue » peut sortir. + */ + bool sessionCumulative = false; + /*! * \brief Énergie chargée dans la session courante (Wh) — ACCUMULATEUR, pas compteur brut. * diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index 3a87659..c5c9301 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -1014,6 +1014,31 @@ part** (`measuredGaps`). Redater à chaque retour effacerait l'histoire au lieu ne rien dire ferait croire à une continuité que le battement dément. L'écran peut alors écrire « mesuré **par intermittence** depuis 09:14 », qui est la phrase vraie. +**LM-1012-d — un accumulateur ne parie JAMAIS sur ce qu'un plugin fait de ses états en tombant.** +*(2026-09-03, avertissement de l'app intégré AVANT l'écriture.)* + +Un compteur qui disparaît peut **publier 0 au lieu de se taire**. Mesuré sur la V2C le +2026-08-30 : `currentPower` 4 396 → 0 et les booléens à `false` à la perte du lien — mais +`sessionEnergy` **resté à sa dernière valeur**. **La politique dépend du plugin, ce qui la rend +inutilisable comme garde.** + +**Et il n'existe aucun signal de joignabilité sur lequel s'appuyer** : `setupComplete` parle du +setup et non du lien ; `connected` est maintenu par le plugin — la V2C bascule après trois échecs, +un compteur Modbus peut avoir un autre seuil, un compteur générique peut ne jamais basculer. + +**La garde porte donc sur ce que le moteur sait sans parier : la SÉMANTIQUE du compteur.** + +| Source | Un recul signifie | Geste | +|---|---|---| +| **session** (`sessionEnergy`) | une nouvelle session s'est ouverte | recapturer la base | +| **vie** (`totalEnergyConsumed`) | la valeur n'est pas fiable | **geler**, ne jamais recapturer | + +> **Ce que cela évite, chiffré.** Base reprise à 0 pendant l'absence, retour du compteur avec ses +> 10 kWh réels, et `deliveredWh` saute à 10 000 en un cycle : **l'obligation se déclare tenue sans +> que rien n'ait été livré**. Et `R6` ne s'y oppose pas — elle n'interdit `ECO_FLOOR_MET` que sous +> `unmeasurable`. **Le chemin mesuré est le seul par lequel un faux « tenue » peut sortir**, et +> c'est précisément celui qu'ouvre l'alimentation de `sessionWh` depuis un compteur. + **LM-1013-b — une obligation en ÉNERGIE devient une cible en WATTS par étalement uniforme.** *(§10 étape D, tranché le 2026-08-31.)* diff --git a/tests/auto/simulation/simulation.cpp b/tests/auto/simulation/simulation.cpp index b555eb5..9d34c6d 100644 --- a/tests/auto/simulation/simulation.cpp +++ b/tests/auto/simulation/simulation.cpp @@ -7551,6 +7551,89 @@ void Simulation::testHealthyButUnmeasuredDiffersFromFaulty() #endif } +/*! + * \brief Un compteur de VIE qui recule ne rebase JAMAIS l'avancement. + * + * \par Le chemin du faux « tenue », et il est le seul + * Un compteur qui disparaît peut publier **0 au lieu de se taire** — mesuré sur la V2C le + * 2026-08-30 : `currentPower` 4 396 → 0 et les booléens à `false` à la perte du lien. Si la base + * se reprenait sur ce zéro, le retour du compteur avec son cumul réel — 10 kWh sur l'ECS du banc + * — ferait sauter `deliveredWh` à 10 000 en un cycle : **l'obligation se déclarerait tenue sans + * que rien n'ait été livré**. + * + * Et `R6` ne s'y oppose pas : elle n'interdit `ECO_FLOOR_MET` que sous `unmeasurable`. Le chemin + * **mesuré** est donc le seul par lequel un faux « tenue » peut sortir — c'est-à-dire exactement + * celui qu'ouvre l'alimentation de `sessionWh` depuis un compteur. + * + * \par Pourquoi la garde porte sur la SÉMANTIQUE et non sur la joignabilité + * Il n'existe **aucun signal de joignabilité fiable** : `setupComplete` parle du setup et non du + * lien, et `connected` est maintenu par le plugin — la V2C bascule après trois échecs, un + * compteur générique peut ne jamais basculer. **Un accumulateur ne doit pas parier sur ce que + * chaque plugin fait de ses états en tombant** : la même V2C a forgé un zéro sur `currentPower` + * et laissé `sessionEnergy` à sa dernière valeur. + * + * Ce que le moteur sait sans parier : **un compteur de VIE ne recule pas.** Un recul y est une + * anomalie ; sur un compteur de SESSION, c'est une session neuve. Deux sources, deux gestes. + */ +void Simulation::testLifetimeCounterDropNeverRebasesTheProgress() +{ +#ifndef ETM_ARBITRATOR + QSKIP("nécessite ETM_ARBITRATOR."); +#else + cleanupTestCase(); + m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite"; + initTestCase(); + EnergyArbitrator *arb = dynamic_cast(m_experiencePlugin->smartChargingManager()); + QVERIFY(arb); + + const QDateTime t0 = utcDateTime(QDate(2026, 9, 3), QTime(13, 0, 0)); + + LoadContext ecs; + ecs.id = "ecs-compteur-vie"; + ecs.needs.minEnergyWhPerDay = 4000; + ecs.needs.dailyDeadline = "07:00"; + ecs.telemetry.sessionMeasurable = true; + ecs.telemetry.sessionCumulative = true; // totalEnergyConsumed : monotone par nature + ecs.telemetry.sessionWh = 9000; // le compteur en est à 9 kWh de vie + + QCOMPARE(qRound(arb->ecoProgress(ecs, t0).deliveredWh), 0); // base prise à 9000 + ecs.telemetry.sessionWh = 9500; + QCOMPARE(qRound(arb->ecoProgress(ecs, t0).deliveredWh), 500); // 500 Wh livrés + + // LE LIEN TOMBE et le plugin publie ZÉRO au lieu de se taire. + ecs.telemetry.sessionWh = 0; + const EcoProgress pendant = arb->ecoProgress(ecs, t0); + // On GÈLE : la valeur ne vaut rien, donc on ne conclut rien. Pas d'avancement publié, pas + // de recapture — le tronçon reste ouvert sur sa dernière valeur saine. + QCOMPARE(pendant.regime, QString(ProgressRegime::Unmeasurable)); + QVERIFY2(!pendant.delivered, "sur une valeur non fiable, on ne publie pas d'avancement"); + + // LE COMPTEUR REVIENT avec son cumul réel. C'est ici que le faux « tenue » sortirait si la + // base avait été reprise à 0 : 10 000 Wh d'un coup sur une obligation de 4 000. + ecs.telemetry.sessionWh = 10000; + const EcoProgress apresRetour = arb->ecoProgress(ecs, t0); + QCOMPARE(apresRetour.regime, QString(ProgressRegime::Measured)); + // 10 000 − 9 000 (base intacte) = 1 000 Wh. PAS 10 000. + QCOMPARE(qRound(apresRetour.deliveredWh), 1000); + QVERIFY2(apresRetour.deliveredWh < ecs.needs.minEnergyWhPerDay, + "l'obligation ne doit PAS se déclarer tenue sur un retour de compteur"); + + // Et la distinction tient : sur un compteur de SESSION, le même recul EST une session neuve. + LoadContext borne; + borne.id = "borne-session"; + borne.needs.minEnergyWhPerDay = 4000; + borne.needs.dailyDeadline = "07:00"; + borne.telemetry.sessionMeasurable = true; + borne.telemetry.sessionCumulative = false; // sessionEnergy : remis à zéro par session + borne.telemetry.sessionWh = 9000; + QCOMPARE(qRound(arb->ecoProgress(borne, t0).deliveredWh), 0); + borne.telemetry.sessionWh = 120; // nouvelle session + const EcoProgress neuve = arb->ecoProgress(borne, t0); + QCOMPARE(neuve.regime, QString(ProgressRegime::Measured)); // pas d'anomalie : on recapture + QCOMPARE(qRound(neuve.deliveredWh), 0); +#endif +} + void Simulation::testCountsSumToTarget() { #ifndef ETM_ARBITRATOR diff --git a/tests/auto/simulation/simulation.h b/tests/auto/simulation/simulation.h index 828dcc7..cc63b4e 100644 --- a/tests/auto/simulation/simulation.h +++ b/tests/auto/simulation/simulation.h @@ -200,6 +200,7 @@ private slots: void testCommandDivergenceIsPublishedNotDiagnosed(); void testProgressMeasurabilityIsDeclaredAtConfigTime(); void testHealthyButUnmeasuredDiffersFromFaulty(); + void testLifetimeCounterDropNeverRebasesTheProgress(); void testRecreditIsALoanAgainstObedience(); void testARefusedStateIsDistinguishableFromAnApplied(); void testL4ShedsAPacAndSaysItForcedTheLock(); diff --git a/tools/audit-controles.py b/tools/audit-controles.py index 43aa367..644e64e 100644 --- a/tools/audit-controles.py +++ b/tools/audit-controles.py @@ -19,6 +19,16 @@ Deux motifs, tous deux rencontrés en vrai : `remainingSurplusW`, filé le long de la cascade, avec `allocatedW` accumulé en trois sites distincts. Le motif signale, il ne juge pas — c'est à la lecture de trancher. +⚠️ CET OUTIL NE TRANCHE PAS, ET NE DOIT JAMAIS TRANCHER. + +Il SIGNALE un motif ; c'est la LECTURE qui décide. Au balayage du 2026-09-03, il a relevé dix +identités auto-référentielles : NEUF contrôlaient réellement quelque chose, une seule était vaine. +Un outil qui aurait rendu un verdict aurait supprimé neuf contrôles utiles. + +Le prochain qui lira ce fichier sera tenté d'automatiser le verdict. Ne le fais pas : la question +« qu'aurait-il attrapé s'il avait échoué ? » demande de savoir d'où viennent les deux membres, et +cette connaissance n'est pas dans le texte du test. + Cet outil ne remplace pas la question, il la pose là où elle se pose souvent. """ import re