// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync // // Juger des messages du protocole optimiseur contre son contrat OpenRPC (lot « contrat OpenRPC »), // par le validateur du dépôt : tools/etm-powersync-schema-check, lancé en sous-processus Python. // // Un seul validateur, pas deux : un second, écrit en C++ pour les tests, divergerait du premier, et // c'est la conformité au validateur PUBLIÉ (celui qu'utilise un auteur d'optimiseur) qui compte. #pragma once #include #include #include #include #include #include #include namespace Contrat { /*! Racine des sources : surchargée par $ETM_POWERSYNC_SOURCE_DIR (arbre mutant, arbre archivé). */ inline QString racineSources() { const QByteArray env = qgetenv("ETM_POWERSYNC_SOURCE_DIR"); return env.isEmpty() ? QStringLiteral(ETM_POWERSYNC_SOURCE_DIR) : QString::fromLocal8Bit(env); } /*! Le contrat de docs/ — à défaut du document EMBARQUÉ (suites qui n'ont pas le sérialiseur). */ inline QString schemaDuDepot() { return racineSources() + QStringLiteral("/docs/etm-powersync-optimizer-protocol.openrpc.json"); } /*! Verdict du validateur. */ struct Verdict { int code = -1; //!< 0 conforme, 1 écart, 2 erreur d'usage ou de schéma, -1 pas lancé. QJsonObject resume; //!< La ligne RÉSUMÉ (comptes, écarts, verdicts). QString sortie; //!< Tout ce qu'il a écrit — à citer dans le message d'échec. int compte(const QString &partie) const { return resume.value(QStringLiteral("comptes")).toObject().value(partie).toInt(); } }; /*! * Juge une session (un message JSON-RPC par élément, dans l'ordre) contre \p schema. * \p arguments : options en plus (`--requests-only`, `--semantic`). */ inline Verdict juger(const QList &messages, const QString &schema, const QStringList &arguments = {}) { Verdict v; QTemporaryDir dir; if (!dir.isValid()) { v.sortie = QStringLiteral("répertoire temporaire impossible"); return v; } QFile f(dir.filePath(QStringLiteral("session.ndjson"))); if (!f.open(QIODevice::WriteOnly)) { v.sortie = QStringLiteral("écriture de la session impossible"); return v; } for (const QJsonObject &m : messages) f.write(QJsonDocument(m).toJson(QJsonDocument::Compact) + '\n'); f.close(); QProcess p; p.setProcessChannelMode(QProcess::MergedChannels); p.start(QStringLiteral("python3"), QStringList{QStringLiteral("-I"), QStringLiteral("-B"), racineSources() + QStringLiteral("/tools/etm-powersync-schema-check"), QStringLiteral("--schema"), schema, QStringLiteral("--ndjson"), f.fileName()} + arguments); if (!p.waitForFinished(60000)) { p.kill(); v.sortie = QStringLiteral("validateur sans réponse en 60 s"); return v; } v.code = p.exitStatus() == QProcess::NormalExit ? p.exitCode() : -1; v.sortie = QString::fromUtf8(p.readAll()); for (const QString &ligne : v.sortie.split(QLatin1Char('\n'))) if (ligne.startsWith(QStringLiteral("RÉSUMÉ "))) v.resume = QJsonDocument::fromJson(ligne.mid(7).toUtf8()).object(); return v; } /*! Écrit \p octets dans \p dir et rend le chemin : le contrat EMBARQUÉ, passé au validateur. */ inline QString ecrireSchema(const QTemporaryDir &dir, const QByteArray &octets) { const QString chemin = dir.filePath(QStringLiteral("contrat-embarque.json")); QFile f(chemin); if (f.open(QIODevice::WriteOnly)) f.write(octets); return chemin; } /*! Les messages de \p messages dont la méthode est \p methode. */ inline QList deMethode(const QList &messages, const QString &methode) { QList out; for (const QJsonObject &m : messages) if (m.value(QStringLiteral("method")).toString() == methode) out << m; return out; } }