Files
moteurs/dist/autonomie/integrateur.js
T
JulesandClaude Fable 5.1 69809df5d1 LOT P1.0 : paquet @transformer/moteurs v0.1.0
Extraction du cœur autonomie de calculs.trans-former.fr (12 modules, sans
singleton, couplages sans mutation, console derrière debug), découpage en trois
moteurs d'axe + intégrateur (mode global = calculs, mode par_axe = plateforme),
moteurs commun.contexte (Corse 2A/2B, DOM 97x corrigés) et commun.geometrie,
table des coefficients harmonisés (docs/CONSTANTES.md pour Jules, R-7),
validation structurelle des données (schemas/), tests hors ligne : 28 assertions
d'origine + épingle générée depuis le code d'astro-pro + écarts d'harmonisation
figés + contrat des moteurs. dist/ committé.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UBHYPdeQYy2m1GVJp6b12
2026-09-28 17:06:49 +02:00

115 lines
5.7 KiB
JavaScript

// 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;