Le conseiller ne lisait pas mal : on lui parlait mal

Reprise de la campagne apres la specialisation en observateur. Le harnais
affichait encore `strategy` et `night_target_soc`, deux champs que
l observateur n emet plus : il mesurait des undefined. Reecrit pour noter
ce qui compte chez un observateur — la gravite tombe-t-elle dans la
fourchette attendue, et a-t-il NOMME la chose, par des mots-cles choisis
hors du vocabulaire du prompt.

Quatre defauts trouves, aucun n etant un defaut de raisonnement du modele :

- « Contacteur FERME » : pour un relais, ferme veut dire qui laisse passer.
  Le modele l a lu comme coupe et a demande, reseau present, « pourquoi le
  contacteur reste FERME alors que le bilan juge le reseau necessaire ».
- Le signe de l excedent : le prompt affichait « 14 W », il a ecrit
  « negatif (-14 W) » — et sur un autre cas « negatif » puis « positif » au
  passage suivant, sur le MEME prompt.
- « day_vehicle_full_idle » ne veut rien dire pour lui : il signalait une
  ressource refusee sans voir que le vehicule est PLEIN.
- Un excedent calcule sous censure ne veut rien dire : sur le BOOST du
  11/09 il concluait « pas de soleil, donc le reseau est normal » alors que
  les 121 W de PV sont bas PARCE QUE la consigne de 8260 W les evince.

Plus une grandeur derivee de plus, `consigne - maison`, parce qu il voyait
la version flagrante du defaut (SOC 20, 0 A) et manquait la version
discrete (SOC 45, +2 A). Elle a d abord fait TOMBER le cas du BOOST, en
annoncant « il reste 7005 W pour recharger » comme une bonne nouvelle sur
une batterie a 96 % : corrigee en lui faisant dire l inverse quand il n y a
rien a recharger, la question ayant deja une reponse dans le bilan.

Trois defauts etaient les miens : deux fixtures decrivaient un etat que
l installation ne peut pas prendre — c est le conseiller qui l a signale —
et un attendu exigeait « haute » la ou l echelle de gravite du prompt ancre
elle-meme cet incident sur « moyenne ».

Incidents 39/40 au final, contre 23/35 au depart. Les deux cas de vrai
silence restent silencieux 5/5.

⚠️ Trouve en marge : cinq helpers du conseiller sont geles depuis la
specialisation, et le cablage de la cible SOC matin les lit encore — ses
quatre gardes passent toutes, la fraicheur datant le RUN et non la VALEUR.

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-13 11:59:24 +02:00
parent 7973fa4c3f
commit 4a9ad0bc65
16 changed files with 12179 additions and 470 deletions

View File

