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:
Vendored
+129
@@ -0,0 +1,129 @@
|
||||
// couplages.ts — Étape 4 : application des couplages contraints
|
||||
// Spec: V2-algo-spec.md §Étape 4 — origine calculs/src/lib/autonomie/couplages.js.
|
||||
// Corrections LOT P1.0 : `reglages` en paramètre (plus de getConfig()) ; la
|
||||
// sélection reçue n'est PLUS mutée (copie des axes et de leurs postes, le
|
||||
// résultat est renvoyé dans `axes`). Tous les appelants d'origine lisaient
|
||||
// déjà le retour, le comportement est identique.
|
||||
//
|
||||
// Note (origine, conservée) : le marqueur `_conflit_avec` de la passe 2 est
|
||||
// posé sur des copies de postes et n'atteint jamais la sortie ; le
|
||||
// « downgrade planifié » n'a donc jamais lieu. À corriger en patch, avec test.
|
||||
import { cout_typologie, dimension_optimale } from './dimension.js';
|
||||
function copieSelection(selection) {
|
||||
const copie = {};
|
||||
for (const axe of Object.keys(selection)) {
|
||||
const a = selection[axe];
|
||||
if (!a)
|
||||
continue;
|
||||
const ax = { ...a, postes: a.postes.map((p) => ({ ...p })) };
|
||||
copie[axe] = ax;
|
||||
}
|
||||
return copie;
|
||||
}
|
||||
/**
|
||||
* Post-traitement déterministe des couplages.
|
||||
*
|
||||
* Gestion alias : algo-config.json § couplages_alias permet de mapper
|
||||
* des codes legacy (ex: stherm_ballon_tampon) vers les codes BDD réels (th_ballon_500).
|
||||
*/
|
||||
export function appliqueCouplages(selection_recue, options, postes, reglages, _debug = false) {
|
||||
const alias = reglages.config.couplages_alias || {};
|
||||
const selection = copieSelection(selection_recue);
|
||||
// Construire un mapping poste_id (num) → axe via postes BDD
|
||||
const posteIdToAxe = {};
|
||||
if (postes) {
|
||||
for (const p of postes) {
|
||||
// Normaliser axe : "alimentation" → "alim"
|
||||
const axe_norm = (p.axe === 'alimentation' ? 'alim' : p.axe);
|
||||
posteIdToAxe[p.Id] = axe_norm;
|
||||
}
|
||||
}
|
||||
function resolveCode(code) {
|
||||
return alias[code] || code;
|
||||
}
|
||||
const axes_list = ['eau', 'energie', 'alim'];
|
||||
const tous_postes = axes_list.flatMap((axe) => (selection[axe] && selection[axe].postes ? selection[axe].postes : []).map((p) => Object.assign({}, p, { _axe: axe })));
|
||||
const codes_selectionnes = new Set(tous_postes.map((p) => p.typologie_choisie));
|
||||
const couplages_log = [];
|
||||
// Passe 1 : couplages_requis
|
||||
for (const poste of tous_postes) {
|
||||
const requis_list = poste.couplages_requis || [];
|
||||
for (const requis_raw of requis_list) {
|
||||
const requis = resolveCode(requis_raw);
|
||||
if (!codes_selectionnes.has(requis)) {
|
||||
// Chercher la typo en BDD (code exact ou alias original)
|
||||
let typo_requise = options.find((o) => o.code === requis);
|
||||
if (!typo_requise && requis !== requis_raw) {
|
||||
typo_requise = options.find((o) => o.code === requis_raw);
|
||||
}
|
||||
if (typo_requise) {
|
||||
// Trouver l'axe via mapping poste_id → axe
|
||||
const axe_cible = posteIdToAxe[typo_requise.poste_id] || trouverAxeParCode(typo_requise.code);
|
||||
const cible = axe_cible ? selection[axe_cible] : undefined;
|
||||
if (axe_cible && cible) {
|
||||
const dim = dimension_optimale(typo_requise, {}, {}, reglages.coefficients);
|
||||
const cout = cout_typologie(typo_requise, dim, {});
|
||||
const cout_force = Math.round(cout || 0);
|
||||
const unite_dim = dim ? dim.unite : typo_requise.unite || '';
|
||||
const valeur_dim = dim ? dim.valeur : typo_requise.quantite_default || 1;
|
||||
cible.postes.push({
|
||||
poste_id: String(typo_requise.poste_id) + '_couplage',
|
||||
poste_nom: 'Couplage requis (' + typo_requise.nom + ')',
|
||||
obligatoire: true,
|
||||
budget_alloue_poste: cout_force,
|
||||
typologie_choisie: typo_requise.code,
|
||||
nom_affichage: typo_requise.nom,
|
||||
description_pedago: typo_requise.description_pedago || '',
|
||||
dimension: { valeur: valeur_dim, unite: unite_dim },
|
||||
cout_eur_ht: cout_force,
|
||||
score_adequation: 0.8,
|
||||
score_final: 0,
|
||||
alternatives: [],
|
||||
notes_techniques: typo_requise.notes_techniques || '',
|
||||
flag_diy: typo_requise.flag_diy || false,
|
||||
complexite_pose: typo_requise.complexite_pose || 2,
|
||||
couplages_requis: [],
|
||||
couplages_exclus: [],
|
||||
manuel: false,
|
||||
force_par_couplage: true,
|
||||
});
|
||||
cible.sous_total_eur_ht += cout_force;
|
||||
codes_selectionnes.add(typo_requise.code);
|
||||
couplages_log.push('FORCE "' + typo_requise.code + '" (requis par "' + poste.typologie_choisie + '") +' + cout_force + 'EUR dans axe ' + axe_cible);
|
||||
}
|
||||
else {
|
||||
couplages_log.push('WARN axe non trouve pour "' + requis + '" (requis par "' + poste.typologie_choisie + '")');
|
||||
}
|
||||
}
|
||||
else {
|
||||
couplages_log.push('MANQUANT "' + requis + '" (requis par "' + poste.typologie_choisie + '") — code BDD non trouve');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Passe 2 : couplages_exclus
|
||||
for (const poste of tous_postes) {
|
||||
const exclus_list = poste.couplages_exclus || [];
|
||||
for (const exclu_raw of exclus_list) {
|
||||
const exclu = resolveCode(exclu_raw);
|
||||
if (codes_selectionnes.has(exclu)) {
|
||||
const poste_conflit = tous_postes.find((p) => p.typologie_choisie === exclu);
|
||||
if (poste_conflit && !poste_conflit.force_par_couplage) {
|
||||
poste_conflit._conflit_avec = poste.typologie_choisie;
|
||||
couplages_log.push('CONFLIT "' + exclu + '" avec "' + poste.typologie_choisie + '" — downgrade planifié');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { axes: selection, couplages_appliques: couplages_log };
|
||||
}
|
||||
// Fallback heuristique si poste_id absent du mapping
|
||||
function trouverAxeParCode(code) {
|
||||
if (/captage|stockage|filtrat|distrib|eg_|stherm_ballon/.test(code))
|
||||
return 'eau';
|
||||
if (/^th_|^pv_|^bois_|^pil_|^emet_|ballon_tampon|conduit_fumee/.test(code))
|
||||
return 'energie';
|
||||
if (/^etude_|^haute_|^moy_|^basse_/.test(code))
|
||||
return 'alim';
|
||||
return 'energie';
|
||||
}
|
||||
Reference in New Issue
Block a user