diff --git a/docs/BRIEF_plugin_depuis_maquettes.md b/docs/BRIEF_plugin_depuis_maquettes.md index be8053f..a71e80b 100644 --- a/docs/BRIEF_plugin_depuis_maquettes.md +++ b/docs/BRIEF_plugin_depuis_maquettes.md @@ -23,10 +23,23 @@ sortie unique (`NymeaService.setLoadConfig`). L'écriture passe, mais la marque la box » disparaîtra donc d'une borne que personne n'a classée, au premier renommage d'une autre charge. -**La question** : un écho **inchangé** — même valeur, même id, rang inchangé — peut-il être -toléré ? Si oui, l'app le renverra tel quel et la marque survivra aux gestes qui ne la -concernent pas. Sinon, la marque est par construction fragile, et il vaut mieux le savoir -que le découvrir sur une installation client. +**La demande** : tolérer l'écho **inchangé** — même id, même rang, même valeur. + +Le refus vise à empêcher un client de **fabriquer une affirmation** sur un geste du moteur. +Renvoyer à l'identique ce que le moteur a écrit n'affirme rien de neuf : **la règle doit +porter sur le changement, pas sur la présence** — exactement comme l'effacement de +`rankOrigin` porte sur « rang différent pour cet id » et non sur « écriture reçue ». Même +distinction, un cran plus loin. + +L'alternative rend la marque **fragile par construction** : l'app doit alors la nettoyer sur +toutes les charges à chaque écriture, donc le badge s'éteint sur une borne que personne n'a +classée, au premier renommage d'une autre charge. Une information qui s'efface au premier +geste voisin ne vaut pas d'être publiée. + +**Et une règle pour le contrat, parce que ça se reproduira** : une règle qui interdit une +valeur à l'écriture doit dire ce qu'elle fait d'un **écho inchangé**, sous peine de rendre +l'écriture verbatim impossible. Le motif est écrit côté app (`docs/UI_data_contract.md` +§10) avec le contrôle à passer une fois par règle. Trois options écartées côté app, pour situer : refuser l'écriture (une installation avec une borne neuve deviendrait non modifiable), renvoyer `"auto"` (refus systématique), diff --git a/docs/UI_data_contract.md b/docs/UI_data_contract.md index 418548c..4a2dd79 100644 --- a/docs/UI_data_contract.md +++ b/docs/UI_data_contract.md @@ -2,6 +2,9 @@ > Compagnon de `DASHBOARD_SPEC.md`. Tranche la question récurrente > « **qui calcule les valeurs affichées ?** » pour chaque écran. +> Rev. 7 — ajout du **§10** : une valeur interdite à l'écriture doit dire ce qu'elle fait +> d'un écho inchangé, sinon l'écriture verbatim devient impossible. Et le motif derrière : +> trois fois en deux jours, le chemin exposé n'était pas celui qu'on regardait. > Rev. 6 — ajout du **§9** : deux réponses de la même box peuvent employer deux > conventions sans qu'aucun signal ne le dise, et l'erreur est toujours muette. > Deux occurrences en une semaine (clés de log sans préfixe `currentPower`, @@ -409,6 +412,74 @@ chance de découvrir le bug ; afficher `0` l'enterre. --- +## 10. Une valeur interdite à l'écriture doit dire ce qu'elle fait d'un ÉCHO inchangé + +> **Motif, comme le §9 — et il vient de le compléter.** Le §9 dit que deux réponses de la +> même box peuvent employer deux conventions sans le signaler. Celui-ci dit que deux +> **règles** posées séparément, chacune juste, peuvent se contredire au point de rendre une +> écriture impossible. Le symptôme est le même : muet. + +### Le cas qui l'a produit + +Deux règles, écrites à des moments différents, chacune correcte : + +| Règle | Où | Pourquoi elle est bonne | +|---|---|---| +| L'app écrit **verbatim** — elle recopie ce que la box lui a rendu | `payloadWithoutLoad`, `flattenToPayload` | `SetLoadConfig` remplace tout l'ensemble : reconstruire depuis le modèle typé perdrait, en silence et pour toutes les charges, un champ qu'une box plus récente porterait | +| `rankOrigin: "auto"` est **refusé** à tout client | `isValid()` côté moteur | empêche un écran de fabriquer une affirmation sur un geste du moteur | + +Leur produit : recopier verbatim une entrée marquée `"auto"`, **c'est écrire `"auto"`**. Donc +un simple **renommage** serait rejeté en bloc dès qu'une installation porte une borne +auto-provisionnée — motif invisible, le refus ne franchissant pas la frontière RPC. Le banc +ne pouvait pas le produire : aucune borne n'y a été détectée depuis `+etm27`. + +### La règle, et pourquoi elle porte sur le CHANGEMENT + +**Un interdit d'écriture doit se prononcer sur l'écho inchangé** — même id, même valeur, +même contexte. Renvoyer à l'identique ce que le moteur a écrit n'affirme rien de neuf : la +règle doit porter sur le **changement**, pas sur la **présence**. C'est exactement la +distinction que le moteur applique déjà à l'effacement de `rankOrigin` — il porte sur « un +rang différent pour cet id », pas sur « une écriture reçue » — à un cran de plus. + +L'alternative rend la marque **fragile par construction** : l'app doit alors la nettoyer sur +toutes les charges à chaque écriture, et le badge s'éteint sur une borne que personne n'a +classée, au premier renommage d'une autre charge. **Une information qui s'efface au premier +geste voisin ne vaut pas d'être publiée.** + +### Ce que ça impose à l'app, aujourd'hui + +Le nettoyage vit à un **point de sortie unique** (`NymeaService.setLoadConfig`), jamais sur +chaque chemin : un chemin ajouté demain hériterait sinon du refus sans que rien ne +l'annonce. C'est un contournement assumé, avec son coût écrit à côté — pas une solution. + +### Le contrôle à faire, une fois par règle d'écriture + +Pour chaque valeur qu'une box refuse en écriture, poser la question **avant** de la +rencontrer : + +1. l'app peut-elle la **lire** ? Si oui, elle finira par la **renvoyer**, puisqu'elle écrit + verbatim ; +2. l'écho inchangé est-il toléré ? Si la réponse n'est pas écrite, **c'est un trou**, pas un + détail d'implémentation ; +3. si l'écho est refusé, que devient l'information au premier geste voisin ? Si elle + disparaît, la publier est un leurre. + +### Et le motif derrière le motif + +**Trois fois en deux jours, le chemin exposé n'était pas celui qu'on regardait** : + +- l'écriture du mode de recharge visait « la première borne configurée » — trouvé en + **déplaçant** les boutons, pas en les testant ; +- le refus sur rangs doublés menaçait la **suppression** (qui réécrit verbatim), pas le + glisser-déposer qu'on surveillait ; +- le refus de `"auto"` menace le **renommage**, pas la gestion des rangs. + +Le point commun n'est pas la malchance : c'est que ces chemins **transportent** des données +qu'ils ne regardent pas. Un chemin qui recopie est exposé à tout ce que la charge utile +contient, y compris ce qui a été ajouté après lui. + +--- + ## Récap des décisions 1. Héos au centre du flow : **Option A — métaphore « cerveau » assumée** diff --git a/lib/services/load_priority.dart b/lib/services/load_priority.dart index 2f63c29..7345d01 100644 --- a/lib/services/load_priority.dart +++ b/lib/services/load_priority.dart @@ -153,8 +153,16 @@ List tiedByPriority(List entries) { /// /// C'est le moindre mal entre trois options, aucune bonne : refuser l'écriture (une /// installation avec une borne neuve deviendrait non modifiable), renvoyer `"auto"` (refus -/// systématique), ou effacer une marque informative. **Question posée au moteur** : un -/// écho *inchangé* de `"auto"` peut-il être toléré ? Cf. `docs/BRIEF_plugin_depuis_maquettes.md`. +/// systématique), ou effacer une marque informative. +/// +/// **Contournement, pas solution.** La demande est portée au moteur : tolérer l'écho +/// *inchangé*, la règle devant porter sur le **changement** et non sur la **présence** — +/// même distinction que l'effacement de `rankOrigin`, qui porte sur « rang différent pour +/// cet id » et non sur « écriture reçue ». Le jour où l'écho passe, cette fonction disparaît +/// et la marque survit aux gestes qui ne la concernent pas. +/// +/// Le motif général — *un interdit d'écriture doit se prononcer sur l'écho inchangé* — est +/// écrit en `docs/UI_data_contract.md` §10, avec le contrôle à faire une fois par règle. List> sansRankOriginAuto(List> payload) => [ for (final m in payload) if (m['rankOrigin'] == 'auto') {...m, 'rankOrigin': ''} else m,