Files
moteurs/scripts/ecarts-correctifs.mjs
T
JulesandClaude Fable 5.1 4f593d2edf Épingles : écarts correctifs face à l'épingle d'origine, écarts d'harmonisation refigés (05/10)
L'épingle d'origine (fixtures/sorties-origine-2026-09-28.json, générée depuis
le code d'astro-pro) porte les défauts corrigés par 91c755f (eg_phytostation ×3
sur C/eau, basse_planche_bec ×2 sur B et C/alim, lignes forcées orphelines,
FORCE ×4 dans le journal, A/eau par axe en INSUFFISANT). La regénérer depuis
astro-pro reproduirait les défauts ; la regénérer depuis ce paquet en ferait
une épingle sur soi-même. Elle reste donc telle quelle, et :

- scripts/ecarts-correctifs.mjs + fixtures/ecarts-correctifs-2026-10-05.json :
  chaque cellule (profil × mode × axe × champ) qui s'écarte de l'origine est
  figée, avec le détail des lignes ; une cellule absente doit rester identique.
- test/decoupage.test.mjs : test 1 compare aux écarts figés (et vérifie qu'un
  correctif n'introduit jamais un INSUFFISANT ni un total hors ±5 % en global ;
  budgets alloués et poids des axes inchangés) ; test 2 vérifie la propriété du
  découpage contre selectionner() de ce paquet ; test 3 inchangé dans l'esprit.
- Écarts d'harmonisation REFIGÉS (npm run ecarts -- --ecrire 2026-10-05) après
  relecture de docs/CONSTANTES.md : neuf lignes au lieu de onze. Les deux lignes
  perdues (A énergie global 22 → 6 %, C énergie par axe 10 → 3 %) étaient des
  taux calculés sur des kWc fantômes hérités par une batterie ; la couverture y
  est null avec les deux tables. Aucun coefficient ne change. Note datée posée
  dans CONSTANTES.md §1. (La suppression du fichier du 28/09 est partie par
  erreur dans 8465bf7, avec la version : même intention, deux commits.)
- fixtures/README.md et AGENTS.md (règles 6 et 8) : l'épingle d'origine ne se
  regénère pas pour absorber un correctif ; invariants tenus depuis v0.1.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tgtn7bJxTLiL2fRCkQMgtR
2026-10-05 16:39:51 +02:00

