@open3cl/engine 1.8.8 → 1.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,476 @@
1
+ import { describe, expect, test, vi } from 'vitest';
2
+ import {
3
+ conventionPuissanceNominale,
4
+ decalageDecimal,
5
+ inverserRendement,
6
+ nombreDecimales,
7
+ puissanceDepuisRpn,
8
+ puissanceRecalculeeDepuisRpn,
9
+ qp0Coherent,
10
+ valeurCoherente
11
+ } from './13.2_generateur_combustion_pn.js';
12
+
13
+ /**
14
+ * Module pur (aucune dépendance importée) : les caractéristiques du générateur sont fournies par
15
+ * une fonction injectée, remplacée ici par un double de test déterministe.
16
+ *
17
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2 et §17.2.1 (virtualisation)
18
+ */
19
+
20
+ /**
21
+ * Double de test des caractéristiques d'une chaudière : rpn = (84 + 2 log10(Pn collectif kW)) / 100
22
+ * et qp0 = 1 % de Pn collectif, ramené au logement (× ratio).
23
+ */
24
+ const caracteristiquesChaudiere = vi.fn((pn, ratio) => {
25
+ const pnCollectifKw = pn / ratio / 1000;
26
+ return {
27
+ rpn: (84 + 2 * Math.log10(pnCollectifKw)) / 100,
28
+ qp0: 0.01 * pnCollectifKw * 1000 * ratio
29
+ };
30
+ });
31
+
32
+ describe('nombreDecimales', () => {
33
+ test.each([
34
+ [12, 0],
35
+ [0.87, 2],
36
+ [0.868943, 6],
37
+ [1e-7, 7],
38
+ [1.5e-7, 8],
39
+ [1.5e3, 0]
40
+ ])('%s possède %s décimale(s)', (valeur, attendu) => {
41
+ expect(nombreDecimales(valeur)).toBe(attendu);
42
+ });
43
+ });
44
+
45
+ describe('valeurCoherente', () => {
46
+ test('valeur arrondie à 2 décimales : tolérance d’une demi-unité (0,005)', () => {
47
+ expect(valeurCoherente(0.87, 0.8749)).toBe(true);
48
+ expect(valeurCoherente(0.87, 0.8651)).toBe(true);
49
+ expect(valeurCoherente(0.87, 0.876)).toBe(false);
50
+ });
51
+
52
+ test('valeur arrondie à 1 décimale : la tolérance reste bornée à 0,005', () => {
53
+ expect(valeurCoherente(0.9, 0.904)).toBe(true);
54
+ expect(valeurCoherente(0.9, 0.908)).toBe(false);
55
+ });
56
+
57
+ test('valeur non arrondie : tolérance relative de 0,1 % (troncature de la puissance)', () => {
58
+ expect(valeurCoherente(0.868943160626844, 0.8696)).toBe(true);
59
+ expect(valeurCoherente(0.868943160626844, 0.8705)).toBe(false);
60
+ });
61
+
62
+ test('grande valeur entière : tolérance relative de 0,1 %', () => {
63
+ expect(valeurCoherente(2400, 2402)).toBe(true);
64
+ expect(valeurCoherente(2400, 2403)).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe('qp0Coherent', () => {
69
+ test('qp0 stocké en W', () => {
70
+ expect(qp0Coherent(240, 240)).toBe(true);
71
+ });
72
+
73
+ test('qp0 stocké en kW (certains logiciels)', () => {
74
+ expect(qp0Coherent(0.24, 240)).toBe(true);
75
+ });
76
+
77
+ test('qp0 incohérent dans les deux unités', () => {
78
+ expect(qp0Coherent(0.5, 240)).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe('conventionPuissanceNominale - cas sans détection', () => {
83
+ test.each([
84
+ ['installation individuelle (ratio = 1)', 23000, 1, { rpn: 0.87 }],
85
+ ['ratio absent', 23000, undefined, { rpn: 0.87 }],
86
+ ['ratio nul', 23000, 0, { rpn: 0.87 }],
87
+ ['puissance absente', 0, 0.1, { rpn: 0.87 }],
88
+ ['rpn du DPE absent', 23000, 0.1, {}],
89
+ ['rpn du DPE non numérique', 23000, 0.1, { rpn: '0.87' }],
90
+ ['valeurs du DPE absentes', 23000, 0.1, undefined]
91
+ ])('%s : convention individualisée conservée', (_, pn, ratio, valeursDpe) => {
92
+ const caracteristiques = vi.fn();
93
+ expect(conventionPuissanceNominale(pn, ratio, valeursDpe, caracteristiques, true)).toEqual({
94
+ pn,
95
+ ratio,
96
+ convention: 'individualisee'
97
+ });
98
+ expect(caracteristiques).not.toHaveBeenCalled();
99
+ });
100
+ });
101
+
102
+ describe('conventionPuissanceNominale - détection de la convention du logiciel', () => {
103
+ const ratio = 0.1;
104
+ const pn = 23000;
105
+
106
+ test('rpn cohérent avec pn / ratio : pn est bien la puissance virtualisée (convention 3CL)', () => {
107
+ // rpn sur Pn collectif = 230 kW
108
+ const rpn = caracteristiquesChaudiere(pn, ratio).rpn;
109
+ expect(
110
+ conventionPuissanceNominale(pn, ratio, { rpn }, caracteristiquesChaudiere, false)
111
+ ).toEqual({ pn, ratio, convention: 'individualisee' });
112
+ });
113
+
114
+ test('rpn non recalculé (caractéristique saisie) : convention individualisée conservée', () => {
115
+ const caracteristiques = vi.fn(() => ({ qp0: 100 }));
116
+ expect(conventionPuissanceNominale(pn, ratio, { rpn: 0.87 }, caracteristiques, false)).toEqual({
117
+ pn,
118
+ ratio,
119
+ convention: 'individualisee'
120
+ });
121
+ expect(caracteristiques).toHaveBeenCalledTimes(1);
122
+ });
123
+
124
+ test('rpn incohérent avec les deux hypothèses : convention individualisée conservée', () => {
125
+ expect(
126
+ conventionPuissanceNominale(pn, ratio, { rpn: 0.5 }, caracteristiquesChaudiere, true)
127
+ ).toEqual({ pn, ratio, convention: 'individualisee' });
128
+ });
129
+
130
+ test('rpn et qp0 calculés par le logiciel sur pn collectif, qp0 virtualisé : pn ramené à Pe = pn × ratio', () => {
131
+ // rpn sur 23 kW, qp0 = 1 % × 23 kW × 0,1 = 23 W
132
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn, qp0: 23 };
133
+ const resultat = conventionPuissanceNominale(
134
+ pn,
135
+ ratio,
136
+ valeursDpe,
137
+ caracteristiquesChaudiere,
138
+ true
139
+ );
140
+ expect(resultat.convention).toBe('collective_virtualisee');
141
+ expect(resultat.pn).toBeCloseTo(2300, 9);
142
+ expect(resultat.ratio).toBe(ratio);
143
+ // Hypothèse virtualisée évaluée sur Pe = pn × ratio
144
+ expect(caracteristiquesChaudiere).toHaveBeenCalledWith(pn * ratio, ratio);
145
+ });
146
+
147
+ test('qp0 du DPE absent : rpn suffit à établir que pn est la puissance collective', () => {
148
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn };
149
+ expect(
150
+ conventionPuissanceNominale(pn, ratio, valeursDpe, caracteristiquesChaudiere, true)
151
+ ).toMatchObject({ convention: 'collective_virtualisee', ratio });
152
+ });
153
+
154
+ test('qp0 non recalculé (méthode de saisie 4/5) : rpn suffit', () => {
155
+ const rpnCollectif = caracteristiquesChaudiere(pn, 1).rpn;
156
+ const caracteristiques = (p, r) => ({ rpn: caracteristiquesChaudiere(p, r).rpn });
157
+ expect(
158
+ conventionPuissanceNominale(
159
+ pn,
160
+ ratio,
161
+ { rpn: rpnCollectif, qp0: 999 },
162
+ caracteristiques,
163
+ true
164
+ )
165
+ ).toMatchObject({ convention: 'collective_virtualisee' });
166
+ });
167
+
168
+ test('logiciel n’ayant pas virtualisé qp0 (qp0 en kW sur pn) + bug_for_bug_compat : calcul non virtualisé reproduit', () => {
169
+ // qp0 = 1 % × 23 kW = 0,23 kW, non multiplié par le ratio
170
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn, qp0: 0.23 };
171
+ expect(
172
+ conventionPuissanceNominale(pn, ratio, valeursDpe, caracteristiquesChaudiere, true)
173
+ ).toEqual({ pn, ratio: 1, convention: 'collective_non_virtualisee' });
174
+ });
175
+
176
+ test('logiciel n’ayant pas virtualisé qp0 sans bug_for_bug_compat : application de la méthode (pn × ratio)', () => {
177
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn, qp0: 230 };
178
+ const resultat = conventionPuissanceNominale(
179
+ pn,
180
+ ratio,
181
+ valeursDpe,
182
+ caracteristiquesChaudiere,
183
+ false
184
+ );
185
+ expect(resultat.convention).toBe('collective_virtualisee');
186
+ expect(resultat.pn).toBeCloseTo(2300, 9);
187
+ expect(resultat.ratio).toBe(ratio);
188
+ });
189
+
190
+ test('qp0 incohérent avec les deux hypothèses : convention individualisée conservée', () => {
191
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn, qp0: 5000 };
192
+ expect(
193
+ conventionPuissanceNominale(pn, ratio, valeursDpe, caracteristiquesChaudiere, true)
194
+ ).toEqual({ pn, ratio, convention: 'individualisee' });
195
+ });
196
+
197
+ test('qp0 non recalculable dans l’hypothèse non virtualisée : convention individualisée conservée', () => {
198
+ const caracteristiques = (p, r) =>
199
+ r === 1 ? { rpn: 0.8 } : { ...caracteristiquesChaudiere(p, r) };
200
+ const valeursDpe = { rpn: caracteristiquesChaudiere(pn, 1).rpn, qp0: 5000 };
201
+ expect(conventionPuissanceNominale(pn, ratio, valeursDpe, caracteristiques, true)).toEqual({
202
+ pn,
203
+ ratio,
204
+ convention: 'individualisee'
205
+ });
206
+ });
207
+
208
+ test('cas réel DPEWin 2415E2886950G (rpn = 0,87 ; qp0 = 0,276 kW) avec une table réaliste', () => {
209
+ // Chaudière gaz standard : rpn = 84 + 2 log(Pn), qp0 = 1,2 % de Pn
210
+ const caracteristiques = (p, r) => ({
211
+ rpn: (84 + 2 * Math.log10(p / r / 1000)) / 100,
212
+ qp0: 0.012 * p
213
+ });
214
+ const resultat = conventionPuissanceNominale(
215
+ 23000,
216
+ 0.1150571,
217
+ { rpn: 0.87, qp0: 0.276 },
218
+ caracteristiques,
219
+ true
220
+ );
221
+ expect(resultat).toEqual({ pn: 23000, ratio: 1, convention: 'collective_non_virtualisee' });
222
+ });
223
+ });
224
+
225
+ /**
226
+ * 4e hypothèse : pn recalculé à partir du rpn stocké dans le DPE (issue #124, DPE 2592E2935275X).
227
+ * Double de test d'une chaudière gaz à condensation 2001-2015 : rpn = 91 + log10(Pn),
228
+ * rpint = 97 + log10(Pn) (Pn collectif en kW), qp0 = 1 % de la puissance saisie.
229
+ */
230
+ const condensation = {
231
+ rpn: (pnCollectif) => (91 + Math.log10(pnCollectif / 1000)) / 100,
232
+ caracteristiques: (pn, ratio) => ({
233
+ rpn: (91 + Math.log10(pn / ratio / 1000)) / 100,
234
+ rpint: (97 + Math.log10(pn / ratio / 1000)) / 100,
235
+ qp0: 0.01 * pn
236
+ })
237
+ };
238
+ const formuleRpnCondensation = () => condensation.rpn;
239
+
240
+ describe('inverserRendement', () => {
241
+ test('formule « A + B × log10(Pn) » : Pn retrouvé (W)', () => {
242
+ // rpn = 91 + log10(696) % -> Pn = 696 kW
243
+ expect(inverserRendement((91 + Math.log10(696)) / 100, condensation.rpn)).toBeCloseTo(
244
+ 696000,
245
+ 6
246
+ );
247
+ const formule = (p) => (84 + 2 * Math.log10(p / 1000)) / 100;
248
+ expect(inverserRendement(formule(23000), formule)).toBeCloseTo(23000, 6);
249
+ });
250
+
251
+ test('rendement indépendant de Pn (B = 0) : non inversible', () => {
252
+ expect(inverserRendement(0.9, () => 0.9)).toBeUndefined();
253
+ });
254
+
255
+ test('formule non affine en log10(Pn) : non inversible', () => {
256
+ expect(inverserRendement(0.9, (p) => (80 + Math.log10(p / 1000) ** 2) / 100)).toBeUndefined();
257
+ });
258
+
259
+ test('formule non évaluable : non inversible', () => {
260
+ expect(inverserRendement(0.9, () => NaN)).toBeUndefined();
261
+ });
262
+ });
263
+
264
+ describe('puissanceDepuisRpn', () => {
265
+ /**
266
+ * Table à deux lignes selon le critère de puissance : Pn ≤ 70 kW -> 84 + 2 log10(Pn),
267
+ * Pn > 70 kW -> 91 + log10(Pn).
268
+ */
269
+ const petite = (p) => (84 + 2 * Math.log10(p / 1000)) / 100;
270
+ const formuleSelonPn = (pnCollectif) => (pnCollectif <= 70000 ? petite : condensation.rpn);
271
+
272
+ test('ligne de table stable : Pn obtenu en une inversion', () => {
273
+ const formuleRpn = vi.fn(formuleSelonPn);
274
+ expect(puissanceDepuisRpn(500000, condensation.rpn(696000), formuleRpn)).toBeCloseTo(696000, 6);
275
+ expect(formuleRpn).toHaveBeenCalledTimes(2);
276
+ });
277
+
278
+ test('première ligne inadaptée : ligne re-sélectionnée sur le Pn obtenu', () => {
279
+ // Départ sur la ligne « Pn ≤ 70 kW », Pn réel 696 kW
280
+ const formuleRpn = vi.fn(formuleSelonPn);
281
+ expect(puissanceDepuisRpn(24000, condensation.rpn(696000), formuleRpn)).toBeCloseTo(696000, 6);
282
+ expect(formuleRpn).toHaveBeenCalledTimes(4);
283
+ });
284
+
285
+ test('aucune ligne de table : abandon', () => {
286
+ expect(puissanceDepuisRpn(24000, 0.93, () => undefined)).toBeUndefined();
287
+ });
288
+
289
+ test('ligne sans formule rpn après re-sélection : abandon après 3 itérations', () => {
290
+ const formuleRpn = vi.fn((p) => (p === 24000 ? condensation.rpn : undefined));
291
+ expect(puissanceDepuisRpn(24000, 0.93, formuleRpn)).toBeUndefined();
292
+ });
293
+
294
+ test('rpn indépendant de Pn : abandon', () => {
295
+ expect(puissanceDepuisRpn(24000, 0.9, () => () => 0.9)).toBeUndefined();
296
+ });
297
+
298
+ test('lignes incompatibles (Pn hors de la plage de chaque ligne) : abandon', () => {
299
+ // 0,93 -> 100 kW sur la ligne « Pn ≤ 70 kW » et 14 kW sur la ligne « Pn > 70 kW »
300
+ const grande = (p) => (70 + 20 * Math.log10(p / 1000)) / 100;
301
+ const formuleRpn = (p) => (p <= 70000 ? condensation.rpn : grande);
302
+ expect(puissanceDepuisRpn(24000, 0.93, formuleRpn)).toBeUndefined();
303
+ });
304
+ });
305
+
306
+ describe('decalageDecimal', () => {
307
+ test('puissance saisie décalée d’une puissance de dix (69600 W pour 696 kW)', () => {
308
+ expect(decalageDecimal(69600, 696000)).toBe(true);
309
+ expect(decalageDecimal(696, 696000)).toBe(true);
310
+ expect(decalageDecimal(2018.32, 201.832)).toBe(true);
311
+ });
312
+
313
+ test('puissance identique ou sans rapport : pas de décalage', () => {
314
+ expect(decalageDecimal(696000, 696000)).toBe(false);
315
+ expect(decalageDecimal(23000, 696000)).toBe(false);
316
+ });
317
+ });
318
+
319
+ describe('puissanceRecalculeeDepuisRpn', () => {
320
+ // Valeurs du DPE 2592E2935275X (WinDpe) : pn = 69600 W, Pn réel = 696 kW
321
+ const ratio = 0.00289987937273824;
322
+ const rpn = 0.938426100909721;
323
+ const rpint = 0.998426100909721;
324
+
325
+ test('rpint du DPE cohérent avec le Pn retrouvé : Pe = a × Pn retenu', () => {
326
+ const resultat = puissanceRecalculeeDepuisRpn(
327
+ 69600,
328
+ ratio,
329
+ { rpn, rpint, qp0: 2.01832 },
330
+ condensation.caracteristiques,
331
+ formuleRpnCondensation
332
+ );
333
+ expect(resultat.convention).toBe('puissance_depuis_rpn');
334
+ expect(resultat.ratio).toBe(ratio);
335
+ /**
336
+ * Référence de régression : le logiciel a calculé rpn sur Pn = Pe / a avec Pe = 2018,32 W
337
+ * (arrondi au centième), soit Pn = 696,0014 kW ; on retrouve exactement Pe = 2018,32 W.
338
+ */
339
+ expect(resultat.pnCollectif).toBeCloseTo(2018.32 / ratio, 3);
340
+ expect(resultat.pn).toBeCloseTo(2018.32, 6);
341
+ });
342
+
343
+ test('rpint du DPE incohérent avec le Pn retrouvé : abandon', () => {
344
+ expect(
345
+ puissanceRecalculeeDepuisRpn(
346
+ 69600,
347
+ ratio,
348
+ { rpn, rpint: 0.95, qp0: 20.1832 },
349
+ condensation.caracteristiques,
350
+ formuleRpnCondensation
351
+ )
352
+ ).toBeUndefined();
353
+ });
354
+
355
+ test('sans rpint : qp0 virtualisé du DPE (W) confirme le Pn retrouvé', () => {
356
+ expect(
357
+ puissanceRecalculeeDepuisRpn(
358
+ 12345,
359
+ ratio,
360
+ { rpn, qp0: 20.1832 },
361
+ condensation.caracteristiques,
362
+ formuleRpnCondensation
363
+ )
364
+ ).toMatchObject({ convention: 'puissance_depuis_rpn' });
365
+ });
366
+
367
+ test('sans rpint : qp0 non virtualisé du DPE (kW) confirme le Pn retrouvé', () => {
368
+ // qp0 = 1 % × 696 kW = 6,96 kW
369
+ expect(
370
+ puissanceRecalculeeDepuisRpn(
371
+ 12345,
372
+ ratio,
373
+ { rpn, qp0: 6.96 },
374
+ condensation.caracteristiques,
375
+ formuleRpnCondensation
376
+ )
377
+ ).toMatchObject({ convention: 'puissance_depuis_rpn' });
378
+ });
379
+
380
+ test('sans rpint ni qp0 cohérent : pn saisi décalé d’une puissance de dix de Pn', () => {
381
+ expect(
382
+ puissanceRecalculeeDepuisRpn(
383
+ 69600,
384
+ ratio,
385
+ { rpn, qp0: 2.01832 },
386
+ condensation.caracteristiques,
387
+ formuleRpnCondensation
388
+ )
389
+ ).toMatchObject({ convention: 'puissance_depuis_rpn' });
390
+ });
391
+
392
+ test('sans rpint ni qp0 : pn saisi décalé d’une puissance de dix de Pe', () => {
393
+ expect(
394
+ puissanceRecalculeeDepuisRpn(
395
+ 201.832,
396
+ ratio,
397
+ { rpn },
398
+ condensation.caracteristiques,
399
+ formuleRpnCondensation
400
+ )
401
+ ).toMatchObject({ convention: 'puissance_depuis_rpn' });
402
+ });
403
+
404
+ test('aucune confirmation (rpint et qp0 non recalculés, pn sans rapport) : abandon', () => {
405
+ const caracteristiques = (pn, r) => ({ rpn: condensation.caracteristiques(pn, r).rpn });
406
+ expect(
407
+ puissanceRecalculeeDepuisRpn(
408
+ 12345,
409
+ ratio,
410
+ { rpn, rpint, qp0: 2.01832 },
411
+ caracteristiques,
412
+ formuleRpnCondensation
413
+ )
414
+ ).toBeUndefined();
415
+ });
416
+
417
+ test('rpn non inversible : abandon sans calcul des caractéristiques', () => {
418
+ const caracteristiques = vi.fn();
419
+ expect(
420
+ puissanceRecalculeeDepuisRpn(69600, ratio, { rpn }, caracteristiques, () => () => 0.9)
421
+ ).toBeUndefined();
422
+ expect(caracteristiques).not.toHaveBeenCalled();
423
+ });
424
+ });
425
+
426
+ describe('conventionPuissanceNominale - 4e hypothèse (pn recalculé depuis rpn)', () => {
427
+ const ratio = 0.00289987937273824;
428
+ const valeursDpe = { rpn: 0.938426100909721, rpint: 0.998426100909721, qp0: 2.01832 };
429
+
430
+ test('cas réel WinDpe 2592E2935275X : pn ni Pe ni Pn -> Pe = a × Pn(rpn) = 2018,32 W', () => {
431
+ const resultat = conventionPuissanceNominale(
432
+ 69600,
433
+ ratio,
434
+ valeursDpe,
435
+ condensation.caracteristiques,
436
+ true,
437
+ formuleRpnCondensation
438
+ );
439
+ expect(resultat).toMatchObject({ convention: 'puissance_depuis_rpn', ratio });
440
+ expect(resultat.pn).toBeCloseTo(2018.32, 6);
441
+ });
442
+
443
+ test('formule rpn non fournie : convention individualisée conservée', () => {
444
+ expect(
445
+ conventionPuissanceNominale(69600, ratio, valeursDpe, condensation.caracteristiques, true)
446
+ ).toEqual({ pn: 69600, ratio, convention: 'individualisee' });
447
+ });
448
+
449
+ test('confirmation impossible : convention individualisée conservée', () => {
450
+ expect(
451
+ conventionPuissanceNominale(
452
+ 69600,
453
+ ratio,
454
+ { ...valeursDpe, rpint: 0.95 },
455
+ condensation.caracteristiques,
456
+ true,
457
+ formuleRpnCondensation
458
+ )
459
+ ).toEqual({ pn: 69600, ratio, convention: 'individualisee' });
460
+ });
461
+
462
+ test('pn cohérent avec Pe : la 4e hypothèse n’est pas évaluée', () => {
463
+ const formuleRpn = vi.fn(formuleRpnCondensation);
464
+ expect(
465
+ conventionPuissanceNominale(
466
+ 2018.32,
467
+ ratio,
468
+ valeursDpe,
469
+ condensation.caracteristiques,
470
+ true,
471
+ formuleRpn
472
+ )
473
+ ).toMatchObject({ convention: 'individualisee' });
474
+ expect(formuleRpn).not.toHaveBeenCalled();
475
+ });
476
+ });
package/README.md CHANGED
@@ -370,15 +370,15 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
370
370
 
371
371
  <!-- CORPUS:START -->
372
372
 
373
- > **Version `1.8.7`** · branche `main` · généré le 2026-10-02
373
+ > **Version `1.8.8`** · à publier · branche `main` · généré le 2026-10-02
374
374
  > Seuil de tolérance **5%**
375
375
 
376
376
  <table>
377
377
  <tr>
378
378
  <td align="center"><strong>9</strong><br/><sub>corpus</sub></td>
379
379
  <td align="center"><strong>89 990</strong><br/><sub>DPE analysés</sub></td>
380
- <td align="center"><strong>59 740</strong><br/><sub>DPE conformes</sub></td>
381
- <td align="center"><strong>66,39 %</strong><br/><sub>réussite globale</sub></td>
380
+ <td align="center"><strong>59 757</strong><br/><sub>DPE conformes</sub></td>
381
+ <td align="center"><strong>66,40 %</strong><br/><sub>réussite globale</sub></td>
382
382
  </tr>
383
383
  </table>
384
384
 
@@ -386,12 +386,12 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
386
386
  | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
387
387
  | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,23 %** | `█████████░░░░░░░░░░░` | 4 623 / 10 000 |
388
388
  | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **91,76 %** | `██████████████████░░` | 9 176 / 10 000 |
389
- | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **88,40 %** | `██████████████████░░` | 8 833 / 9 992 |
390
- | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,38 %** | `██████████████████░░` | 8 838 / 10 000 |
391
- | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **74,11 %** | `███████████████░░░░░` | 7 410 / 9 999 |
392
- | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **69,74 %** | `██████████████░░░░░░` | 6 974 / 10 000 |
393
- | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,44 %** | `████████████░░░░░░░░` | 6 243 / 9 999 |
394
- | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **49,04 %** | `██████████░░░░░░░░░░` | 4 904 / 10 000 |
389
+ | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **88,41 %** | `██████████████████░░` | 8 834 / 9 992 |
390
+ | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,40 %** | `██████████████████░░` | 8 840 / 10 000 |
391
+ | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **74,14 %** | `███████████████░░░░░` | 7 413 / 9 999 |
392
+ | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **69,77 %** | `██████████████░░░░░░` | 6 977 / 10 000 |
393
+ | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,50 %** | `████████████░░░░░░░░` | 6 249 / 9 999 |
394
+ | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **49,06 %** | `██████████░░░░░░░░░░` | 4 906 / 10 000 |
395
395
  | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **27,39 %** | `█████░░░░░░░░░░░░░░░` | 2 739 / 10 000 |
396
396
 
397
397
  <sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
@@ -403,24 +403,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
403
403
 
404
404
  <table>
405
405
  <tr>
406
- <td align="center"><strong>4,80 ms</strong><br/><sub>moyenne</sub></td>
407
- <td align="center"><strong>4,20 ms</strong><br/><sub>médiane</sub></td>
406
+ <td align="center"><strong>4,74 ms</strong><br/><sub>moyenne</sub></td>
407
+ <td align="center"><strong>4,14 ms</strong><br/><sub>médiane</sub></td>
408
408
  <td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
409
- <td align="center"><strong>518,4 ms</strong><br/><sub>max</sub></td>
409
+ <td align="center"><strong>480,3 ms</strong><br/><sub>max</sub></td>
410
410
  </tr>
411
411
  </table>
412
412
 
413
413
  | Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
414
414
  | :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
415
- | **Généraliste** | 4,33 ms | 3,90 ms | 0,20 ms | 36,9 ms | 7,87 ms | 11,1 ms |
416
- | **Appartement · chauffage individuel (2025)** | 3,48 ms | 3,15 ms | 1,06 ms | 34,8 ms | 6,09 ms | 7,83 ms |
417
- | **Logement individuel (2025)** | 4,86 ms | 4,30 ms | 1,30 ms | 29,8 ms | 9,41 ms | 13,1 ms |
418
- | **Maison individuelle (2025)** | 5,48 ms | 4,96 ms | 1,65 ms | 35,8 ms | 9,87 ms | 12,8 ms |
419
- | **Immeuble · chauffage individuel** | 5,89 ms | 5,04 ms | 0,11 ms | 75,0 ms | 11,9 ms | 18,7 ms |
420
- | **Appartement · chauffage collectif (2025)** | 3,86 ms | 3,58 ms | 1,18 ms | 31,7 ms | 6,29 ms | 7,87 ms |
421
- | **Immeuble · chauffage collectif** | 5,21 ms | 4,43 ms | 0,11 ms | 110,0 ms | 9,58 ms | 16,9 ms |
422
- | **Immeuble · chauffage mixte** | 4,89 ms | 4,01 ms | 0,20 ms | 518,4 ms | 10,0 ms | 17,0 ms |
423
- | **Individuel généré depuis l'immeuble (2026)** | 5,22 ms | 4,47 ms | 1,61 ms | 40,6 ms | 9,86 ms | 15,4 ms |
415
+ | **Généraliste** | 4,07 ms | 3,66 ms | 0,21 ms | 29,4 ms | 7,48 ms | 10,1 ms |
416
+ | **Appartement · chauffage individuel (2025)** | 3,58 ms | 3,27 ms | 1,22 ms | 23,6 ms | 6,14 ms | 7,97 ms |
417
+ | **Logement individuel (2025)** | 4,62 ms | 4,06 ms | 1,45 ms | 35,7 ms | 8,83 ms | 12,5 ms |
418
+ | **Maison individuelle (2025)** | 5,74 ms | 5,21 ms | 1,74 ms | 49,6 ms | 10,4 ms | 13,7 ms |
419
+ | **Immeuble · chauffage individuel** | 5,70 ms | 4,84 ms | 0,11 ms | 73,3 ms | 11,6 ms | 17,9 ms |
420
+ | **Appartement · chauffage collectif (2025)** | 4,07 ms | 3,70 ms | 1,30 ms | 30,5 ms | 6,90 ms | 10,3 ms |
421
+ | **Immeuble · chauffage collectif** | 4,90 ms | 4,11 ms | 0,12 ms | 101,4 ms | 9,06 ms | 15,4 ms |
422
+ | **Immeuble · chauffage mixte** | 4,60 ms | 3,75 ms | 0,19 ms | 480,3 ms | 9,46 ms | 15,9 ms |
423
+ | **Individuel généré depuis l'immeuble (2026)** | 5,38 ms | 4,62 ms | 1,56 ms | 41,0 ms | 10,3 ms | 15,8 ms |
424
424
 
425
425
  <sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
426
426
  dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas