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
This commit is contained in:
Jules
2026-10-05 16:39:16 +02:00
co-authored by Claude Fable 5.1
parent 648015a879
commit 91c755f884
7 changed files with 383 additions and 570 deletions
@@ -1,535 +0,0 @@
{
"_commentaire": "Écarts de sélection et de taux entre le jeu de coefficients d'origine et la table harmonisée, sur les trois profils de référence, figés le 2026-09-28. Un changement de coefficient ou de code qui modifie ces écarts casse test/decoupage.test.mjs : c'est voulu, on relit CONSTANTES.md avant de refiger.",
"date": "2026-09-28",
"ecarts": [
{
"profil": "A — Frugal urbain",
"mode": "taux (par_axe)",
"axe": "eau",
"origine": "23 %",
"harmonisee": "27 %"
},
{
"profil": "A — Frugal urbain",
"mode": "taux (global)",
"axe": "eau",
"origine": "23 %",
"harmonisee": "27 %"
},
{
"profil": "A — Frugal urbain",
"mode": "taux (par_axe)",
"axe": "energie",
"origine": "22 %",
"harmonisee": "6 %"
},
{
"profil": "A — Frugal urbain",
"mode": "taux (global)",
"axe": "energie",
"origine": "22 %",
"harmonisee": "6 %"
},
{
"profil": "B — Famille moyenne",
"mode": "taux (par_axe)",
"axe": "eau",
"origine": "24 %",
"harmonisee": "27 %"
},
{
"profil": "B — Famille moyenne",
"mode": "taux (global)",
"axe": "eau",
"origine": "24 %",
"harmonisee": "27 %"
},
{
"profil": "B — Famille moyenne",
"mode": "taux (par_axe)",
"axe": "energie",
"origine": "100 %",
"harmonisee": "80 %"
},
{
"profil": "C — Autonomie large",
"mode": "taux (par_axe)",
"axe": "eau",
"origine": "20 %",
"harmonisee": "23 %"
},
{
"profil": "C — Autonomie large",
"mode": "taux (global)",
"axe": "eau",
"origine": "20 %",
"harmonisee": "23 %"
},
{
"profil": "C — Autonomie large",
"mode": "taux (par_axe)",
"axe": "energie",
"origine": "10 %",
"harmonisee": "3 %"
},
{
"profil": "C — Autonomie large",
"mode": "taux (global)",
"axe": "energie",
"origine": "100 %",
"harmonisee": "63 %"
}
],
"lignes": [
{
"profil": "A — Frugal urbain",
"jeu": "origine",
"global": {
"total": 15180,
"status": "OK",
"typologies": {
"eau": [
"captage_descente_directe",
"filtration_particulaire_simple",
"stockage_souple_5m3",
"distrib_gravitaire",
"eg_noue",
"eg_pedoepuration",
"eg_fpv_autoconstruit"
],
"energie": [
"pv_batterie_plomb_5",
"th_ballon_500",
"bois_poele_double",
"emet_vannes_thermo",
"pil_home_assistant",
"bois_conduit_fumee"
],
"alim": []
},
"taux": {
"eau": 23,
"energie": 22,
"alim": null
}
},
"par_axe": {
"eau": {
"typologies": [
"captage_descente_directe",
"filtration_particulaire_simple",
"stockage_souple_5m3",
"distrib_gravitaire",
"eg_pedoepuration",
"distrib_kit_double_reseau",
"eg_fpv_autoconstruit"
],
"sous_total": 8130,
"status": "BUDGET_INSUFFISANT",
"taux": 23
},
"energie": {
"typologies": [
"pv_kit_beem_balcon",
"th_ballon_500",
"bois_poele_granules",
"emet_radiateur_acier",
"pil_routeur_ecs",
"bois_conduit_fumee"
],
"sous_total": 7110,
"status": "OK",
"taux": 22
},
"alim": {
"typologies": [],
"sous_total": 0,
"status": "OK",
"taux": null
}
}
},
{
"profil": "A — Frugal urbain",
"jeu": "harmonisee",
"global": {
"total": 15180,
"status": "OK",
"typologies": {
"eau": [
"captage_descente_directe",
"filtration_particulaire_simple",
"stockage_souple_5m3",
"distrib_gravitaire",
"eg_noue",
"eg_pedoepuration",
"eg_fpv_autoconstruit"
],
"energie": [
"pv_batterie_plomb_5",
"th_ballon_500",
"bois_poele_double",
"emet_vannes_thermo",
"pil_home_assistant",
"bois_conduit_fumee"
],
"alim": []
},
"taux": {
"eau": 27,
"energie": 6,
"alim": null
}
},
"par_axe": {
"eau": {
"typologies": [
"captage_descente_directe",
"filtration_particulaire_simple",
"stockage_souple_5m3",
"distrib_gravitaire",
"eg_pedoepuration",
"distrib_kit_double_reseau",
"eg_fpv_autoconstruit"
],
"sous_total": 8130,
"status": "BUDGET_INSUFFISANT",
"taux": 27
},
"energie": {
"typologies": [
"pv_kit_beem_balcon",
"th_ballon_500",
"bois_poele_granules",
"emet_radiateur_acier",
"pil_routeur_ecs",
"bois_conduit_fumee"
],
"sous_total": 7110,
"status": "OK",
"taux": 6
},
"alim": {
"typologies": [],
"sous_total": 0,
"status": "OK",
"taux": null
}
}
},
{
"profil": "B — Famille moyenne",
"jeu": "origine",
"global": {
"total": 36661,
"status": "OK",
"typologies": {
"eau": [
"captage_descente_directe",
"filtration_particulaire_simple",
"stockage_souple_5m3",
"distrib_gravitaire",
"eg_noue",
"eg_pedoepuration",
"eg_fpv_autoconstruit"
],
"energie": [
"pv_monitoring",
"th_ballon_500",
"bois_poele_masse_indus",
"emet_seche_serviette",
"pil_delesteur",
"bois_conduit_fumee"
],
"alim": [
"etude_topo_ensoleil",
"haute_fruit_coque",
"moy_petit_fruit_3L",
"basse_compost_3bacs"
]
},
"taux": {
"eau": 24,
"energie": null,
"alim": null
}
},
"par_axe": {
"eau": {
"typologies": [
"captage_descente_directe",
"filtration_particulaire_simple",
"stockage_pehd_enterree_5m3",
"distrib_gravitaire",
"eg_pedoepuration",
"eg_pedoepuration"
],
"sous_total": 14645,
"status": "OK",
"taux": 24
},
"energie": {
"typologies": [
"pv_kit_3kwc_string",
"th_ballon_500",
"bois_poele_granules",
"emet_seche_serviette",
"pil_delesteur",
"bois_conduit_fumee"
],
"sous_total": 13455,
"status": "OK",
"taux": 100
},
"alim": {
"typologies": [
"etude_analyse_labo",
"haute_plantation_pose_pro",
"moy_haie_biodiv_complete",
"basse_planche_bec",
"basse_planche_bec"
],
"sous_total": 6985,
"status": "OK",
"taux": 50
}
}
},
{
"profil": "B — Famille moyenne",
"jeu": "harmonisee",
"global": {
"total": 36661,
"status": "OK",
"typologies": {
"eau": [
"captage_descente_directe",
"filtration_particulaire_simple",
"stockage_souple_5m3",
"distrib_gravitaire",
"eg_noue",
"eg_pedoepuration",
"eg_fpv_autoconstruit"
],
"energie": [
"pv_monitoring",
"th_ballon_500",
"bois_poele_masse_indus",
"emet_seche_serviette",
"pil_delesteur",
"bois_conduit_fumee"
],
"alim": [
"etude_topo_ensoleil",
"haute_fruit_coque",
"moy_petit_fruit_3L",
"basse_compost_3bacs"
]
},
"taux": {
"eau": 27,
"energie": null,
"alim": null
}
},
"par_axe": {
"eau": {
"typologies": [
"captage_descente_directe",
"filtration_particulaire_simple",
"stockage_pehd_enterree_5m3",
"distrib_gravitaire",
"eg_pedoepuration",
"eg_pedoepuration"
],
"sous_total": 14645,
"status": "OK",
"taux": 27
},
"energie": {
"typologies": [
"pv_kit_3kwc_string",
"th_ballon_500",
"bois_poele_granules",
"emet_seche_serviette",
"pil_delesteur",
"bois_conduit_fumee"
],
"sous_total": 13455,
"status": "OK",
"taux": 80
},
"alim": {
"typologies": [
"etude_analyse_labo",
"haute_plantation_pose_pro",
"moy_haie_biodiv_complete",
"basse_planche_bec",
"basse_planche_bec"
],
"sous_total": 6985,
"status": "OK",
"taux": 50
}
}
},
{
"profil": "C — Autonomie large",
"jeu": "origine",
"global": {
"total": 77465,
"status": "OK",
"typologies": {
"eau": [
"captage_capteur_sol",
"filtration_3_corps_uv",
"stockage_beton_5m3",
"distrib_gravitaire",
"eg_phytostation"
],
"energie": [
"pv_kit_3kwc_micro",
"th_ballon_500",
"bois_chaudiere_granules",
"emet_vannes_thermo",
"pil_onduleur_hybride",
"bois_conduit_fumee"
],
"alim": [
"etude_analyse_labo",
"haute_haute_tige",
"moy_haie_biodiv_complete",
"basse_planche_bec"
]
},
"taux": {
"eau": 20,
"energie": 100,
"alim": 67
}
},
"par_axe": {
"eau": {
"typologies": [
"captage_capteur_sol",
"filtration_3_corps_uv",
"stockage_beton_5m3",
"distrib_per_bleu_interieur",
"eg_phytostation",
"eg_phytostation",
"eg_phytostation",
"eg_pedoepuration",
"distrib_kit_double_reseau"
],
"sous_total": 41000,
"status": "OK",
"taux": 20
},
"energie": {
"typologies": [
"pv_batterie_plomb_5",
"th_ballon_500",
"bois_poele_hydro",
"emet_radiateur_acier",
"pil_routeur_ecs",
"bois_conduit_fumee"
],
"sous_total": 19210,
"status": "OK",
"taux": 10
},
"alim": {
"typologies": [
"etude_analyse_labo",
"haute_haute_tige",
"moy_haie_biodiv_complete",
"basse_planche_bec",
"basse_planche_bec"
],
"sous_total": 20855,
"status": "OK",
"taux": 67
}
}
},
{
"profil": "C — Autonomie large",
"jeu": "harmonisee",
"global": {
"total": 77465,
"status": "OK",
"typologies": {
"eau": [
"captage_capteur_sol",
"filtration_3_corps_uv",
"stockage_beton_5m3",
"distrib_gravitaire",
"eg_phytostation"
],
"energie": [
"pv_kit_3kwc_micro",
"th_ballon_500",
"bois_chaudiere_granules",
"emet_vannes_thermo",
"pil_onduleur_hybride",
"bois_conduit_fumee"
],
"alim": [
"etude_analyse_labo",
"haute_haute_tige",
"moy_haie_biodiv_complete",
"basse_planche_bec"
]
},
"taux": {
"eau": 23,
"energie": 63,
"alim": 67
}
},
"par_axe": {
"eau": {
"typologies": [
"captage_capteur_sol",
"filtration_3_corps_uv",
"stockage_beton_5m3",
"distrib_per_bleu_interieur",
"eg_phytostation",
"eg_phytostation",
"eg_phytostation",
"eg_pedoepuration",
"distrib_kit_double_reseau"
],
"sous_total": 41000,
"status": "OK",
"taux": 23
},
"energie": {
"typologies": [
"pv_batterie_plomb_5",
"th_ballon_500",
"bois_poele_hydro",
"emet_radiateur_acier",
"pil_routeur_ecs",
"bois_conduit_fumee"
],
"sous_total": 19210,
"status": "OK",
"taux": 3
},
"alim": {
"typologies": [
"etude_analyse_labo",
"haute_haute_tige",
"moy_haie_biodiv_complete",
"basse_planche_bec",
"basse_planche_bec"
],
"sous_total": 20855,
"status": "OK",
"taux": 67
}
}
}
]
}