R1: déduplication du moteur nuit — WINDOWS_SOURCEV2 unique calculateur
- Le moteur nuit (161 lignes) était calculé 2x (WINDOWS + BCL), chaque patch nuit devait être appliqué 2x (P2, 04/08, P7) -> risque de divergence. - WINDOWS calcule tout (+ startMs, needsCapture strict, recoveryAllowedNow, targetReached) et exporte nightEvcsPreview étendu. - BCL consomme prev.nightEvcsPreview avec fallbacks sûrs (preview absent -> 'blocked'). Export nightEvcs inchangé pour DECISION/HA_SERVICE. - A/B git HEAD vs patché : 7 scénarios + 2 cas limites, sorties strictement identiques. Déployé en production, exécutions vérifiées success.
This commit is contained in:
253
tools/patch-20260807-r1-dedup-night.mjs
Normal file
253
tools/patch-20260807-r1-dedup-night.mjs
Normal file
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env node
|
||||
// R1 (07/08/2026) : déduplication du moteur nuit EVCS.
|
||||
// - WINDOWS_SOURCEV2 devient l'UNIQUE calculateur (ajoute les 4 champs que seul
|
||||
// BCL calculait : nightSessionStartMs, needsCapture strict, batteryRecoveryAllowedNow,
|
||||
// batteryTargetReached) et exporte tout dans nightEvcsPreview.
|
||||
// - BATTERY_CLOUD_LOCKS ne recalcule plus rien : il consomme prev.nightEvcsPreview.
|
||||
// Le shape de l'export nightEvcs de BCL (consommé par DECISION/HA_SERVICE) est inchangé.
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
const FILE = 'workflows/evcs-surplus-controller-v2.6.json';
|
||||
const wf = JSON.parse(fs.readFileSync(FILE, 'utf8'));
|
||||
|
||||
function getNode(name) {
|
||||
const node = wf.nodes.find(n => n.name === name);
|
||||
if (!node) throw new Error(`Node introuvable: ${name}`);
|
||||
return node;
|
||||
}
|
||||
|
||||
function replaceExact(code, from, to, label) {
|
||||
if (!code.includes(from)) throw new Error(`Pattern introuvable (${label}):\n---\n${from}\n---`);
|
||||
if (code.split(from).length !== 2) throw new Error(`Pattern non unique (${label})`);
|
||||
return code.replace(from, to);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 1. WINDOWS_SOURCEV2
|
||||
// =====================================================
|
||||
const wNode = getNode('WINDOWS_SOURCEV2');
|
||||
let w = wNode.parameters.jsCode;
|
||||
|
||||
// 1a. nightSessionStartMs (repris de BCL — bookkeeping de la mémoire de session)
|
||||
w = replaceExact(
|
||||
w,
|
||||
`const nightSessionStartAt =
|
||||
nightSessionActive
|
||||
? (history.nightSessionStartAt || '')
|
||||
: '';`,
|
||||
`const nightSessionStartAt =
|
||||
nightSessionActive
|
||||
? (history.nightSessionStartAt || '')
|
||||
: '';
|
||||
|
||||
const nightSessionStartMs =
|
||||
nightSessionActive
|
||||
? (history.nightSessionStartMs ?? null)
|
||||
: null;`,
|
||||
'W-1a startMs'
|
||||
);
|
||||
|
||||
// 1b. needsCapture : version stricte (celle de BCL, qui faisait foi en prod
|
||||
// via DECISION) — inclut le critère nightSessionStartMs.
|
||||
w = replaceExact(
|
||||
w,
|
||||
`const nightSessionNeedsCapture =
|
||||
nightSessionActive &&
|
||||
(
|
||||
!Number.isFinite(history.nightSessionStartSocPct) ||
|
||||
!history.nightSessionStartAt
|
||||
);`,
|
||||
`const nightSessionNeedsCapture =
|
||||
nightSessionActive &&
|
||||
(
|
||||
!Number.isFinite(history.nightSessionStartSocPct) ||
|
||||
!nightSessionStartAt ||
|
||||
nightSessionStartMs === null
|
||||
);`,
|
||||
'W-1b needsCapture strict'
|
||||
);
|
||||
|
||||
// 1c. batteryRecoveryAllowedNow + batteryTargetReached (repris de BCL)
|
||||
w = replaceExact(
|
||||
w,
|
||||
`const nightPreferredSourceMode =
|
||||
!nightSessionActive
|
||||
? 'blocked'
|
||||
: batteryAssistAllowed
|
||||
? 'battery_assist_night'
|
||||
: (
|
||||
batteryBelowHardFloor ||
|
||||
batteryBelowMorningTarget ||
|
||||
batteryRecoveryUrgent
|
||||
)
|
||||
? 'grid_forced'
|
||||
: 'grid_hc_allowed';`,
|
||||
`const nightPreferredSourceMode =
|
||||
!nightSessionActive
|
||||
? 'blocked'
|
||||
: batteryAssistAllowed
|
||||
? 'battery_assist_night'
|
||||
: (
|
||||
batteryBelowHardFloor ||
|
||||
batteryBelowMorningTarget ||
|
||||
batteryRecoveryUrgent
|
||||
)
|
||||
? 'grid_forced'
|
||||
: 'grid_hc_allowed';
|
||||
|
||||
const batteryRecoveryAllowedNow =
|
||||
nightSessionActive &&
|
||||
nightGridTakeoverNeeded &&
|
||||
(
|
||||
batteryBelowMorningTarget ||
|
||||
batteryRecoveryUrgent
|
||||
);
|
||||
|
||||
const batteryTargetReached =
|
||||
nightSessionActive &&
|
||||
parsed.soc >= batteryMorningTargetPct;`,
|
||||
'W-1c recoveryAllowedNow + targetReached'
|
||||
);
|
||||
|
||||
// 1d. Export complet (nightEvcsPreview ⊇ nightEvcs de BCL + champs debug)
|
||||
w = replaceExact(
|
||||
w,
|
||||
` nightEvcsPreview: {
|
||||
nightSessionActive,
|
||||
nightSessionNeedsCapture,
|
||||
nightSessionNeedsClear,
|
||||
nightSessionStartSocPct,
|
||||
nightSessionStartAt,
|
||||
|
||||
batteryHardFloorPct,
|
||||
batteryMorningTargetPct,
|
||||
maxBatteryDischargePoints,
|
||||
gridTakeoverTriggerPct,
|
||||
|
||||
batteryBelowHardFloor,
|
||||
batteryBelowMorningTarget,
|
||||
batteryAtOrBelowGridTakeoverTrigger,
|
||||
|
||||
batteryKwhMissingToTarget,
|
||||
batteryRecoveryMinutesNeeded,
|
||||
batteryRecoveryUrgent,
|
||||
|
||||
batteryAssistAllowed,
|
||||
nightGridTakeoverNeeded,
|
||||
nightPreferredSourceMode,
|
||||
}`,
|
||||
` // R1 : source de vérité UNIQUE du moteur nuit.
|
||||
// BATTERY_CLOUD_LOCKS consomme cet objet, il ne recalcule plus rien.
|
||||
nightEvcsPreview: {
|
||||
nightSessionActive,
|
||||
nightSessionNeedsCapture,
|
||||
nightSessionNeedsClear,
|
||||
nightSessionStartSocPct,
|
||||
nightSessionStartAt,
|
||||
nightSessionStartMs,
|
||||
|
||||
batteryHardFloorPct,
|
||||
batteryMorningTargetPct,
|
||||
maxBatteryDischargePoints,
|
||||
gridTakeoverTriggerPct,
|
||||
|
||||
batteryBelowHardFloor,
|
||||
batteryBelowMorningTarget,
|
||||
batteryAtOrBelowGridTakeoverTrigger,
|
||||
|
||||
batteryKwhMissingToTarget,
|
||||
batteryRecoveryMinutesNeeded,
|
||||
batteryRecoveryUrgent,
|
||||
batteryRecoveryAllowedNow,
|
||||
batteryTargetReached,
|
||||
|
||||
batteryAssistAllowed,
|
||||
nightGridTakeoverNeeded,
|
||||
nightTakeoverLatched,
|
||||
nightGridHcPreferred,
|
||||
nightPreferredSourceMode,
|
||||
}`,
|
||||
'W-1d export étendu'
|
||||
);
|
||||
|
||||
wNode.parameters.jsCode = w;
|
||||
console.log('OK: WINDOWS_SOURCEV2 (4 modifications)');
|
||||
|
||||
// =====================================================
|
||||
// 2. BATTERY_CLOUD_LOCKS : remplacement du bloc dupliqué
|
||||
// =====================================================
|
||||
const bNode = getNode('BATTERY_CLOUD_LOCKS');
|
||||
let b = bNode.parameters.jsCode;
|
||||
|
||||
const START_MARKER = `// =====================================================
|
||||
// EVCS NIGHT ENGINE
|
||||
// =====================================================`;
|
||||
const END_MARKER = `const batteryTargetReached =
|
||||
nightSessionActive &&
|
||||
parsed.soc >= batteryMorningTargetPct;`;
|
||||
|
||||
const startIdx = b.indexOf(START_MARKER);
|
||||
const endIdx = b.indexOf(END_MARKER);
|
||||
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
|
||||
throw new Error('Bornes du bloc EVCS NIGHT ENGINE introuvables dans BCL');
|
||||
}
|
||||
const removed = b.slice(startIdx, endIdx + END_MARKER.length);
|
||||
console.log(`BCL: bloc dupliqué localisé (${removed.split('\n').length} lignes, ${removed.length} chars)`);
|
||||
|
||||
const CONSUME_BLOCK = `// =====================================================
|
||||
// EVCS NIGHT ENGINE — R1 (07/08/2026)
|
||||
// Le moteur nuit est calculé UNE SEULE FOIS dans WINDOWS_SOURCEV2
|
||||
// (payload nightEvcsPreview). Ce node CONSOMME, il ne recalcule plus.
|
||||
// TOUTE évolution de la logique nuit se fait dans WINDOWS_SOURCEV2.
|
||||
// Fallback sûr si preview absent : session inactive / 'blocked'.
|
||||
// =====================================================
|
||||
const nightPrev = prev.nightEvcsPreview || {};
|
||||
|
||||
const nightSessionActive = !!nightPrev.nightSessionActive;
|
||||
const nightSessionNeedsCapture = !!nightPrev.nightSessionNeedsCapture;
|
||||
const nightSessionNeedsClear = !!nightPrev.nightSessionNeedsClear;
|
||||
const nightSessionStartSocPct =
|
||||
Number.isFinite(nightPrev.nightSessionStartSocPct) ? nightPrev.nightSessionStartSocPct : null;
|
||||
const nightSessionStartAt = nightPrev.nightSessionStartAt || '';
|
||||
const nightSessionStartMs =
|
||||
Number.isFinite(nightPrev.nightSessionStartMs) ? nightPrev.nightSessionStartMs : null;
|
||||
|
||||
const batteryHardFloorPct =
|
||||
Number.isFinite(nightPrev.batteryHardFloorPct)
|
||||
? nightPrev.batteryHardFloorPct
|
||||
: params.evcsNightBatteryHardFloorPct;
|
||||
const batteryMorningTargetPct =
|
||||
Number.isFinite(nightPrev.batteryMorningTargetPct)
|
||||
? nightPrev.batteryMorningTargetPct
|
||||
: params.evcsNightTargetWeakForecastPct;
|
||||
const maxBatteryDischargePoints =
|
||||
Number.isFinite(nightPrev.maxBatteryDischargePoints) ? nightPrev.maxBatteryDischargePoints : 0;
|
||||
const gridTakeoverTriggerPct =
|
||||
Number.isFinite(nightPrev.gridTakeoverTriggerPct)
|
||||
? nightPrev.gridTakeoverTriggerPct
|
||||
: Math.max(batteryHardFloorPct, batteryMorningTargetPct);
|
||||
|
||||
const batteryBelowHardFloor = !!nightPrev.batteryBelowHardFloor;
|
||||
const batteryBelowMorningTarget = !!nightPrev.batteryBelowMorningTarget;
|
||||
const batteryAtOrBelowGridTakeoverTrigger = !!nightPrev.batteryAtOrBelowGridTakeoverTrigger;
|
||||
|
||||
const batteryKwhMissingToTarget =
|
||||
Number.isFinite(nightPrev.batteryKwhMissingToTarget) ? nightPrev.batteryKwhMissingToTarget : 0;
|
||||
const batteryRecoveryMinutesNeeded =
|
||||
Number.isFinite(nightPrev.batteryRecoveryMinutesNeeded) ? nightPrev.batteryRecoveryMinutesNeeded : 0;
|
||||
const batteryRecoveryUrgent = !!nightPrev.batteryRecoveryUrgent;
|
||||
const batteryRecoveryAllowedNow = !!nightPrev.batteryRecoveryAllowedNow;
|
||||
const batteryTargetReached = !!nightPrev.batteryTargetReached;
|
||||
|
||||
const batteryAssistAllowed = !!nightPrev.batteryAssistAllowed;
|
||||
const nightGridTakeoverNeeded = !!nightPrev.nightGridTakeoverNeeded;
|
||||
const nightTakeoverLatched = !!nightPrev.nightTakeoverLatched;
|
||||
const nightPreferredSourceMode = nightPrev.nightPreferredSourceMode || 'blocked';`;
|
||||
|
||||
b = b.slice(0, startIdx) + CONSUME_BLOCK + b.slice(endIdx + END_MARKER.length);
|
||||
bNode.parameters.jsCode = b;
|
||||
console.log('OK: BATTERY_CLOUD_LOCKS (bloc dupliqué remplacé par la consommation du preview)');
|
||||
|
||||
fs.writeFileSync(FILE, JSON.stringify(wf, null, 2) + '\n');
|
||||
console.log(`Écrit: ${FILE}`);
|
||||
Reference in New Issue
Block a user