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
105 lines
4.8 KiB
JavaScript
105 lines
4.8 KiB
JavaScript
// Tests du paquet lui-même : version, schémas en phase, tables de coefficients
|
|
// bien formées, pureté du code (aucune I/O, aucune horloge, aucun aléa dans
|
|
// src/), et absence des anciennes constantes dupliquées (LOT P1.0 point 6).
|
|
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { VERSION, SCHEMAS, NOMS_TABLES, COEFFICIENTS, verifieTableCoefficients, MOTEURS } from '../dist/index.js'
|
|
|
|
const racine = fileURLToPath(new URL('..', import.meta.url))
|
|
const lire = (rel) => readFileSync(join(racine, rel), 'utf8')
|
|
|
|
function fichiersTs(dossier) {
|
|
const out = []
|
|
for (const nom of readdirSync(dossier)) {
|
|
const chemin = join(dossier, nom)
|
|
if (statSync(chemin).isDirectory()) out.push(...fichiersTs(chemin))
|
|
else if (nom.endsWith('.ts')) out.push(chemin)
|
|
}
|
|
return out
|
|
}
|
|
|
|
/** Lignes de code hors commentaires (// et /* *\/ sur une ligne) et hors chaînes de `note:`/`source:`. */
|
|
function lignesDeCode(texte) {
|
|
return texte
|
|
.split('\n')
|
|
.map((l) => l.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, ''))
|
|
.filter((l) => l.trim() && !/^\s*\*/.test(l) && !/^\s*(note|source):\s*['"`]/.test(l))
|
|
}
|
|
|
|
test('VERSION du code = version de package.json', () => {
|
|
const pkg = JSON.parse(lire('package.json'))
|
|
assert.equal(VERSION, pkg.version)
|
|
assert.equal(pkg.name, '@transformer/moteurs')
|
|
assert.deepEqual(pkg.dependencies ?? {}, {}, 'zéro dépendance de production')
|
|
})
|
|
|
|
test('schemas/*.schema.json en phase avec src/commun/schemas.ts', () => {
|
|
for (const table of NOMS_TABLES) {
|
|
const json = JSON.parse(lire(`schemas/${table}.schema.json`))
|
|
delete json.$schema
|
|
assert.deepEqual(json, JSON.parse(JSON.stringify(SCHEMAS[table])), `${table}.schema.json diffère de SCHEMAS.${table} : relancer npm run build`)
|
|
}
|
|
})
|
|
|
|
test('les deux tables de coefficients sont complètes, sourcées et datées', () => {
|
|
assert.deepEqual(verifieTableCoefficients(COEFFICIENTS), [])
|
|
const origine = JSON.parse(lire('fixtures/coefficients-2026-05.json'))
|
|
assert.deepEqual(verifieTableCoefficients(origine), [])
|
|
for (const cle of Object.keys(COEFFICIENTS)) assert.ok(cle in origine, `fixture d'origine : clé ${cle} manquante`)
|
|
})
|
|
|
|
test('six moteurs livrés, identifiants stables', () => {
|
|
assert.deepEqual([...MOTEURS], ['commun.contexte', 'commun.geometrie', 'autonomie.eau', 'autonomie.energie', 'autonomie.alimentation', 'autonomie.integrateur'])
|
|
})
|
|
|
|
test('pureté : src/ ne lit ni réseau, ni disque, ni horloge, ni DOM, ni aléa, ni console', () => {
|
|
const interdits = [
|
|
[/\bfetch\s*\(/, 'fetch'],
|
|
[/\brequire\s*\(/, 'require'],
|
|
[/from\s+['"]node:/, 'import node:'],
|
|
[/from\s+['"](fs|path|os|http|https|child_process)['"]/, 'module Node'],
|
|
[/\bprocess\./, 'process'],
|
|
[/\bDate\.now\b|\bnew Date\b/, 'horloge'],
|
|
[/\bMath\.random\b/, 'aléa'],
|
|
[/\b(localStorage|sessionStorage|document|window)\b/, 'DOM'],
|
|
[/\bconsole\./, 'console'],
|
|
[/\bimport\s.*\.json['"]/, 'import JSON (les données s\'injectent)'],
|
|
]
|
|
const fautes = []
|
|
for (const f of fichiersTs(join(racine, 'src'))) {
|
|
const lignes = lignesDeCode(readFileSync(f, 'utf8'))
|
|
for (const [re, nom] of interdits) {
|
|
const l = lignes.find((x) => re.test(x))
|
|
if (l) fautes.push(`${f.slice(racine.length)} : ${nom} — ${l.trim()}`)
|
|
}
|
|
}
|
|
assert.deepEqual(fautes, [])
|
|
})
|
|
|
|
test('anciennes constantes : plus aucune définition en double dans src/', () => {
|
|
// Chaque grandeur existe UNE fois, dans coefficients.ts (LOT P1.0 point 6).
|
|
const patrons = [
|
|
[/ORIENTATION_FACTORS|DJU_DEFAULTS_BY_ZONE|FALLBACK_PVGIS|RENDEMENT_CAPTAGE|BESOIN_EAU_L_PERS_AN|PART_SUBSTITUABLE|BESOIN_ELEC_KWH_PERS_AN|ML_HAIE_PAR_PERS/, 'constante de module d\'origine'],
|
|
[/\b0\.93\b|\b0\.78\b|\b0\.40\b/, 'facteurs d\'orientation d\'origine'],
|
|
[/\b1550\b|\b1480\b|\b54750\b/, 'productible 1 550 / 1 480 ou besoin 54 750'],
|
|
[/\(pluvio \/ 1000\) \* 0\.7\b/, 'rendement de captage 0,7 dans score_pluviometrie'],
|
|
[/\b0\.85\b/, 'rendement 0,85'],
|
|
]
|
|
const fautes = []
|
|
for (const f of fichiersTs(join(racine, 'src'))) {
|
|
if (f.endsWith('coefficients.ts')) continue
|
|
const lignes = lignesDeCode(readFileSync(f, 'utf8'))
|
|
for (const [re, nom] of patrons) {
|
|
const l = lignes.find((x) => re.test(x))
|
|
if (l) fautes.push(`${f.slice(racine.length)} : ${nom} — ${l.trim()}`)
|
|
}
|
|
}
|
|
assert.deepEqual(fautes, [])
|
|
// Dans coefficients.ts, les anciennes valeurs n'apparaissent qu'en note (chaîne), jamais en valeur.
|
|
const coefs = lignesDeCode(lire('src/commun/coefficients.ts')).join('\n')
|
|
assert.ok(!/valeur:\s*0\.85\b/.test(coefs) && !/\b1550\b/.test(coefs) && !/nord:\s*0\.4\b/.test(coefs), 'coefficients.ts porte une valeur d\'origine')
|
|
})
|