Files
moteurs/test/monotonie.test.mjs
JulesandClaude Fable 5.1 91c755f884 Noyau : doublons, accessoires forcés orphelins, statut non monotone (SPEC menu §12 n° 1, 2, 3, 5, 7)
Observé le 02/10 sur une maison d'essai et reproduit sur les trois profils de
référence : une même typologie retenue jusqu'à sept fois dans un axe, des
accessoires forcés sans principal (conduit sans poêle, ballon sans poêle hydro),
EAU correcte à 3 000 € mais « budget insuffisant, minimum 7 280 € » à 6 000 €,
chaque FORCE écrit deux à quatre fois dans le journal. Un test par défaut, qui
échoue sur v0.1.0 (master 648015a) et passe ici.

CHANGELOG
- reajuste (CAS B, stratégie 1) : une ligne forcée par couplage n'est plus
  « upgradée » (l'origine la triait en tête avec son score 0,8 et la
  transformait, puis la passe couplages re-forçait l'original) ; la cible d'un
  upgrade n'est jamais le choix d'un autre poste ; le coût des couplages requis
  compte le requis égal au code actuel de la ligne ; un upgrade dont l'effet net
  baisse le total (il lâche un accessoire plus cher que lui) est refusé.
- reajuste (CAS B, stratégie 2) : un poste = une ligne (l'origine testait le
  code et pas le poste, sur une liste figée avant la boucle : jusqu'à cinq
  basse_planche_bec). La quantité d'un poste est sa dimension. La ligne ajoutée
  porte ses vrais couplages (l'origine écrivait []).
- reajuste (CAS A) : un downgrade ne cible jamais le choix d'un autre poste ; la
  baisse réelle compte les accessoires forcés lâchés ; après chaque retrait ou
  downgrade, les accessoires orphelins sont lâchés (le plancher de coût n'est
  plus verrouillé par des couplages que plus rien ne requiert).
- reajuste : une ligne remplacée porte SA dimension, pas celle de la ligne
  remplacée (une batterie au plomb affichait « 0,4 kWc », une chaudière « 6 ml » ;
  le taux énergie lisait ces kWc fantômes).
- couplages : passe 0 — RETIRE les lignes forcées que plus rien ne requiert,
  ABSORBE celles dont la typologie est déjà le choix d'un poste ; helpers
  codesRequisPar, codesChoisis, retireForcesOrphelins exportés.
- algo (finaliseSelection) : le journal ne se ré-ajoute plus à lui-même à
  chaque tour (reajusteBudget le renvoie en écho) ; la boucle vise la bande
  [−underspend, +overrun] et pas seulement le dépassement, trois tours au plus,
  toujours une passe couplages en dernier ; si le dernier forçage refranchit le
  seuil, le statut le dit. finaliseSelection exporté du noyau.

Tests : test/correctifs-v0.1.1.test.mjs (n° 1, 2, 3, 5, 7 + dimension héritée),
test/monotonie.test.mjs (trois profils × trois axes, 1 000 → 40 000 € par 500 :
le statut ne repasse jamais de correct à insuffisant ; le sous-total ne baisse
jamais au-delà de la tolérance de dépassement — la stricte croissance n'est pas
tenue par l'heuristique, −4,4 % au plus sur A/énergie, voir RECAP-LOT-v0.1.1).
Les 28 tests d'origine passent, inchangés.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tgtn7bJxTLiL2fRCkQMgtR
2026-10-05 16:39:16 +02:00

45 lines
2.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Monotonie en fonction du budget (SPEC menu branches fiches §12, v0.1.1) : sur
// les trois profils de référence et les trois axes, de 1 000 à 40 000 € par pas
// de 500 €, le statut ne repasse jamais de « correct » à « budget insuffisant »,
// et le sous-total ne décroît pas au-delà de la tolérance de dépassement
// (`budget_overrun_pct`, 5 % du budget). La stricte croissance n'est pas tenue
// par l'heuristique (l'étape 3 choisit poste par poste, par paliers ; constaté
// −4,4 % au plus sur A/énergie, voir RECAP-LOT-v0.1.1) : la borne retenue est
// la bande de tolérance, celle que l'habitant voit déjà comme « correct ».
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { eau, energie, alimentation, contexte as moteurContexte } from '../dist/index.js'
const lire = (f) => JSON.parse(readFileSync(new URL(`../fixtures/${f}`, import.meta.url), 'utf8'))
const options = lire('options.json')
const postes = lire('postes.json')
const france_climat = lire('france_climat.json')
const algo_config = lire('algo-config.json')
const profils = lire('profils-reference.json')
const donnees = { postes, options, algo_config, france_climat }
const MOTEURS = { eau, energie, alim: alimentation }
const tol = (algo_config.tolerances.budget_overrun_pct || 5) / 100
const BUDGETS = []
for (let b = 1000; b <= 40000; b += 500) BUDGETS.push(b)
for (const profil of profils) {
const ctx = moteurContexte.calculer({ code_postal: profil.inputs.code_postal }, { france_climat }).sorties
for (const [axe, moteur] of Object.entries(MOTEURS)) {
test(`${profil.nom} / ${axe} : statut jamais OK → INSUFFISANT, sous-total jamais en baisse au-delà de la tolérance (1 000 → 40 000 € par 500)`, () => {
const serie = BUDGETS.map((b) => {
const s = moteur.calculer({ autodiag: profil.inputs, budget_axe: b, contexte: ctx }, donnees).sorties
assert.equal(s.sous_total_eur_ht, s.postes.reduce((a, p) => a + p.cout_eur_ht, 0), `${b} : sous-total = somme des lignes`)
return { b, status: s.status, st: s.sous_total_eur_ht }
})
for (let i = 1; i < serie.length; i++) {
const p = serie[i - 1]
const c = serie[i]
assert.ok(!(p.status !== 'BUDGET_INSUFFISANT' && c.status === 'BUDGET_INSUFFISANT'), `statut ${p.status} à ${p.b} € puis BUDGET_INSUFFISANT à ${c.b} €`)
assert.ok(c.st >= p.st - c.b * tol, `sous-total ${p.st} € à ${p.b} € puis ${c.st} € à ${c.b} € (baisse > ${tol * 100} % du budget)`)
}
})
}
}