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
This commit is contained in:
+148
@@ -0,0 +1,148 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user