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:
@@ -65,39 +65,24 @@ check('BUILD_PROMPT: kill-switch OFF → enabled=false', killSwitchOut.enabled =
|
||||
|
||||
// ---------- VALIDATE_PARSE : 7 réponses LLM simulées ----------
|
||||
const ctxNode = { BUILD_PROMPT: promptOut };
|
||||
const validAdvice = '{"constat":"Contacteur reseau stable, batterie en charge.","gravite":"aucune","ce_qui_cloche":"","strategy":"auto_solar_plus","night_target_soc":35,"current_max_a":22,"ev_charge_tonight":"oui","confidence":0.85,"reason":"Forecast fort demain, HC nuit pour la VW."}';
|
||||
const validAdvice = '{"constat":"Contacteur reseau stable, batterie en charge.","gravite":"aucune","ce_qui_cloche":"","piste":"","confidence":0.85}';
|
||||
|
||||
const cases = [
|
||||
{
|
||||
label: 'réponse valide → ok',
|
||||
resp: { model: 'qwen3.6-35b', choices: [{ message: { content: validAdvice } }] },
|
||||
expect: o => o.status === 'ok' && o.advice.strategy === 'auto_solar_plus' && o.advice.nightTargetSoc === 35,
|
||||
expect: o => o.status === 'ok' && o.advice.gravite === 'aucune' && o.advice.constat.includes('stable'),
|
||||
},
|
||||
{
|
||||
label: 'balises <think> + fences markdown → ok',
|
||||
resp: { model: 'qwen3-vl', choices: [{ message: { content: `<think>je réfléchis...\nblabla</think>\n\`\`\`json\n${validAdvice}\n\`\`\`` } }] },
|
||||
expect: o => o.status === 'ok' && o.advice.evChargeTonight === 'oui',
|
||||
expect: o => o.status === 'ok' && o.advice.confidence === 0.85,
|
||||
},
|
||||
{
|
||||
label: 'stratégie hors enum → invalid (rejet)',
|
||||
resp: { model: 'x', choices: [{ message: { content: validAdvice.replace('auto_solar_plus', 'charge_maximale') } }] },
|
||||
label: 'confidence hors bornes → invalid',
|
||||
resp: { model: 'x', choices: [{ message: { content: validAdvice.replace('"confidence":0.85', '"confidence":7') } }] },
|
||||
expect: o => o.status === 'invalid' && o.advice === null,
|
||||
},
|
||||
{
|
||||
// ⚠️ SEMANTIQUE CHANGEE LE 12/09 : un champ d ACTION hors bornes ne fait
|
||||
// plus echouer la reponse — il est NEUTRALISE et l observation survit.
|
||||
// Le constat est devenu la sortie principale ; le perdre a cause d un
|
||||
// champ qu on ne demande plus serait absurde. `actionUtilisable` reste
|
||||
// le seul drapeau qui autoriserait un cablage.
|
||||
label: 'night_target_soc 15 (< plancher 22) → observation gardee, action neutralisee',
|
||||
resp: { model: 'x', choices: [{ message: { content: validAdvice.replace('"night_target_soc":35', '"night_target_soc":15') } }] },
|
||||
expect: o => o.status === 'ok' && o.advice.nightTargetSoc === null && o.advice.actionUtilisable === false && o.advice.constat,
|
||||
},
|
||||
{
|
||||
label: 'current_max_a 40 (> 28) → observation gardee, action neutralisee',
|
||||
resp: { model: 'x', choices: [{ message: { content: validAdvice.replace('"current_max_a":22', '"current_max_a":40') } }] },
|
||||
expect: o => o.status === 'ok' && o.advice.currentMaxA === null && o.advice.actionUtilisable === false,
|
||||
},
|
||||
{
|
||||
label: 'texte sans JSON → invalid',
|
||||
resp: { model: 'x', choices: [{ message: { content: 'Je recommande le mode solaire.' } }] },
|
||||
@@ -121,31 +106,21 @@ const calls = run(BUILD_HA_CALLS, okValidated, ctxNode).map(i => i.json);
|
||||
const ALLOWED = /^(input_text|input_number)\.evcs_advisor_/;
|
||||
const violations = calls.filter(c => !(c.domain === 'logbook' || ALLOWED.test(c.data.entity_id || '')));
|
||||
check('OMBRE: aucune écriture hors advisor_*/logbook', violations.length === 0, JSON.stringify(violations));
|
||||
check('OMBRE: écrit strategy + reason', calls.some(c => (c.data.entity_id || '').includes('advisor_strategy')) && calls.some(c => (c.data.entity_id || '').includes('advisor_reason')));
|
||||
check('OMBRE: écrit constat + reason', calls.some(c => (c.data.entity_id || '').includes('advisor_constat')) && calls.some(c => (c.data.entity_id || '').includes('advisor_reason')));
|
||||
|
||||
// ── LE COMPTEUR MESURE LA CIBLE, CONTRE LE BILAN (12/09/2026) ──────────
|
||||
// Il comparait la STRATEGIE, qui vaut auto_solar_plus la quasi-totalite du
|
||||
// temps : 517/23, soit 95,7 % — un score qui mesurait une constante. La cible,
|
||||
// seul champ cable, divergeait de 13 points du bilan sans etre comptee.
|
||||
function compteur(ctx) {
|
||||
const node = { BUILD_PROMPT: { ...promptOut, context: ctx } };
|
||||
const validated = run(VALIDATE_PARSE, cases[0].resp, node)[0].json;
|
||||
const out = run(BUILD_HA_CALLS, validated, node).map(i => i.json);
|
||||
// ── LE COMPTEUR MESURE LE VOLUME DE SIGNALEMENTS (12/09/2026) ─────────
|
||||
// Un observateur qui signale en permanence ne sert a rien. On ne peut pas
|
||||
// mesurer automatiquement s il a RAISON ; on peut mesurer s il crie au loup.
|
||||
function compteur(json) {
|
||||
const v = run(VALIDATE_PARSE, { model: 'x', choices: [{ message: { content: json } }] }, ctxNode)[0].json;
|
||||
const out = run(BUILD_HA_CALLS, v, ctxNode).map(i => i.json);
|
||||
const id = k => out.some(c => (c.data.entity_id || '').includes(k));
|
||||
return { accord: id('agree_count'), divergence: id('diverge_count'), ecart: id('advisor_divergence') };
|
||||
return { calme: id('agree_count'), signale: id('diverge_count') };
|
||||
}
|
||||
const base = promptOut.context;
|
||||
|
||||
// L'agent dit 35 (le validAdvice des cas ci-dessus).
|
||||
const proche = compteur({ ...base, balanceStatus: 'ok', balanceRequiredSoc: 33 });
|
||||
check('COMPTEUR: cible a 2 points du bilan → accord', proche.accord && !proche.divergence);
|
||||
|
||||
const loin = compteur({ ...base, balanceStatus: 'ok', balanceRequiredSoc: 48.4 });
|
||||
check('COMPTEUR: cible a 13 points du bilan → divergence (le cas reel du 12/09)', loin.divergence && !loin.accord);
|
||||
|
||||
const sansBilan = compteur({ ...base, balanceStatus: 'degrade', balanceRequiredSoc: 48.4 });
|
||||
check('COMPTEUR: bilan indisponible → on ne compte RIEN plutot que de compter faux',
|
||||
!sansBilan.accord && !sansBilan.divergence);
|
||||
const rienASignaler = compteur(validAdvice);
|
||||
check('COMPTEUR: gravite aucune → compte comme calme', rienASignaler.calme && !rienASignaler.signale);
|
||||
const signalement = compteur(validAdvice.replace('"gravite":"aucune"', '"gravite":"haute"'));
|
||||
check('COMPTEUR: gravite haute → compte comme signalement', signalement.signale && !signalement.calme);
|
||||
|
||||
// ── OBSERVATION : le constat et la gravite sont obligatoires ───────────
|
||||
function valider(json) {
|
||||
@@ -163,28 +138,16 @@ check('OBSERVATION: constat vide → REJET',
|
||||
check('OBSERVATION: gravite hors enum → REJET',
|
||||
valider(validAdvice.replace('"gravite":"aucune"', '"gravite":"catastrophique"')).status === 'invalid');
|
||||
|
||||
// Le constat doit survivre a un champ d action hors bornes.
|
||||
const horsBornes = valider(validAdvice
|
||||
.replace('"constat":"Contacteur reseau stable, batterie en charge."', '"constat":"Bascule du contacteur toutes les 1-2 min depuis 06:25."')
|
||||
.replace('"gravite":"aucune"', '"gravite":"haute"')
|
||||
.replace('"current_max_a":22', '"current_max_a":100'));
|
||||
check('OBSERVATION: champ action hors bornes → le constat SURVIT',
|
||||
horsBornes.status === 'ok' && horsBornes.advice.constat.includes('Bascule') && horsBornes.advice.actionUtilisable === false);
|
||||
const hbCalls = run(BUILD_HA_CALLS, horsBornes, ctxNode).map(i => i.json);
|
||||
check('OBSERVATION: action hors bornes → aucun helper numerique ecrit',
|
||||
hbCalls.some(c => (c.data.entity_id || '').includes('advisor_constat')) &&
|
||||
!hbCalls.some(c => (c.data.entity_id || '').includes('night_target_soc')) &&
|
||||
!hbCalls.some(c => (c.data.entity_id || '').includes('agree_count')));
|
||||
|
||||
const oscCalls = run(BUILD_HA_CALLS, osc, ctxNode).map(i => i.json);
|
||||
check('OBSERVATION: constat, gravite et diagnostic publies',
|
||||
['constat', 'gravite', 'ce_qui_cloche'].every(k => oscCalls.some(c => (c.data.entity_id || '').includes(`advisor_${k}`))));
|
||||
check('OMBRE: l observation ne sort pas du perimetre advisor_*',
|
||||
oscCalls.every(c => c.domain === 'logbook' || /^(input_text|input_number)\.evcs_advisor_/.test(c.data.entity_id || '')));
|
||||
|
||||
const offlineValidated = run(VALIDATE_PARSE, cases[6].resp, ctxNode)[0].json;
|
||||
const casHorsLigne = cases.find(c => c.label.includes('llm_offline'));
|
||||
const offlineValidated = run(VALIDATE_PARSE, casHorsLigne.resp, ctxNode)[0].json;
|
||||
const offlineCalls = run(BUILD_HA_CALLS, offlineValidated, ctxNode).map(i => i.json);
|
||||
check('OMBRE: llm_offline → statut écrit, pas de recommandation', offlineCalls.every(c => !(c.data.entity_id || '').includes('advisor_strategy')) && offlineCalls.some(c => (c.data.value || '').toString().startsWith('llm_offline')));
|
||||
check('OMBRE: llm_offline → statut écrit, pas de recommandation', offlineCalls.every(c => !(c.data.entity_id || '').includes('advisor_constat')) && offlineCalls.some(c => (c.data.value || '').toString().startsWith('llm_offline')));
|
||||
|
||||
console.log(failures ? `\n${failures} ÉCHEC(S) — NE PAS POUSSER` : '\nADVISOR OK — tous les tests passent');
|
||||
process.exit(failures ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user