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
45 lines
2.5 KiB
JavaScript
45 lines
2.5 KiB
JavaScript
// 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)`)
|
||
}
|
||
})
|
||
}
|
||
}
|