@@ -56,7 +56,9 @@ export const CAS = [
// ─────────── NUIT ET RESEAU ───────────
{ id:'1-nuit-bloquee-sans-vehicule', t:'2026-09-12T04:00:00+02:00', certain:true,
titre:"12/09 04:00 — SOC 20, HC ouvertes, aucun vehicule, bilan 59,1 %",
attendu:"AGIR : engager le reseau, cible ~59 %",
attendu:"SIGNALER : reseau engage mais la batterie reste plate a 20 % alors que le bilan reclame 3,4 kWh",
graviteAttendue:["moyenne","haute"],
motsCles:[["recharg","remonte","ne monte","stagne","plate","0 a","0a","immobile"]],
s:{ soc_percent:'20', battery_current_a:'0', house_ac_power_w:'612',
grid_state:'on', acin1_available:'1', night_target_soc_actual:'59.1',
tomorrow_kwh:'75.2', balance_required_soc:'59.1', balance_required_soc_prudent:'66.2',
@@ -65,7 +67,9 @@ export const CAS = [
{ id:'2-nuit-relachee-trop-tot', t:'2026-09-03T03:00:00+02:00', certain:true,
titre:"03/09 03:00 — SOC 45, bilan 50,4 % (releche a tort : batterie a 20 % a 06:30)",
attendu:"AGIR : tenir le reseau, cible ~50 %",
attendu:"SIGNALER : SOC sous le requis alors que la session est marquee terminee",
graviteAttendue:["faible","moyenne","haute"],
motsCles:[["sous","en dessous","inferieur","manque","deficit","insuffisant"],["terminee","done","fini","relach","session"]],
s:{ soc_percent:'45', battery_current_a:'2', house_ac_power_w:'700',
grid_state:'on', acin1_available:'1', evcs_vehicle_connected:'on',
evcs_surplus_state:'night_session_done_idle', evcs_source_mode:'Réseau HC autorisé',
@@ -75,7 +79,8 @@ export const CAS = [
{ id:'3-nuit-couverte-ne-rien-faire', t:'2026-09-12T04:00:00+02:00', certain:true,
titre:"CONTREFACTUEL — meme nuit mais SOC 70, au-dessus des 59,1 % requis",
attendu:"NE RIEN FAIRE : auto_solar_plus, pas de reseau",
attendu:"NE RIEN SIGNALER : au-dessus de la ligne, reseau ouvert, tout est coherent",
graviteAttendue:["aucune"],
s:{ soc_percent:'70', battery_current_a:'-12', house_ac_power_w:'612',
grid_state:'off', acin1_available:'0', night_target_soc_actual:'59.1',
tomorrow_kwh:'75.2', balance_required_soc:'59.1', balance_required_soc_prudent:'66.2',
@@ -83,7 +88,8 @@ export const CAS = [
{ id:'4-hiver-nuit-longue', t:'2026-12-15T23:30:00+01:00', certain:false,
titre:"HIVER — 15/12 23:30, prevision 6 kWh demain, croisement tardif a 11 h",
attendu:"OUVERT : quelle cible pour une nuit d hiver ?",
attendu:"OUVERT : voit-il l ecart entre la cible 85 et les 88 % requis, et le deficit de stockage ?",
graviteAttendue:null,
s:{ soc_percent:'55', battery_current_a:'-18', house_ac_power_w:'1400',
grid_state:'off', acin1_available:'0', night_target_soc_actual:'85',
tomorrow_kwh:'6.2', balance_required_soc:'88', balance_required_soc_prudent:'97',
@@ -94,19 +100,21 @@ export const CAS = [
// ─────────── SOLAIRE ET VOITURE ───────────
{ id:'5-soir-deficit-ne-pas-demarrer', t:'2026-09-03T19:44:00+02:00', certain:true,
titre:"03/09 19:44 — PV 758 W mais maison 1202 W : excedent NEGATIF (-444 W)",
attendu:"NE PAS DEMARRER la charge",
attendu:"CALME (le correctif du 03/09 tient) : la charge ne demarre pas, excedent negatif",
graviteAttendue:["aucune","faible"],
s:{ soc_percent:'94', battery_current_a:'-12.2', pv_power_w:'758', pv_power_10m_avg_w:'900',
house_ac_power_w:'1202', evcs_vehicle_connected:'on', vehicle_selected:'VW',
evcs_source_mode:'Potentiel solaire', mppt_operation_modes:MAX,
evcs_source_mode:'Potentiel solaire', evcs_surplus_state:'idle_wait_conditions', mppt_operation_modes:MAX,
balance_required_soc:'99.7', balance_required_soc_prudent:'100',
balance_energy_to_cover_kwh:'11.66', balance_grid_needed:'on', tomorrow_kwh:'33.9' } },
{ id:'6-censure-demarrer', t:'2026-09-03T18:50:00+02:00', certain:true,
titre:"03/09 18:50 — PV 769 W, maison 755 W, chargeurs BRIDES : ressource refusee",
attendu:"DEMARRER : la ressource est gratuite",
attendu:"SIGNALER : ressource refusee et vehicule branche a l arret, energie perdue",
graviteAttendue:["faible","moyenne"],
s:{ soc_percent:'94', battery_current_a:'0', pv_power_w:'769', pv_power_10m_avg_w:'800',
house_ac_power_w:'755', evcs_vehicle_connected:'on', vehicle_selected:'VW',
evcs_source_mode:'Potentiel solaire', mppt_operation_modes:LIM,
evcs_source_mode:'Potentiel solaire', evcs_surplus_state:'idle_wait_conditions', mppt_operation_modes:LIM,
balance_pv_censored:'on', balance_censor_reason:'batterie_pleine_sans_puits',
balance_required_soc:'99.7', balance_required_soc_prudent:'100',
balance_energy_to_cover_kwh:'11.66', balance_grid_needed:'on',
@@ -114,7 +122,8 @@ export const CAS = [
{ id:'7-boost-evince-le-solaire', t:'2026-09-11T17:02:00+02:00', certain:true,
titre:"11/09 17:02 — BOOST 28 A, consigne 8260 W, PV effondre a 121 W",
attendu:"AGIR : reduire la demande reseau, le solaire est evince",
attendu:"SIGNALER : la consigne reseau evince le solaire — l ancrage du prompt dit moyenne",
graviteAttendue:["moyenne","haute"],
s:{ soc_percent:'96', battery_current_a:'0', pv_power_w:'121', pv_power_10m_avg_w:'150',
house_ac_power_w:'7695', evcs_vehicle_connected:'on', evcs_charging:'on',
evcs_current_a:'28', vehicle_selected:'JEEP', user_strategy:'force_grid_now',
@@ -129,7 +138,8 @@ export const CAS = [
{ id:'8-voiture-pleine-ne-pas-reessayer', t:'2026-09-07T14:28:00+02:00', certain:true,
titre:"07/09 14:28 — borne annonce charge terminee, vehicule branche immobile",
attendu:"NE RIEN FAIRE : ne pas relancer la charge",
attendu:"CALME : l etat collant fait son travail, aucune relance",
graviteAttendue:["aucune","faible"],
s:{ soc_percent:'97', battery_current_a:'1', pv_power_w:'2114', pv_power_10m_avg_w:'2000',
house_ac_power_w:'1965', evcs_vehicle_connected:'on', evcs_current_a:'0',
vehicle_selected:'JEEP', evcs_surplus_state:'day_vehicle_full_idle',
@@ -142,7 +152,9 @@ export const CAS = [
// ─────────── PROTECTION DU MATERIEL ───────────
{ id:'9-pointe-cuisson-proteger-le-parc', t:'2026-09-05T20:40:00+02:00', certain:true,
titre:"05/09 20:40 — maison 4944 W, batterie a -97,4 A pour une limite de 100 A",
attendu:"AGIR : appeler le reseau, le parc est a sa limite",
attendu:"SIGNALER GRAVE : le parc est a 97,4 A pour une limite de 100 A, reseau ouvert",
graviteAttendue:["haute"],
motsCles:[["limite","plafond","maximum","97","capacite","au bord","saturation"]],
s:{ soc_percent:'79', battery_current_a:'-97.4', pv_power_w:'320', pv_power_10m_avg_w:'500',
house_ac_power_w:'4944', grid_state:'off', acin1_available:'0',
balance_required_soc:'62', balance_required_soc_prudent:'70',
@@ -150,7 +162,9 @@ export const CAS = [
{ id:'10-nuage-sur-charge-28a', t:'2026-09-11T18:00:00+02:00', certain:true,
titre:"11/09 18:00 — voiture a 28 A, consigne descendue a 1281 W, PV 6058 W",
attendu:"AGIR : garder de la marge reseau, un nuage tomberait sur la batterie",
attendu:"SIGNALER : consigne a 1281 W sous une voiture a 28 A, un nuage tombe sur la batterie",
graviteAttendue:["faible","moyenne","haute"],
motsCles:[["nuage","marge","brutal","chute","passage","si le solaire"]],
s:{ soc_percent:'96', battery_current_a:'-0.7', pv_power_w:'6058', pv_power_10m_avg_w:'5500',
house_ac_power_w:'6845', evcs_vehicle_connected:'on', evcs_charging:'on',
evcs_current_a:'28', vehicle_selected:'JEEP', user_strategy:'force_grid_now',
@@ -165,7 +179,8 @@ export const CAS = [
// ─────────── CAS OUVERTS ───────────
{ id:'11-batterie-pleine-midi', t:'2026-09-11T12:00:00+02:00', certain:false,
titre:"11/09 12:00 — SOC 98, production refusee, AUCUN vehicule branche",
attendu:"OUVERT : y a-t-il quelque chose a faire ?",
attendu:"OUVERT : production refusee sans preneur, aucun vehicule. Voit-il l energie perdue ?",
graviteAttendue:null,
s:{ soc_percent:'98', battery_current_a:'2.2', pv_power_w:'810', pv_power_10m_avg_w:'900',
house_ac_power_w:'702', mppt_operation_modes:LIM,
balance_pv_censored:'on', balance_censor_reason:'batterie_pleine_sans_puits',
@@ -177,7 +192,8 @@ export const CAS = [
{ id:'12-orage-annonce', t:'2026-09-12T15:30:00+02:00', certain:false,
titre:"Vigilance ORANGE annoncee, SOC 72, coupures frequentes sur ce site",
attendu:"OUVERT : jusqu ou constituer la reserve ?",
attendu:"OUVERT : vigilance orange, reserve 88 % contre SOC 72. Quel arbitrage ?",
graviteAttendue:null,
s:{ soc_percent:'72', battery_current_a:'35', pv_power_w:'4200', pv_power_10m_avg_w:'4000',
house_ac_power_w:'900', mppt_operation_modes:MAX,
balance_storm_level:'Orange', balance_storm_reserve_soc:'88',
@@ -187,7 +203,8 @@ export const CAS = [
{ id:'13-voiture-empruntee-echeance', t:'2026-09-11T17:00:00+02:00', certain:false,
titre:"11/09 17:00 — voiture EMPRUNTEE a rendre a 19 h, 28 A possible, soleil present",
attendu:"OUVERT : vitesse contre solaire, arbitrage humain",
attendu:"OUVERT : echeance 19 h contre solaire. Voit-il l arbitrage humain ?",
graviteAttendue:null,
s:{ soc_percent:'96', battery_current_a:'-10', pv_power_w:'4255', pv_power_10m_avg_w:'4000',
house_ac_power_w:'4888', evcs_vehicle_connected:'on', evcs_charging:'on',
evcs_current_a:'20', vehicle_selected:'JEEP', user_strategy:'force_charge_now',
@@ -202,7 +219,8 @@ export const CAS = [
{ id:'4b-hiver-parc-12', t:'2026-12-15T23:30:00+01:00', certain:false,
titre:"HIVER avec le parc de cet hiver — 12 equivalents (4 US2000C + 4 US5000)",
attendu:"OUVERT : la cible baisse-t-elle quand la reserve triple ?",
attendu:"OUVERT : le parc triple change-t-il sa lecture de la nuit d hiver ?",
graviteAttendue:null,
s:{ soc_percent:'55', battery_current_a:'-18', house_ac_power_w:'1400',
grid_state:'off', acin1_available:'0', night_target_soc_actual:'85',
tomorrow_kwh:'6.2', balance_required_soc:'52', balance_required_soc_prudent:'59',
@@ -214,7 +232,8 @@ export const CAS = [
{ id:'4c-hiver-parc-16', t:'2026-12-15T23:30:00+01:00', certain:false,
titre:"HIVER avec 16 equivalents (vieux modules repares)",
attendu:"OUVERT : la cible baisse-t-elle encore ?",
attendu:"OUVERT : et avec 16 equivalents ?",
graviteAttendue:null,
s:{ soc_percent:'55', battery_current_a:'-18', house_ac_power_w:'1400',
grid_state:'off', acin1_available:'0', night_target_soc_actual:'85',
tomorrow_kwh:'6.2', balance_required_soc:'44', balance_required_soc_prudent:'50',
@@ -226,7 +245,9 @@ export const CAS = [
{ id:'15-oscillation-contacteur', t:'2026-09-12T06:36:00+02:00', certain:true,
titre:"INCIDENT 12/09 06:25-06:35 — le contacteur bascule six fois en dix minutes",
attendu:"REPERER : gravite haute, nommer le conflit entre ecrivains",
attendu:"SIGNALER GRAVE : six bascules en dix minutes, nommer le conflit entre ecrivains",
graviteAttendue:["haute"],
motsCles:[["oscill","bascul","va-et-vient","aller-retour","repet","six fois","6 fois","cycl"],["conflit","deux","ecriv","contradict","se battent","concurren","automatisation","hysteresis"]],
s:{ soc_percent:'35', battery_current_a:'20', pv_power_w:'0', house_ac_power_w:'950',
grid_state:'on', acin1_available:'1', night_target_soc_actual:'42.8',
balance_required_soc:'42.8', balance_required_soc_prudent:'49',
@@ -237,7 +258,8 @@ export const CAS = [
{ id:'16-systeme-calme', t:'2026-09-12T07:00:00+02:00', certain:true,
titre:"CONTREFACTUEL — meme nuit, contacteur stable depuis 90 minutes",
attendu:"NE RIEN SIGNALER : gravite aucune",
attendu:"NE RIEN SIGNALER : meme nuit, contacteur stable depuis 90 minutes",
graviteAttendue:["aucune"],
s:{ soc_percent:'42', battery_current_a:'20', pv_power_w:'0', house_ac_power_w:'950',
grid_state:'on', acin1_available:'1', night_target_soc_actual:'42.8',
balance_required_soc:'42.8', balance_required_soc_prudent:'49',
@@ -247,13 +269,33 @@ export const CAS = [
{ id:'14-bilan-indisponible', t:'2026-09-12T04:00:00+02:00', certain:true,
titre:"ROBUSTESSE — meme nuit que le cas 1 mais bilan DEGRADE",
attendu:"Le dire dans sa raison et rester prudent",
attendu:"SIGNALER en le disant : le bilan est degrade, donc prudence assumee",
graviteAttendue:["faible","moyenne","haute"],
motsCles:[["degrad","indisponible","sans bilan","pas de bilan","inconnu","unknown"]],
s:{ soc_percent:'20', battery_current_a:'0', house_ac_power_w:'612',
grid_state:'on', acin1_available:'1', night_target_soc_actual:'35',
tomorrow_kwh:'75.2', balance_status:'degrade',
balance_required_soc:'unknown', balance_energy_to_cover_kwh:'unknown' } },
];
// ── Notation ────────────────────────────────────────────────────────────────
// Un observateur ne se juge pas a une valeur qu'il propose mais a sa capacite
// a SEPARER un systeme qui derape d'un systeme qui va bien. D'ou deux mesures
// seulement : la gravite tombe-t-elle dans la fourchette attendue, et a-t-il
// NOMME la chose (mots-cles choisis hors du vocabulaire du prompt).
// Les cas ouverts ne sont pas notes : ils se lisent.
const sansAccent = s => String(s ?? '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
function noter(cas, x) {
if (x.erreur) return { g: false, k: false };
const g = cas.graviteAttendue ? cas.graviteAttendue.includes(String(x.gravite)) : null;
const texte = sansAccent(`${x.constat} ${x.ce_qui_cloche} ${x.piste}`);
const k = cas.motsCles?.length
? cas.motsCles.every(groupe => groupe.some(t => texte.includes(sansAccent(t))))
: null;
return { g, k };
}
async function demander(messages) {
const r = await fetch(URL, { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ model:'qwen3-coder', temperature:0.1, max_tokens:600, messages }) });
@@ -271,19 +313,48 @@ for (const cas of CAS) {
const p = buildPrompt({ ...BASE, ...cas.s }, cas.t);
const passages = [];
for (let i = 0; i < PASSAGES; i++) passages.push(await demander(p.messages));
resultats.push({ ...cas, passages });
const cibles = passages.map(x => x.night_target_soc).filter(Number.isFinite);
const ecart = cibles.length ? Math.max(...cibles) - Math.min(...cibles) : null;
console.log(`\n═══ ${cas.id}${cas.certain ? '' : ' [OUVERT]'}`);
const notes = passages.map(x => noter(cas, x));
const gravites = passages.map(x => x.erreur ? '✖' : String(x.gravite));
const distinctes = [...new Set(gravites)];
resultats.push({ ...cas, passages, notes });
console.log(`\n═══ ${cas.id}${cas.graviteAttendue ? '' : ' [OUVERT]'}`);
console.log(` ${cas.titre}`);
console.log(` attendu : ${cas.attendu}`);
for (const x of passages) {
console.log(x.erreur ? ` ✖ ${x.erreur}`
: ` [${String(x.gravite).padEnd(7)}] ${String(x.constat).slice(0,88).padEnd(90)} ${String(x.strategy).padEnd(16)} cible ${String(x.night_target_soc).padStart(3)}`);
if (!x.erreur && x.ce_qui_cloche) console.log(` ↳ ${String(x.ce_qui_cloche).slice(0,120)}`);
for (let i = 0; i < passages.length; i++) {
const x = passages[i], n = notes[i];
if (x.erreur) { console.log(` ✖ ${x.erreur}`); continue; }
const marque = n.g === null ? ' ' : n.g ? '✔' : '✘';
const mot = n.k === null ? '' : n.k ? ' [nomme]' : ' [ne nomme pas]';
console.log(` ${marque} [${String(x.gravite).padEnd(7)}] ${String(x.constat).slice(0,92)}`);
if (x.ce_qui_cloche) console.log(` ↳ ${String(x.ce_qui_cloche).slice(0,120)}`);
if (x.piste) console.log(` → ${String(x.piste).slice(0,120)}${mot}`);
else if (mot) console.log(` ${mot.trim()}`);
}
if (ecart !== null) console.log(` → ecart de cible entre passages : ${ecart} pts`);
const justes = notes.filter(n => n.g === true).length;
const nommes = notes.filter(n => n.k === true).length;
console.log(` gravites : ${gravites.join(' ')} ${distinctes.length === 1 ? 'STABLE' : `INSTABLE (${distinctes.length} valeurs)`}`);
if (cas.graviteAttendue) console.log(` gravite dans la fourchette ${cas.graviteAttendue.join('|')} : ${justes}/${PASSAGES}`);
if (cas.motsCles?.length) console.log(` a nomme la chose : ${nommes}/${PASSAGES}`);
}
fs.writeFileSync('docs/mesures/campagne-conseiller-' + new Date().toISOString().slice(0,10) + '.json',
JSON.stringify(resultats, null, 2));
console.log('\nJournal complet : docs/mesures/campagne-conseiller-*.json');
// ── Verdict : la separation incident / systeme calme ─────────────────────────
const notes = c => c.notes.filter(n => n.g === true).length;
const incidents = resultats.filter(c => c.graviteAttendue && !c.graviteAttendue.includes('aucune'));
const calmes = resultats.filter(c => c.graviteAttendue && c.graviteAttendue.includes('aucune'));
const ouverts = resultats.filter(c => !c.graviteAttendue);
const somme = l => l.reduce((s, c) => s + notes(c), 0);
console.log('\n────────────────────────────────────────────────────────────');
console.log(`INCIDENTS : ${somme(incidents)}/${incidents.length * PASSAGES} gravites justes (${incidents.length} cas)`);
for (const c of incidents) {
const nommes = c.motsCles?.length ? ` nomme ${c.notes.filter(n => n.k === true).length}/${PASSAGES}` : '';
console.log(` ${String(notes(c)).padStart(2)}/${PASSAGES} ${c.id}${nommes}`);
}
console.log(`SYSTEME CALME : ${somme(calmes)}/${calmes.length * PASSAGES} (silence attendu) (${calmes.length} cas)`);
for (const c of calmes) console.log(` ${String(notes(c)).padStart(2)}/${PASSAGES} ${c.id}`);
console.log(`CAS OUVERTS : ${ouverts.length}, non notes, a lire`);
const fichier = 'docs/mesures/campagne-conseiller-' + new Date().toISOString().slice(0,10) + '.json';
fs.writeFileSync(fichier, JSON.stringify(resultats, null, 2));
console.log(`\nJournal complet : ${fichier}`);