Le conseiller devient observateur, et une couche d enquete voit le jour

SPECIALISATION. Campagne du 12/09 : lui demander d observer ET de
planifier degradait les deux — l ecart de cible entre passages est passe
de 0 a 35 points sur certains cas des l ajout de la mission
d observation. Il ne planifie plus : la planification est faite par le
bilan, qui est calcule, mesure et teste a 48/48. Sortie reduite a
constat / gravite / ce_qui_cloche / piste. Prompt 7609 -> 6250 caracteres.

Mesure apres specialisation, horloge epinglee : oscillation reelle
detectee 5/5, contrefactuel calme 5/5. La discrimination est parfaite.
⚠️ Mais la gravite est retombee de « haute » a « moyenne » : mon echelle
a quatre niveaux n est ancree sur aucun exemple. Defaut de briefing a
corriger, pas defaut de detection.

Le compteur mesure desormais le VOLUME de signalements : on ne peut pas
verifier automatiquement s il a raison, on peut mesurer s il crie au loup.

COUCHE D ENQUETE. Trouver le coupable ce matin a demande sept requetes
enchainees ; rien n y exigeait un humain, seulement d appeler des outils
et d enchainer d apres ce qu on vient de lire. Six sous-commandes en
LECTURE SEULE, exposees en ligne de commande pour qu OpenClaw — que le
proprietaire veut utiliser — ou tout autre pilote les appelle sans
reecriture : bascules, mesures, decisions, ecrivains, bilan, cherche.

`bascules` calcule l ecart entre transitions successives du meme objet :
c est la signature d une oscillation, et l appelant n a pas a la refaire.

L enquete de ce matin se rejoue en trois commandes, jusqu a
automations.yaml ligne 22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
Tallagrand Thierry
2026-09-12 08:36:31 +02:00
parent 6a647dc685
commit 1945fdfe5c
9 changed files with 4586 additions and 185 deletions

View File

