From fc8bc1acd3143f1b864bb339c2f4154589fdcea9 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sat, 17 Oct 2026 21:15:51 +0200 Subject: [PATCH] feat(ref-optimizer): --capture de la session et mode wrong-type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --capture écrit chaque message reçu et envoyé (NDJSON), token remplacé par autant d'astérisques. wrong-type rend un planId numérique, que le moteur accepte et que le contrat refuse : contre-épreuve de la conformité. Co-Authored-By: Claude Opus 5.5 --- tools/etm-powersync-ref-optimizer/README.md | 7 ++-- .../etm-powersync-ref-optimizer | 35 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/tools/etm-powersync-ref-optimizer/README.md b/tools/etm-powersync-ref-optimizer/README.md index 92968da..23bf33e 100644 --- a/tools/etm-powersync-ref-optimizer/README.md +++ b/tools/etm-powersync-ref-optimizer/README.md @@ -61,10 +61,13 @@ Le rejeu compare, cycle par cycle, les actions du créneau courant au plan enreg 2.0) · `drop` (ferme la connexion à chaque `GetPlan`) · `unknown-load` · `forbidden-kind` (kind `constraint`) · `missing-load` (une charge disponible absente du créneau courant) · `echo-token` (recopie le token dans `reason` et `planId`) · `protocol-1.0` (optimiseur 1.0 : sans champs 1.1) · -`deviate` (cascade en ordre inverse : doit diverger). +`deviate` (cascade en ordre inverse : doit diverger) · `wrong-type` (`planId` numérique : le moteur +l'accepte, le contrat OpenRPC le refuse — contre-épreuve de la suite de conformité). `--mode-file FICHIER` : le mode est relu dans ce fichier à chaque message — un test change de mode -sans reconnexion. `--list-codes` : le catalogue des codes émissibles qu'il embarque (le test +sans reconnexion. `--capture FICHIER` : chaque message reçu et envoyé, une ligne JSON-RPC par +message — la session que `tools/etm-powersync-schema-check --ndjson` juge contre le contrat ; le +token y devient autant d'astérisques (forme gardée, valeur jamais écrite). `--list-codes` : le catalogue des codes émissibles qu'il embarque (le test d'intégration le confronte au moteur). Tests : `tests/auto/refoptimizer` (suite `etm-powersync-refoptimizer`, moteur réel + ce script en diff --git a/tools/etm-powersync-ref-optimizer/etm-powersync-ref-optimizer b/tools/etm-powersync-ref-optimizer/etm-powersync-ref-optimizer index dc5b72e..ba71a36 100755 --- a/tools/etm-powersync-ref-optimizer/etm-powersync-ref-optimizer +++ b/tools/etm-powersync-ref-optimizer/etm-powersync-ref-optimizer @@ -5,7 +5,7 @@ etm-powersync-ref-optimizer [--listen unix:///run/powersync/optimizer.sock | tcp://hôte:port] [--token-file FICHIER] [--mode MODE | --mode-file FICHIER] - [--slow-s N] [--horizon-s S] + [--slow-s N] [--horizon-s S] [--capture FICHIER] etm-powersync-ref-optimizer --list-codes Un serveur JSON-RPC 2.0 sur NDJSON qui répond au `Handshake` et à `GetPlan` par un plan à UN @@ -35,6 +35,12 @@ Modes (`--mode`, pour les tests ; un seul à la fois) : protocol-1.0 un optimiseur 1.0 : handshake 1.0 sans `capabilities`, plan sans `validUntil`, `reasonCode`, `reasonParams` ni `diagnostics` (§9 : reste accepté) deviate cascade servie en ordre de rang INVERSE (contre-épreuve : doit diverger) + wrong-type `planId` numérique au lieu d'une chaîne : le moteur l'ACCEPTE (il le lit comme + texte), le contrat OpenRPC le REFUSE — contre-épreuve de la suite de conformité + +`--capture FICHIER` : chaque message reçu et envoyé, une ligne JSON-RPC par message, dans l'ordre +(la session que `tools/etm-powersync-schema-check --ndjson` juge). Le token d'un handshake y est +remplacé par autant d'astérisques : sa FORME reste vérifiable, sa valeur n'est jamais écrite. `--mode-file` : le mode est relu dans ce fichier à chaque message (un mot ; absent ou illisible = `normal`). Un test change ainsi de mode SANS reconnexion — ce qui distingue « abstention, optimiseur @@ -78,7 +84,7 @@ LIGNE_MAX = 1024 * 1024 # E-T2-1 : une ligne de plus de 1 Mio est écar CODE_TOKEN_REFUSE = -32001 # E-T2-6 MODES = ['normal', 'abstain', 'slow', 'invalid', 'expired', 'wrong-major', 'drop', 'unknown-load', - 'forbidden-kind', 'missing-load', 'echo-token', 'protocol-1.0', 'deviate'] + 'forbidden-kind', 'missing-load', 'echo-token', 'protocol-1.0', 'deviate', 'wrong-type'] # Catalogue (energyplugin/etm/types/decisionreason.cpp) : codes qu'un optimiseur peut émettre et # leurs clés de paramètres documentées (INTERFACE.md). Le test d'intégration les confronte au @@ -341,6 +347,8 @@ def plan(ctx, mode, horizon_s, token): } if mode == 'invalid': resultat['slots'] = 'mode invalid (test) : slots n\'est pas une liste' + elif mode == 'wrong-type': + resultat['planId'] = 42 elif mode == 'protocol-1.0': # Ce qu'un optimiseur 1.0 savait dire : identifiant, stratégie, créneaux, actions et texte. for champ in ('validUntil', 'computedAt', 'diagnostics'): @@ -375,10 +383,31 @@ def lire_token(chemin): return '' # configuré mais illisible : rien ne correspond +VERROU_CAPTURE = threading.Lock() + + +def capturer(cfg, message): + """--capture : une ligne par message. Le token reçu au handshake, et toute recopie du token + configuré (mode echo-token), deviennent autant d'astérisques : forme gardée, valeur jamais écrite.""" + if not cfg.capture: + return + if isinstance(message, dict) and isinstance(message.get('params'), dict) \ + and isinstance(message['params'].get('token'), str): + message = dict(message, params=dict(message['params'], token='*' * len(message['params']['token']))) + ligne = json.dumps(message, separators=(',', ':'), ensure_ascii=False) + token = lire_token(cfg.token_file) + if token: + for forme in (token, json.dumps(token, ensure_ascii=False)[1:-1]): + ligne = ligne.replace(forme, '*' * len(forme)) + with VERROU_CAPTURE, open(cfg.capture, 'a', encoding='utf-8') as f: + f.write(ligne + '\n') + + class Connexion(socketserver.StreamRequestHandler): """Une connexion = un fil ; requêtes traitées dans l'ordre d'arrivée.""" def envoyer(self, message): + capturer(self.server.cfg, message) ligne = json.dumps(message, separators=(',', ':'), ensure_ascii=False).encode() + b'\n' self.wfile.write(ligne) self.wfile.flush() @@ -402,6 +431,7 @@ class Connexion(socketserver.StreamRequestHandler): except ValueError: journal('ligne non JSON écartée') continue + capturer(cfg, message) if not isinstance(message, dict) or 'method' not in message: continue ident, methode = message.get('id'), message.get('method') @@ -497,6 +527,7 @@ def main(argv=None): p.add_argument('--slow-s', type=float, default=12.0, help='délai du mode slow, en s (défaut 12)') p.add_argument('--horizon-s', type=int, default=300, help='durée du créneau et de validUntil, en s (défaut 300)') + p.add_argument('--capture', help='écrit chaque message reçu et envoyé dans ce fichier (NDJSON)') p.add_argument('--list-codes', action='store_true', help='écrit le catalogue des codes émissibles (JSON) et sort') args = p.parse_args(argv)