Épingles : écarts correctifs face à l'épingle d'origine, écarts d'harmonisation refigés (05/10)
L'épingle d'origine (fixtures/sorties-origine-2026-09-28.json, générée depuis le code d'astro-pro) porte les défauts corrigés par91c755f(eg_phytostation ×3 sur C/eau, basse_planche_bec ×2 sur B et C/alim, lignes forcées orphelines, FORCE ×4 dans le journal, A/eau par axe en INSUFFISANT). La regénérer depuis astro-pro reproduirait les défauts ; la regénérer depuis ce paquet en ferait une épingle sur soi-même. Elle reste donc telle quelle, et : - scripts/ecarts-correctifs.mjs + fixtures/ecarts-correctifs-2026-10-05.json : chaque cellule (profil × mode × axe × champ) qui s'écarte de l'origine est figée, avec le détail des lignes ; une cellule absente doit rester identique. - test/decoupage.test.mjs : test 1 compare aux écarts figés (et vérifie qu'un correctif n'introduit jamais un INSUFFISANT ni un total hors ±5 % en global ; budgets alloués et poids des axes inchangés) ; test 2 vérifie la propriété du découpage contre selectionner() de ce paquet ; test 3 inchangé dans l'esprit. - Écarts d'harmonisation REFIGÉS (npm run ecarts -- --ecrire 2026-10-05) après relecture de docs/CONSTANTES.md : neuf lignes au lieu de onze. Les deux lignes perdues (A énergie global 22 → 6 %, C énergie par axe 10 → 3 %) étaient des taux calculés sur des kWc fantômes hérités par une batterie ; la couverture y est null avec les deux tables. Aucun coefficient ne change. Note datée posée dans CONSTANTES.md §1. (La suppression du fichier du 28/09 est partie par erreur dans8465bf7, avec la version : même intention, deux commits.) - fixtures/README.md et AGENTS.md (règles 6 et 8) : l'épingle d'origine ne se regénère pas pour absorber un correctif ; invariants tenus depuis v0.1.1. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tgtn7bJxTLiL2fRCkQMgtR
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
8465bf7204
commit
4f593d2edf
@@ -35,14 +35,21 @@ taux de couverture, prix des options). `isolation`, `autoconstruction`, `estimat
|
||||
Les 28 tests d'origine du cœur autonomie (`test/noyau-origine.test.mjs`) et le test de découpage par
|
||||
axe (`test/decoupage.test.mjs`, épingle générée depuis le code d'astro-pro) **ne se désactivent
|
||||
jamais**. Refiger une épingle (`scripts/pin-origine.mjs`, `npm run ecarts -- --ecrire <date>`) est un
|
||||
acte volontaire, écrit dans le commit, après relecture de `docs/CONSTANTES.md`.
|
||||
acte volontaire, écrit dans le commit, après relecture de `docs/CONSTANTES.md`. **L'épingle d'origine
|
||||
ne se regénère pas pour absorber un correctif** : depuis v0.1.1 elle porte des défauts corrigés, et
|
||||
chaque cellule qui s'en écarte est figée dans `fixtures/ecarts-correctifs-<date>.json`
|
||||
(`node scripts/ecarts-correctifs.mjs --ecrire <date>`) ; une cellule absente de ce fichier doit
|
||||
rester identique à l'origine. Les tests de correctifs (`test/correctifs-v0.1.1.test.mjs`) et de
|
||||
monotonie (`test/monotonie.test.mjs`) ne se désactivent pas non plus.
|
||||
7. Français avec accents dans les libellés, la doc et les messages ; identifiants en français sans
|
||||
accents (`sous_total_eur_ht`, `taux_couverture`) ; codes d'axe internes `eau` / `energie` / `alim`
|
||||
(les postes publiés disent `alimentation`).
|
||||
8. Un module du noyau (`src/autonomie/noyau/`) se modifie pour corriger un bug **avec un test qui le
|
||||
montre** et une ligne dans le `CHANGELOG` du commit ; jamais « en passant ». Les bugs latents connus
|
||||
sont listés dans le RECAP du lot P1.0 (scoreurs sans parse de `parametre_dim`, `_conflit_avec` perdu,
|
||||
Intl dans les messages de `reajuste`).
|
||||
Intl dans les messages de `reajuste`). Invariants tenus depuis v0.1.1 : une typologie n'apparaît
|
||||
qu'une fois par axe, un poste n'a qu'une ligne (la quantité est `dimension`), une ligne forcée par
|
||||
couplage est lâchée dès que plus rien ne la requiert, le journal n'écrit chaque FORCE qu'une fois.
|
||||
|
||||
## Carte du dépôt
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
| **Productible PV** (kWh/kWc/an) | table par zone **biogéographique**, 1 050 à **1 550** (zone 8, au-dessus du maximum de la source) | table par zone **RT** : H1a 1 000 · H1b 1 050 · H1c 1 200 · H2a 1 100 · H2b 1 200 · H2c 1 250 · **H2d 1 300** · H3 1 400 ; défaut 1 100 | recherche énergie §3.2 (PVGIS, sud 30°, pertes 14 %). H1a = moyenne Lille 950 / Paris 1 050. **H2d interpolé par la MOE** (04, 07, 26, 84 : pas de ville dans la source), déclaré en avertissement à chaque usage | inclus dans les écarts d'énergie ci-dessus (Paris 1 100 → 1 000, Bouches-du-Rhône 1 480 → 1 400) |
|
||||
| **Contexte Corse et DOM** | `algo.js` prenait deux chiffres du code postal : 2A, 2B, 971-976 retombaient sur **H2a, 800 mm, 2 200 DJU** pendant que l'affichage utilisait la vraie ligne | une seule fonction, `departementDepuisCodePostal` (20000-20190 → 2A, 20200-20620 → 2B, 97xxx → trois caractères) | code de `data.js` (l'interface avait déjà la bonne logique) | Corse : H3 (700-750 mm, 1 500-1 600 DJU). DOM : la table `france_climat` n'a **ni pluviométrie ni DJU** pour 971-976 (« DOM tropical ») → défauts 800 mm / 2 200 DJU appliqués **et déclarés** ; à compléter dans NocoDB si un jour une adresse DOM arrive |
|
||||
|
||||
> **Correction du 05/10 (v0.1.1), sans changement de coefficient.** Deux des effets « mesurés » ci-dessus n'existaient pas : **A énergie 22 → 6 %** (mode global) et **C énergie 10 → 3 %** (mode par axe) étaient calculés sur des kWc fantômes — une batterie au plomb retenue comme ligne du poste PV **héritait de la dimension « 0,4 kWc »** de la ligne qu'elle remplaçait (défaut du réajustement, corrigé). Dans ces deux cas la sélection ne porte aucun kit PV : la couverture énergie est **null** (« non calculable »), avec l'une ou l'autre table. Les écarts refigés (`fixtures/ecarts-harmonisation-2026-10-05.json`) ne portent plus ces deux lignes ; les quatre autres effets du tableau (captage, autoconsommation A par axe 22 → 6 %, B 100 → 80 %, C global 100 → 63 %) sont inchangés. Le reste de cette table reste à relire tel quel.
|
||||
|
||||
## 2. Ce qui ne change pas, désormais sourcé et unique
|
||||
|
||||
| Grandeur | Valeur | Source | Note |
|
||||
|
||||
+3
-2
@@ -8,7 +8,8 @@ Les tests tournent hors ligne sur ces copies. Aucun fichier d'ici n'est importé
|
||||
| `algo-config.json` | heuristiques de conception du noyau (poids, modulateurs, matrices, tolérances, alias, poids des postes) ; les clés `orientation_factors` et `dju_defaults_by_zone` y figurent encore mais **ne sont plus lues** (les coefficients vivent dans `src/commun/coefficients.ts`) | `astro-pro/src/lib/autonomie/algo-config.json` | 2026-09-28 (inchangé depuis mai 2026) |
|
||||
| `profils-reference.json` | les trois profils de calibrage A / B / C | `astro-pro/tests/profils-reference.json` | 2026-09-28 |
|
||||
| `coefficients-2026-05.json` | jeu de coefficients **d'origine** (valeurs du code de calculs), injecté par le test de non-régression | relevé sur `astro-pro` master `c628cf9` | 2026-09-28 |
|
||||
| `sorties-origine-2026-09-28.json` | sorties de `selectionner()` **d'origine** sur les trois profils (pipeline complet + mode un seul axe), générées par `scripts/pin-origine.mjs` depuis le code d'astro-pro | `astro-pro` master `c628cf9` | 2026-09-28 |
|
||||
| `ecarts-harmonisation-2026-09-28.json` | écarts de sélection et de taux entre coefficients d'origine et table harmonisée (`npm run ecarts -- --ecrire <date>`) ; relus par Jules avec `docs/CONSTANTES.md` | ce paquet | 2026-09-28 |
|
||||
| `sorties-origine-2026-09-28.json` | sorties de `selectionner()` **d'origine** sur les trois profils (pipeline complet + mode un seul axe), générées par `scripts/pin-origine.mjs` depuis le code d'astro-pro. **Porte les défauts corrigés en v0.1.1** (doublons, accessoires forcés orphelins, statut non monotone, journal répété) : elle n'est pas regénérée, les écarts voulus sont figés à côté | `astro-pro` master `c628cf9` | 2026-09-28 |
|
||||
| `ecarts-correctifs-2026-10-05.json` | écarts voulus entre l'épingle d'origine et ce paquet (coefficients d'origine), cellule par cellule, avec le détail des lignes (`node scripts/ecarts-correctifs.mjs --ecrire <date>`). Une cellule absente doit rester identique à l'origine ; un nouvel écart ne se refige que pour corriger un défaut du noyau, avec son test | ce paquet v0.1.1 | 2026-10-05 |
|
||||
| `ecarts-harmonisation-2026-10-05.json` | écarts de sélection et de taux entre coefficients d'origine et table harmonisée (`npm run ecarts -- --ecrire <date>`) ; relus par Jules avec `docs/CONSTANTES.md`. Refigé le 05/10 après le correctif v0.1.1 : les écarts sont identiques à ceux du 28/09 (seuls les totaux de référence des lignes ont bougé) | ce paquet | 2026-10-05 |
|
||||
|
||||
Refiger une épingle est un acte volontaire : on relit `docs/CONSTANTES.md`, on relance le script avec la date du jour, on met à jour les tests qui la citent, et on l'écrit dans le message de commit.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,503 @@
|
||||
{
|
||||
"_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-10-05. 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-10-05",
|
||||
"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": "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 (global)",
|
||||
"axe": "energie",
|
||||
"origine": "100 %",
|
||||
"harmonisee": "63 %"
|
||||
}
|
||||
],
|
||||
"lignes": [
|
||||
{
|
||||
"profil": "A — Frugal urbain",
|
||||
"jeu": "origine",
|
||||
"global": {
|
||||
"total": 15700,
|
||||
"status": "OK",
|
||||
"typologies": {
|
||||
"eau": [
|
||||
"captage_descente_directe",
|
||||
"filtration_3_corps_uv",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_kit_double_reseau",
|
||||
"eg_noue",
|
||||
"eg_fpv_autoconstruit"
|
||||
],
|
||||
"energie": [
|
||||
"pv_batterie_plomb_5",
|
||||
"th_ballon_500",
|
||||
"bois_poele_masse_diy",
|
||||
"emet_vannes_thermo",
|
||||
"pil_routeur_ecs",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"alim": []
|
||||
},
|
||||
"taux": {
|
||||
"eau": 23,
|
||||
"energie": null,
|
||||
"alim": null
|
||||
}
|
||||
},
|
||||
"par_axe": {
|
||||
"eau": {
|
||||
"typologies": [
|
||||
"captage_descente_directe",
|
||||
"filtration_3_corps_uv",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_kit_double_reseau",
|
||||
"eg_recyclage_spareau",
|
||||
"eg_separation_source"
|
||||
],
|
||||
"sous_total": 7575,
|
||||
"status": "OK",
|
||||
"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": "AJOUTER_OPTIONNEL",
|
||||
"taux": 22
|
||||
},
|
||||
"alim": {
|
||||
"typologies": [],
|
||||
"sous_total": 0,
|
||||
"status": "OK",
|
||||
"taux": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"profil": "A — Frugal urbain",
|
||||
"jeu": "harmonisee",
|
||||
"global": {
|
||||
"total": 15700,
|
||||
"status": "OK",
|
||||
"typologies": {
|
||||
"eau": [
|
||||
"captage_descente_directe",
|
||||
"filtration_3_corps_uv",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_kit_double_reseau",
|
||||
"eg_noue",
|
||||
"eg_fpv_autoconstruit"
|
||||
],
|
||||
"energie": [
|
||||
"pv_batterie_plomb_5",
|
||||
"th_ballon_500",
|
||||
"bois_poele_masse_diy",
|
||||
"emet_vannes_thermo",
|
||||
"pil_routeur_ecs",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"alim": []
|
||||
},
|
||||
"taux": {
|
||||
"eau": 27,
|
||||
"energie": null,
|
||||
"alim": null
|
||||
}
|
||||
},
|
||||
"par_axe": {
|
||||
"eau": {
|
||||
"typologies": [
|
||||
"captage_descente_directe",
|
||||
"filtration_3_corps_uv",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_kit_double_reseau",
|
||||
"eg_recyclage_spareau",
|
||||
"eg_separation_source"
|
||||
],
|
||||
"sous_total": 7575,
|
||||
"status": "OK",
|
||||
"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": "AJOUTER_OPTIONNEL",
|
||||
"taux": 6
|
||||
},
|
||||
"alim": {
|
||||
"typologies": [],
|
||||
"sous_total": 0,
|
||||
"status": "OK",
|
||||
"taux": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"profil": "B — Famille moyenne",
|
||||
"jeu": "origine",
|
||||
"global": {
|
||||
"total": 34183,
|
||||
"status": "OK",
|
||||
"typologies": {
|
||||
"eau": [
|
||||
"captage_descente_directe",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_autoepure"
|
||||
],
|
||||
"energie": [
|
||||
"pv_monitoring",
|
||||
"th_ballon_500",
|
||||
"bois_poele_masse_indus",
|
||||
"emet_seche_serviette",
|
||||
"pil_delesteur",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"alim": [
|
||||
"etude_analyse_labo",
|
||||
"haute_plantation_pose_pro",
|
||||
"moy_petit_fruit_3L",
|
||||
"basse_medicinales_lot30"
|
||||
]
|
||||
},
|
||||
"taux": {
|
||||
"eau": 24,
|
||||
"energie": null,
|
||||
"alim": null
|
||||
}
|
||||
},
|
||||
"par_axe": {
|
||||
"eau": {
|
||||
"typologies": [
|
||||
"captage_capteur_sol",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_pehd_enterree_5m3",
|
||||
"distrib_per_bleu_interieur",
|
||||
"eg_autoepure",
|
||||
"distrib_kit_double_reseau"
|
||||
],
|
||||
"sous_total": 13595,
|
||||
"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"
|
||||
],
|
||||
"sous_total": 4360,
|
||||
"status": "AJOUTER_OPTIONNEL",
|
||||
"taux": 50
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"profil": "B — Famille moyenne",
|
||||
"jeu": "harmonisee",
|
||||
"global": {
|
||||
"total": 34183,
|
||||
"status": "OK",
|
||||
"typologies": {
|
||||
"eau": [
|
||||
"captage_descente_directe",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_autoepure"
|
||||
],
|
||||
"energie": [
|
||||
"pv_monitoring",
|
||||
"th_ballon_500",
|
||||
"bois_poele_masse_indus",
|
||||
"emet_seche_serviette",
|
||||
"pil_delesteur",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"alim": [
|
||||
"etude_analyse_labo",
|
||||
"haute_plantation_pose_pro",
|
||||
"moy_petit_fruit_3L",
|
||||
"basse_medicinales_lot30"
|
||||
]
|
||||
},
|
||||
"taux": {
|
||||
"eau": 27,
|
||||
"energie": null,
|
||||
"alim": null
|
||||
}
|
||||
},
|
||||
"par_axe": {
|
||||
"eau": {
|
||||
"typologies": [
|
||||
"captage_capteur_sol",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_pehd_enterree_5m3",
|
||||
"distrib_per_bleu_interieur",
|
||||
"eg_autoepure",
|
||||
"distrib_kit_double_reseau"
|
||||
],
|
||||
"sous_total": 13595,
|
||||
"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"
|
||||
],
|
||||
"sous_total": 4360,
|
||||
"status": "AJOUTER_OPTIONNEL",
|
||||
"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",
|
||||
"distrib_kit_double_reseau"
|
||||
],
|
||||
"sous_total": 18400,
|
||||
"status": "AJOUTER_OPTIONNEL",
|
||||
"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": null
|
||||
},
|
||||
"alim": {
|
||||
"typologies": [
|
||||
"etude_analyse_labo",
|
||||
"haute_haute_tige",
|
||||
"moy_haie_biodiv_complete",
|
||||
"basse_planche_bec"
|
||||
],
|
||||
"sous_total": 11855,
|
||||
"status": "AJOUTER_OPTIONNEL",
|
||||
"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",
|
||||
"distrib_kit_double_reseau"
|
||||
],
|
||||
"sous_total": 18400,
|
||||
"status": "AJOUTER_OPTIONNEL",
|
||||
"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": null
|
||||
},
|
||||
"alim": {
|
||||
"typologies": [
|
||||
"etude_analyse_labo",
|
||||
"haute_haute_tige",
|
||||
"moy_haie_biodiv_complete",
|
||||
"basse_planche_bec"
|
||||
],
|
||||
"sous_total": 11855,
|
||||
"status": "AJOUTER_OPTIONNEL",
|
||||
"taux": 67
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// ecarts-correctifs.mjs — compare, sur les trois profils de référence et avec le
|
||||
// jeu de coefficients D'ORIGINE, ce que le paquet rend aujourd'hui à l'épingle
|
||||
// générée depuis le code d'astro-pro (fixtures/sorties-origine-<date>.json).
|
||||
//
|
||||
// Pourquoi ce script existe (v0.1.1) : l'épingle d'origine porte les défauts
|
||||
// que le correctif répare (SPEC menu §12 n° 1, 2, 3, 5 : doublons, accessoires
|
||||
// forcés orphelins, statut non monotone, journal FORCE répété). La regénérer
|
||||
// depuis astro-pro reproduirait les défauts ; la regénérer depuis ce paquet en
|
||||
// ferait une épingle sur soi-même. Elle reste donc telle quelle, et chaque écart
|
||||
// voulu est écrit ici, cellule par cellule, dans fixtures/ecarts-correctifs-<date>.json.
|
||||
// test/decoupage.test.mjs exige que les écarts calculés soient EXACTEMENT ceux
|
||||
// figés : une cellule non listée doit rester identique à l'origine.
|
||||
// Usage : node scripts/ecarts-correctifs.mjs [--ecrire AAAA-MM-JJ]
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { eau, energie, alimentation, autonomie } from '../dist/index.js'
|
||||
|
||||
const fx = new URL('../fixtures/', import.meta.url)
|
||||
const lire = (f) => JSON.parse(readFileSync(new URL(f, fx), '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 origine = lire('coefficients-2026-05.json')
|
||||
export const PIN_ORIGINE = 'sorties-origine-2026-09-28.json'
|
||||
const pin = lire(PIN_ORIGINE)
|
||||
const AXES = ['eau', 'energie', 'alim']
|
||||
const MOTEURS = { eau, energie, alim: alimentation }
|
||||
|
||||
const resumePostes = (liste) => liste.map((p) => ({ poste_id: p.poste_id, typologie_choisie: p.typologie_choisie, dimension: p.dimension ? { valeur: p.dimension.valeur, unite: p.dimension.unite } : null, cout_eur_ht: p.cout_eur_ht, force_par_couplage: !!p.force_par_couplage }))
|
||||
const lignePostes = (liste) => liste.map((p) => `${p.typologie_choisie}${p.force_par_couplage ? '*' : ''} ${p.cout_eur_ht}`).join(' + ')
|
||||
|
||||
/** Compare une cellule (profil × mode × axe) à l'épingle ; rend les écarts champ par champ. */
|
||||
function compareAxe(profil, mode, axe, attendu, obtenu) {
|
||||
const ecarts = []
|
||||
const push = (champ, o, c) => ecarts.push({ profil, mode, axe, champ, origine: o, corrige: c })
|
||||
if (attendu.sous_total_eur_ht !== obtenu.sous_total_eur_ht) push('sous_total_eur_ht', attendu.sous_total_eur_ht, obtenu.sous_total_eur_ht)
|
||||
const po = JSON.stringify(attendu.postes)
|
||||
const pc = JSON.stringify(resumePostes(obtenu.postes))
|
||||
if (po !== pc) push('postes', lignePostes(attendu.postes), lignePostes(obtenu.postes))
|
||||
return ecarts
|
||||
}
|
||||
|
||||
export function calculeEcartsCorrectifs() {
|
||||
const ecarts = []
|
||||
const cellules = {}
|
||||
for (const profil of profils) {
|
||||
const p = pin.profils[profil.nom]
|
||||
const g = autonomie.noyau.selectionner(profil.inputs, options, postes, france_climat, { config: algo_config, coefficients: origine }, false)
|
||||
for (const axe of AXES) ecarts.push(...compareAxe(profil.nom, 'global', axe, p.global.axes[axe], g.axes[axe]))
|
||||
if (p.global.total_eur_ht !== g.total_eur_ht) ecarts.push({ profil: profil.nom, mode: 'global', axe: 'total', champ: 'total_eur_ht', origine: p.global.total_eur_ht, corrige: g.total_eur_ht })
|
||||
if (p.global.status !== g.status) ecarts.push({ profil: profil.nom, mode: 'global', axe: 'total', champ: 'status', origine: p.global.status, corrige: g.status })
|
||||
if (p.global.ecart_budget_pct !== g.ecart_budget_pct) ecarts.push({ profil: profil.nom, mode: 'global', axe: 'total', champ: 'ecart_budget_pct', origine: p.global.ecart_budget_pct, corrige: g.ecart_budget_pct })
|
||||
if (JSON.stringify(p.global.couplages) !== JSON.stringify(g.couplages)) ecarts.push({ profil: profil.nom, mode: 'global', axe: 'total', champ: 'couplages', origine: p.global.couplages, corrige: g.couplages })
|
||||
cellules[`${profil.nom} / global`] = { total: g.total_eur_ht, status: g.status, couplages: g.couplages, axes: Object.fromEntries(AXES.map((a) => [a, { sous_total: g.axes[a].sous_total_eur_ht, postes: resumePostes(g.axes[a].postes) }])) }
|
||||
|
||||
for (const axe of AXES) {
|
||||
const a = p.par_axe[axe]
|
||||
const r = MOTEURS[axe].calculer({ autodiag: profil.inputs, budget_axe: a.budget_axe, contexte: g.contexte }, { postes, options, algo_config, coefficients: origine }).sorties
|
||||
const obtenu = { sous_total_eur_ht: r.sous_total_eur_ht, postes: r.postes }
|
||||
ecarts.push(...compareAxe(profil.nom, 'par_axe', axe, a.axes[axe], obtenu))
|
||||
if (a.status !== r.status) ecarts.push({ profil: profil.nom, mode: 'par_axe', axe, champ: 'status', origine: a.status, corrige: r.status })
|
||||
if (JSON.stringify(a.couplages) !== JSON.stringify(r.couplages)) ecarts.push({ profil: profil.nom, mode: 'par_axe', axe, champ: 'couplages', origine: a.couplages, corrige: r.couplages })
|
||||
cellules[`${profil.nom} / par_axe / ${axe}`] = { budget_axe: a.budget_axe, sous_total: r.sous_total_eur_ht, status: r.status, couplages: r.couplages, postes: resumePostes(r.postes) }
|
||||
}
|
||||
}
|
||||
return { ecarts, cellules }
|
||||
}
|
||||
|
||||
const estCli = process.argv[1]?.replace(/\\/g, '/').endsWith('ecarts-correctifs.mjs')
|
||||
if (estCli) {
|
||||
const { ecarts, cellules } = calculeEcartsCorrectifs()
|
||||
console.log('| Profil | Mode | Axe | Champ | Origine (astro-pro) | Corrigé (ce paquet) |')
|
||||
console.log('|---|---|---|---|---|---|')
|
||||
const court = (v) => (Array.isArray(v) ? `${v.length} ligne(s)` : String(v))
|
||||
for (const e of ecarts) console.log(`| ${e.profil} | ${e.mode} | ${e.axe} | ${e.champ} | ${court(e.origine)} | ${court(e.corrige)} |`)
|
||||
if (ecarts.length === 0) console.log('| (aucun écart) | | | | | |')
|
||||
const i = process.argv.indexOf('--ecrire')
|
||||
if (i > 0) {
|
||||
const date = process.argv[i + 1] ?? new Date().toISOString().slice(0, 10)
|
||||
const cible = new URL(`ecarts-correctifs-${date}.json`, fx)
|
||||
writeFileSync(
|
||||
cible,
|
||||
JSON.stringify(
|
||||
{
|
||||
_commentaire: `Écarts voulus entre l'épingle d'origine (${PIN_ORIGINE}, code astro-pro) et ce paquet, coefficients d'origine, trois profils de référence, figés le ${date} par scripts/ecarts-correctifs.mjs. L'épingle d'origine n'est pas regénérée : elle porte les défauts corrigés (SPEC menu branches fiches §12 n° 1, 2, 3, 5). Toute cellule absente d'ici doit rester identique à l'origine ; un nouvel écart casse test/decoupage.test.mjs, et se refige sciemment, avec sa cause dans le commit.`,
|
||||
date,
|
||||
epingle_origine: PIN_ORIGINE,
|
||||
ecarts,
|
||||
cellules,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + '\n',
|
||||
'utf8',
|
||||
)
|
||||
console.log(`\nécrit : fixtures/ecarts-correctifs-${date}.json`)
|
||||
}
|
||||
}
|
||||
+36
-22
@@ -1,20 +1,26 @@
|
||||
// Non-régression du découpage par axe (LOT P1.0 point 4, amendé le 28/09).
|
||||
// Non-régression du découpage par axe (LOT P1.0 point 4, amendé le 28/09 et le 05/10).
|
||||
//
|
||||
// 1. Avec le jeu de coefficients D'ORIGINE (fixtures/coefficients-2026-05.json),
|
||||
// le pipeline complet extrait rend, sur les trois profils de référence, la
|
||||
// même sélection que selectionner() d'origine — épinglée dans
|
||||
// sélection de selectionner() d'origine — épinglée dans
|
||||
// fixtures/sorties-origine-2026-09-28.json, générée DEPUIS le code d'astro-pro
|
||||
// (scripts/pin-origine.mjs), pas depuis ce paquet.
|
||||
// 2. Chaque moteur d'axe, à budget d'axe égal, rend la sélection du mode
|
||||
// « un seul axe actif » de selectionner() d'origine (même épingle).
|
||||
// (scripts/pin-origine.mjs), pas depuis ce paquet — AUX ÉCARTS CORRECTIFS PRÈS :
|
||||
// l'épingle d'origine porte les défauts corrigés en v0.1.1 (doublons,
|
||||
// accessoires forcés orphelins, statut non monotone, journal répété ; SPEC menu
|
||||
// §12) et n'est pas regénérée. Chaque cellule qui s'en écarte est figée dans
|
||||
// fixtures/ecarts-correctifs-<date>.json (scripts/ecarts-correctifs.mjs) ; une
|
||||
// cellule absente de ce fichier doit rester identique à l'origine.
|
||||
// 2. Chaque moteur d'axe, à budget d'axe égal, rend la sélection du mode « un
|
||||
// seul axe actif » de selectionner() de ce paquet (propriété du découpage).
|
||||
// 3. Avec la table HARMONISÉE, les écarts de sélection et de taux par rapport à
|
||||
// l'origine sont ceux figés dans fixtures/ecarts-harmonisation-2026-09-28.json
|
||||
// l'origine sont ceux figés dans fixtures/ecarts-harmonisation-<date>.json
|
||||
// (relus par Jules avec docs/CONSTANTES.md). Les modifier = refiger sciemment.
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { eau, energie, alimentation, integrateur, autonomie, COEFFICIENTS } from '../dist/index.js'
|
||||
import { calculeEcarts } from '../scripts/ecarts-harmonisation.mjs'
|
||||
import { calculeEcartsCorrectifs, PIN_ORIGINE } from '../scripts/ecarts-correctifs.mjs'
|
||||
|
||||
const lire = (f) => JSON.parse(readFileSync(new URL(`../fixtures/${f}`, import.meta.url), 'utf8'))
|
||||
const options = lire('options.json')
|
||||
@@ -24,40 +30,48 @@ const algo_config = lire('algo-config.json')
|
||||
const profils = lire('profils-reference.json')
|
||||
const origine = lire('coefficients-2026-05.json')
|
||||
const pin = lire('sorties-origine-2026-09-28.json')
|
||||
const ecartsPin = lire('ecarts-harmonisation-2026-09-28.json')
|
||||
const ecartsPin = lire('ecarts-harmonisation-2026-10-05.json')
|
||||
const correctifsPin = lire('ecarts-correctifs-2026-10-05.json')
|
||||
const AXES = ['eau', 'energie', 'alim']
|
||||
const MOTEURS = { eau, energie, alim: alimentation }
|
||||
|
||||
const resumePostes = (liste) => liste.map((p) => ({ poste_id: p.poste_id, typologie_choisie: p.typologie_choisie, dimension: p.dimension ? { valeur: p.dimension.valeur, unite: p.dimension.unite } : null, cout_eur_ht: p.cout_eur_ht, force_par_couplage: !!p.force_par_couplage }))
|
||||
|
||||
test('pipeline complet extrait = selectionner() d\'origine, coefficients d\'origine, trois profils', () => {
|
||||
test('pipeline complet extrait = selectionner() d\'origine aux écarts correctifs figés près (coefficients d\'origine, trois profils)', () => {
|
||||
assert.equal(PIN_ORIGINE, 'sorties-origine-2026-09-28.json', 'l\'épingle d\'origine reste celle du code d\'astro-pro')
|
||||
assert.equal(correctifsPin.epingle_origine, PIN_ORIGINE)
|
||||
const { ecarts } = calculeEcartsCorrectifs()
|
||||
assert.deepEqual(ecarts, correctifsPin.ecarts, 'les écarts avec l\'origine ont changé : ce n\'est voulu que pour corriger un défaut du noyau, avec test ; refiger avec node scripts/ecarts-correctifs.mjs --ecrire <date> et le dire dans le commit')
|
||||
// Un correctif ne fait jamais apparaître un « budget insuffisant » là où l'origine n'en avait pas,
|
||||
// ni un total hors de la bande ±5 % en mode global.
|
||||
for (const e of ecarts) {
|
||||
if (e.champ === 'status') assert.notEqual(e.corrige, 'BUDGET_INSUFFISANT', `${e.profil}/${e.mode}/${e.axe}`)
|
||||
if (e.champ === 'ecart_budget_pct') assert.ok(Math.abs(e.corrige) <= 5, `${e.profil} : écart global ${e.corrige} %`)
|
||||
}
|
||||
// Ce que l'origine avait de juste reste : budgets alloués et poids des axes, inchangés
|
||||
for (const profil of profils) {
|
||||
const attendu = pin.profils[profil.nom].global
|
||||
const r = autonomie.noyau.selectionner(profil.inputs, options, postes, france_climat, { config: algo_config, coefficients: origine }, false)
|
||||
assert.equal(r.total_eur_ht, attendu.total_eur_ht, `${profil.nom} : total`)
|
||||
assert.equal(r.status, attendu.status, `${profil.nom} : statut`)
|
||||
assert.equal(r.ecart_budget_pct, attendu.ecart_budget_pct, `${profil.nom} : écart`)
|
||||
assert.deepEqual(r.couplages, attendu.couplages, `${profil.nom} : journal des couplages`)
|
||||
for (const axe of AXES) {
|
||||
assert.equal(r.axes[axe].budget_alloue, attendu.axes[axe].budget_alloue, `${profil.nom}/${axe} : budget alloué`)
|
||||
assert.equal(r.axes[axe].poids, attendu.axes[axe].poids, `${profil.nom}/${axe} : poids`)
|
||||
assert.equal(r.axes[axe].sous_total_eur_ht, attendu.axes[axe].sous_total_eur_ht, `${profil.nom}/${axe} : sous-total`)
|
||||
assert.deepEqual(resumePostes(r.axes[axe].postes), attendu.axes[axe].postes, `${profil.nom}/${axe} : postes`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('moteur d\'axe = mode « un seul axe actif » de selectionner() d\'origine, à budget d\'axe égal', () => {
|
||||
test('moteur d\'axe = mode « un seul axe actif » de selectionner() de ce paquet, à budget d\'axe égal', () => {
|
||||
for (const profil of profils) {
|
||||
const contexte = autonomie.noyau.selectionner(profil.inputs, options, postes, france_climat, { config: algo_config, coefficients: origine }, false).contexte
|
||||
for (const axe of AXES) {
|
||||
const attendu = pin.profils[profil.nom].par_axe[axe]
|
||||
const r = MOTEURS[axe].calculer({ autodiag: profil.inputs, budget_axe: attendu.budget_axe, contexte }, { postes, options, algo_config, coefficients: origine })
|
||||
const budget = pin.profils[profil.nom].par_axe[axe].budget_axe
|
||||
const actifs = Object.fromEntries(AXES.map((a) => [a, a === axe]))
|
||||
const attendu = autonomie.noyau.selectionner({ ...profil.inputs, budget_eur: budget }, options, postes, france_climat, { config: autonomie.noyau.configPourAxes(algo_config, actifs), coefficients: origine }, false)
|
||||
const r = MOTEURS[axe].calculer({ autodiag: profil.inputs, budget_axe: budget, contexte }, { postes, options, algo_config, coefficients: origine })
|
||||
assert.equal(r.sorties.axe, axe)
|
||||
assert.equal(r.sorties.sous_total_eur_ht, attendu.axes[axe].sous_total_eur_ht, `${profil.nom}/${axe} : sous-total`)
|
||||
assert.equal(r.sorties.status, attendu.status, `${profil.nom}/${axe} : statut`)
|
||||
assert.deepEqual(resumePostes(r.sorties.postes), attendu.axes[axe].postes, `${profil.nom}/${axe} : postes`)
|
||||
assert.deepEqual(r.sorties.typologies, attendu.axes[axe].postes.map((p) => p.typologie_choisie))
|
||||
assert.deepEqual(resumePostes(r.sorties.postes), resumePostes(attendu.axes[axe].postes), `${profil.nom}/${axe} : postes`)
|
||||
assert.deepEqual(r.sorties.couplages, attendu.couplages, `${profil.nom}/${axe} : journal des couplages`)
|
||||
assert.ok(!r.avertissements.some((a) => /inter-axes/.test(a)), 'aucun couplage inter-axes dans les données')
|
||||
}
|
||||
}
|
||||
@@ -67,7 +81,7 @@ test('intégrateur mode global = selectionner() ; mode par_axe = somme des moteu
|
||||
for (const profil of profils) {
|
||||
const donnees = { postes, options, algo_config, france_climat, coefficients: origine }
|
||||
const g = integrateur.calculer({ autodiag: profil.inputs, budget_total: profil.inputs.budget_eur, mode: 'global' }, donnees)
|
||||
const attendu = pin.profils[profil.nom].global
|
||||
const attendu = autonomie.noyau.selectionner(profil.inputs, options, postes, france_climat, { config: algo_config, coefficients: origine }, false)
|
||||
assert.equal(g.sorties.mode, 'global')
|
||||
assert.equal(g.sorties.total_eur_ht, attendu.total_eur_ht)
|
||||
assert.equal(g.sorties.status, attendu.status)
|
||||
@@ -78,8 +92,8 @@ test('intégrateur mode global = selectionner() ; mode par_axe = somme des moteu
|
||||
assert.deepEqual(p.sorties.budgets, g.sorties.budgets, 'même allocation de budget dans les deux modes')
|
||||
let somme = 0
|
||||
for (const axe of AXES) {
|
||||
const a = pin.profils[profil.nom].par_axe[axe]
|
||||
assert.deepEqual(p.sorties.axes[axe].typologies, a.axes[axe].postes.map((x) => x.typologie_choisie), `${profil.nom}/${axe} : par_axe = moteur d'axe`)
|
||||
const a = MOTEURS[axe].calculer({ autodiag: profil.inputs, budget_axe: g.sorties.budgets[axe], contexte: g.sorties.contexte }, donnees).sorties
|
||||
assert.deepEqual(p.sorties.axes[axe].typologies, a.typologies, `${profil.nom}/${axe} : par_axe = moteur d'axe`)
|
||||
somme += p.sorties.axes[axe].sous_total_eur_ht
|
||||
}
|
||||
assert.equal(p.sorties.total_eur_ht, somme)
|
||||
|
||||
Reference in New Issue
Block a user