// 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-.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-.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`) } }