refactor(plan): le plan rule-based porte strategy « rules », comme activeStrategy (brief 4 T6)
Décision de Patrick du 2026-10-17 : un seul vocabulaire. Plan::strategy du rule-based, GetPlan.strategy et la doc passent de « rule-based » à « rules » (INTERFACE.md, protocole §6). Les libellés de journal (« repli rule-based ») ne sont pas des valeurs et restent. Référence decisions régénérée. Preuve AVANT régénération, moteur de e592618 + ce seul renommage, contre l'ancienne référence : comparateur --strategie (nouvelle option, qui ne tolère que plan.strategy rule-based → rules) IDENTIQUE HORS STRATÉGIE — 23 792 lignes, 23 728 identiques à l'octet, 64 qui ne diffèrent que par .plan.strategy (décompte indépendant des chemins JSON : 64 × .plan.strategy "rule-based" → "rules", rien d'autre). 0 rejeu. Le même build, relu contre la nouvelle référence : IDENTIQUE ; 6 suites vertes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e5926189e9
commit
1f6c7ec27a
@ -431,7 +431,9 @@ Règles d'exécution côté plugin :
|
|||||||
protection de surcharge (L4 et son extension au waterfall). Une action écrêtée est
|
protection de surcharge (L4 et son extension au waterfall). Une action écrêtée est
|
||||||
exécutée au plus proche possible et loggée (`clamped`).
|
exécutée au plus proche possible et loggée (`clamped`).
|
||||||
- `reason` absent ou vide → action **rejetée**, log d'avertissement.
|
- `reason` absent ou vide → action **rejetée**, log d'avertissement.
|
||||||
- Un rule-based interne répond au même contrat avec un plan à un seul créneau.
|
- Un rule-based interne répond au même contrat avec un plan à un seul créneau. Côté moteur
|
||||||
|
(plan publié vers l'app, traces), ce plan porte `strategy: "rules"`, la même valeur que
|
||||||
|
`activeStrategy` ; le `strategy` d'un optimiseur reste une chaîne libre, informative.
|
||||||
- **Le contournement de verrou (`force`) n'existe pas sur le fil.** Il est réservé au repli
|
- **Le contournement de verrou (`force`) n'existe pas sur le fil.** Il est réservé au repli
|
||||||
de sécurité interne (L2) ; un champ `force` reçu du socket est ignoré.
|
de sécurité interne (L2) ; un champ `force` reçu du socket est ignoré.
|
||||||
- **`validUntil`** : au-delà, le plan est **expiré** → repli rule-based jusqu'au prochain
|
- **`validUntil`** : au-delà, le plan est **expiré** → repli rule-based jusqu'au prochain
|
||||||
|
|||||||
@ -23,8 +23,9 @@
|
|||||||
*/
|
*/
|
||||||
namespace PublishedPlan {
|
namespace PublishedPlan {
|
||||||
|
|
||||||
//! Valeur de \c strategy d'un plan du rule-based publié (celle de \c Plan::strategy).
|
//! Valeur de \c strategy d'un plan du rule-based publié (celle de \c Plan::strategy, et la même
|
||||||
constexpr const char *StrategieRegles = "rule-based";
|
//! que \c activeStrategy : \c OptimizerStatus::StrategieRegles).
|
||||||
|
constexpr const char *StrategieRegles = "rules";
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Le plan du cycle, pour l'app.
|
* \brief Le plan du cycle, pour l'app.
|
||||||
|
|||||||
@ -559,7 +559,7 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
|
|||||||
|
|
||||||
Plan plan;
|
Plan plan;
|
||||||
plan.planId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
plan.planId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
plan.strategy = QStringLiteral("rule-based");
|
plan.strategy = QStringLiteral("rules");
|
||||||
plan.timeSlots.append(slot);
|
plan.timeSlots.append(slot);
|
||||||
|
|
||||||
compta.remainingW = remainingSurplusW;
|
compta.remainingW = remainingSurplusW;
|
||||||
|
|||||||
@ -156,7 +156,7 @@ struct PlanDraw {
|
|||||||
*/
|
*/
|
||||||
struct Plan {
|
struct Plan {
|
||||||
QString planId; //!< Identifiant unique (UUID string).
|
QString planId; //!< Identifiant unique (UUID string).
|
||||||
//! \c "rule-based" (plan du rule-based, y compris le repli d'une stratégie externe) ou
|
//! \c "rules" (plan du rule-based, y compris le repli d'une stratégie externe) ou
|
||||||
//! \c "optimizer" (créneau courant d'un plan d'optimiseur, \c OptimizerPlan::substituer()).
|
//! \c "optimizer" (créneau courant d'un plan d'optimiseur, \c OptimizerPlan::substituer()).
|
||||||
QString strategy;
|
QString strategy;
|
||||||
//! Créneaux du plan. Nommé \c timeSlots (pas \c slots, mot-clé Qt).
|
//! Créneaux du plan. Nommé \c timeSlots (pas \c slots, mot-clé Qt).
|
||||||
|
|||||||
@ -609,13 +609,13 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
|||||||
description = "The plan IN FORCE at the last planned cycle. Under an optimizer: the current "
|
description = "The plan IN FORCE at the last planned cycle. Under an optimizer: the current "
|
||||||
"slot with the power APPLIED by the adapters, then the following slots with the "
|
"slot with the power APPLIED by the adapters, then the following slots with the "
|
||||||
"power REQUESTED (informative). Under the rule-based: one slot, strategy "
|
"power REQUESTED (informative). Under the rule-based: one slot, strategy "
|
||||||
"rule-based, and fallbackCode when an optimizer is configured but did not command "
|
"rules, and fallbackCode when an optimizer is configured but did not command "
|
||||||
"this cycle. Reasons are codes and params only, never text. inForce is false, "
|
"this cycle. Reasons are codes and params only, never text. inForce is false, "
|
||||||
"and nothing else is returned, before the first cycle and in degraded mode L2 (no "
|
"and nothing else is returned, before the first cycle and in degraded mode L2 (no "
|
||||||
"plan in force). No notification: read it again on LoadTelemetryChanged or "
|
"plan in force). No notification: read it again on LoadTelemetryChanged or "
|
||||||
"OptimizerStatusChanged.";
|
"OptimizerStatusChanged.";
|
||||||
returns.insert("inForce", enumValueName(Bool));
|
returns.insert("inForce", enumValueName(Bool));
|
||||||
returns.insert("o:strategy", enumValueName(String)); // optimizer | rule-based
|
returns.insert("o:strategy", enumValueName(String)); // optimizer | rules
|
||||||
returns.insert("o:computedAt", enumValueName(String));
|
returns.insert("o:computedAt", enumValueName(String));
|
||||||
returns.insert("o:validUntil", enumValueName(String));
|
returns.insert("o:validUntil", enumValueName(String));
|
||||||
returns.insert("o:slots", QVariantList() << planSlot);
|
returns.insert("o:slots", QVariantList() << planSlot);
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
# Comparateur « IDENTIQUE HORS MOTIFS » (correctifs moteur a et b, etm70).
|
# Comparateur « IDENTIQUE HORS MOTIFS » (correctifs moteur a et b, etm70).
|
||||||
#
|
#
|
||||||
# etm-powersync-compare-hors-motifs.py <attendu.ndjson[.xz]> <obtenu.ndjson[.xz]> [--cas <sortie.md>]
|
# etm-powersync-compare-hors-motifs.py <attendu.ndjson[.xz]> <obtenu.ndjson[.xz]> [--cas <sortie.md>]
|
||||||
# [--achat] [--lignes <sortie.txt>]
|
# [--achat] [--strategie] [--lignes <sortie.txt>]
|
||||||
#
|
#
|
||||||
# Un lot qui ne change QUE des motifs doit laisser tout le reste de la référence `decisions`
|
# Un lot qui ne change QUE des motifs doit laisser tout le reste de la référence `decisions`
|
||||||
# intact à l'octet près. Ce script retire de chaque ligne, et SEULEMENT d'elle :
|
# intact à l'octet près. Ce script retire de chaque ligne, et SEULEMENT d'elle :
|
||||||
@ -13,6 +13,8 @@
|
|||||||
# Avec --achat (correctif c, etm71), il retire EN PLUS `progress.purchasedWh` de chaque niveau :
|
# Avec --achat (correctif c, etm71), il retire EN PLUS `progress.purchasedWh` de chaque niveau :
|
||||||
# l'acheté se compte désormais sur l'énergie mesurée, et il est absent sans mesure. Un lot qui
|
# l'acheté se compte désormais sur l'énergie mesurée, et il est absent sans mesure. Un lot qui
|
||||||
# change cette VALEUR PUBLIÉE sans toucher aux consignes doit passer ce mode-là.
|
# change cette VALEUR PUBLIÉE sans toucher aux consignes doit passer ce mode-là.
|
||||||
|
# Avec --strategie (brief 4 T6, décision du 2026-10-17), il ramène EN PLUS `plan.strategy`
|
||||||
|
# `rule-based` à `rules` d'une ligne de plan direct : le renommage du plan rule-based, et lui seul.
|
||||||
# Tout le reste — consignes, powerW, estimatedPowerW, états, relais, courants, budget, draw,
|
# Tout le reste — consignes, powerW, estimatedPowerW, états, relais, courants, budget, draw,
|
||||||
# counts, niveaux et leurs motifs, avancement — doit être IDENTIQUE. Code 0 si c'est le cas,
|
# counts, niveaux et leurs motifs, avancement — doit être IDENTIQUE. Code 0 si c'est le cas,
|
||||||
# 1 sinon (et le premier écart est montré). La liste des motifs qui changent est imprimée, ou
|
# 1 sinon (et le premier écart est montré). La liste des motifs qui changent est imprimée, ou
|
||||||
@ -48,11 +50,13 @@ def sans_achat(niveau, charge, achats):
|
|||||||
achats.append((charge, prog.pop('purchasedWh', ABSENT)))
|
achats.append((charge, prog.pop('purchasedWh', ABSENT)))
|
||||||
|
|
||||||
|
|
||||||
def normaliser(ligne, achat=False):
|
def normaliser(ligne, achat=False, strategie=False):
|
||||||
"""Rend (ligne sans motifs, [(charge, motif retiré)], [(charge, purchasedWh retiré)])."""
|
"""Rend (ligne sans motifs, [(charge, motif retiré)], [(charge, purchasedWh retiré)])."""
|
||||||
d = json.loads(ligne)
|
d = json.loads(ligne)
|
||||||
retires = []
|
retires = []
|
||||||
achats = []
|
achats = []
|
||||||
|
if strategie and d.get('src') == 'plan' and (d.get('plan') or {}).get('strategy') == 'rule-based':
|
||||||
|
d['plan']['strategy'] = 'rules'
|
||||||
if d.get('src') == 'cycle':
|
if d.get('src') == 'cycle':
|
||||||
for ch in (d.get('telemetry') or {}).get('loads', []) or []:
|
for ch in (d.get('telemetry') or {}).get('loads', []) or []:
|
||||||
retires.append((ch.get('loadId'), ch.pop('decision', None)))
|
retires.append((ch.get('loadId'), ch.pop('decision', None)))
|
||||||
@ -95,6 +99,7 @@ def main():
|
|||||||
sortie_cas = sys.argv[sys.argv.index('--cas') + 1] if '--cas' in sys.argv else None
|
sortie_cas = sys.argv[sys.argv.index('--cas') + 1] if '--cas' in sys.argv else None
|
||||||
sortie_lignes = sys.argv[sys.argv.index('--lignes') + 1] if '--lignes' in sys.argv else None
|
sortie_lignes = sys.argv[sys.argv.index('--lignes') + 1] if '--lignes' in sys.argv else None
|
||||||
achat = '--achat' in sys.argv
|
achat = '--achat' in sys.argv
|
||||||
|
strategie = '--strategie' in sys.argv
|
||||||
lignes_changees = []
|
lignes_changees = []
|
||||||
|
|
||||||
if len(attendu) != len(obtenu):
|
if len(attendu) != len(obtenu):
|
||||||
@ -107,11 +112,12 @@ def main():
|
|||||||
if a == b:
|
if a == b:
|
||||||
brutes_identiques += 1
|
brutes_identiques += 1
|
||||||
continue
|
continue
|
||||||
na, ra, aa = normaliser(a, achat)
|
na, ra, aa = normaliser(a, achat, strategie)
|
||||||
nb, rb, ab = normaliser(b, achat)
|
nb, rb, ab = normaliser(b, achat, strategie)
|
||||||
lignes_changees.append((i, a, b))
|
lignes_changees.append((i, a, b))
|
||||||
if na != nb:
|
if na != nb:
|
||||||
print(f'ÉCART HORS MOTIFS{" ET HORS ACHAT" if achat else ""} à la ligne {i} :')
|
print(f'ÉCART HORS MOTIFS{" ET HORS ACHAT" if achat else ""}'
|
||||||
|
f'{" ET HORS STRATÉGIE" if strategie else ""} à la ligne {i} :')
|
||||||
print('- ' + a[:3000])
|
print('- ' + a[:3000])
|
||||||
print('+ ' + b[:3000])
|
print('+ ' + b[:3000])
|
||||||
return 1
|
return 1
|
||||||
@ -123,14 +129,18 @@ def main():
|
|||||||
for (charge, va), (_, vb) in zip(aa, ab):
|
for (charge, va), (_, vb) in zip(aa, ab):
|
||||||
if va != vb:
|
if va != vb:
|
||||||
cas[(fn, charge, f'purchasedWh {achat_court(va)}', f'purchasedWh {achat_court(vb)}')] += 1
|
cas[(fn, charge, f'purchasedWh {achat_court(va)}', f'purchasedWh {achat_court(vb)}')] += 1
|
||||||
|
sa = (json.loads(a).get('plan') or {}).get('strategy')
|
||||||
|
sb = (json.loads(b).get('plan') or {}).get('strategy')
|
||||||
|
if sa != sb:
|
||||||
|
cas[(fn, '(plan)', f'strategy {sa}', f'strategy {sb}')] += 1
|
||||||
# Le manque d'un niveau change sans que le motif de tête change : à dire aussi.
|
# Le manque d'un niveau change sans que le motif de tête change : à dire aussi.
|
||||||
if ra == rb and aa == ab:
|
elif ra == rb and aa == ab:
|
||||||
cas[(fn, '(niveau)', 'missingWh', 'missingWh corrigé')] += 1
|
cas[(fn, '(niveau)', 'missingWh', 'missingWh corrigé')] += 1
|
||||||
|
|
||||||
hors = 'HORS MOTIFS ET HORS ACHAT' if achat else 'HORS MOTIFS'
|
hors = 'HORS MOTIFS' + (' ET HORS ACHAT' if achat else '') + (' ET HORS STRATÉGIE' if strategie else '')
|
||||||
print(f'IDENTIQUE {hors} : {len(attendu)} lignes, dont {brutes_identiques} identiques '
|
print(f'IDENTIQUE {hors} : {len(attendu)} lignes, dont {brutes_identiques} identiques '
|
||||||
f'à l\'octet et {len(attendu) - brutes_identiques} qui ne diffèrent que par '
|
f'à l\'octet et {len(attendu) - brutes_identiques} qui ne diffèrent que par '
|
||||||
f'{"les motifs ou purchasedWh" if achat else "les motifs"}')
|
f'{"les motifs" + (", purchasedWh" if achat else "") + (" ou plan.strategy" if strategie else "")}')
|
||||||
if sortie_lignes:
|
if sortie_lignes:
|
||||||
with open(sortie_lignes, 'w', encoding='utf-8') as f:
|
with open(sortie_lignes, 'w', encoding='utf-8') as f:
|
||||||
for i, a, b in lignes_changees:
|
for i, a, b in lignes_changees:
|
||||||
|
|||||||
Binary file not shown.
@ -323,7 +323,7 @@ void TestLoadModel::testOptimizerUnavailableLoadKeepsTheShadow()
|
|||||||
|
|
||||||
// Plan ombre : une action par charge, dont LOAD_UNAVAILABLE pour la charge en défaut.
|
// Plan ombre : une action par charge, dont LOAD_UNAVAILABLE pour la charge en défaut.
|
||||||
Plan ombre;
|
Plan ombre;
|
||||||
ombre.strategy = "rule-based";
|
ombre.strategy = "rules";
|
||||||
ombre.planId = "ombre";
|
ombre.planId = "ombre";
|
||||||
ombre.budget.surplusW = 777; // ne doit JAMAIS être publié sous l'optimiseur
|
ombre.budget.surplusW = 777; // ne doit JAMAIS être publié sous l'optimiseur
|
||||||
ombre.draw.committedW = 333;
|
ombre.draw.committedW = 333;
|
||||||
@ -374,7 +374,7 @@ void TestLoadModel::testOptimizerFinaliseUsesTheAppliedAction()
|
|||||||
avecAction(1, [](QVariantMap &a) { a["state"] = 4; }), ctx, {}, false);
|
avecAction(1, [](QVariantMap &a) { a["state"] = 4; }), ctx, {}, false);
|
||||||
QVERIFY(v.issue == OptimizerPlan::Issue::Valide);
|
QVERIFY(v.issue == OptimizerPlan::Issue::Valide);
|
||||||
Plan ombre;
|
Plan ombre;
|
||||||
ombre.strategy = "rule-based";
|
ombre.strategy = "rules";
|
||||||
Slot s;
|
Slot s;
|
||||||
s.from = kMaintenant;
|
s.from = kMaintenant;
|
||||||
s.to = kMaintenant.addSecs(60);
|
s.to = kMaintenant.addSecs(60);
|
||||||
|
|||||||
@ -912,7 +912,7 @@ void TestSocketScheduler::testStatusWithoutEndpoint()
|
|||||||
// refusé, il n'y a personne d'autre — c'est ce qui distingue « pas d'optimiseur ».
|
// refusé, il n'y a personne d'autre — c'est ce qui distingue « pas d'optimiseur ».
|
||||||
const QVariantMap p = rpc("GetPlan");
|
const QVariantMap p = rpc("GetPlan");
|
||||||
QCOMPARE(p.value("inForce").toBool(), true);
|
QCOMPARE(p.value("inForce").toBool(), true);
|
||||||
QCOMPARE(p.value("strategy").toString(), QString("rule-based"));
|
QCOMPARE(p.value("strategy").toString(), QString("rules"));
|
||||||
QCOMPARE(p.value("computedAt").toString(), t0.addSecs(60).toUTC().toString(Qt::ISODate));
|
QCOMPARE(p.value("computedAt").toString(), t0.addSecs(60).toUTC().toString(Qt::ISODate));
|
||||||
QVERIFY(p.contains("validUntil"));
|
QVERIFY(p.contains("validUntil"));
|
||||||
QVERIFY(!p.contains("fallbackCode") && !p.contains("diagnostics"));
|
QVERIFY(!p.contains("fallbackCode") && !p.contains("diagnostics"));
|
||||||
@ -938,7 +938,7 @@ void TestSocketScheduler::testStatusWithoutEndpoint()
|
|||||||
QCOMPARE(triplet(), QString("-|mort|none"));
|
QCOMPARE(triplet(), QString("-|mort|none"));
|
||||||
cycle(t0.addSecs(300), -3000);
|
cycle(t0.addSecs(300), -3000);
|
||||||
QCOMPARE(triplets(spy), (QStringList{"-|mort|none", "-|mort|rules"}));
|
QCOMPARE(triplets(spy), (QStringList{"-|mort|none", "-|mort|rules"}));
|
||||||
QCOMPARE(rpc("GetPlan").value("strategy").toString(), QString("rule-based"));
|
QCOMPARE(rpc("GetPlan").value("strategy").toString(), QString("rules"));
|
||||||
// La notification JSON-RPC porte la charge utile du Get, une fois par transition.
|
// La notification JSON-RPC porte la charge utile du Get, une fois par transition.
|
||||||
const QVariantList notifs = checkNotifications(paquets, "NymeaEnergy.OptimizerStatusChanged");
|
const QVariantList notifs = checkNotifications(paquets, "NymeaEnergy.OptimizerStatusChanged");
|
||||||
QCOMPARE(notifs.count(), 2);
|
QCOMPARE(notifs.count(), 2);
|
||||||
@ -1293,7 +1293,7 @@ void TestSocketScheduler::testGetPlanPublishesThePlanInForce()
|
|||||||
// Cycle 1 : un optimiseur est configuré mais n'a pas commandé — « pas de plan ce cycle ».
|
// Cycle 1 : un optimiseur est configuré mais n'a pas commandé — « pas de plan ce cycle ».
|
||||||
cycle(t0, -3000);
|
cycle(t0, -3000);
|
||||||
QVariantMap p = rpc("GetPlan");
|
QVariantMap p = rpc("GetPlan");
|
||||||
QCOMPARE(p.value("strategy").toString(), QString("rule-based"));
|
QCOMPARE(p.value("strategy").toString(), QString("rules"));
|
||||||
QCOMPARE(p.value("fallbackCode").toString(), QString("no-plan"));
|
QCOMPARE(p.value("fallbackCode").toString(), QString("no-plan"));
|
||||||
QCOMPARE(p.value("slots").toList().count(), 1);
|
QCOMPARE(p.value("slots").toList().count(), 1);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user