100 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ecarts-correctifs.mjs — compare, sur les trois profils de référence et avec le
// jeu de coefficients D'ORIGINE, ce que le paquet rend aujourd'hui à l'épingle
// générée depuis le code d'astro-pro (fixtures/sorties-origine-<date>.json).
//
// Pourquoi ce script existe (v0.1.1) : l'épingle d'origine porte les défauts
// que le correctif répare (SPEC menu §12 n° 1, 2, 3, 5 : doublons, accessoires
// forcés orphelins, statut non monotone, journal FORCE répété). La regénérer
// depuis astro-pro reproduirait les défauts ; la regénérer depuis ce paquet en
// ferait une épingle sur soi-même. Elle reste donc telle quelle, et chaque écart
// voulu est écrit ici, cellule par cellule, dans fixtures/ecarts-correctifs-<date>.json.
// test/decoupage.test.mjs exige que les écarts calculés soient EXACTEMENT ceux
// figés : une cellule non listée doit rester identique à l'origine.
// Usage : node scripts/ecarts-correctifs.mjs [--ecrire AAAA-MM-JJ]
import { readFileSync, writeFileSync } from 'node:fs'
import { eau, energie, alimentation, autonomie } from '../dist/index.js'
const fx = new URL('../fixtures/', import.meta.url)
const lire = (f) => JSON.parse(readFileSync(new URL(f, fx), 'utf8'))
const options = lire('options.json')
const postes = lire('postes.json')
const france_climat = lire('france_climat.json')
const algo_config = lire('algo-config.json')
const profils = lire('profils-reference.json')
const origine = lire('coefficients-2026-05.json')
export const PIN_ORIGINE = 'sorties-origine-2026-09-28.json'
const pin = lire(PIN_ORIGINE)
const AXES = ['eau', 'energie', 'alim']
const MOTEURS = { eau, energie, alim: alimentation }
const resumePostes = (liste) => liste.map((p) => ({ poste_id: p.poste_id, typologie_choisie: p.typologie_choisie, dimension: p.dimension ? { valeur: p.dimension.valeur, unite: p.dimension.unite } : null, cout_eur_ht: p.cout_eur_ht, force_par_couplage: !!p.force_par_couplage }))
const lignePostes = (liste) => liste.map((p) => `${p.typologie_choisie}${p.force_par_couplage ? '*' : ''} ${p.cout_eur_ht}`).join(' + ')
/** Compare une cellule (profil × mode × axe) à l'épingle ; rend les écarts champ par champ. */
function compareAxe(profil, mode, axe, attendu, obtenu) {
const ecarts = []
const push = (champ, o, c) => ecarts.push({ profil, mode, axe, champ, origine: o, corrige: c })
if (attendu.sous_total_eur_ht !== obtenu.sous_total_eur_ht) push('sous_total_eur_ht', attendu.sous_total_eur_ht, obtenu.sous_total_eur_ht)
const po = JSON.stringify(attendu.postes)
const pc = JSON.stringify(resumePostes(obtenu.postes))
if (po !== pc) push('postes', lignePostes(attendu.postes), lignePostes(obtenu.postes))
return ecarts
}
export function calculeEcartsCorrectifs() {
const ecarts = []
const cellules = {}
for (const profil of profils) {
const p = pin.profils[profil.nom]
const g = autonomie.noyau.selectionner(profil.inputs, options, postes, france_climat, { config: algo_config, coefficients: origine }, false)
for (const axe of AXES) ecarts.push(...compareAxe(profil.nom, 'global', axe, p.global.axes[axe], g.axes[axe]))
if (p.global.total_eur_ht !== g.total_eur_ht) ecarts.push({ profil: profil.nom, mode: 'global', axe: 'total', champ: 'total_eur_ht', origine: p.global.total_eur_ht, corrige: g.total_eur_ht })
if (p.global.status !== g.status) ecarts.push({ profil: profil.nom, mode: 'global', axe: 'total', champ: 'status', origine: p.global.status, corrige: g.status })
if (p.global.ecart_budget_pct !== g.ecart_budget_pct) ecarts.push({ profil: profil.nom, mode: 'global', axe: 'total', champ: 'ecart_budget_pct', origine: p.global.ecart_budget_pct, corrige: g.ecart_budget_pct })
if (JSON.stringify(p.global.couplages) !== JSON.stringify(g.couplages)) ecarts.push({ profil: profil.nom, mode: 'global', axe: 'total', champ: 'couplages', origine: p.global.couplages, corrige: g.couplages })
cellules[`${profil.nom} / global`] = { total: g.total_eur_ht, status: g.status, couplages: g.couplages, axes: Object.fromEntries(AXES.map((a) => [a, { sous_total: g.axes[a].sous_total_eur_ht, postes: resumePostes(g.axes[a].postes) }])) }
for (const axe of AXES) {
const a = p.par_axe[axe]
const r = MOTEURS[axe].calculer({ autodiag: profil.inputs, budget_axe: a.budget_axe, contexte: g.contexte }, { postes, options, algo_config, coefficients: origine }).sorties
const obtenu = { sous_total_eur_ht: r.sous_total_eur_ht, postes: r.postes }
ecarts.push(...compareAxe(profil.nom, 'par_axe', axe, a.axes[axe], obtenu))
if (a.status !== r.status) ecarts.push({ profil: profil.nom, mode: 'par_axe', axe, champ: 'status', origine: a.status, corrige: r.status })
if (JSON.stringify(a.couplages) !== JSON.stringify(r.couplages)) ecarts.push({ profil: profil.nom, mode: 'par_axe', axe, champ: 'couplages', origine: a.couplages, corrige: r.couplages })
cellules[`${profil.nom} / par_axe / ${axe}`] = { budget_axe: a.budget_axe, sous_total: r.sous_total_eur_ht, status: r.status, couplages: r.couplages, postes: resumePostes(r.postes) }
}
}
return { ecarts, cellules }
}
const estCli = process.argv[1]?.replace(/\\/g, '/').endsWith('ecarts-correctifs.mjs')
if (estCli) {
const { ecarts, cellules } = calculeEcartsCorrectifs()
console.log('| Profil | Mode | Axe | Champ | Origine (astro-pro) | Corrigé (ce paquet) |')
console.log('|---|---|---|---|---|---|')
const court = (v) => (Array.isArray(v) ? `${v.length} ligne(s)` : String(v))
for (const e of ecarts) console.log(`| ${e.profil} | ${e.mode} | ${e.axe} | ${e.champ} | ${court(e.origine)} | ${court(e.corrige)} |`)
if (ecarts.length === 0) console.log('| (aucun écart) | | | | | |')
const i = process.argv.indexOf('--ecrire')
if (i > 0) {
const date = process.argv[i + 1] ?? new Date().toISOString().slice(0, 10)
const cible = new URL(`ecarts-correctifs-${date}.json`, fx)
writeFileSync(
cible,
JSON.stringify(
{
_commentaire: `Écarts voulus entre l'épingle d'origine (${PIN_ORIGINE}, code astro-pro) et ce paquet, coefficients d'origine, trois profils de référence, figés le ${date} par scripts/ecarts-correctifs.mjs. L'épingle d'origine n'est pas regénérée : elle porte les défauts corrigés (SPEC menu branches fiches §12 n° 1, 2, 3, 5). Toute cellule absente d'ici doit rester identique à l'origine ; un nouvel écart casse test/decoupage.test.mjs, et se refige sciemment, avec sa cause dans le commit.`,
date,
epingle_origine: PIN_ORIGINE,
ecarts,
cellules,
},
null,
2,
) + '\n',
'utf8',
)
console.log(`\nécrit : fixtures/ecarts-correctifs-${date}.json`)
}
}