Files
moteurs/dist/autonomie/noyau/selectionne-poste.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

149 lines
7.3 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.
// selectionne-poste.ts — Étape 3 : sélection des typologies par poste
// Spec: V2-algo-spec.md §Étape 3 + V2-algo-spec-FONCTIONS.md §3
// Origine calculs/src/lib/autonomie/selectionne-poste.js.
// Correction LOT P1.0 : `reglages` en paramètre (plus de getConfig()) ; le
// console.warn de debug est versé dans `debug_log` du résultat.
import { adequation_contexte } from './adequation.js';
import { dimension_optimale, cout_typologie } from './dimension.js';
import { parseJsonArr } from './types.js';
/**
* Sélectionne les meilleures typologies pour un axe donné.
*
* @param budget_axe — EUR alloué à cet axe
* @param axe — "eau" | "energie" | "alim"
*/
export function selectionnePoste(budget_axe, inputs, contexte_derived, options, postes, axe, reglages, debug = false) {
const config = reglages.config;
const bonus_malus = config.bonus_malus || {};
const postes_poids_cfg = config.postes_poids || {};
const tolerances = config.tolerances || {};
const tolerance_pct = (tolerances.tolerance_par_poste_pct || 20) / 100;
const debug_log = debug ? [] : undefined;
// Normaliser le code axe pour matcher la BDD ("alimentation" dans postes BDD)
const axe_bdd = axe === 'alim' ? 'alimentation' : axe;
// Postes de l'axe, triés par ordre d'affichage
const postes_axe = postes.filter((p) => p.axe === axe_bdd && p.actif !== false).sort((a, b) => (a.ordre || 0) - (b.ordre || 0));
if (postes_axe.length === 0) {
return { postes: [], sous_total_eur_ht: 0, budget_alloue: budget_axe, poids: 0 };
}
// Enrichir les postes avec poids depuis config (fallback 50)
const postes_enrichis = postes_axe.map((p) => ({
...p,
poids_effectif: postes_poids_cfg[p.code] && postes_poids_cfg[p.code].poids != null ? postes_poids_cfg[p.code].poids : 50,
}));
// Poids total pour répartition budget
const poids_total = postes_enrichis.reduce((s, p) => s + p.poids_effectif, 0);
const resultats_postes = [];
for (const poste of postes_enrichis) {
// Budget alloué à ce poste (proportionnel au poids)
const budget_poste = poids_total > 0 ? (budget_axe * poste.poids_effectif) / poids_total : budget_axe / postes_enrichis.length;
// Si alim avec budget 0 (parcelle trop petite) → skip
if (budget_axe === 0)
continue;
// Typologies disponibles pour ce poste
const typos_poste = options.filter((o) => o.poste_id === poste.Id && o.actif !== false);
if (typos_poste.length === 0)
continue;
// Filtrer par accessibilité contexte (§3.4)
let typos_filtrees = typos_poste.filter((typo) => {
// Exclure complexité 3 si temps_dispo = 1 (sauf si c'est la seule option)
if (inputs.temps_dispo === 1 && typo.complexite_pose === 3)
return false;
// Exclure PV sur toit nord
if (poste.code === 'energie_pv' && inputs.orientation_toit === 'nord')
return false;
return true;
});
// Garde-fou : si trop restrictif, revenir à toutes
if (typos_filtrees.length === 0)
typos_filtrees = typos_poste;
// Scorer chaque typologie (§3.5)
const scored = typos_filtrees.map((typo) => {
const dim = dimension_optimale(typo, inputs, contexte_derived, reglages.coefficients);
const cout = cout_typologie(typo, dim, inputs);
const adq_result = debug
? adequation_contexte(typo, inputs, poste, contexte_derived, reglages, [], true)
: adequation_contexte(typo, inputs, poste, contexte_derived, reglages, [], false);
const score_adequation = debug && adq_result && typeof adq_result === 'object' && 'score' in adq_result
? adq_result.score
: typeof adq_result === 'number'
? adq_result
: 0.5;
// Boost DIY temps=3
let score_utilite = (poste.poids_effectif || 50) * score_adequation;
if (inputs.temps_dispo === 3 && typo.flag_diy) {
score_utilite *= bonus_malus.boost_diy_temps3 || 1.3;
}
const score_final = cout > 0 ? score_utilite / cout : 0;
return {
typo,
dim,
cout: Math.round(cout),
score_adequation,
score_utilite,
score_final,
debug_adq: debug && typeof adq_result === 'object' ? adq_result : undefined,
};
});
// Trier par score_final décroissant
scored.sort((a, b) => b.score_final - a.score_final);
// §3.6 : sélectionner le meilleur dont cout <= budget_poste × (1 + tolerance)
// Ne PAS prendre un poste si le coût dépasse largement le budget (anti-overrun)
const seuil_strict = budget_poste * (1 + tolerance_pct);
// Seuil assoupli : si aucun poste ne rentre, prendre le moins cher
let choix = scored.find((s) => s.cout <= seuil_strict);
if (!choix && scored.length > 0) {
// Pas de typo dans le budget : prendre la moins chère
const par_cout = [...scored].sort((a, b) => a.cout - b.cout);
choix = par_cout[0];
if (debug_log)
debug_log.push('[selectionnePoste] Aucune typo dans budget pour poste ' + poste.code + ' (budget=' + Math.round(budget_poste) + ', min cout=' + choix.cout + ')');
}
if (!choix)
continue;
// Alternatives (2-4 autres options scorées)
const alternatives = scored
.filter((s) => s.typo.code !== choix.typo.code)
.slice(0, 3)
.map((s) => ({
code: s.typo.code,
nom_affichage: s.typo.nom,
cout_eur_ht: s.cout,
score_final: +s.score_final.toFixed(4),
dimension: s.dim ? { valeur: s.dim.valeur, unite: s.dim.unite } : null,
}));
const couplages_requis = parseJsonArr(choix.typo.couplages_requis);
const couplages_exclus = parseJsonArr(choix.typo.couplages_exclus);
resultats_postes.push({
poste_id: poste.code,
poste_nom: poste.poste,
obligatoire: poste.obligatoire || false,
budget_alloue_poste: Math.round(budget_poste),
typologie_choisie: choix.typo.code,
nom_affichage: choix.typo.nom,
description_pedago: choix.typo.description_pedago || '',
dimension: choix.dim || { valeur: choix.typo.quantite_default || 1, unite: choix.typo.unite || '' },
cout_eur_ht: choix.cout,
score_adequation: +choix.score_adequation.toFixed(3),
score_final: +choix.score_final.toFixed(4),
alternatives,
notes_techniques: choix.typo.notes_techniques || '',
flag_diy: choix.typo.flag_diy || false,
complexite_pose: choix.typo.complexite_pose || 2,
couplages_requis,
couplages_exclus,
manuel: false,
});
}
const sous_total = resultats_postes.reduce((s, p) => s + p.cout_eur_ht, 0);
const resultat = {
postes: resultats_postes,
sous_total_eur_ht: sous_total,
budget_alloue: budget_axe,
poids: 0, // rempli par l'orchestrateur
};
if (debug_log && debug_log.length)
resultat.debug_log = debug_log;
return resultat;
}