Premier usage réel de l'index, et il donne un résultat inattendu : sur les 70
règles « spécifiée seule », une part notable n'est PAS un trou d'implémentation
mais un trou de CITATION. Vérifié en cherchant le CONTENU dans le code plutôt que
l'identifiant :
ECS-110-b un Thing n'appartient qu'à une charge active → validateSet()
LM-302-b l'exclusivité ne se valide pas dans une charge utile → idem
LM-1209-a-bis écarter et refuser ne sont pas la même fonction → validateRanks()
ECS-410-b l'issue de ClearLoadFault est journalisée par l'arbitre
LM-303 les verrous appartiennent au mécanisme, pas au domaine
LM-1208-c sans voiture, le nombre de phases vient de la borne
ECS-308 MaxRelays TRONQUE au lieu de refuser
Toutes portées par du code, aucune citée. L'index les comptait donc comme
spécifiées seules — il SOUS-DÉCLARE la couverture, exactement comme il peut la
surdéclarer. C'est la même limite que celle écrite en tête de la page générée
(« cité par n'est pas couvert par »), prise par l'autre bout : un identifiant
absent d'un commentaire ne dit rien de ce que le code fait.
Citations ajoutées avec \rule{}, l'alias posé hier — donc navigables du
commentaire vers la règle. Aucun changement de comportement : ce sont des
commentaires. 63 « spécifiée seule » au lieu de 70, 21 citées par le code au lieu
de 14.
ci-quality 3/3, build 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
124 lines
6.5 KiB
C++
124 lines
6.5 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
|
#pragma once
|
|
|
|
#include <QObject>
|
|
#include <QString>
|
|
#include "../types/loadconfig.h"
|
|
|
|
/*!
|
|
* \brief Store persistant des configs de charge pilotée (contrat etmvariableload §4).
|
|
*
|
|
* Master store unique : \c setConfigs() valide, **persiste** (\c /var/lib/nymea/\c
|
|
* energy-load-configuration.json, à côté de \c energy-manager-configuration.json) et émet
|
|
* \c changed() — l'arbitre reconstruit ses adaptateurs sur ce signal, le handler notifie l'app.
|
|
*
|
|
* Format fichier : \c {"version":1,"loads":[ ... ]}, chaque entrée étant un \c LoadConfig §4.
|
|
* Le tableau \c loads[] est **mot pour mot** le \c LoadDescriptor §4 (jonction inter-repos
|
|
* avec l'app).
|
|
*
|
|
* \invariant Écriture atomique (fichier temporaire + \c rename) — jamais de fichier mi-écrit.
|
|
* \invariant \c setConfigs() rejette en bloc (rien persisté) si une seule entrée est invalide.
|
|
*/
|
|
class LoadConfigStore : public QObject
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
/*!
|
|
* \brief Construit le store et charge le fichier s'il existe.
|
|
* \param parent Propriétaire Qt.
|
|
* \note Un fichier absent, illisible ou au JSON invalide n'est pas une erreur :
|
|
* le store démarre à vide. Une ENTRÉE invalide est ignorée individuellement
|
|
* (tolérance ascendante) — on ne perd pas les autres pour une seule.
|
|
*/
|
|
explicit LoadConfigStore(QObject *parent = nullptr);
|
|
|
|
/*! \brief Configs actuellement en mémoire (chargées au démarrage ou via setConfigs).
|
|
* \return Copie de l'ensemble courant ; toutes ont passé \c LoadConfig::isValid(). */
|
|
LoadConfigs configs() const { return m_configs; }
|
|
|
|
/*!
|
|
* \brief Remplace l'ensemble des configs : valide tout → persiste → \c emit changed().
|
|
* \param configs Nouvel ensemble (remplace l'existant — l'app envoie la liste complète).
|
|
* \param[out] error Message FR si rejet (aucune écriture dans ce cas).
|
|
* \return true si validé + persisté ; false si invalide ou échec d'écriture.
|
|
*/
|
|
bool setConfigs(const LoadConfigs &configs, QString *error = nullptr);
|
|
|
|
/*! \brief Cohérence de l'ENSEMBLE, ce qu'aucune charge ne peut vérifier seule.
|
|
*
|
|
* ECS-110 s'arrête au bord d'une charge : \c LoadConfig::isValid() ne voit pas les
|
|
* autres. Deux exigences ne vivent donc que ici — identifiants uniques, et un Thing
|
|
* revendiqué par au plus UNE charge active. Le second cas est le même défaut que la
|
|
* garde de l'arbitre contre la PAC codée en dur, mais entre deux charges de
|
|
* configuration : deux commandes contradictoires sur un organe, et sa puissance
|
|
* comptée deux fois dans le budget (règle absolue 1).
|
|
*
|
|
* \note Seules les charges \c enabled sont confrontées. Une charge désactivée ne
|
|
* construit aucun adaptateur et ne commande rien ; l'interdire empêcherait de
|
|
* préparer une configuration de remplacement. La bascule \c enabled repasse par
|
|
* \c setConfigs(), donc par cette vérification.
|
|
*
|
|
* \param configs Ensemble complet à confronter (les charges désactivées sont ignorées).
|
|
* \param[out] error Motif de refus, en français, exploitable tel quel.
|
|
* \return true si l'ensemble est cohérent ; false au PREMIER conflit rencontré.
|
|
*/
|
|
static bool validateSet(const LoadConfigs &configs, QString *error = nullptr);
|
|
|
|
/*!
|
|
* \brief Contrôle de BONNE FORME, appliqué aux ÉCRITURES et à elles seules. §12 / LM-1209-a.
|
|
*
|
|
* \param configs Ensemble candidat. \param error Motif du refus, si refus.
|
|
* \return Faux si deux charges portent le même rang.
|
|
*
|
|
* \note **Distincte de \c validateSet(), et la distinction est de sûreté, pas de propreté**
|
|
* — \rule{LM-1209-a-bis} : une vérification qui ÉCARTE et une qui REFUSE ne peuvent pas
|
|
* être la même fonction.
|
|
* \c validateSet() nomme ce qui rend un ensemble DANGEREUX — deux charges sur un même
|
|
* organe — et \c load() s'en sert pour ÉCARTER l'entrée fautive. Celle-ci nomme ce qui
|
|
* rend un ensemble MAL FORMÉ, et le seul geste correct y est de REFUSER l'écriture
|
|
* entière. Les fusionner ferait écarter au démarrage les entrées d'une configuration
|
|
* héritée à rangs doublés : des charges disparaîtraient de la configuration ET de
|
|
* l'arbitrage, sans \c applySafeState — une règle de contrat convertie en perte de
|
|
* configuration silencieuse.
|
|
* \note N'est donc JAMAIS appelée par \c load(). Un doublon hérité est **signalé
|
|
* bruyamment et CONSERVÉ** ; la première écriture cliente le répare.
|
|
*/
|
|
static bool validateRanks(const LoadConfigs &configs, QString *error = nullptr);
|
|
|
|
/*!
|
|
* \brief Réconcilie \c rankOrigin d'une écriture CLIENTE avec l'état persisté. LM-1209-c.
|
|
*
|
|
* \param incoming Ensemble reçu du client. \param error Motif du refus, si refus.
|
|
* \param[out] out Ensemble à persister, \c rankOrigin réconcilié.
|
|
* \return Faux si le client tente de FABRIQUER un \c "auto".
|
|
*
|
|
* Trois règles, et la deuxième est celle qu'on se serait trompé à écrire :
|
|
* 1. Rang CHANGÉ pour cet id → la marque est **effacée** : quelqu'un a classé la charge.
|
|
* 2. Rang INCHANGÉ → la marque est **conservée telle quelle**, même si le client l'omet.
|
|
* `SetLoadConfig` remplace en bloc : réordonner une AUTRE charge renvoie forcément
|
|
* l'entrée de celle-ci, et l'effacer à cette occasion perdrait une marque encore
|
|
* vraie — la charge n'a toujours été classée par personne. **L'effacement porte sur le
|
|
* RANG, jamais sur la réception d'une écriture.**
|
|
* 3. \c "auto" reçu là où le persisté ne l'a pas → **refus**. Un client ne fabrique pas
|
|
* une affirmation sur un geste du moteur. L'aller-retour verbatim d'une entrée qui la
|
|
* porte déjà reste licite, sans quoi la neutralité Get→Set serait rompue.
|
|
*
|
|
* \note Un client lève la marque SANS déplacer la charge en envoyant \c "user" — c'est la
|
|
* seule voie, et elle est explicite. L'omission ne lève rien (règle 2).
|
|
*/
|
|
bool reconcileRankOrigin(const LoadConfigs &incoming, LoadConfigs *out,
|
|
QString *error = nullptr) const;
|
|
|
|
signals:
|
|
/*! \brief Émis après une persistance réussie de \c setConfigs(). */
|
|
void changed();
|
|
|
|
private:
|
|
QString filePath() const;
|
|
bool load();
|
|
bool save(QString *error) const;
|
|
|
|
LoadConfigs m_configs;
|
|
};
|