Patrick Schurig 9c5e3b146c fix: la contradiction progressMeasurable/regime, et la garde qui la rend sûre
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-09-03 07:21:13 +02:00

76 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Cherche les CONTRÔLES QUI NE CONTRÔLENT RIEN.
La question, posée à chaque assertion : **qu'aurait-elle attrapé si elle avait échoué ?**
S'il n'y a pas de réponse, elle ne contrôle rien — et le voyant vert ment.
Deux motifs, tous deux rencontrés en vrai :
1. ASSERTION CONDITIONNELLE DANS UNE BOUCLE. Si la condition n'est jamais vraie, l'assertion
ne s'exécute pas et le test passe. Vu le 2026-09-01 : « sans mesure, pas de `command` »
n'avait AUCUNE charge à examiner, et la garde retirée le test passait toujours.
Parade : compter les cas examinés, échouer si le compte est nul.
2. IDENTITÉ QUI SE REFERME SUR ELLE-MÊME. Quand les deux membres viennent de la même source,
l'identité est vraie par construction et rassure là où rien ne protège. Vu le 2026-09-03 :
`authorisedW − committedW == remainingW` relisait l'affectation d'une seule ligne.
⚠️ Toutes les identités auto-référentielles ne sont pas vaines : celle du BUDGET croise
`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
from pathlib import Path
ASSERT = r'(?:QCOMPARE|QVERIFY2?|QTRY_COMPARE|QTRY_VERIFY2?)'
def fonctions(txt):
for m in re.finditer(r'\nvoid\s+\w+::(\w+)\(\)\s*\n\{', txt):
deb = m.end(); prof, i = 1, deb
while i < len(txt) and prof:
if txt[i] == '{': prof += 1
elif txt[i] == '}': prof -= 1
i += 1
yield m.group(1), txt[deb:i], txt[:m.start()].count('\n')+2
cond, miroir = [], []
for f in Path('tests/auto').rglob('*.cpp'):
if 'mock' in str(f) or 'experience' in str(f): continue
txt = f.read_text(encoding='utf-8')
for nom, corps, ligne in fonctions(txt):
if nom.endswith('_data'): continue
# MOTIF 1 — assertion sous un `if` DANS une boucle : si la condition n'est jamais
# vraie, l'assertion ne s'exécute pas et le test passe sans rien vérifier.
for m in re.finditer(r'for\s*\([^)]*\)\s*\{(.{0,900}?)\n\s*\}', corps, re.S):
bloc = m.group(1)
if re.search(r'\n\s*if\s*\([^)]*\)[^;{]*\n?\s*' + ASSERT, bloc):
garde = re.search(r'\b(examin|vus|compte|trouv)\w*\b', corps, re.I)
cond.append((f, ligne, nom, "gardé" if garde else "NON GARDÉ"))
break
# MOTIF 2 — comparaison dont les deux membres partent de la MÊME racine.
for m in re.finditer(r'QCOMPARE\(\s*([^,]+?)\s*,\s*([^;]+?)\s*\)\s*;', corps):
a, b = m.group(1), m.group(2)
ra = re.match(r'\s*(?:qRound\()?\s*([A-Za-z_][\w]*)', a)
rb = re.match(r'\s*(?:qRound\()?\s*([A-Za-z_][\w]*)', b)
if ra and rb and ra.group(1) == rb.group(1) and '.' in a and '.' in b:
miroir.append((f, ligne, nom, m.group(0)[:96]))
print("=== MOTIF 1 — assertion conditionnelle dans une boucle : %d ===" % len(cond))
print(" Si la condition n'est jamais vraie, rien ne s'exécute et le test PASSE.")
for f, l, n, g in cond: print(" [%s] %s:%d %s" % (g, f, l, n))
print()
print("=== MOTIF 2 — les deux membres viennent de la MÊME racine : %d ===" % len(miroir))
print(" L'identité se referme sur elle-même : vraie par construction.")
for f, l, n, t in miroir: print(" %s:%d %s\n %s" % (f, l, n, t))