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
+28
@@ -0,0 +1,28 @@
|
||||
export interface Fourchette {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
/** Fourchette dont les deux bornes peuvent manquer (prix pro absent, etc.). */
|
||||
export interface FourchetteOuverte {
|
||||
min: number | null;
|
||||
max: number | null;
|
||||
}
|
||||
export declare function fourchette(min: number, max: number): Fourchette;
|
||||
export declare function pointMedian(f: Fourchette): number;
|
||||
/** Somme de fourchettes ; une borne manquante est remplacée par l'autre. */
|
||||
export declare function additionneFourchettes(liste: FourchetteOuverte[]): Fourchette;
|
||||
/** Arrondi au pas donné (pas ≤ 0 → valeur inchangée). */
|
||||
export declare function arrondiAuPas(valeur: number, pas: number): number;
|
||||
/** Arrondi à deux décimales. */
|
||||
export declare function arrondi2(n: number): number;
|
||||
/**
|
||||
* Mensualité d'un prêt à taux fixe (formule d'annuité constante).
|
||||
* @param capital montant emprunté
|
||||
* @param taux_annuel_pct taux nominal annuel en % (0 → capital / nombre de mois)
|
||||
* @param duree_annees durée en années
|
||||
*/
|
||||
export declare function mensualite(capital: number, taux_annuel_pct: number, duree_annees: number): number;
|
||||
/** Annuité (12 mensualités). */
|
||||
export declare function annuite(capital: number, taux_annuel_pct: number, duree_annees: number): number;
|
||||
/** Coût total du crédit (intérêts seuls). */
|
||||
export declare function coutDuCredit(capital: number, taux_annuel_pct: number, duree_annees: number): number;
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
// argent.ts — fourchettes, arrondis, annuité d'un prêt. Fonctions pures,
|
||||
// sans formatage (l'affichage « 1 200 € HT » appartient aux sites, pas au
|
||||
// paquet : Intl dépend de l'environnement).
|
||||
export function fourchette(min, max) {
|
||||
return min <= max ? { min, max } : { min: max, max: min };
|
||||
}
|
||||
export function pointMedian(f) {
|
||||
return (f.min + f.max) / 2;
|
||||
}
|
||||
/** Somme de fourchettes ; une borne manquante est remplacée par l'autre. */
|
||||
export function additionneFourchettes(liste) {
|
||||
let min = 0;
|
||||
let max = 0;
|
||||
for (const f of liste) {
|
||||
if (f.min == null && f.max == null)
|
||||
continue;
|
||||
const a = f.min ?? f.max ?? 0;
|
||||
const b = f.max ?? f.min ?? 0;
|
||||
min += a;
|
||||
max += b;
|
||||
}
|
||||
return { min, max };
|
||||
}
|
||||
/** Arrondi au pas donné (pas ≤ 0 → valeur inchangée). */
|
||||
export function arrondiAuPas(valeur, pas) {
|
||||
if (!pas || pas <= 0)
|
||||
return valeur;
|
||||
return Math.round(valeur / pas) * pas;
|
||||
}
|
||||
/** Arrondi à deux décimales. */
|
||||
export function arrondi2(n) {
|
||||
return Math.round(n * 100) / 100;
|
||||
}
|
||||
/**
|
||||
* Mensualité d'un prêt à taux fixe (formule d'annuité constante).
|
||||
* @param capital montant emprunté
|
||||
* @param taux_annuel_pct taux nominal annuel en % (0 → capital / nombre de mois)
|
||||
* @param duree_annees durée en années
|
||||
*/
|
||||
export function mensualite(capital, taux_annuel_pct, duree_annees) {
|
||||
const n = Math.round(duree_annees * 12);
|
||||
if (n <= 0 || capital <= 0)
|
||||
return 0;
|
||||
const r = taux_annuel_pct / 100 / 12;
|
||||
if (r === 0)
|
||||
return capital / n;
|
||||
return (capital * r) / (1 - Math.pow(1 + r, -n));
|
||||
}
|
||||
/** Annuité (12 mensualités). */
|
||||
export function annuite(capital, taux_annuel_pct, duree_annees) {
|
||||
return mensualite(capital, taux_annuel_pct, duree_annees) * 12;
|
||||
}
|
||||
/** Coût total du crédit (intérêts seuls). */
|
||||
export function coutDuCredit(capital, taux_annuel_pct, duree_annees) {
|
||||
const n = Math.round(duree_annees * 12);
|
||||
return mensualite(capital, taux_annuel_pct, duree_annees) * n - capital;
|
||||
}
|
||||
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
export type Orientation = 'sud' | 'sud-est' | 'sud-ouest' | 'est' | 'ouest' | 'nord';
|
||||
export type ClasseRt = 'H1' | 'H2' | 'H3';
|
||||
export interface Coefficient<T = number> {
|
||||
valeur: T;
|
||||
/** Document ou norme qui fixe la valeur ; « heuristique … » si non sourcée. */
|
||||
source: string;
|
||||
/** Date du document source (AAAA-MM-JJ). */
|
||||
date: string;
|
||||
/** Fourchette, variante, réserve : ce que Jules doit savoir pour mettre un veto. */
|
||||
note?: string;
|
||||
}
|
||||
export interface TableCoefficients {
|
||||
/** Rendement de captage d'une toiture (part de la pluie réellement collectée). */
|
||||
captage_rendement: Coefficient;
|
||||
/** Rendement par type de toiture, pour un formulaire enrichi (non utilisé par les moteurs v0.1). */
|
||||
captage_rendement_par_toiture: Coefficient<Record<'tuile' | 'ardoise' | 'gravillonnee' | 'vegetalisee', number>>;
|
||||
/** Besoin domestique total, litres par jour et par personne. */
|
||||
besoin_eau_l_j_pers: Coefficient;
|
||||
/** Part du besoin substituable par l'eau de pluie (WC, douche, lessive, jardin). */
|
||||
part_eau_substituable: Coefficient;
|
||||
/** Facteur de production PV selon l'orientation du toit (sud 30° = 1). */
|
||||
orientation_pv: Coefficient<Record<Orientation, number>>;
|
||||
/** Productible PV annuel (kWh/kWc/an), par zone : `par` dit quelle clé du contexte indexe la table. */
|
||||
productible_pv_kwh_kwc: Coefficient<{
|
||||
par: 'zone_rt' | 'zone_biogeo_code';
|
||||
table: Record<string, number>;
|
||||
defaut: number;
|
||||
}>;
|
||||
/** Part de la production PV autoconsommée, sans batterie. */
|
||||
autoconso_sans_batterie: Coefficient;
|
||||
/** Part de la production PV autoconsommée, avec 10 kWh de batterie. */
|
||||
autoconso_avec_batterie_10kwh: Coefficient;
|
||||
/** Besoin d'électricité spécifique, kWh par personne et par an (hors chauffage électrique). */
|
||||
besoin_elec_kwh_pers_an: Coefficient;
|
||||
/** DJU base 18 par défaut, par classe de zone RT, si la ligne climat n'en porte pas. */
|
||||
dju_defaut_par_classe_rt: Coefficient<Record<ClasseRt, number>>;
|
||||
/** Mètres linéaires de haie fruitière par personne pour couvrir les fruits d'un foyer. */
|
||||
ml_haie_par_pers: Coefficient;
|
||||
/** Contexte climatique appliqué quand le département est inconnu (déclaré en avertissement). */
|
||||
contexte_defaut: Coefficient<{
|
||||
zone_rt: string;
|
||||
zone_biogeo_code: number;
|
||||
pluviometrie_mm: number;
|
||||
dju: number;
|
||||
}>;
|
||||
/** SHAB estimée = emprise × niveaux × ce ratio (murs, refends, escalier). */
|
||||
geometrie_ratio_shab_emprise: Coefficient;
|
||||
/** Surface vitrée = SHAB × ce ratio. */
|
||||
geometrie_ratio_vitrage_shab: Coefficient;
|
||||
/** Hauteur d'un niveau par défaut (m), si la hauteur totale n'est pas connue. */
|
||||
geometrie_hauteur_etage_m: Coefficient;
|
||||
/** Périmètre estimé = 4 × √(emprise × facteur) (rectangle allongé). */
|
||||
geometrie_facteur_perimetre: Coefficient;
|
||||
/** Part du périmètre retirée par côté mitoyen. */
|
||||
geometrie_part_mur_mitoyen: Coefficient;
|
||||
/** Majoration de la surface de toiture (débords, recouvrements). */
|
||||
geometrie_majoration_toiture: Coefficient;
|
||||
/** Pente de toit par défaut (degrés) si inconnue. */
|
||||
geometrie_pente_defaut_deg: Coefficient;
|
||||
}
|
||||
/** Table harmonisée (LOT P1.0, 28/09/2026). Référence tant que Jules ne met pas de veto (R-7). */
|
||||
export declare const COEFFICIENTS: TableCoefficients;
|
||||
/** Clés de la table, pour valider un jeu de coefficients injecté (fixture, veto). */
|
||||
export declare const CLES_COEFFICIENTS: (keyof TableCoefficients)[];
|
||||
/**
|
||||
* Vérifie qu'un objet a la forme d'une table de coefficients (toutes les clés,
|
||||
* chaque entrée avec valeur, source et date). Renvoie la liste des défauts.
|
||||
*/
|
||||
export declare function verifieTableCoefficients(table: unknown): string[];
|
||||
Vendored
+160
@@ -0,0 +1,160 @@
|
||||
// coefficients.ts — chaque grandeur physique UNE fois, avec source et date.
|
||||
//
|
||||
// Règle : le document de recherche l'emporte sur le code quand ils divergent
|
||||
// (SPEC phase 1 §4.1, LOT P1.0 point 5). La table lisible par Jules est
|
||||
// docs/CONSTANTES.md (une ligne par grandeur : ancienne(s) valeur(s) →
|
||||
// retenue, source, date). Le jeu de coefficients d'ORIGINE (valeurs du code
|
||||
// de calculs.trans-former.fr au 28/09/2026) vit dans
|
||||
// fixtures/coefficients-2026-05.json et sert au test de non-régression.
|
||||
//
|
||||
// Les poids d'axes, modulateurs, matrices d'adéquation et tolérances de
|
||||
// algo-config.json ne sont PAS ici : ce sont des heuristiques de conception,
|
||||
// non sourcées, livrées comme données (fixtures/algo-config.json) et injectées.
|
||||
const D_RECHERCHE = '2026-04-28';
|
||||
const D_LOT = '2026-09-28';
|
||||
const S_EAU = '1 PROJETS/DEV/Simulateur autonomie V2/01-recherche-eau.md';
|
||||
const S_ENERGIE = '1 PROJETS/DEV/Simulateur autonomie V2/02-recherche-energie.md';
|
||||
const S_BRIEF = '1 PROJETS/DEV/Simulateur autonomie V2/99-brief-atis-dev.md';
|
||||
const S_V1 = 'calculs.trans-former.fr, simulateur autoconstruction V1 (public/simulateurs/autoconstruction/app.js, calcSurfaces)';
|
||||
/** Table harmonisée (LOT P1.0, 28/09/2026). Référence tant que Jules ne met pas de veto (R-7). */
|
||||
export const COEFFICIENTS = {
|
||||
captage_rendement: {
|
||||
valeur: 0.8,
|
||||
source: `${S_EAU} §3 « Captable max » et §7 n°6 (NF EN 16941-1, recueil Nény p.16)`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'fourchette 0,75 (zone sèche) à 0,85 (zone humide, gouttières optimisées) ; le code portait 0,7 (scoreurs), 0,8 (taux) et 0,85 (V1 bioclimatique)',
|
||||
},
|
||||
captage_rendement_par_toiture: {
|
||||
valeur: { tuile: 0.85, ardoise: 0.85, gravillonnee: 0.5, vegetalisee: 0.3 },
|
||||
source: `${S_EAU} §7 n°6 (ADEME, réf. NF EN 16941-1)`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'à brancher quand le formulaire demande le type de toiture ; non utilisé en v0.1',
|
||||
},
|
||||
besoin_eau_l_j_pers: {
|
||||
valeur: 150,
|
||||
source: `${S_EAU} §3 « Besoin eau » (INSEE / Eau France 2024) ; ${S_BRIEF} §7.1`,
|
||||
date: D_RECHERCHE,
|
||||
note: '54 750 L/an/personne ; le V1 comptait 46,5 m³/an',
|
||||
},
|
||||
part_eau_substituable: {
|
||||
valeur: 0.7,
|
||||
source: `${S_EAU} §3 ; ${S_BRIEF} §7.1 (WC 30 + douche 60 + lessive 30 + jardin 25 L/j sur 150)`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'le V1 comptait 32 % (15 m³ sur 46,5)',
|
||||
},
|
||||
orientation_pv: {
|
||||
valeur: { sud: 1, 'sud-est': 0.95, 'sud-ouest': 0.95, est: 0.8, ouest: 0.8, nord: 0.6 },
|
||||
source: `${S_ENERGIE} §3.2 « Variantes orientation toiture »`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'source : SE/SO 95 %, E/O 80-85 %, N 60-65 % (bas de fourchette retenu) ; le brief §7.2 prenait les milieux 0,82 et 0,62 ; le code portait 0,93 / 0,78 / 0,40',
|
||||
},
|
||||
productible_pv_kwh_kwc: {
|
||||
valeur: {
|
||||
par: 'zone_rt',
|
||||
table: { H1a: 1000, H1b: 1050, H1c: 1200, H2a: 1100, H2b: 1200, H2c: 1250, H2d: 1300, H3: 1400 },
|
||||
defaut: 1100,
|
||||
},
|
||||
source: `${S_ENERGIE} §3.2 (PVGIS, sud 30°, pertes 14 %)`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'H1a : moyenne Lille 950 / Paris 1 050 ; H2d : interpolé entre H2c et H3 par la MOE, non sourcé (déclaré en avertissement) ; défaut = H2a ; le code portait une table par zone biogéographique allant jusqu\'à 1 550, au-dessus du maximum de la source',
|
||||
},
|
||||
autoconso_sans_batterie: {
|
||||
valeur: 0.3,
|
||||
source: `${S_ENERGIE} §3.2 ; ${S_BRIEF} §7.2`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'absent du code : la couverture énergie comparait la production brute au besoin',
|
||||
},
|
||||
autoconso_avec_batterie_10kwh: {
|
||||
valeur: 0.7,
|
||||
source: `${S_ENERGIE} §3.2 ; ${S_BRIEF} §7.2`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'entre 0 et 10 kWh de batterie, le moteur interpole linéairement (choix MOE, non sourcé)',
|
||||
},
|
||||
besoin_elec_kwh_pers_an: {
|
||||
valeur: 1000,
|
||||
source: `${S_ENERGIE} §3.1 (ADEME 2023, RTE 2024) ; ${S_BRIEF} §7.2`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'fourchette 900 à 1 200 ; hors chauffage électrique, ECS et véhicule',
|
||||
},
|
||||
dju_defaut_par_classe_rt: {
|
||||
valeur: { H1: 2800, H2: 2200, H3: 1500 },
|
||||
source: 'heuristique de conception V2 (calibrage.js et algo-config.json), non sourcée',
|
||||
date: '2026-05-01',
|
||||
note: 'repli seulement : la table france_climat porte un DJU par département',
|
||||
},
|
||||
ml_haie_par_pers: {
|
||||
valeur: 25,
|
||||
source: `${S_BRIEF} §7.3 (correction Jules du 28/04/2026) ; 03-recherche-alimentation.md §5`,
|
||||
date: D_RECHERCHE,
|
||||
note: '50 ml/personne pour un excédent transformable',
|
||||
},
|
||||
contexte_defaut: {
|
||||
valeur: { zone_rt: 'H2a', zone_biogeo_code: 4, pluviometrie_mm: 800, dju: 2200 },
|
||||
source: 'heuristique de conception V2 (algo.js deriveContexte, simulation.js), non sourcée',
|
||||
date: '2026-05-01',
|
||||
note: 'appliqué seulement si le département est absent de france_climat ; toujours déclaré en avertissement',
|
||||
},
|
||||
geometrie_ratio_shab_emprise: {
|
||||
valeur: 0.8,
|
||||
source: 'heuristique MOE (murs extérieurs ~0,3 m, refends, escalier), non sourcée',
|
||||
date: D_LOT,
|
||||
note: 'hypothèse porteuse du P1.2 : à confronter à la surface DPE des adresses A et B (écart visé < 20 %)',
|
||||
},
|
||||
geometrie_ratio_vitrage_shab: {
|
||||
valeur: 1 / 6,
|
||||
source: `${S_V1} (vit = S/6) ; règle RT 2012 (baies ≥ 1/6 de la SHAB)`,
|
||||
date: '2025-01-01',
|
||||
note: 'la SPEC §4.1 écrit « 15 % » ; 1/6 = 16,7 %',
|
||||
},
|
||||
geometrie_hauteur_etage_m: {
|
||||
valeur: 2.5,
|
||||
source: 'heuristique MOE (hauteur sous plafond courante), non sourcée',
|
||||
date: D_LOT,
|
||||
},
|
||||
geometrie_facteur_perimetre: {
|
||||
valeur: 1.3,
|
||||
source: `${S_V1} (perimetre = √(S × 1,3) × 4)`,
|
||||
date: '2025-01-01',
|
||||
},
|
||||
geometrie_part_mur_mitoyen: {
|
||||
valeur: 0.25,
|
||||
source: 'heuristique MOE (un côté mitoyen = un quart du périmètre), non sourcée',
|
||||
date: D_LOT,
|
||||
},
|
||||
geometrie_majoration_toiture: {
|
||||
valeur: 1.05,
|
||||
source: `${S_V1} (toiture = S / cos α × 1,05)`,
|
||||
date: '2025-01-01',
|
||||
},
|
||||
geometrie_pente_defaut_deg: {
|
||||
valeur: 30,
|
||||
source: 'heuristique MOE (pente courante des toitures à deux versants), non sourcée',
|
||||
date: D_LOT,
|
||||
},
|
||||
};
|
||||
/** Clés de la table, pour valider un jeu de coefficients injecté (fixture, veto). */
|
||||
export const CLES_COEFFICIENTS = Object.keys(COEFFICIENTS);
|
||||
/**
|
||||
* Vérifie qu'un objet a la forme d'une table de coefficients (toutes les clés,
|
||||
* chaque entrée avec valeur, source et date). Renvoie la liste des défauts.
|
||||
*/
|
||||
export function verifieTableCoefficients(table) {
|
||||
const erreurs = [];
|
||||
if (!table || typeof table !== 'object')
|
||||
return ['table absente ou non objet'];
|
||||
const t = table;
|
||||
for (const cle of CLES_COEFFICIENTS) {
|
||||
const c = t[cle];
|
||||
if (!c || typeof c !== 'object') {
|
||||
erreurs.push(`${cle} : absent`);
|
||||
continue;
|
||||
}
|
||||
if (!('valeur' in c))
|
||||
erreurs.push(`${cle} : valeur absente`);
|
||||
if (typeof c.source !== 'string' || !c.source)
|
||||
erreurs.push(`${cle} : source absente`);
|
||||
if (typeof c.date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(c.date))
|
||||
erreurs.push(`${cle} : date absente ou non AAAA-MM-JJ`);
|
||||
}
|
||||
return erreurs;
|
||||
}
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
import type { Moteur, Resultat } from './contrat.js';
|
||||
import { type TableCoefficients } from './coefficients.js';
|
||||
export interface LigneClimat {
|
||||
dept: string;
|
||||
nom_dept?: string;
|
||||
zone_rt: string;
|
||||
pluvio_mm_an?: number | null;
|
||||
dju_base_18?: number | null;
|
||||
zone_biogeo?: number | null;
|
||||
[autre: string]: unknown;
|
||||
}
|
||||
export type ClasseRt = 'H1' | 'H2' | 'H3';
|
||||
export type ZoneBiogeo = 'C' | 'M' | 'OC' | 'MED';
|
||||
export interface Contexte {
|
||||
/** Département tel que trouvé (« 2A », « 971 », « 75 ») ou null si inconnu. */
|
||||
departement: string | null;
|
||||
zone_climatique_rt: string;
|
||||
zone_rt_class: ClasseRt;
|
||||
/** Zone écologique (biogéographique) en lettres : C, M, OC, MED. */
|
||||
zone_biogeo: ZoneBiogeo;
|
||||
/** Zone biogéographique numérique (1 à 8) telle que portée par france_climat. */
|
||||
zone_biogeo_code: number;
|
||||
pluviometrie_mm: number;
|
||||
dju: number;
|
||||
}
|
||||
export interface EntreesContexte {
|
||||
code_postal: string;
|
||||
}
|
||||
export interface DonneesContexte {
|
||||
france_climat: LigneClimat[];
|
||||
coefficients?: TableCoefficients;
|
||||
}
|
||||
/** Zone biogéographique numérique → libellé (V2 calibrage.js, inchangé). */
|
||||
export declare const ZONE_BIOGEO_MAP: Record<number, ZoneBiogeo>;
|
||||
/** Sous-zone RT (H1a, H2d…) → classe principale. */
|
||||
export declare function zoneRtToClass(zone_rt: string | null | undefined): ClasseRt;
|
||||
/**
|
||||
* Département depuis un code postal : DOM (97x → trois caractères), Corse
|
||||
* (20000-20190 → 2A, 20200-20620 → 2B, autre 20xxx → 2A), sinon deux chiffres.
|
||||
* Reprend `deptFromCp` de calculs (scripts/autonomie/data.js), seule version
|
||||
* qui gérait ces cas. Renvoie null si le code postal est inexploitable.
|
||||
*/
|
||||
export declare function departementDepuisCodePostal(code_postal: string | number | null | undefined): string | null;
|
||||
/**
|
||||
* Dérive le contexte climatique. Fonction pure, utilisée par le moteur
|
||||
* `commun.contexte` et par le noyau autonomie (`selectionner`).
|
||||
*/
|
||||
export declare function deriveContexte(code_postal: string | number | null | undefined, climat: LigneClimat[], coefficients?: TableCoefficients): Resultat<Contexte>;
|
||||
export declare const contexte: Moteur<EntreesContexte, DonneesContexte, Contexte>;
|
||||
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
// contexte.ts — moteur `commun.contexte` : code postal → département → zone.
|
||||
// UNE seule version (le noyau autonomie l'utilise aussi). Corse 2A/2B et
|
||||
// DOM 971-976 corrigés : le code d'origine (algo.js) prenait deux chiffres et
|
||||
// retombait sur H2a pour ces départements.
|
||||
import { Trace } from './contrat.js';
|
||||
import { COEFFICIENTS } from './coefficients.js';
|
||||
import { VERSION } from '../version.js';
|
||||
/** Zone biogéographique numérique → libellé (V2 calibrage.js, inchangé). */
|
||||
export const ZONE_BIOGEO_MAP = {
|
||||
1: 'C', // Continental
|
||||
2: 'M', // Montagnard
|
||||
3: 'OC', // Océanique côtier
|
||||
4: 'OC', // Océanique intérieur
|
||||
5: 'MED', // Méditerranéen continental
|
||||
6: 'MED', // Méditerranéen côtier
|
||||
7: 'MED', // Pré-alpin méditerranéen
|
||||
8: 'OC', // Semi-océanique
|
||||
};
|
||||
/** Sous-zone RT (H1a, H2d…) → classe principale. */
|
||||
export function zoneRtToClass(zone_rt) {
|
||||
if (!zone_rt)
|
||||
return 'H2';
|
||||
if (zone_rt.startsWith('H1'))
|
||||
return 'H1';
|
||||
if (zone_rt.startsWith('H2'))
|
||||
return 'H2';
|
||||
if (zone_rt === 'H3')
|
||||
return 'H3';
|
||||
return 'H2';
|
||||
}
|
||||
/**
|
||||
* Département depuis un code postal : DOM (97x → trois caractères), Corse
|
||||
* (20000-20190 → 2A, 20200-20620 → 2B, autre 20xxx → 2A), sinon deux chiffres.
|
||||
* Reprend `deptFromCp` de calculs (scripts/autonomie/data.js), seule version
|
||||
* qui gérait ces cas. Renvoie null si le code postal est inexploitable.
|
||||
*/
|
||||
export function departementDepuisCodePostal(code_postal) {
|
||||
const cp = String(code_postal ?? '').trim();
|
||||
if (!/^\d{5}$/.test(cp))
|
||||
return cp.length >= 2 && /^\d{2}/.test(cp) ? cp.slice(0, 2) : null;
|
||||
if (cp.startsWith('97'))
|
||||
return cp.slice(0, 3);
|
||||
if (cp.startsWith('20')) {
|
||||
const n = Number(cp);
|
||||
if (n >= 20000 && n <= 20190)
|
||||
return '2A';
|
||||
if (n >= 20200 && n <= 20620)
|
||||
return '2B';
|
||||
return '2A';
|
||||
}
|
||||
return cp.slice(0, 2);
|
||||
}
|
||||
/**
|
||||
* Dérive le contexte climatique. Fonction pure, utilisée par le moteur
|
||||
* `commun.contexte` et par le noyau autonomie (`selectionner`).
|
||||
*/
|
||||
export function deriveContexte(code_postal, climat, coefficients = COEFFICIENTS) {
|
||||
const trace = new Trace();
|
||||
const departement = departementDepuisCodePostal(code_postal);
|
||||
const ligne = departement ? climat.find((c) => c.dept === departement) : undefined;
|
||||
if (!ligne) {
|
||||
const d = coefficients.contexte_defaut;
|
||||
trace.avertit(`contexte par défaut appliqué (${d.valeur.zone_rt}, ${d.valeur.pluviometrie_mm} mm, ${d.valeur.dju} DJU) : ` +
|
||||
(departement ? `département ${departement} absent de france_climat` : `code postal « ${code_postal ?? ''} » inexploitable`));
|
||||
trace.source('contexte_defaut', d.valeur.zone_rt, d.source, d.date);
|
||||
return trace.resultat({
|
||||
departement,
|
||||
zone_climatique_rt: d.valeur.zone_rt,
|
||||
zone_rt_class: zoneRtToClass(d.valeur.zone_rt),
|
||||
zone_biogeo: ZONE_BIOGEO_MAP[d.valeur.zone_biogeo_code] ?? 'OC',
|
||||
zone_biogeo_code: d.valeur.zone_biogeo_code,
|
||||
pluviometrie_mm: d.valeur.pluviometrie_mm,
|
||||
dju: d.valeur.dju,
|
||||
}, VERSION);
|
||||
}
|
||||
const d = coefficients.contexte_defaut;
|
||||
const zone_biogeo_code = ligne.zone_biogeo ?? d.valeur.zone_biogeo_code;
|
||||
if (ligne.zone_biogeo == null)
|
||||
trace.avertit(`zone biogéographique absente pour ${departement}, défaut ${d.valeur.zone_biogeo_code}`);
|
||||
const pluviometrie_mm = ligne.pluvio_mm_an ?? d.valeur.pluviometrie_mm;
|
||||
if (ligne.pluvio_mm_an == null)
|
||||
trace.avertit(`pluviométrie absente pour ${departement}, défaut ${d.valeur.pluviometrie_mm} mm`);
|
||||
const dju = ligne.dju_base_18 ?? d.valeur.dju;
|
||||
if (ligne.dju_base_18 == null)
|
||||
trace.avertit(`DJU absent pour ${departement}, défaut ${d.valeur.dju}`);
|
||||
trace.source(`france_climat.${departement}`, ligne.zone_rt, String(ligne.source_climat ?? 'france_climat'), String(ligne.source_date ?? ''));
|
||||
return trace.resultat({
|
||||
departement,
|
||||
zone_climatique_rt: ligne.zone_rt,
|
||||
zone_rt_class: zoneRtToClass(ligne.zone_rt),
|
||||
zone_biogeo: ZONE_BIOGEO_MAP[zone_biogeo_code] ?? 'OC',
|
||||
zone_biogeo_code,
|
||||
pluviometrie_mm,
|
||||
dju,
|
||||
}, VERSION);
|
||||
}
|
||||
export const contexte = {
|
||||
id: 'commun.contexte',
|
||||
calculer(entrees, donnees) {
|
||||
return deriveContexte(entrees.code_postal, donnees.france_climat, donnees.coefficients ?? COEFFICIENTS);
|
||||
},
|
||||
};
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
/** Une constante ou une donnée effectivement utilisée par un calcul. */
|
||||
export interface Source {
|
||||
cle: string;
|
||||
valeur: number | string;
|
||||
source: string;
|
||||
date: string;
|
||||
}
|
||||
export interface Resultat<S> {
|
||||
sorties: S;
|
||||
/** Tout coefficient et tout prix effectivement utilisé. */
|
||||
sources: Source[];
|
||||
/** Toute donnée manquante, toute valeur par défaut appliquée. */
|
||||
avertissements: string[];
|
||||
/** Étiquette du paquet (`vX.Y.Z`). */
|
||||
version: string;
|
||||
}
|
||||
export interface Moteur<E, D, S> {
|
||||
/** 'autonomie.eau', 'commun.geometrie', … */
|
||||
id: string;
|
||||
calculer(entrees: E, donnees: D): Resultat<S>;
|
||||
}
|
||||
/**
|
||||
* Collecteur de sources et d'avertissements d'un calcul. Une source est
|
||||
* enregistrée une fois par clé (la première valeur gagne) ; un avertissement
|
||||
* une fois par texte. Objet interne, sans I/O.
|
||||
*/
|
||||
export declare class Trace {
|
||||
private readonly sources;
|
||||
private readonly avertissements;
|
||||
source(cle: string, valeur: number | string, source: string, date: string): void;
|
||||
avertit(texte: string): void;
|
||||
/** Verse dans ce collecteur les sources et avertissements d'un autre résultat. */
|
||||
absorbe(autre: {
|
||||
sources: Source[];
|
||||
avertissements: string[];
|
||||
}): void;
|
||||
resultat<S>(sorties: S, version: string): Resultat<S>;
|
||||
}
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
// contrat.ts — le contrat d'un moteur (SPEC phase 1 §4.1).
|
||||
//
|
||||
// Quatre règles opposables au code :
|
||||
// ① `calculer` ne lit ni réseau, ni disque, ni horloge, ni DOM, ni variable
|
||||
// globale, ni aléa : mêmes entrées + mêmes données = même résultat.
|
||||
// ② les données entrent en paramètre, validées à la frontière par l'appelant
|
||||
// (`valider(table, json)`) ;
|
||||
// ③ aucun prix, aucun barème dans le paquet ;
|
||||
// ④ toute valeur par défaut appliquée est déclarée dans `avertissements`,
|
||||
// toute constante utilisée dans `sources`.
|
||||
/**
|
||||
* Collecteur de sources et d'avertissements d'un calcul. Une source est
|
||||
* enregistrée une fois par clé (la première valeur gagne) ; un avertissement
|
||||
* une fois par texte. Objet interne, sans I/O.
|
||||
*/
|
||||
export class Trace {
|
||||
sources = new Map();
|
||||
avertissements = new Set();
|
||||
source(cle, valeur, source, date) {
|
||||
if (!this.sources.has(cle))
|
||||
this.sources.set(cle, { cle, valeur, source, date });
|
||||
}
|
||||
avertit(texte) {
|
||||
this.avertissements.add(texte);
|
||||
}
|
||||
/** Verse dans ce collecteur les sources et avertissements d'un autre résultat. */
|
||||
absorbe(autre) {
|
||||
for (const s of autre.sources)
|
||||
this.source(s.cle, s.valeur, s.source, s.date);
|
||||
for (const a of autre.avertissements)
|
||||
this.avertit(a);
|
||||
}
|
||||
resultat(sorties, version) {
|
||||
return {
|
||||
sorties,
|
||||
sources: [...this.sources.values()],
|
||||
avertissements: [...this.avertissements],
|
||||
version,
|
||||
};
|
||||
}
|
||||
}
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
import type { Moteur } from './contrat.js';
|
||||
import { type TableCoefficients } from './coefficients.js';
|
||||
export interface EntreesGeometrie {
|
||||
/** Emprise au sol du bâti (m²). */
|
||||
emprise_m2: number;
|
||||
/** Nombre de niveaux habitables (1 = plain-pied). */
|
||||
niveaux?: number;
|
||||
/** Hauteur totale du bâti hors combles (m) ; sinon niveaux × hauteur d'étage par défaut. */
|
||||
hauteur_m?: number;
|
||||
/** Pente du toit (degrés) ; sinon pente par défaut. */
|
||||
pente_toit_deg?: number;
|
||||
/** Côtés mitoyens (0, 1 ou 2). */
|
||||
mitoyennete?: 0 | 1 | 2;
|
||||
/** Surface habitable connue (DPE, acte) : remplace l'estimation. */
|
||||
surface_habitable_m2?: number;
|
||||
}
|
||||
export interface DonneesGeometrie {
|
||||
coefficients?: TableCoefficients;
|
||||
}
|
||||
export interface SortiesGeometrie {
|
||||
/** Surface habitable, connue ou estimée (m²). */
|
||||
shab_m2: number;
|
||||
shab_estimee: boolean;
|
||||
perimetre_m: number;
|
||||
/** Périmètre exposé après retrait des côtés mitoyens (m). */
|
||||
perimetre_expose_m: number;
|
||||
murs_exterieurs_m2: number;
|
||||
murs_opaques_m2: number;
|
||||
vitrage_m2: number;
|
||||
toiture_m2: number;
|
||||
plancher_bas_m2: number;
|
||||
volume_m3: number;
|
||||
hauteur_m: number;
|
||||
hauteur_etage_m: number;
|
||||
}
|
||||
export declare function calculeGeometrie(entrees: EntreesGeometrie, coefficients?: TableCoefficients): import("./contrat.js").Resultat<SortiesGeometrie>;
|
||||
export declare const geometrie: Moteur<EntreesGeometrie, DonneesGeometrie, SortiesGeometrie>;
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
// geometrie.ts — moteur `commun.geometrie` : emprise, niveaux, hauteur, pente,
|
||||
// mitoyenneté → surfaces de parois, SHAB estimée, volume. Reprend les formules
|
||||
// de `calcSurfaces` du simulateur autoconstruction V1 (calculs), sans le DOM,
|
||||
// avec les coefficients sortis dans coefficients.ts.
|
||||
import { Trace } from './contrat.js';
|
||||
import { COEFFICIENTS } from './coefficients.js';
|
||||
import { arrondi2 } from './argent.js';
|
||||
import { VERSION } from '../version.js';
|
||||
export function calculeGeometrie(entrees, coefficients = COEFFICIENTS) {
|
||||
const trace = new Trace();
|
||||
const c = coefficients;
|
||||
const emprise = Math.max(0, entrees.emprise_m2 || 0);
|
||||
const niveaux = Math.max(1, Math.round(entrees.niveaux ?? 1));
|
||||
if (entrees.niveaux == null)
|
||||
trace.avertit('nombre de niveaux inconnu : 1 niveau supposé');
|
||||
let hauteur = entrees.hauteur_m ?? 0;
|
||||
if (!(hauteur > 0)) {
|
||||
hauteur = niveaux * c.geometrie_hauteur_etage_m.valeur;
|
||||
trace.avertit(`hauteur inconnue : ${niveaux} × ${c.geometrie_hauteur_etage_m.valeur} m supposés`);
|
||||
trace.source('geometrie_hauteur_etage_m', c.geometrie_hauteur_etage_m.valeur, c.geometrie_hauteur_etage_m.source, c.geometrie_hauteur_etage_m.date);
|
||||
}
|
||||
const hauteur_etage = hauteur / niveaux;
|
||||
let pente = entrees.pente_toit_deg;
|
||||
if (pente == null || !(pente >= 0) || pente >= 90) {
|
||||
pente = c.geometrie_pente_defaut_deg.valeur;
|
||||
trace.avertit(`pente de toit inconnue : ${pente}° supposés`);
|
||||
trace.source('geometrie_pente_defaut_deg', pente, c.geometrie_pente_defaut_deg.source, c.geometrie_pente_defaut_deg.date);
|
||||
}
|
||||
const mitoyen = Math.min(2, Math.max(0, Math.round(entrees.mitoyennete ?? 0)));
|
||||
// SHAB
|
||||
let shab;
|
||||
let shab_estimee;
|
||||
if (entrees.surface_habitable_m2 && entrees.surface_habitable_m2 > 0) {
|
||||
shab = entrees.surface_habitable_m2;
|
||||
shab_estimee = false;
|
||||
}
|
||||
else {
|
||||
shab = emprise * niveaux * c.geometrie_ratio_shab_emprise.valeur;
|
||||
shab_estimee = true;
|
||||
trace.avertit('surface habitable estimée depuis l\'emprise (aucune surface connue)');
|
||||
trace.source('geometrie_ratio_shab_emprise', c.geometrie_ratio_shab_emprise.valeur, c.geometrie_ratio_shab_emprise.source, c.geometrie_ratio_shab_emprise.date);
|
||||
}
|
||||
// Périmètre (V1 : √(S × 1,3) × 4) et part exposée
|
||||
const perimetre = 4 * Math.sqrt(emprise * c.geometrie_facteur_perimetre.valeur);
|
||||
trace.source('geometrie_facteur_perimetre', c.geometrie_facteur_perimetre.valeur, c.geometrie_facteur_perimetre.source, c.geometrie_facteur_perimetre.date);
|
||||
const perimetre_expose = perimetre * (1 - c.geometrie_part_mur_mitoyen.valeur * mitoyen);
|
||||
if (mitoyen > 0)
|
||||
trace.source('geometrie_part_mur_mitoyen', c.geometrie_part_mur_mitoyen.valeur, c.geometrie_part_mur_mitoyen.source, c.geometrie_part_mur_mitoyen.date);
|
||||
// Parois
|
||||
const murs_exterieurs = perimetre_expose * hauteur;
|
||||
const vitrage = shab * c.geometrie_ratio_vitrage_shab.valeur;
|
||||
trace.source('geometrie_ratio_vitrage_shab', arrondi2(c.geometrie_ratio_vitrage_shab.valeur), c.geometrie_ratio_vitrage_shab.source, c.geometrie_ratio_vitrage_shab.date);
|
||||
const murs_opaques = Math.max(0, murs_exterieurs - vitrage);
|
||||
const rad = (pente * Math.PI) / 180;
|
||||
const toiture = (emprise / Math.cos(rad)) * c.geometrie_majoration_toiture.valeur;
|
||||
trace.source('geometrie_majoration_toiture', c.geometrie_majoration_toiture.valeur, c.geometrie_majoration_toiture.source, c.geometrie_majoration_toiture.date);
|
||||
const volume = shab * hauteur_etage;
|
||||
const sorties = {
|
||||
shab_m2: arrondi2(shab),
|
||||
shab_estimee,
|
||||
perimetre_m: arrondi2(perimetre),
|
||||
perimetre_expose_m: arrondi2(perimetre_expose),
|
||||
murs_exterieurs_m2: arrondi2(murs_exterieurs),
|
||||
murs_opaques_m2: arrondi2(murs_opaques),
|
||||
vitrage_m2: arrondi2(vitrage),
|
||||
toiture_m2: arrondi2(toiture),
|
||||
plancher_bas_m2: arrondi2(emprise),
|
||||
volume_m3: arrondi2(volume),
|
||||
hauteur_m: arrondi2(hauteur),
|
||||
hauteur_etage_m: arrondi2(hauteur_etage),
|
||||
};
|
||||
return trace.resultat(sorties, VERSION);
|
||||
}
|
||||
export const geometrie = {
|
||||
id: 'commun.geometrie',
|
||||
calculer(entrees, donnees) {
|
||||
return calculeGeometrie(entrees, donnees?.coefficients ?? COEFFICIENTS);
|
||||
},
|
||||
};
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
export type { Source, Resultat, Moteur } from './contrat.js';
|
||||
export { Trace } from './contrat.js';
|
||||
export type { Coefficient, TableCoefficients, Orientation, ClasseRt } from './coefficients.js';
|
||||
export { COEFFICIENTS, CLES_COEFFICIENTS, verifieTableCoefficients } from './coefficients.js';
|
||||
export type { LigneClimat, Contexte, EntreesContexte, DonneesContexte, ZoneBiogeo } from './contexte.js';
|
||||
export { contexte, deriveContexte, departementDepuisCodePostal, zoneRtToClass, ZONE_BIOGEO_MAP } from './contexte.js';
|
||||
export type { EntreesGeometrie, DonneesGeometrie, SortiesGeometrie } from './geometrie.js';
|
||||
export { geometrie, calculeGeometrie } from './geometrie.js';
|
||||
export type { Fourchette, FourchetteOuverte } from './argent.js';
|
||||
export { fourchette, pointMedian, additionneFourchettes, arrondiAuPas, arrondi2, mensualite, annuite, coutDuCredit } from './argent.js';
|
||||
export type { Schema, NomTable, TypeJson } from './schemas.js';
|
||||
export { SCHEMAS, NOMS_TABLES } from './schemas.js';
|
||||
export type { Validation } from './valider.js';
|
||||
export { valider, validerContre } from './valider.js';
|
||||
export { VERSION } from '../version.js';
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export { Trace } from './contrat.js';
|
||||
export { COEFFICIENTS, CLES_COEFFICIENTS, verifieTableCoefficients } from './coefficients.js';
|
||||
export { contexte, deriveContexte, departementDepuisCodePostal, zoneRtToClass, ZONE_BIOGEO_MAP } from './contexte.js';
|
||||
export { geometrie, calculeGeometrie } from './geometrie.js';
|
||||
export { fourchette, pointMedian, additionneFourchettes, arrondiAuPas, arrondi2, mensualite, annuite, coutDuCredit } from './argent.js';
|
||||
export { SCHEMAS, NOMS_TABLES } from './schemas.js';
|
||||
export { valider, validerContre } from './valider.js';
|
||||
export { VERSION } from '../version.js';
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
export type TypeJson = 'string' | 'number' | 'integer' | 'boolean' | 'null' | 'object' | 'array';
|
||||
export interface Schema {
|
||||
$id?: string;
|
||||
description?: string;
|
||||
type: TypeJson | TypeJson[];
|
||||
required?: string[];
|
||||
properties?: Record<string, Schema>;
|
||||
items?: Schema;
|
||||
enum?: (string | number | boolean | null)[];
|
||||
minItems?: number;
|
||||
}
|
||||
export type NomTable = 'postes' | 'options' | 'france_climat' | 'algo_config';
|
||||
export declare const SCHEMAS: Record<NomTable, Schema>;
|
||||
export declare const NOMS_TABLES: NomTable[];
|
||||
Vendored
+121
@@ -0,0 +1,121 @@
|
||||
// schemas.ts — forme attendue de chaque JSON de données injecté dans les
|
||||
// moteurs. Source de vérité des fichiers `schemas/<table>.schema.json`, que
|
||||
// `npm run build` réécrit depuis ce module (scripts/ecrire-schemas.mjs) : le
|
||||
// paquet reste sans I/O, et un consommateur peut lire les JSON.
|
||||
//
|
||||
// Sous-ensemble de JSON Schema compris par `valider()` : type (simple ou
|
||||
// liste), required, properties, items, enum, minItems. Vérification
|
||||
// structurelle maison, pas un validateur JSON Schema complet.
|
||||
const nombreOuNull = { type: ['number', 'null'] };
|
||||
const chaineOuNull = { type: ['string', 'null'] };
|
||||
const booleenOuNull = { type: ['boolean', 'null'] };
|
||||
export const SCHEMAS = {
|
||||
postes: {
|
||||
$id: 'https://git.trans-former.fr/jules/moteurs/schemas/postes.schema.json',
|
||||
description: 'Postes du simulateur autonomie (calculs.trans-former.fr/calculs/data/postes.json). Champs lus par les moteurs.',
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['Id', 'axe', 'code', 'poste'],
|
||||
properties: {
|
||||
Id: { type: 'integer' },
|
||||
axe: { type: 'string', enum: ['eau', 'energie', 'alimentation'] },
|
||||
code: { type: 'string' },
|
||||
poste: { type: 'string' },
|
||||
ordre: nombreOuNull,
|
||||
obligatoire: booleenOuNull,
|
||||
actif: booleenOuNull,
|
||||
description: chaineOuNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
$id: 'https://git.trans-former.fr/jules/moteurs/schemas/options.schema.json',
|
||||
description: 'Typologies (options) du simulateur autonomie. Les prix y sont des DONNÉES publiées par calculs, jamais des constantes du paquet. parametre_dim, couplages_requis et couplages_exclus arrivent en JSON sérialisé (NocoDB) ou déjà parsés.',
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['Id', 'poste_id', 'code', 'nom'],
|
||||
properties: {
|
||||
Id: { type: 'integer' },
|
||||
poste_id: { type: 'integer' },
|
||||
code: { type: 'string' },
|
||||
nom: { type: 'string' },
|
||||
typologie: chaineOuNull,
|
||||
actif: booleenOuNull,
|
||||
prix_mat_min: nombreOuNull,
|
||||
prix_mat_max: nombreOuNull,
|
||||
prix_pro_min: nombreOuNull,
|
||||
prix_pro_max: nombreOuNull,
|
||||
prix_par_unite: nombreOuNull,
|
||||
prix_fixe: nombreOuNull,
|
||||
unite: chaineOuNull,
|
||||
quantite_default: nombreOuNull,
|
||||
parametre_dim: { type: ['string', 'object', 'null'] },
|
||||
couplages_requis: { type: ['string', 'array', 'null'] },
|
||||
couplages_exclus: { type: ['string', 'array', 'null'] },
|
||||
flag_diy: booleenOuNull,
|
||||
complexite_pose: nombreOuNull,
|
||||
accessible_fr: booleenOuNull,
|
||||
filiere_agreee: booleenOuNull,
|
||||
surface_min_m2: nombreOuNull,
|
||||
surface_optimale_m2: nombreOuNull,
|
||||
tags: { type: ['array', 'null'] },
|
||||
source: chaineOuNull,
|
||||
source_date: chaineOuNull,
|
||||
description: chaineOuNull,
|
||||
description_pedago: chaineOuNull,
|
||||
notes_techniques: chaineOuNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
france_climat: {
|
||||
$id: 'https://git.trans-former.fr/jules/moteurs/schemas/france_climat.schema.json',
|
||||
description: 'Une ligne par département (2A, 2B et 971-976 compris) : zone RT, pluviométrie, DJU base 18, zone biogéographique numérique.',
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['dept', 'zone_rt'],
|
||||
properties: {
|
||||
dept: { type: 'string' },
|
||||
nom_dept: chaineOuNull,
|
||||
zone_rt: { type: 'string', enum: ['H1a', 'H1b', 'H1c', 'H2a', 'H2b', 'H2c', 'H2d', 'H3'] },
|
||||
pluvio_mm_an: nombreOuNull,
|
||||
dju_base_18: nombreOuNull,
|
||||
zone_biogeo: nombreOuNull,
|
||||
source_climat: chaineOuNull,
|
||||
source_date: chaineOuNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
algo_config: {
|
||||
$id: 'https://git.trans-former.fr/jules/moteurs/schemas/algo_config.schema.json',
|
||||
description: 'Heuristiques de conception du simulateur autonomie (poids, modulateurs, matrices, tolérances, alias, poids des postes). Données non sourcées, injectées, jamais importées.',
|
||||
type: 'object',
|
||||
required: ['poids_axes_base', 'modulateurs', 'adequation_weights', 'tolerances', 'bonus_malus', 'couplages_alias', 'postes_poids'],
|
||||
properties: {
|
||||
poids_axes_base: {
|
||||
type: 'object',
|
||||
required: ['eau', 'energie', 'alim'],
|
||||
properties: { eau: { type: 'number' }, energie: { type: 'number' }, alim: { type: 'number' } },
|
||||
},
|
||||
modulateurs: { type: 'object' },
|
||||
adequation_weights: { type: 'object' },
|
||||
tolerances: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
budget_overrun_pct: { type: 'number' },
|
||||
budget_underspend_pct: { type: 'number' },
|
||||
tolerance_par_poste_pct: { type: 'number' },
|
||||
},
|
||||
},
|
||||
bonus_malus: { type: 'object' },
|
||||
couplages_alias: { type: 'object' },
|
||||
postes_poids: { type: 'object' },
|
||||
},
|
||||
},
|
||||
};
|
||||
export const NOMS_TABLES = Object.keys(SCHEMAS);
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import { type NomTable, type Schema } from './schemas.js';
|
||||
export interface Validation {
|
||||
ok: boolean;
|
||||
erreurs: string[];
|
||||
}
|
||||
/**
|
||||
* Valide un JSON de données contre le schéma de sa table. Renvoie au plus
|
||||
* vingt erreurs, chacune avec son chemin.
|
||||
*/
|
||||
export declare function valider(table: NomTable, json: unknown): Validation;
|
||||
/** Valide un objet contre un schéma arbitraire (même sous-ensemble). */
|
||||
export declare function validerContre(schema: Schema, json: unknown, nom?: string): Validation;
|
||||
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
// valider.ts — validation structurelle d'un JSON de données contre le schéma
|
||||
// de sa table (schemas/<table>.schema.json, incarné par schemas.ts). Sans
|
||||
// dépendance, sans I/O. Ce n'est pas un validateur JSON Schema complet : type,
|
||||
// required, properties, items, enum, minItems.
|
||||
import { SCHEMAS } from './schemas.js';
|
||||
const MAX_ERREURS = 20;
|
||||
function typeDe(v) {
|
||||
if (v === null)
|
||||
return 'null';
|
||||
if (Array.isArray(v))
|
||||
return 'array';
|
||||
if (typeof v === 'number')
|
||||
return Number.isInteger(v) ? 'integer' : 'number';
|
||||
if (typeof v === 'string')
|
||||
return 'string';
|
||||
if (typeof v === 'boolean')
|
||||
return 'boolean';
|
||||
return 'object';
|
||||
}
|
||||
function typeAccepte(attendu, reel) {
|
||||
const liste = Array.isArray(attendu) ? attendu : [attendu];
|
||||
return liste.some((t) => t === reel || (t === 'number' && reel === 'integer'));
|
||||
}
|
||||
function verifie(schema, valeur, chemin, erreurs) {
|
||||
if (erreurs.length >= MAX_ERREURS)
|
||||
return;
|
||||
const reel = typeDe(valeur);
|
||||
if (!typeAccepte(schema.type, reel)) {
|
||||
erreurs.push(`${chemin} : type ${reel}, attendu ${Array.isArray(schema.type) ? schema.type.join('|') : schema.type}`);
|
||||
return;
|
||||
}
|
||||
if (schema.enum && !schema.enum.includes(valeur)) {
|
||||
erreurs.push(`${chemin} : valeur « ${String(valeur)} » hors de ${schema.enum.map(String).join(', ')}`);
|
||||
}
|
||||
if (reel === 'array') {
|
||||
const tableau = valeur;
|
||||
if (schema.minItems != null && tableau.length < schema.minItems) {
|
||||
erreurs.push(`${chemin} : ${tableau.length} élément(s), minimum ${schema.minItems}`);
|
||||
}
|
||||
if (schema.items) {
|
||||
for (let i = 0; i < tableau.length && erreurs.length < MAX_ERREURS; i++) {
|
||||
verifie(schema.items, tableau[i], `${chemin}[${i}]`, erreurs);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (reel === 'object') {
|
||||
const objet = valeur;
|
||||
for (const cle of schema.required ?? []) {
|
||||
if (!(cle in objet) || objet[cle] === undefined)
|
||||
erreurs.push(`${chemin}.${cle} : requis, absent`);
|
||||
}
|
||||
if (schema.properties) {
|
||||
for (const [cle, sous] of Object.entries(schema.properties)) {
|
||||
if (cle in objet && objet[cle] !== undefined)
|
||||
verifie(sous, objet[cle], `${chemin}.${cle}`, erreurs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Valide un JSON de données contre le schéma de sa table. Renvoie au plus
|
||||
* vingt erreurs, chacune avec son chemin.
|
||||
*/
|
||||
export function valider(table, json) {
|
||||
const schema = SCHEMAS[table];
|
||||
if (!schema)
|
||||
return { ok: false, erreurs: [`table inconnue : ${String(table)}`] };
|
||||
const erreurs = [];
|
||||
verifie(schema, json, table, erreurs);
|
||||
return { ok: erreurs.length === 0, erreurs };
|
||||
}
|
||||
/** Valide un objet contre un schéma arbitraire (même sous-ensemble). */
|
||||
export function validerContre(schema, json, nom = '$') {
|
||||
const erreurs = [];
|
||||
verifie(schema, json, nom, erreurs);
|
||||
return { ok: erreurs.length === 0, erreurs };
|
||||
}
|
||||
Reference in New Issue
Block a user