From a7d5cf6995bcadf9056a504bf899cda89f5b7ab1 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sun, 9 Aug 2026 16:29:28 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec):=20ECS-110-b=20et=20LM-302-b=20?= =?UTF-8?q?=E2=80=94=20exclusivit=C3=A9=20d'un=20Thing=20entre=20charges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- specs/spec_ecs.md | 33 +++++++++++++++++++++++++++++++++ specs/spec_loadmodel.md | 7 +++++++ 2 files changed, 40 insertions(+) diff --git a/specs/spec_ecs.md b/specs/spec_ecs.md index 6c54d6e..f3341ec 100644 --- a/specs/spec_ecs.md +++ b/specs/spec_ecs.md @@ -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 | diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index 83e5422..b0c1910 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -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.