// ecarts-harmonisation.mjs — fait tourner les trois profils de référence avec // le jeu de coefficients D'ORIGINE puis avec la table HARMONISÉE, et écrit les // écarts de sélection ligne par ligne (markdown sur stdout ; JSON dans // fixtures/ecarts-harmonisation-.json avec --ecrire). Ce sont ces écarts // que Jules relit avec docs/CONSTANTES.md (LOT P1.0 point 4, amendement du 28/09). // Usage : node scripts/ecarts-harmonisation.mjs [--ecrire AAAA-MM-JJ] import { readFileSync, writeFileSync } from 'node:fs' import { COEFFICIENTS } from '../dist/commun/coefficients.js' import { deriveContexte } from '../dist/commun/contexte.js' import { eau, energie, alimentation, integrateur } from '../dist/autonomie/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') const AXES = ['eau', 'energie', 'alim'] const MOTEURS = { eau, energie, alim: alimentation } export function calculeEcarts() { const lignes = [] for (const profil of profils) { for (const jeu of [ ['origine', origine], ['harmonisee', COEFFICIENTS], ]) { const [nom, coefficients] = jeu const donnees = { postes, options, algo_config, france_climat, coefficients } const g = integrateur.calculer({ autodiag: profil.inputs, budget_total: profil.inputs.budget_eur, mode: 'global' }, donnees).sorties const contexte = deriveContexte(profil.inputs.code_postal, france_climat, coefficients).sorties const par_axe = {} for (const axe of AXES) { const r = MOTEURS[axe].calculer({ autodiag: profil.inputs, budget_axe: g.budgets[axe], contexte }, donnees).sorties par_axe[axe] = { typologies: r.typologies, sous_total: r.sous_total_eur_ht, status: r.status, taux: r.taux_couverture } } lignes.push({ profil: profil.nom, jeu: nom, global: { total: g.total_eur_ht, status: g.status, typologies: Object.fromEntries(AXES.map((a) => [a, g.axes[a].typologies])), taux: g.taux_couverture }, par_axe }) } } // Diff origine → harmonisée, par profil const ecarts = [] for (const profil of profils) { const o = lignes.find((l) => l.profil === profil.nom && l.jeu === 'origine') const h = lignes.find((l) => l.profil === profil.nom && l.jeu === 'harmonisee') for (const axe of AXES) { const to = o.global.typologies[axe].join(' + ') const th = h.global.typologies[axe].join(' + ') if (to !== th) ecarts.push({ profil: profil.nom, mode: 'global', axe, origine: to, harmonisee: th }) const ao = o.par_axe[axe] const ah = h.par_axe[axe] if (ao.typologies.join('+') !== ah.typologies.join('+') || ao.sous_total !== ah.sous_total) { ecarts.push({ profil: profil.nom, mode: 'par_axe', axe, origine: `${ao.typologies.join(' + ')} (${ao.sous_total} €)`, harmonisee: `${ah.typologies.join(' + ')} (${ah.sous_total} €)` }) } if (ao.taux !== ah.taux) ecarts.push({ profil: profil.nom, mode: 'taux (par_axe)', axe, origine: `${ao.taux} %`, harmonisee: `${ah.taux} %` }) if (o.global.taux[axe] !== h.global.taux[axe]) ecarts.push({ profil: profil.nom, mode: 'taux (global)', axe, origine: `${o.global.taux[axe]} %`, harmonisee: `${h.global.taux[axe]} %` }) } if (o.global.total !== h.global.total || o.global.status !== h.global.status) { ecarts.push({ profil: profil.nom, mode: 'global', axe: 'total', origine: `${o.global.total} € (${o.global.status})`, harmonisee: `${h.global.total} € (${h.global.status})` }) } } return { lignes, ecarts } } const estCli = import.meta.url === new URL(`file:///${process.argv[1].replace(/\\/g, '/')}`).href || process.argv[1]?.endsWith('ecarts-harmonisation.mjs') if (estCli) { const { lignes, ecarts } = calculeEcarts() console.log('| Profil | Mode | Axe | Origine (coefficients du code) | Harmonisée (table du paquet) |') console.log('|---|---|---|---|---|') for (const e of ecarts) console.log(`| ${e.profil} | ${e.mode} | ${e.axe} | ${e.origine} | ${e.harmonisee} |`) if (ecarts.length === 0) console.log('| (aucun écart) | | | | |') console.log() console.log('Taux de couverture (eau / énergie / alim), par profil :') for (const l of lignes) console.log(`- ${l.profil} — ${l.jeu} : ${AXES.map((a) => `${a} ${l.global.taux[a] ?? 'null'} %`).join(' · ')} — total ${l.global.total} € (${l.global.status})`) 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-harmonisation-${date}.json`, fx) writeFileSync(cible, JSON.stringify({ _commentaire: `Écarts de sélection et de taux entre le jeu de coefficients d'origine et la table harmonisée, sur les trois profils de référence, figés le ${date}. Un changement de coefficient ou de code qui modifie ces écarts casse test/decoupage.test.mjs : c'est voulu, on relit CONSTANTES.md avant de refiger.`, date, ecarts, lignes }, null, 2) + '\n', 'utf8') console.log(`\nécrit : fixtures/ecarts-harmonisation-${date}.json`) } }