Fonctions pures (ni E/S ni horloge, « maintenant » = ctx.timestamp) qui portent toute la sémantique du plan reçu ; le SocketScheduler les enchaînera. - valider() : JSON → plan interne ou verdict à code stable. Créneaux à décalage horaire explicite, ordonnés, sans chevauchement ; loadId connu et unique ; kind cohérent avec le mécanisme et annoncé dans capabilities.kinds ; reason non vide ; valeurs dans leur domaine ; validUntil ; créneau courant ; COUVERTURE Q1 (toute charge disponible nommée). Abstention / expiré / invalide distingués ; un plan invalide n'est jamais converti en partie. `force` et champs réservés (annexe F) ignorés et listés. - motifFiltre() : code hors liste blanche → OPTIMIZER_DECISION sans paramètres ; sinon seules les clés documentées (optimizerParamKeys, testé contre le tableau d'INTERFACE.md), valeurs nombre/booléen/code [A-Za-z]+. Le texte libre reste dans ActionRecue::texte, assaini, jamais dans le motif. - substituer() : le créneau courant remplace les actions de l'ombre à leur place ; charges indisponibles → action de l'ombre ; bornes commandées sorties du proxy (waterfallEvIds) ; budget/draw de l'ombre jamais gardés. - finaliser() : après dispatch, puissance APPLIQUÉE, niveau unique « optimizer », financement dérivé de la mesure (surplus puis réseau), counts cohérents, budget et draw recalculés (Q3, Q4), `clamped`. LoadAction::clamped ajouté (interne, non publié dans ce lot). Aucun appelant dans ce commit : le moteur est inchangé. Tests (loadmodel) : 7 fonctions, dont 31 verdicts data-driven. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
195 lines
10 KiB
C++
195 lines
10 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||
#pragma once
|
||
|
||
#include <QDateTime>
|
||
#include <QHash>
|
||
#include <QList>
|
||
#include <QSet>
|
||
#include <QString>
|
||
#include <QStringList>
|
||
#include <QVariantMap>
|
||
#include "../types/plan.h"
|
||
#include "../types/surpluscontext.h"
|
||
|
||
/*!
|
||
* \file optimizerplan.h
|
||
* \brief Plan d'un optimiseur externe : validation, conversion, substitution au plan ombre et
|
||
* comptabilité après dispatch (OPTIMIZER_PROTOCOL.md 1.1 §6-§7, brief 4 T3).
|
||
*
|
||
* Fonctions PURES : ni E/S, ni horloge (« maintenant » est \c ctx.timestamp), ni accès à
|
||
* l'arbitre ou à un adaptateur. Le \c SocketScheduler les enchaîne ; les tests les éprouvent
|
||
* seules.
|
||
*
|
||
* \par Le chemin des actions — un seul arbitre
|
||
* \c valider() convertit le \c result JSON en \c LoadAction internes ; \c substituer() les met
|
||
* à la place de celles du plan ombre (rule-based) dans le créneau courant ; l'arbitre les
|
||
* dispatche par \c applyActionsToAdapters(), exactement comme celles du rule-based — mêmes
|
||
* bornes, mêmes verrous, même L4 ; \c finaliser() relit ensuite ce que les adaptateurs ont
|
||
* RENDU pour publier la puissance appliquée, son financement et le budget du cycle (Q3, Q4).
|
||
*/
|
||
namespace OptimizerPlan {
|
||
|
||
//! Valeur de \c Plan::strategy d'un plan d'optimiseur.
|
||
constexpr const char *StrategieOptimiseur = "optimizer";
|
||
//! Niveau UNIQUE porté par une action d'optimiseur (décision Q3) : jamais ceux du plan ombre.
|
||
constexpr const char *NiveauOptimiseur = "optimizer";
|
||
//! Longueur maximale gardée d'un \c reason libre (journal, traces).
|
||
constexpr int TexteMax = 200;
|
||
|
||
/*! \brief Issue de la validation d'une réponse \c GetPlan. */
|
||
enum class Issue {
|
||
Valide, //!< Le créneau courant peut être appliqué.
|
||
Abstention, //!< Pas de plan pour maintenant (\c abstain, plan vide, aucun créneau courant) — §6-§7, pas un échec.
|
||
Expire, //!< \c validUntil dépassé — repli jusqu'au prochain plan valide, pas un échec.
|
||
Invalide //!< Plan refusé en entier, jamais appliqué partiellement — un échec (§7).
|
||
};
|
||
|
||
/*! \brief Une action reçue, convertie. */
|
||
struct ActionRecue {
|
||
LoadAction action; //!< Action interne : mécanisme, motif filtré, niveau \c optimizer provisoire.
|
||
QString adapter; //!< Mécanisme de la charge visée (\c LoadContext::adapter).
|
||
QString texte; //!< \c reason libre, assaini : journal et traces, JAMAIS une charge utile RPC.
|
||
QString codeRecu; //!< \c reasonCode tel que reçu (journal) ; vide s'il manquait.
|
||
bool codeSubstitue = false; //!< Code remplacé par \c OPTIMIZER_DECISION (hors liste blanche).
|
||
QStringList parametresSupprimes; //!< Clés de \c reasonParams retirées (non documentées ou valeur refusée).
|
||
};
|
||
|
||
/*! \brief Un créneau reçu. */
|
||
struct CreneauRecu {
|
||
QDateTime from; //!< Début, inclus.
|
||
QDateTime to; //!< Fin, exclue.
|
||
QList<ActionRecue> actions; //!< Actions converties, dans l'ordre reçu.
|
||
QVariantMap expected; //!< Trajectoire prévue — affichage et traces, JAMAIS pour commander.
|
||
};
|
||
|
||
/*! \brief Un plan reçu, validé. */
|
||
struct PlanRecu {
|
||
QString planId; //!< \c planId reçu (peut être vide).
|
||
QString strategy; //!< \c strategy reçue (informative).
|
||
QDateTime computedAt; //!< Invalide si absente ou illisible (informatif).
|
||
QDateTime validUntil; //!< Invalide = absente : valable tant qu'un créneau couvre maintenant.
|
||
QList<CreneauRecu> creneaux; //!< Ordonnés, sans chevauchement.
|
||
QVariantMap diagnostics; //!< Conservé pour l'app et les traces, jamais pour commander.
|
||
int indexCourant = -1; //!< Créneau couvrant \c ctx.timestamp ; -1 si aucun.
|
||
};
|
||
|
||
/*!
|
||
* \brief Verdict de \c valider().
|
||
*
|
||
* Codes (\c code, stables, destinés au compteur d'échecs §7 de T4) :
|
||
* - Abstention : \c abstain, \c empty-plan, \c no-current-slot ;
|
||
* - Expire : \c expired ;
|
||
* - Invalide : \c malformed-plan, \c bad-time, \c slots-unordered, \c malformed-action,
|
||
* \c unknown-load, \c duplicate-load, \c unknown-kind, \c kind-mismatch,
|
||
* \c kind-not-announced, \c missing-reason, \c bad-value, \c incomplete-slot.
|
||
*/
|
||
struct Verdict {
|
||
Issue issue = Issue::Invalide; //!< Issue.
|
||
QString code; //!< Vide si \c Valide.
|
||
QString detail; //!< Précision en français — JOURNAL seulement.
|
||
PlanRecu plan; //!< Plan converti ; complet seulement si \c Valide.
|
||
QStringList champsReserves; //!< Champs réservés ou \c force vus, ignorés (annexe F, §6).
|
||
//! Charges INDISPONIBLES dont le créneau courant portait une action : action rejetée, la
|
||
//! charge garde celle du plan ombre (brief §T3).
|
||
QStringList actionsSurIndisponibles;
|
||
};
|
||
|
||
/*!
|
||
* \brief Valide une réponse \c GetPlan et la convertit.
|
||
*
|
||
* Contrôles, dans cet ordre : forme (\c abstain, \c slots) ; chaque créneau (\c from < \c to,
|
||
* décalage horaire explicite, ordonnés et sans chevauchement) ; chaque action de CHAQUE
|
||
* créneau (\c loadId connu du contexte et unique dans son créneau, \c kind connu, cohérent avec
|
||
* le mécanisme et annoncé dans \c capabilities.kinds s'il est déclaré, \c reason non vide,
|
||
* valeurs numériques présentes et dans leur domaine) ; \c validUntil ; existence d'un créneau
|
||
* courant ; enfin COUVERTURE (décision Q1) : toute charge \c available du contexte figure dans
|
||
* le créneau courant.
|
||
*
|
||
* \param result \c result JSON-RPC de \c GetPlan, non validé.
|
||
* \param ctx Contexte du cycle : charges connues, disponibilité, et maintenant (\c ctx.timestamp).
|
||
* \param capabilities \c capabilities du handshake.
|
||
* \param hasCapabilities Vrai si le handshake portait \c capabilities.
|
||
* \return Le verdict. Un plan \c Invalide n'est JAMAIS appliqué, même en partie.
|
||
* \note \c force et les champs réservés (annexe F) sont ignorés et listés ; un champ inconnu est
|
||
* ignoré sans bruit (§9). Le texte \c reason ne quitte jamais \c ActionRecue::texte.
|
||
*/
|
||
Verdict valider(const QVariantMap &result, const SurplusContext &ctx,
|
||
const QVariantMap &capabilities, bool hasCapabilities);
|
||
|
||
/*!
|
||
* \brief Motif transmissible à l'app depuis un couple reçu (décision du 2026-10-16).
|
||
* \param code \c reasonCode reçu ; absent, inconnu ou hors liste blanche → \c OPTIMIZER_DECISION sans paramètres.
|
||
* \param params \c reasonParams reçus : seules les clés documentées du code passent
|
||
* (\c DecisionCode::optimizerParamKeys), avec une valeur nombre fini, booléen ou code \c [A-Za-z]+.
|
||
* \param[out] supprimes Clés retirées.
|
||
* \param[out] substitue Vrai si le code a été remplacé.
|
||
* \return Le motif — jamais de texte libre.
|
||
*/
|
||
DecisionReason motifFiltre(const QString &code, const QVariantMap ¶ms,
|
||
QStringList *supprimes = nullptr, bool *substitue = nullptr);
|
||
|
||
/*!
|
||
* \brief Texte libre d'un optimiseur rendu sûr pour une ligne de journal.
|
||
* \param v Valeur reçue. \return Caractères de contrôle remplacés par des espaces, blancs
|
||
* resserrés, coupé à \c TexteMax ; vide si \p v n'est pas une chaîne.
|
||
*/
|
||
QString assainirTexte(const QVariant &v);
|
||
|
||
/*!
|
||
* \brief Plan du cycle : le créneau courant du plan reçu À LA PLACE des actions de l'ombre.
|
||
*
|
||
* Pour chaque charge \c available visée par le créneau courant, l'action de l'optimiseur
|
||
* remplace celle de l'ombre, à la même place (l'ordre de priorité est conservé). Les charges
|
||
* indisponibles, et celles que le contexte ne connaît pas, gardent l'action de l'ombre. Les
|
||
* bornes commandées par l'optimiseur rejoignent \c waterfallEvIds : le proxy ne les commande
|
||
* plus (une charge, un commandeur).
|
||
*
|
||
* \param ombre Plan du rule-based de ce cycle (ses effets de bord ont eu lieu).
|
||
* \param recu Plan validé (\c Issue::Valide, \c indexCourant ≥ 0).
|
||
* \param ctx Contexte du cycle.
|
||
* \return Un plan \c strategy == \c "optimizer". \c budget est remis à zéro et \c draw.committedW
|
||
* aussi : ils sont recalculés après le dispatch par \c finaliser() — ceux de l'ombre ne sont
|
||
* jamais publiés (décision Q4). Les champs d'autorisation de \c draw, qui viennent du contexte
|
||
* (\c ctx.drawMargin) et non d'une décision, sont gardés.
|
||
*/
|
||
Plan substituer(const Plan &ombre, const PlanRecu &recu, const SurplusContext &ctx);
|
||
|
||
/*! \brief Une action d'optimiseur écrêtée, pour le journal. */
|
||
struct Ecretage {
|
||
QString loadId; //!< Charge.
|
||
QString demande; //!< Ce que l'optimiseur demandait (texte de journal).
|
||
QString applique; //!< Ce que l'adaptateur a appliqué (texte de journal).
|
||
};
|
||
|
||
/*!
|
||
* \brief Après le dispatch : puissance APPLIQUÉE, financement et budget du cycle (Q3, Q4).
|
||
*
|
||
* Pour chaque action d'optimiseur du créneau (niveau \c optimizer), dans l'ordre de priorité :
|
||
* la puissance publiée est celle que l'adaptateur a rendue (\c appliquees) ; elle est financée
|
||
* au surplus tant que la mesure le couvre (export net + consommation courante des charges
|
||
* commandées, recrédit anti-clignotement), au réseau pour le reliquat ; \c levels devient un
|
||
* niveau unique \c optimizer, \c counts suit le partage (Σ = \c estimatedPowerW) ; \c clamped
|
||
* est posé si l'appliqué diffère de la demande, ou si rien n'a été appliqué. Une charge délestée
|
||
* par L4 est laissée à L4, qui remplace son action ensuite. Les actions de l'ombre gardées
|
||
* (charges indisponibles) entrent dans le budget par leurs \c counts.
|
||
*
|
||
* \param plan Plan du cycle (\c strategy == \c "optimizer") ; \c budget et \c draw réécrits.
|
||
* \param slot Créneau dispatché ; ses actions d'optimiseur sont réécrites.
|
||
* \param appliquees Retours d'\c applyAction() du dispatch (\c m_actionsAppliquees).
|
||
* \param delestees Charges revendiquées par L4 ce cycle.
|
||
* \param ctx Contexte du cycle (mesure, consommation courante).
|
||
* \return Les écrêtages, pour le journal.
|
||
* \invariant budget : \c surplusW + \c recreditedW − \c allocatedW == \c remainingW.
|
||
*/
|
||
QList<Ecretage> finaliser(Plan &plan, Slot &slot, const QHash<QString, LoadAction> &appliquees,
|
||
const QSet<QString> &delestees, const SurplusContext &ctx);
|
||
|
||
/*! \brief \param action Action du créneau. \return Vrai si elle vient d'un optimiseur (niveau \c optimizer). */
|
||
bool estActionOptimiseur(const LoadAction &action);
|
||
|
||
/*! \brief \param issue Issue. \return Son nom de journal (« valide », « abstention »…). */
|
||
QString nomIssue(Issue issue);
|
||
|
||
}
|