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