// integrateur.ts — moteur `autonomie.integrateur` : contexte + pondération des // axes + allocation du budget + agrégation. // // Deux modes : // - 'global' (défaut) : le pipeline complet d'origine (`selectionner`), avec // couplages et réajustement GLOBAUX entre axes — celui de calculs ; // - 'par_axe' : chaque axe est calculé par son moteur d'axe sur son budget, // puis les sorties sont additionnées — celui du menu de la plateforme. // Le mode « aménagements cochés » (recalcul sans relancer l'allocation) est // P1.5 : une liste transmise est ignorée et signalée. import { Trace } from '../commun/contrat.js'; import { COEFFICIENTS } from '../commun/coefficients.js'; import { deriveContexte } from '../commun/contexte.js'; import { VERSION } from '../version.js'; import { calculeAxe, enregistreLectures, traceSelection } from './axe.js'; import { selectionner } from './noyau/algo.js'; import { alloueBudget } from './noyau/alloue-budget.js'; import { configPourAxes } from './noyau/axes-actifs.js'; import { pondereAxes } from './noyau/pondere-axes.js'; import { AXES } from './noyau/types.js'; import { aCaptage, batterieSelectionnee, kwcSelectionnes, mlHaiesSelectionnes, tauxAlim, tauxEau, tauxEnergie } from './taux.js'; /** Statut agrégé : BUDGET_INSUFFISANT l'emporte, puis AJOUTER_OPTIONNEL, sinon OK. */ export function agregeStatuts(statuts) { if (statuts.includes('BUDGET_INSUFFISANT')) return 'BUDGET_INSUFFISANT'; if (statuts.includes('AJOUTER_OPTIONNEL')) return 'AJOUTER_OPTIONNEL'; return 'OK'; } export function calculeIntegrateur(entrees, donnees) { const trace = new Trace(); const mode = entrees.mode ?? 'global'; const coefficients_base = donnees.coefficients ?? COEFFICIENTS; const { table: coefficients, lues } = enregistreLectures(coefficients_base); const config = configPourAxes(donnees.algo_config, entrees.axes_actifs); const inputs = { ...entrees.autodiag, budget_eur: entrees.budget_total }; if (entrees.amenagements_coches && entrees.amenagements_coches.length) { trace.avertit('aménagements cochés ignorés : le mode « coché » arrive en P1.5'); } const ctx = deriveContexte(inputs.code_postal, donnees.france_climat, coefficients); trace.absorbe(ctx); const contexte = ctx.sorties; const poids = pondereAxes(inputs, contexte, config, false); const budgets = alloueBudget(entrees.budget_total, poids); const axes = {}; const taux_couverture = {}; let total = 0; let status; let message_status; let couplages = []; if (mode === 'par_axe') { const statuts = []; for (const axe of AXES) { const actif = !entrees.axes_actifs || entrees.axes_actifs[axe] !== false; if (!actif || budgets[axe] <= 0) { axes[axe] = { postes: [], typologies: [], sous_total_eur_ht: 0, budget_alloue: budgets[axe], status: 'OK', taux_couverture: null, taux_detail: null }; taux_couverture[axe] = null; continue; } const r = calculeAxe(axe, { autodiag: entrees.autodiag, budget_axe: budgets[axe], contexte }, { ...donnees, coefficients: coefficients_base }); trace.absorbe(r); const s = r.sorties; axes[axe] = { postes: s.postes, typologies: s.typologies, sous_total_eur_ht: s.sous_total_eur_ht, budget_alloue: s.budget_alloue, status: s.status, taux_couverture: s.taux_couverture, taux_detail: s.taux_detail }; taux_couverture[axe] = s.taux_couverture; total += s.sous_total_eur_ht; statuts.push(s.status); couplages = couplages.concat(s.couplages); } status = agregeStatuts(statuts); } else { const r = selectionner(inputs, donnees.options, donnees.postes, donnees.france_climat, { config, coefficients }, false); const tous = []; for (const axe of AXES) { const a = r.axes[axe]; const postes = a?.postes ?? []; tous.push(...postes); let t; if (axe === 'eau') t = tauxEau({ surface_toit_m2: inputs.surface_toit_m2, nb_personnes: inputs.nb_personnes, pluviometrie_mm: contexte.pluviometrie_mm, a_captage: aCaptage(postes) }, coefficients, trace); else if (axe === 'energie') t = tauxEnergie({ kwc: kwcSelectionnes(postes), batterie_kwh: batterieSelectionnee(postes), orientation_toit: inputs.orientation_toit, nb_personnes: inputs.nb_personnes, contexte }, coefficients, trace); else t = tauxAlim({ ml: mlHaiesSelectionnes(postes), nb_personnes: inputs.nb_personnes }, coefficients, trace); axes[axe] = { postes, typologies: postes.map((p) => p.typologie_choisie), sous_total_eur_ht: a?.sous_total_eur_ht ?? 0, budget_alloue: a?.budget_alloue ?? budgets[axe], status: r.status, taux_couverture: t.taux, taux_detail: t.detail }; taux_couverture[axe] = t.taux; } total = r.total_eur_ht; status = r.status; message_status = r.message_status; couplages = r.couplages; traceSelection(trace, coefficients_base, lues, tous, donnees.options, couplages); } const sorties = { mode, contexte, poids, budgets, axes, total_eur_ht: total, budget_cible: entrees.budget_total, ecart_budget_pct: entrees.budget_total > 0 ? +(((total - entrees.budget_total) / entrees.budget_total) * 100).toFixed(2) : 0, status, message_status, couplages, taux_couverture, }; return trace.resultat(sorties, VERSION); } export const integrateur = { id: 'autonomie.integrateur', calculer: calculeIntegrateur, }; export default integrateur;