docs(spec): ECS-110-b et LM-302-b — exclusivité d'un Thing entre charges

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-08-09 16:29:28 +02:00
parent e37f204047
commit a7d5cf6995
2 changed files with 40 additions and 0 deletions

View File

@ -558,6 +558,37 @@ chemin d'erreur explicite (refus + message). Les `Q_ASSERT` restants ne sont
admis que sur des invariants **structurels**, impossibles à violer depuis la
configuration — les deux premières lignes du tableau.
**ECS-110-b — Un Thing n'appartient qu'à UNE charge active.** Ajouté le 2026-08-09,
après vérification demandée : `LoadConfig::isValid()` s'arrête au bord d'une charge et
ne voyait donc **rien** du cas général. Le trou était réel et complet — deux charges de
configuration pouvaient revendiquer le même Thing (un relais déclaré à la fois dans la
charge ECS et dans un état SG-Ready), avec exactement les conséquences que la garde de
l'arbitre évite pour la PAC codée en dur : deux commandes contradictoires sur un organe,
et sa puissance comptée deux fois dans le budget (règle absolue 1). Rien ne l'empêchait,
et rien ne l'aurait signalé. Deux doublons **intra**-charge manquaient aussi : deux
étages de `relay-router` sur le même Thing (le routeur les fusionnait en silence, en
annonçant le double de la puissance réelle) et un relais répété dans un même état
SG-Ready.
La vérification d'ensemble ne peut pas vivre dans `isValid()` : elle est portée par
`LoadConfigStore::validateSet()`, appelée par `setConfigs()` (rejet total, rien persisté)
et au chargement (l'entrée en conflit est écartée, le reste du fichier survit — écarter
une charge ne commande rien, la charger commanderait faux).
Deux points de portée, tranchés :
- **Seules les charges `enabled` sont confrontées.** Une charge désactivée ne construit
aucun adaptateur ; l'interdire empêcherait de préparer une configuration de
remplacement. Activer repasse par `setConfigs()`, donc par la vérification ;
- **un relais présent dans plusieurs états SG-Ready reste légitime** — l'état 4 est
précisément l'union des états 1 et 3. Seule la répétition dans un **même** état est
refusée.
Les identifiants sont normalisés via `QUuid` : la configuration mélange `{uuid}` et
`uuid`, une comparaison textuelle aurait laissé passer le conflit. Les identifiants de
charge en double sont refusés par la même passe — la seconde entrée était jusqu'ici
perdue en silence à la construction.
**ECS-111 — Suppression d'un Thing référencé.** Comportement non défini
aujourd'hui : `findConfiguredThing` renvoie `nullptr`, `writeRelay` logue et
continue, `telemetry()` saute le relais donc `metered = false` et l'on retombe
@ -640,6 +671,7 @@ mécanique reste **L0**. Perte de sonde → mode sans sonde, pas arrêt d'urgenc
| ECS-411 | simulation | `testEcsRestartRecovery` |
| ECS-413 | simulation | `testEcsDisableLeavesSafeState` — + cas négatif : un rebuild sans désactivation ne coupe pas |
| ECS-414 | simulation | `testSgReadyPartialFailure` — plancher = état 2, atomicité du repli, contact injoignable |
| ECS-110-b | simulation | `testThingOwnershipIsExclusive` — conflit entre charges (3 mécanismes), formes d'uuid, id dupliqué, doublons intra-charge, tolérance d'une charge désactivée |
| ECS-110 (SG-Ready) / LM-300 | simulation | `testSgReadyFromConfig` — refus sans état 2, refus de charges utiles mélangées, round-trip, pilotage, désactivation → état 2 |
| ECS-110, ECS-111 | unitaire | `testEcsConfigValidation` — DOIT s'exécuter aussi en build release (`QT_NO_DEBUG`), sinon il ne prouve rien du binaire livré |
| ECS-501, ECS-502 | simulation | `testEcsStageFault` |
@ -711,6 +743,7 @@ protocole dans le même lot.**
| 2026-08-08 | **Étape 1 OUVERTE** — trois exigences : ECS-306, ECS-411, ECS-412 |
| 2026-08-08 | ECS-412 : au démarrage à froid, le verrou est **ARMÉ** (défaut sûr), jamais purgé |
| 2026-08-09 | ECS-412 précisé : cet armement est **TRANSITOIRE** et posé paresseusement au premier `now`. Défaut de blocage circulaire constaté **au banc**, corrigé, couvert par `testEcsColdStartLockExpires` |
| 2026-08-09 | **ECS-110-b créé et FAIT** — un Thing n'appartient qu'à une charge active. Trou trouvé en répondant à la question « le cas général est-il couvert ? » : il ne l'était pas du tout. `LoadConfigStore::validateSet()` ; doublons intra-charge également refusés |
| 2026-08-09 | **SG-Ready configurable (LM-300)** — union discriminée par mécanisme ; les deux `Q_ASSERT` de `sgreadyadapter.cpp` retirés au profit d'un refus `isValid()` + drapeau `m_usable` (ECS-110 tenu hors `Q_ASSERT`) ; principe général porté en LM-104, hors ECS |
| 2026-08-09 | **ECS-414 FAIT** — échelle généralisée à SgReady et EtmVariableLoad ; plancher = état 2 pour la PAC ; repli soumis au même `transientHarm` que l'aller ; contact injoignable supposé fermé mais toujours commandé |
| 2026-08-09 | **ECS-414 créé** — généralisation d'ECS-410 à SgReady et EtmVariableLoad, rattachée au lot SG-Ready en configuration |

View File

@ -129,6 +129,13 @@ une branche de fabrique. Rien d'autre ne bouge. **Vérifié à l'usage** : l'ajo
branche de fabrique dans `rebuildLoadAdapters()`, et un champ optionnel au schéma
RPC. Aucun code d'arbitrage n'a bougé.
**LM-302-b — Ce qu'une charge utile ne peut pas valider.** L'exclusivité d'un Thing est
une propriété de l'**ensemble** des charges, pas d'une charge : aucune charge utile ne
peut la vérifier, quelle que soit sa rigueur. Elle vit donc au niveau du store
(`validateSet()`), et tout mécanisme futur DOIT déclarer les Things qu'il revendique via
`LoadConfig::claimedThingIds()` — c'est le seul point à étendre, et l'oublier rend le
mécanisme invisible à la vérification. Voir ECS-110-b.
**LM-302** — Chaque charge utile valide la sienne. Les états invalides doivent
être inexprimables : pas de registre Modbus dans une configuration relais.