@@ -34,64 +34,30 @@ setText('input_text.evcs_advisor_last_run', stampParis());
setText('input_text.evcs_advisor_model', modelUsed);
if (status === 'ok' && advice) {
// Observation — la sortie principale desormais.
// Observation — la seule sortie desormais. Le conseiller ne planifie plus :
// la planification est faite par le bilan, qui est calcule et mesure, et la
// campagne du 12/09 a montre que lui demander les deux degradait les deux
// (ecart de cible passe de 0 a 35 points sur certains cas des l ajout de la
// mission d observation).
setText('input_text.evcs_advisor_constat', advice.constat);
setText('input_text.evcs_advisor_gravite', advice.gravite);
setText('input_text.evcs_advisor_ce_qui_cloche', advice.ceQuiCloche || 'rien de signale');
setText('input_text.evcs_advisor_reason', `[${Math.round(advice.confidence * 100)}%] ${advice.piste || advice.constat}`);
// Recommandations
setText('input_text.evcs_advisor_strategy', advice.strategy);
// Les champs d action ne sont publies que s ils sont exploitables : un
// helper numerique ne doit jamais porter une valeur hors bornes.
if (advice.actionUtilisable) {
setNumber('input_number.evcs_advisor_night_target_soc', advice.nightTargetSoc);
setNumber('input_number.evcs_advisor_current_max_a', advice.currentMaxA);
}
setText('input_text.evcs_advisor_ev_charge_tonight', advice.evChargeTonight);
setText('input_text.evcs_advisor_reason', `[${Math.round(advice.confidence * 100)}%] ${advice.reason}`);
// Divergence vs réel (stratégie + cible nuit)
const diffs = [];
if (advice.strategy !== context.userStrategy) {
diffs.push(`stratégie: agent=${advice.strategy} ≠ réel=${context.userStrategy}`);
}
if (Math.abs(advice.nightTargetSoc - context.nightTargetActual) > 10) {
diffs.push(`cible nuit: agent=${advice.nightTargetSoc}% vs réel=${context.nightTargetActual}%`);
}
const divergenceText = diffs.length ? diffs.join(' | ') : 'accord avec le réel';
setText('input_text.evcs_advisor_divergence', divergenceText);
// ⚠️ LE COMPTEUR MESURAIT LE MAUVAIS CHAMP (corrige le 12/09/2026).
// Il comparait la STRATEGIE, qui vaut `auto_solar_plus` la quasi-totalite du
// temps : 517 accords contre 23 divergences, soit 95,7 % — un score qui
// mesurait une constante, pas une decision. Pendant ce temps la cible SOC,
// seul champ reellement cable, divergeait de treize points du bilan sans
// que rien ne le compte (12/09 05:30 : conseiller 35 %, bilan 48,4 %).
//
// On compte desormais l'accord sur la CIBLE, et contre le BILAN — la
// reference, pas contre la valeur appliquee qui peut elle-meme etre fausse.
// Bilan indisponible => on ne compte rien plutot que de compter faux.
const refSoc = Number(context.balanceRequiredSoc);
const bilanUtilisable = advice.actionUtilisable && context.balanceStatus === 'ok' && Number.isFinite(refSoc);
if (bilanUtilisable) {
const ecart = Math.abs(advice.nightTargetSoc - refSoc);
if (ecart <= 5) {
setNumber('input_number.evcs_advisor_agree_count', context.agreeCount + 1);
} else {
setNumber('input_number.evcs_advisor_diverge_count', context.divergeCount + 1);
}
setText(
'input_text.evcs_advisor_divergence',
`cible: agent=${advice.nightTargetSoc}% vs bilan=${Math.round(refSoc)}% (ecart ${Math.round(ecart)} pts)`
+ (diffs.length ? ` | ${diffs.join(' | ')}` : '')
);
// Un observateur qui signale en permanence ne sert a rien : on compte le
// VOLUME de signalements pour pouvoir le constater.
if (advice.gravite === 'aucune') {
setNumber('input_number.evcs_advisor_agree_count', context.agreeCount + 1);
} else {
setNumber('input_number.evcs_advisor_diverge_count', context.divergeCount + 1);
}
// Trace logbook pour l'historique long terme
push('logbook', 'log', {
name: 'EVCS Advisor (ombre)',
message: `[${advice.gravite}] ${advice.constat}${advice.ceQuiCloche ? ` || ${advice.ceQuiCloche}` : ''} || ${advice.strategy} | cible nuit ${advice.nightTargetSoc}% | ${advice.evChargeTonight === 'oui' ? 'charge VE cette nuit' : advice.evChargeTonight === 'non' ? 'pas de charge nuit' : 'VE sans objet'} | ${divergenceText} | ${advice.reason}`,
entity_id: 'input_text.evcs_advisor_strategy',
name: 'EVCS Advisor (observateur)',
message: `[${advice.gravite}] ${advice.constat}`
+ (advice.ceQuiCloche ? ` || ${advice.ceQuiCloche}` : '')
+ (advice.piste ? ` || piste : ${advice.piste}` : ''),
entity_id: 'input_text.evcs_advisor_constat',
});
}

View File

@@ -192,45 +192,20 @@ const systemPrompt = [
'est un ecrivain AUTRE QUE SON PROPRIETAIRE. Nomme-le.',
// CARTE GENEREE >>>
'',
'STRATEGIES POSSIBLES :',
'- auto_solar_plus : automatique, solaire prioritaire, reseau en heures creuses si besoin',
'- auto_solar : solaire strict, jamais de reseau',
'- force_grid_now : charge sur reseau EDF, immediate',
'- force_charge_now : charge locale immediate quelle que soit la source',
'- stop : tout couper',
'',
'TA MISSION PRINCIPALE : REPERER CE QUI CLOCHE.',
'Cinq automates ecrivent sur la meme installation : le controleur EVCS, un',
'automate d hysteresis pour le contacteur reseau, un gestionnaire de charge',
'batterie, un bilan energetique, et des automatisations Home Assistant. Chacun',
'est localement correct et aucun ne voit les autres. Les pannes de ce systeme',
'ne sont donc presque jamais des erreurs de calcul : ce sont des CONTRADICTIONS',
'entre automates, et elles se voient dans la SUITE des evenements, pas dans',
'un instantane.',
'',
'Exemple reel du 12/09 : le contacteur reseau a bascule toutes les 4 minutes',
'pendant dix minutes — coupe, remis, coupe. Un automate coupait a 35 % de SOC,',
'un autre rallumait. Chacun faisait son travail. Le resultat etait absurde, et',
'seul un humain qui regardait l a vu.',
'',
'Un comportement est ABSURDE quand : un actionneur bascule plusieurs fois en',
'peu de temps ; deux decisions se contredisent ; une grandeur reste figee alors',
'que ce qui la pilote bouge ; une action est repetee sans effet ; une valeur',
'contredit ce que le bilan demande.',
'',
'Tu as le DERNIER HISTORIQUE DES EVENEMENTS plus bas. Sers-t en.',
'TU NE PILOTES RIEN ET TU NE CHOISIS AUCUNE STRATEGIE.',
'La planification energetique est faite par le bilan, qui est calcule et mesure.',
'Ton seul travail est de REGARDER LE SYSTEME SE COMPORTER et de dire ce qui cloche.',
'',
'Reponds UNIQUEMENT avec un objet JSON valide, sans texte autour, au format EXACT :',
'{"constat":"ce que tu observes, factuel, max 200 caracteres",',
'"gravite":"aucune|faible|moyenne|haute",',
'"ce_qui_cloche":"la contradiction ou l incoherence, ou \'\' si rien",',
'"strategy":"auto_solar_plus|auto_solar|force_grid_now|force_charge_now|stop",',
'"night_target_soc":<entier 22-90>,"current_max_a":<entier 6-28>,',
'"ev_charge_tonight":"oui|non|sans_objet","confidence":<0.0-1.0>,',
'"reason":"une phrase courte en francais (max 200 caracteres)"}',
'"piste":"ce qu il faudrait verifier ou corriger, ou \'\' si rien",',
'"confidence":<0.0-1.0>}',
'',
'gravite "aucune" quand tout est coherent — c est le cas le plus frequent et',
'il ne faut pas inventer un probleme pour justifier une reponse.',
'gravite "aucune" quand tout est coherent — c est le cas le plus frequent et de',
'loin. N invente pas un probleme pour justifier une reponse : un observateur qui',
'signale en permanence ne sert a rien.',
].join('\n');
function nombre(v, unite, defaut) {

View File

@@ -7,8 +7,6 @@
const resp = $input.first().json;
const context = $('BUILD_PROMPT').first().json.context;
const STRATEGIES = ['auto_solar_plus', 'auto_solar', 'force_grid_now', 'force_charge_now', 'stop'];
const TONIGHT = ['oui', 'non', 'sans_objet'];
function fail(status, detail) {
return [{
@@ -52,36 +50,6 @@ try {
}
// 4. Validation stricte du schéma (enums + bornes)
const strategy = (obj.strategy ?? '').toString().trim().toLowerCase();
if (!STRATEGIES.includes(strategy)) return fail('invalid', `strategy hors enum: ${strategy}`);
const nightTargetRaw = Number(obj.night_target_soc);
const nightTargetValide = Number.isFinite(nightTargetRaw) && nightTargetRaw >= 22 && nightTargetRaw <= 90;
const nightTarget = nightTargetValide ? nightTargetRaw : null;
// ⚠️ L OBSERVATION NE DOIT PAS ETRE PERDUE A CAUSE D UN CHAMP D ACTION.
// Essai du 12/09 : sur l oscillation du contacteur, le modele a produit un
// constat JUSTE (« bascule toutes les 1-2 min depuis 06:25, gravite haute »)
// ET un `current_max_a` de 100, hors bornes. Une validation tout-ou-rien
// jetait le diagnostic avec le champ fautif — c est-a-dire exactement ce
// qu on lui demande, perdu a cause de ce qu on ne lui demande plus.
// Les champs d ACTION restent bornes aussi strictement qu avant ; seulement,
// hors bornes, ils sont NEUTRALISES au lieu de faire echouer le constat.
const currentMaxRaw = Number(obj.current_max_a);
const currentMaxValide = Number.isFinite(currentMaxRaw) && currentMaxRaw >= 6 && currentMaxRaw <= 28;
const currentMax = currentMaxValide ? currentMaxRaw : null;
const tonight = (obj.ev_charge_tonight ?? '').toString().trim().toLowerCase();
if (!TONIGHT.includes(tonight)) return fail('invalid', `ev_charge_tonight hors enum: ${tonight}`);
const confidence = Number(obj.confidence);
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
return fail('invalid', `confidence hors bornes: ${obj.confidence}`);
}
const reason = (obj.reason ?? '').toString().trim();
if (!reason) return fail('invalid', 'reason vide');
// ── OBSERVATION (12/09/2026) ───────────────────────────────────────────
// La mission principale du conseiller n est plus de choisir une strategie
// parmi cinq, mais de REPERER CE QUI CLOCHE. Les pannes de ce systeme sont
@@ -95,6 +63,12 @@ if (!constat) return fail('invalid', 'constat vide');
const gravite = (obj.gravite ?? '').toString().trim().toLowerCase();
if (!GRAVITES.includes(gravite)) return fail('invalid', `gravite hors enum: ${obj.gravite}`);
const ceQuiCloche = (obj.ce_qui_cloche ?? '').toString().trim();
const piste = (obj.piste ?? '').toString().trim();
const confidence = Number(obj.confidence);
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
return fail('invalid', `confidence hors bornes: ${obj.confidence}`);
}
return [{
json: {
@@ -104,15 +78,8 @@ return [{
constat: constat.slice(0, 200),
gravite,
ceQuiCloche: ceQuiCloche.slice(0, 200),
strategy,
nightTargetSoc: nightTarget === null ? null : Math.round(nightTarget),
currentMaxA: currentMax === null ? null : Math.round(currentMax),
// Vrai seulement si TOUS les champs d action sont dans leurs bornes.
// C est ce drapeau, et lui seul, qui autoriserait un jour un cablage.
actionUtilisable: currentMaxValide && nightTargetValide,
evChargeTonight: tonight,
piste: piste.slice(0, 200),
confidence: Math.round(confidence * 100) / 100,
reason: reason.slice(0, 250),
},
modelUsed: (resp.model || 'inconnu').toString().slice(0, 60),
context,