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:
Jules
2026-09-28 17:06:49 +02:00
co-authored by Claude Fable 5.1
commit 69809df5d1
123 changed files with 13225 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
// dimension.ts — dimension_optimale(typo, inputs, contexte_derived, coefficients) → Dimension | null
// Spec: V2-algo-spec-FONCTIONS.md §2 — origine calculs/src/lib/autonomie/dimension.js.
// Correction LOT P1.0 : facteurs d'orientation et DJU par défaut lus dans la
// table de coefficients passée en paramètre (plus de constante de module).
import { zoneRtToClass, clamp, roundToStep } from './calibrage.js';
import { parseParam } from './types.js';
/**
* Calcule la valeur optimale du paramètre dimensionnant pour une typologie.
*
* Particularité ballon_tampon (th_ballon_500) :
* - unite BDD = "forfait" (contrainte schéma NocoDB)
* - prix_par_unite = 1.2 EUR/L
* - parametre_dim.unite = "L" (source de vérité pour l'affichage et le calcul)
* → L'algo utilise toujours parametre_dim.unite quand présent, sinon fallback option.unite
*/
export function dimension_optimale(typo, inputs, contexte_derived, coefficients) {
const param = parseParam(typo.parametre_dim);
if (!param)
return null;
const strategie = param.strategie || 'fixe';
let valeur;
switch (strategie) {
case 'par_personne': {
const base = param.base != null ? param.base : 0;
const coef = param.coef != null ? param.coef : 1;
valeur = base + (inputs.nb_personnes || 4) * coef;
break;
}
case 'par_surface_toit': {
const table = coefficients.orientation_pv.valeur;
const orient = (inputs.orientation_toit ? table[inputs.orientation_toit] : undefined) || 0.8;
const coef = param.coef != null ? param.coef : 0.1;
valeur = (inputs.surface_toit_m2 || 0) * coef * orient;
break;
}
case 'par_surface_parcelle': {
const reserve = param.reserve_construit != null ? param.reserve_construit : 100;
const utile = Math.max(0, (inputs.surface_parcelle_m2 || 0) - reserve);
const coef = param.coef != null ? param.coef : 0.05;
valeur = utile * coef;
break;
}
case 'par_dju': {
const zone_class = zoneRtToClass(contexte_derived.zone_climatique_rt);
const dju = contexte_derived.dju || coefficients.dju_defaut_par_classe_rt.valeur[zone_class] || 2200;
const coef = param.coef != null ? param.coef : 0.005;
valeur = dju * coef;
break;
}
case 'par_metres_haie': {
const coef = param.coef != null ? param.coef : 1.0;
valeur = (inputs.metres_haie_possibles || 0) * coef;
break;
}
case 'fixe':
default:
valeur = param.default != null ? param.default : 1;
break;
}
// Clamp + arrondi step
const min = param.min != null ? param.min : 0;
const max = param.max != null ? param.max : Infinity;
const step = param.step != null ? param.step : 0;
valeur = clamp(valeur, min, max);
valeur = roundToStep(valeur, step);
// Unite : parametre_dim.unite prioritaire sur option.unite (cas ballon_tampon)
const unite = param.unite || typo.unite || '';
return { valeur, unite, strategie };
}
/**
* Calcule le coût HT d'une typologie pour une dimension donnée.
*
* Hiérarchie des prix (3 cas BDD) :
* 1. prix_fixe + prix_par_unite × dim.valeur (V2 enrichi, ex: cuve béton, PV)
* 2. prix_par_unite × dim.valeur seul (ex: th_ballon_500)
* 3. prix_mat_min + pose si pro requis (forfaits legacy sans parametre_dim enrichi)
*
* Pour les postes pro (complexite_pose=3) et temps_dispo < 3, ajouter prix_pro_min.
* Fallback final : (prix_mat_min + prix_mat_max) / 2
*
* Les prix sont des DONNÉES de l'option (publiées par calculs) ; la seule
* heuristique du code est « pose pro = 50 % du matériel si prix_pro absent ».
*/
export function cout_typologie(typo, dim_result, inputs) {
const prix_fixe = typo.prix_fixe != null ? typo.prix_fixe : 0;
const prix_par_unite = typo.prix_par_unite;
const prix_mat_min = typo.prix_mat_min != null ? typo.prix_mat_min : 0;
const prix_mat_max = typo.prix_mat_max != null ? typo.prix_mat_max : prix_mat_min;
const prix_pro_min = typo.prix_pro_min != null ? typo.prix_pro_min : 0;
// Cas 1 & 2 : prix_par_unite disponible (schéma V2 enrichi)
if (prix_par_unite != null && prix_par_unite > 0) {
const dim_val = dim_result ? dim_result.valeur : typo.quantite_default || 1;
const base = prix_fixe + prix_par_unite * dim_val;
// Forfait pro si complexite=3 et temps_dispo=1
if (typo.complexite_pose === 3 && inputs && inputs.temps_dispo === 1 && prix_pro_min > 0) {
return Math.max(base, prix_pro_min);
}
return base;
}
// Cas 3 : forfait legacy (prix_mat_min/max seulement)
if (prix_mat_min > 0) {
const mid_mat = (prix_mat_min + prix_mat_max) / 2;
// Si complexite_pose = 3 et pose pro : ajouter coût pose pro
if (typo.complexite_pose === 3) {
const pose = prix_pro_min > 0 ? prix_pro_min : mid_mat * 0.5;
return mid_mat + pose;
}
// complexite 1-2 : coût mat seul (DIY inclus)
return mid_mat;
}
// Fallback ultime : 0 (sécurité, ne devrait pas arriver)
return 0;
}