feat(moteur): listes fermées du protocole exposées par le code

OptimizerPlan::kinds() (désormais appliquée par convertirAction) et
verdictCodes(), dans l'ordre des contrôles ; SocketScheduler::knownTriggers()
(appliquée par rangDeclencheur), knownFallbackCodes(), knownLastErrorCodes() ;
OptimizerClient::knownErrorCodes(). Le contrat OpenRPC en régénère ses
énumérations. Aucun comportement changé : mêmes listes, mêmes ordres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-10-17 21:30:44 +02:00
parent fb581218c8
commit b45d0496c6
6 changed files with 119 additions and 7 deletions

View File

@ -200,6 +200,18 @@ bool OptimizerClient::writeToken(const QString &path, const QByteArray &token, Q
return true;
}
QStringList OptimizerClient::knownErrorCodes()
{
return {QStringLiteral("invalid-endpoint"), QStringLiteral("token-too-short"),
QStringLiteral("connect-failed"), QStringLiteral("closed-during-handshake"),
QStringLiteral("connection-lost"), QStringLiteral("handshake-timeout"),
QStringLiteral("handshake-refused"), QStringLiteral("invalid-handshake"),
QStringLiteral("major-mismatch"), QStringLiteral("line-too-long"),
QStringLiteral("invalid-json"), QStringLiteral("missing-id"), QStringLiteral("unknown-id"),
QStringLiteral("timeout"), QStringLiteral("rpc-error"), QStringLiteral("invalid-response"),
QStringLiteral("disconnected")};
}
int OptimizerClient::tokenLength(const QByteArray &token)
{
return int(QString::fromUtf8(token).toUcs4().size());

View File

@ -165,6 +165,14 @@ public:
*/
static constexpr int kMinTokenChars = 16;
/*!
* \brief Les codes d'erreur que le client peut retenir (\c lastErrorCode()) ou émettre
* (\c planFailed(), \c protocolError()).
* \return Connexion, handshake, ligne écartée, requête. La suite \c etm-powersync-protocolschema
* confronte la liste au code source du client.
*/
static QStringList knownErrorCodes();
/*! \brief Longueur d'un token, comparée à \c kMinTokenChars.
* \param token Le token, en UTF-8. \return Son nombre de caractères (points de code). */
static int tokenLength(const QByteArray &token);

View File

@ -103,9 +103,7 @@ QString convertirAction(const QVariant &brut, const QHash<QString, const LoadCon
}
const QString kind = a.value(QStringLiteral("kind")).toString();
if (!estChaine(a.value(QStringLiteral("kind")))
|| (kind != QLatin1String("setpoint") && kind != QLatin1String("state")
&& kind != QLatin1String("constraint"))) {
if (!estChaine(a.value(QStringLiteral("kind"))) || !OptimizerPlan::kinds().contains(kind)) {
*detail = QStringLiteral("%1 : kind « %2 » inconnu").arg(id, kind);
return QStringLiteral("unknown-kind");
}
@ -240,6 +238,44 @@ bool differe(const LoadAction &demande, const LoadAction &applique, const QStrin
namespace OptimizerPlan {
QStringList kinds()
{
return {QStringLiteral("setpoint"), QStringLiteral("state"), QStringLiteral("constraint")};
}
namespace {
// Les verdicts de valider(), dans l'ORDRE de ses contrôles : c'est l'ordre dans lequel un plan
// fautif se voit attribuer son code, et celui que rejoue tools/etm-powersync-schema-check.
struct CodeVerdict { const char *code; Issue issue; };
constexpr CodeVerdict kVerdicts[] = {
{"abstain", Issue::Abstention}, {"malformed-plan", Issue::Invalide},
{"empty-plan", Issue::Abstention}, {"bad-time", Issue::Invalide},
{"slots-unordered", Issue::Invalide}, {"malformed-action", Issue::Invalide},
{"unknown-load", Issue::Invalide}, {"unknown-kind", Issue::Invalide},
{"kind-mismatch", Issue::Invalide}, {"kind-not-announced", Issue::Invalide},
{"missing-reason", Issue::Invalide}, {"bad-value", Issue::Invalide},
{"duplicate-load", Issue::Invalide}, {"expired", Issue::Expire},
{"no-current-slot", Issue::Abstention}, {"incomplete-slot", Issue::Invalide},
};
}
QStringList verdictCodes(Issue issue)
{
QStringList out;
for (const CodeVerdict &v : kVerdicts)
if (v.issue == issue)
out << QString::fromLatin1(v.code);
return out;
}
QStringList verdictCodes()
{
QStringList out;
for (const CodeVerdict &v : kVerdicts)
out << QString::fromLatin1(v.code);
return out;
}
QString assainirTexte(const QVariant &v)
{
if (!estChaine(v))

View File

@ -45,6 +45,26 @@ enum class Issue {
Invalide //!< Plan refusé en entier, jamais appliqué partiellement — un échec (§7).
};
/*!
* \brief Les \c kind que le validateur RECONNAÎT (§6) — la liste qu'il applique, pas une copie.
* \return \c setpoint, \c state, \c constraint. Un kind hors liste est refusé (\c unknown-kind) ;
* \c constraint est reconnu mais toujours refusé (\c kind-mismatch) : aucun mécanisme ne le porte
* (annexe F). Le contrat OpenRPC en régénère son énumération (\c ActionKind).
*/
QStringList kinds();
/*!
* \brief Les codes de verdict que \c valider() peut rendre, dans l'ordre de ses contrôles.
* \param issue Issue dont on veut les codes.
* \return Les codes de cette issue (\c Valide : aucun). Ce sont les noms des règles
* \c x-etm-semantic-rules du contrat OpenRPC et des valeurs de \c fallbackCode ; la suite
* \c etm-powersync-protocolschema confronte la liste au code source de \c valider().
*/
QStringList verdictCodes(Issue issue);
/*! \brief \return Tous les codes de \c verdictCodes(), dans l'ordre des contrôles de \c valider(). */
QStringList verdictCodes();
/*! \brief Une action reçue, convertie. */
struct ActionRecue {
LoadAction action; //!< Action interne : mécanisme, motif filtré, niveau \c optimizer provisoire.

View File

@ -11,13 +11,29 @@ namespace {
// Rang d'un déclencheur : le plus significatif l'emporte quand plusieurs se cumulent.
int rangDeclencheur(const QString &t)
{
static const QStringList ordre{QStringLiteral("cycle"), QStringLiteral("forecast"),
QStringLiteral("config"), QStringLiteral("load-change"),
QStringLiteral("intent")};
return ordre.indexOf(t);
return SocketScheduler::knownTriggers().indexOf(t);
}
}
QStringList SocketScheduler::knownTriggers()
{
return {QStringLiteral("cycle"), QStringLiteral("forecast"), QStringLiteral("config"),
QStringLiteral("load-change"), QStringLiteral("intent")};
}
QStringList SocketScheduler::knownFallbackCodes()
{
using OptimizerPlan::Issue;
return QStringList{QStringLiteral("no-plan")} + OptimizerPlan::verdictCodes(Issue::Abstention)
+ OptimizerPlan::verdictCodes(Issue::Expire) + OptimizerPlan::verdictCodes(Issue::Invalide)
+ QStringList{QStringLiteral("no-shadow-slot")};
}
QStringList SocketScheduler::knownLastErrorCodes()
{
return OptimizerClient::knownErrorCodes() + OptimizerPlan::verdictCodes(OptimizerPlan::Issue::Invalide);
}
SocketScheduler::SocketScheduler(IScheduler *ombre, OptimizerClient *client, FournisseurFil fil,
QObject *parent)
: QObject(parent)

View File

@ -92,6 +92,26 @@ public:
*/
void noterDeclencheur(const QString &trigger);
/*!
* \brief Les valeurs de \c trigger (§5.1), de la moins à la plus significative.
* \return \c cycle, \c forecast, \c config, \c load-change, \c intent — l'ordre qu'applique
* \c noterDeclencheur(), et l'énumération \c Trigger du contrat OpenRPC.
*/
static QStringList knownTriggers();
/*!
* \brief Les valeurs de \c fallbackCode publiées par \c NymeaEnergy.GetPlan (INTERFACE.md).
* \return \c no-plan, les codes de verdict d'\c OptimizerPlan::valider() (abstention, expiré,
* invalide), \c no-shadow-slot.
*/
static QStringList knownFallbackCodes();
/*!
* \brief Les valeurs de \c lastErrorCode publiées par \c NymeaEnergy.GetOptimizerStatus.
* \return \c OptimizerClient::knownErrorCodes() et les codes de plan invalide.
*/
static QStringList knownLastErrorCodes();
/*! \brief \return La cause que portera la prochaine requête. */
QString declencheurEnAttente() const { return m_declencheur; }