@open3cl/engine 1.8.0 → 1.8.2

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.
package/11_ecs.js CHANGED
@@ -13,7 +13,11 @@ function tv_rendement_distribution_ecs(di, de, du, pvc) {
13
13
  let configuration_logement;
14
14
  if (type_installation.includes('individuelle')) {
15
15
  if (pvc === 1) {
16
- configuration_logement = 'production volume habitable [+] pièces alimentées contiguës';
16
+ // Motif ancré « ^…+…$ » : compatible avec les deux matchers de tv().
17
+ // - tvMatchLegacy échappe le « + » d'un motif ancré avant le .match() regex ;
18
+ // - tvMatchOptimized retire les ancres puis compare la chaîne littérale.
19
+ // L'ancien « [+] » (échappement regex) ne matchait jamais en version optimisée (#206).
20
+ configuration_logement = '^production volume habitable + pièces alimentées contiguës$';
17
21
  } else configuration_logement = 'production hors volume habitable';
18
22
  } else if (type_installation.includes('collective')) {
19
23
  let type_reseau_collectif;
package/11_ecs.spec.js CHANGED
@@ -169,7 +169,7 @@ describe('calc_ecs - rendement de distribution', () => {
169
169
  calc_ecs({}, ecs, 100, 200, 1, 'ca1', 'h1a', 'maison', false, null, null, false);
170
170
 
171
171
  expect(tv).toHaveBeenCalledWith('rendement_distribution_ecs', {
172
- configuration_logement: 'production volume habitable [+] pièces alimentées contiguës'
172
+ configuration_logement: '^production volume habitable + pièces alimentées contiguës$'
173
173
  });
174
174
  });
175
175
 
@@ -32,13 +32,43 @@ const K = {
32
32
  'réseau de froid urbain': undefined
33
33
  };
34
34
 
35
+ /**
36
+ * Planchers / plafonds chauffants sur réseau d'eau chaude « basse ou moyenne température
37
+ * (inf 65°c) » (enum_type_emission_distribution_id 12, 14, 16, 18).
38
+ */
39
+ export const EMISSION_PLANCHER_PLAFOND_CHAUFFANT_BT = ['12', '14', '16', '18'];
40
+
41
+ /**
42
+ * Température de distribution à utiliser pour les tables temp_fonc_30 / temp_fonc_100.
43
+ *
44
+ * Un plancher (ou plafond) chauffant à eau < 65 °C fonctionne en basse température : comme le
45
+ * moteur de référence CSTB (Tribu), qui déduit la ligne « basse » du type d'émetteur plancher
46
+ * chauffant, on retient « basse » (id 2) lorsque le DPE déclare « moyenne » (id 3) pour ces
47
+ * émetteurs. Les logiciels du marché (ex. LICIEL) calculent ainsi mais exportent « moyenne ».
48
+ *
49
+ * @param emChDe {object} donnee_entree de l'émetteur
50
+ * @param emChDu {object} donnee_utilisateur de l'émetteur
51
+ * @return {string|undefined}
52
+ */
53
+ export function tempDistributionChPourTempFonc(emChDe, emChDu) {
54
+ const tempDistribution = requestInputID(emChDe, emChDu, 'temp_distribution_ch');
55
+ const typeEmission = String(emChDe?.enum_type_emission_distribution_id ?? '');
56
+ if (
57
+ String(tempDistribution) === '3' &&
58
+ EMISSION_PLANCHER_PLAFOND_CHAUFFANT_BT.includes(typeEmission)
59
+ ) {
60
+ return '2';
61
+ }
62
+ return tempDistribution;
63
+ }
64
+
35
65
  export function tv_temp_fonc_30_100(di, de, du, em_ch, ac) {
36
66
  for (const em of em_ch) {
37
67
  const em_ch_de = em.donnee_entree;
38
68
  const em_ch_du = em.donnee_utilisateur;
39
69
  const matcher = {
40
70
  enum_type_generateur_ch_id: de.enum_type_generateur_ch_id,
41
- enum_temp_distribution_ch_id: requestInputID(em_ch_de, em_ch_du, 'temp_distribution_ch'),
71
+ enum_temp_distribution_ch_id: tempDistributionChPourTempFonc(em_ch_de, em_ch_du),
42
72
  periode_emetteurs: requestInput(em_ch_de, em_ch_du, 'periode_installation_emetteur')
43
73
  };
44
74
 
@@ -180,7 +210,9 @@ export function calc_generateur_combustion_ch(dpe, di, de, du) {
180
210
 
181
211
  // Pveil and QP0 are in kW
182
212
  const Pveil = di.pveil / 1000;
183
- const QP0 = di.qp0 / 1000;
213
+ // rg est d'abord calculé sur PCS (puis × k pour revenir en PCI) : QP0 doit donc être exprimé
214
+ // sur PCS, comme dans QPx() (qp0 × k). Cf. issue #205.
215
+ const QP0 = (di.qp0 * k) / 1000;
184
216
 
185
217
  const rg_pcs = Pmfou / (Pmcons + 0.45 * QP0 + Pveil);
186
218
  const rg_pcs_dep = Pmfou_dep / (Pmcons_dep + 0.45 * QP0 + Pveil);
@@ -38,7 +38,7 @@ vi.mock('./utils.js', () => ({
38
38
  requestInputID: vi.fn((de, du, field) => de[`enum_${field}_id`])
39
39
  }));
40
40
 
41
- const { tv_temp_fonc_30_100, calc_generateur_combustion_ch } =
41
+ const { tv_temp_fonc_30_100, tempDistributionChPourTempFonc, calc_generateur_combustion_ch } =
42
42
  await import('./13.2_generateur_combustion_ch.js');
43
43
  const { tv } = await import('./utils.js');
44
44
 
@@ -138,6 +138,75 @@ describe('tv_temp_fonc_30_100 - températures de fonctionnement', () => {
138
138
  expect(errSpy).toHaveBeenCalledTimes(2);
139
139
  errSpy.mockRestore();
140
140
  });
141
+
142
+ /**
143
+ * Plancher / plafond chauffant à eau < 65 °C : la ligne « basse » est retenue même si le DPE
144
+ * déclare « moyenne » (comportement du moteur de référence CSTB / Tribu).
145
+ */
146
+ test.each(['12', '14', '16', '18'])(
147
+ 'plancher/plafond chauffant BT (émission %s) déclaré « moyenne » : ligne « basse » utilisée',
148
+ (typeEmission) => {
149
+ tv.mockReturnValue({ tv_temp_fonc_30_id: '1', temp_fonc_30: '24.5' });
150
+ tv_temp_fonc_30_100(
151
+ {},
152
+ { enum_type_generateur_ch_id: '97' },
153
+ {},
154
+ [emetteur({ enum_type_emission_distribution_id: typeEmission })],
155
+ 2018
156
+ );
157
+ expect(tv).toHaveBeenCalledWith(
158
+ 'temp_fonc_30',
159
+ expect.objectContaining({ enum_temp_distribution_ch_id: '2' })
160
+ );
161
+ expect(tv).toHaveBeenCalledWith(
162
+ 'temp_fonc_100',
163
+ expect.objectContaining({ enum_temp_distribution_ch_id: '2' })
164
+ );
165
+ }
166
+ );
167
+
168
+ test.each([
169
+ ['radiateur (35), moyenne', '35', '3', '3'],
170
+ ['plancher chauffant HT (13), moyenne', '13', '3', '3'],
171
+ ['plancher chauffant BT (14), haute', '14', '4', '4'],
172
+ ['plancher chauffant BT (14), basse', '14', '2', '2']
173
+ ])(
174
+ '%s : température de distribution déclarée conservée',
175
+ (_, typeEmission, declaree, attendue) => {
176
+ tv.mockReturnValue({ tv_temp_fonc_30_id: '1', temp_fonc_30: '40' });
177
+ tv_temp_fonc_30_100(
178
+ {},
179
+ { enum_type_generateur_ch_id: '97' },
180
+ {},
181
+ [
182
+ emetteur({
183
+ enum_type_emission_distribution_id: typeEmission,
184
+ enum_temp_distribution_ch_id: declaree
185
+ })
186
+ ],
187
+ 2018
188
+ );
189
+ expect(tv).toHaveBeenCalledWith(
190
+ 'temp_fonc_30',
191
+ expect.objectContaining({ enum_temp_distribution_ch_id: attendue })
192
+ );
193
+ }
194
+ );
195
+ });
196
+
197
+ describe('tempDistributionChPourTempFonc', () => {
198
+ test('émetteur sans type d’émission : température déclarée renvoyée telle quelle', () => {
199
+ expect(tempDistributionChPourTempFonc({ enum_temp_distribution_ch_id: '3' }, {})).toBe('3');
200
+ });
201
+
202
+ test('type d’émission numérique (14) : reconnu comme plancher chauffant BT', () => {
203
+ expect(
204
+ tempDistributionChPourTempFonc(
205
+ { enum_temp_distribution_ch_id: 3, enum_type_emission_distribution_id: 14 },
206
+ {}
207
+ )
208
+ ).toBe('2');
209
+ });
141
210
  });
142
211
 
143
212
  /**
@@ -156,9 +225,9 @@ describe('calc_generateur_combustion_ch - rendement de génération', () => {
156
225
  };
157
226
  calc_generateur_combustion_ch({}, di, de, { cdimref: 0.8, cdimrefDep: 0.6 });
158
227
  // valeurs de référence de régression (module réel, enums réels équivalents)
159
- expect(di.rg).toBeCloseTo(0.8582190936918361, 9);
160
- expect(di.rg_dep).toBeCloseTo(0.8706053178411902, 9);
161
- expect(di.rendement_generation).toBeCloseTo(0.8582190936918361, 9);
228
+ expect(di.rg).toBeCloseTo(0.8575549045559866, 9);
229
+ expect(di.rg_dep).toBeCloseTo(0.8700736982536121, 9);
230
+ expect(di.rendement_generation).toBeCloseTo(0.8575549045559866, 9);
162
231
  });
163
232
 
164
233
  test('chaudière bois : branche de calcul dédiée (QP50 / QP100)', () => {
@@ -171,8 +240,8 @@ describe('calc_generateur_combustion_ch - rendement de génération', () => {
171
240
  };
172
241
  calc_generateur_combustion_ch({}, di, de, { cdimref: 0.9, cdimrefDep: 0.7 });
173
242
  // valeurs de référence de régression
174
- expect(di.rg).toBeCloseTo(0.7044775930164003, 9);
175
- expect(di.rg_dep).toBeCloseTo(0.7138933157828895, 9);
243
+ expect(di.rg).toBeCloseTo(0.7040089707689774, 9);
244
+ expect(di.rg_dep).toBeCloseTo(0.713512984577247, 9);
176
245
  });
177
246
 
178
247
  test('chaudière à condensation avec régulation : température de fonctionnement à 30 % utilisée', () => {
@@ -186,8 +255,8 @@ describe('calc_generateur_combustion_ch - rendement de génération', () => {
186
255
  };
187
256
  calc_generateur_combustion_ch({}, di, de, { cdimref: 0.8, cdimrefDep: 0.6 });
188
257
  // valeurs de référence de régression
189
- expect(di.rg).toBeCloseTo(0.9281152399114512, 9);
190
- expect(di.rg_dep).toBeCloseTo(0.9344861707155592, 9);
258
+ expect(di.rg).toBeCloseTo(0.9275325681176758, 9);
259
+ expect(di.rg_dep).toBeCloseTo(0.9340267431264844, 9);
191
260
  });
192
261
 
193
262
  test('radiateur à gaz : branche de calcul dédiée', () => {
@@ -201,8 +270,8 @@ describe('calc_generateur_combustion_ch - rendement de génération', () => {
201
270
  };
202
271
  calc_generateur_combustion_ch({}, di, de, { cdimref: 0.8, cdimrefDep: 0.6 });
203
272
  // valeurs de référence de régression
204
- expect(di.rg).toBeCloseTo(0.8851321252668564, 9);
205
- expect(di.rg_dep).toBeCloseTo(0.8869221230591896, 9);
273
+ expect(di.rg).toBeCloseTo(0.8842491989709215, 9);
274
+ expect(di.rg_dep).toBeCloseTo(0.8862325713702337, 9);
206
275
  });
207
276
 
208
277
  test('générateur à air chaud : branche de calcul dédiée', () => {
@@ -231,8 +300,8 @@ describe('calc_generateur_combustion_ch - rendement de génération', () => {
231
300
  };
232
301
  calc_generateur_combustion_ch({}, di, de, { cdimref: 0.8, cdimrefDep: 0.6 });
233
302
  // valeurs de référence de régression
234
- expect(di.rg).toBeCloseTo(0.8513271171577527, 9);
235
- expect(di.rg_dep).toBeCloseTo(0.8655664711109103, 9);
303
+ expect(di.rg).toBeCloseTo(0.8505103134051144, 9);
304
+ expect(di.rg_dep).toBeCloseTo(0.8649097139284332, 9);
236
305
  });
237
306
 
238
307
  test('type de générateur inconnu : aucun rendement calculé (avertissement)', () => {
@@ -262,8 +331,8 @@ describe('calc_generateur_combustion_ch - rendement de génération', () => {
262
331
  };
263
332
  calc_generateur_combustion_ch({}, di, de, {});
264
333
  // Cdimref = 0 -> x/0 = Infinity -> min(1, ...) = 1 pour tous les x, nominal = dépensier
265
- expect(di.rg).toBeCloseTo(0.8967281540325838, 9);
266
- expect(di.rg_dep).toBeCloseTo(0.8967281540325838, 9);
334
+ expect(di.rg).toBeCloseTo(0.8963697027040485, 9);
335
+ expect(di.rg_dep).toBeCloseTo(0.8963697027040485, 9);
267
336
  });
268
337
 
269
338
  test('bug_for_bug_compat : correction de qp0 exprimé en kW (< 1) vers des W', () => {
@@ -281,7 +350,7 @@ describe('calc_generateur_combustion_ch - rendement de génération', () => {
281
350
  // qp0 < 1 -> multiplié par 1000 => 200 (passage kW -> W)
282
351
  expect(di.qp0).toBe(200);
283
352
  // valeur de référence de régression (chaudière standard, pn = 20000, qp0 corrigé à 200)
284
- expect(di.rg).toBeCloseTo(0.8566147558351076, 9);
353
+ expect(di.rg).toBeCloseTo(0.8557877790761849, 9);
285
354
  warnSpy.mockRestore();
286
355
  });
287
356
 
@@ -320,4 +389,68 @@ describe('calc_generateur_combustion_ch - rendement de génération', () => {
320
389
  // Avec régulation (tf plus basse) le rendement est plus élevé
321
390
  expect(diAvec.rg).toBeGreaterThan(diSans.rg);
322
391
  });
392
+
393
+ /**
394
+ * Référence externe (et non régression) : autotest État CSTB `APP2-0-1`, moteur
395
+ * Moteur_DPE.dll v2025.11.1.0 (Tribu). Chaudière gaz à condensation 2001-2015, Pn = 23 kW,
396
+ * valeurs par défaut. Les grandeurs intermédiaires ci-dessous sont celles de la sortie Tribu
397
+ * (Rpn, Rpint, Qp0, Tfonc30/100, Cdimref = 0.05 / TchFinal(Ch_5)).
398
+ * Avant correctif (QP0 non converti sur PCS) : rg = 0.8597041838253313. Cf. issue #205.
399
+ */
400
+ test('autotest État APP2-0-1 : rg identique à Tribu (QP0 exprimé sur PCS)', () => {
401
+ const di = {
402
+ pn: 23000,
403
+ rpn: 0.9236172783601759,
404
+ rpint: 0.983617278360176,
405
+ qp0: 230,
406
+ pveil: 0,
407
+ temp_fonc_30: 38,
408
+ temp_fonc_100: 80
409
+ };
410
+ const de = {
411
+ enum_type_generateur_ch_id: '96',
412
+ enum_type_energie_id: '2',
413
+ type_energie: 'gaz naturel',
414
+ presence_regulation_combustion: true,
415
+ description: 'Chaudière gaz condensation'
416
+ };
417
+ const cdimref = 0.05 / 0.00603495141488493;
418
+ calc_generateur_combustion_ch({}, di, de, { cdimref, cdimrefDep: cdimref });
419
+ // Rg Tribu (sortie APP2-0-1_Sortie.xml) : 0.8511886946001722
420
+ expect(di.rg).toBeCloseTo(0.8511886946001722, 12);
421
+ });
422
+
423
+ test('les pertes à charge nulle QP0 réduisent le rendement de génération', () => {
424
+ const di = () => ({
425
+ pn: 23000,
426
+ rpn: 0.92,
427
+ rpint: 0.98,
428
+ qp0: 230,
429
+ pveil: 0,
430
+ temp_fonc_30: 38,
431
+ temp_fonc_100: 80
432
+ });
433
+ const de = {
434
+ enum_type_generateur_ch_id: '96',
435
+ enum_type_energie_id: '2',
436
+ presence_regulation_combustion: true,
437
+ description: 't'
438
+ };
439
+ const avecPertes = di();
440
+ calc_generateur_combustion_ch(
441
+ {},
442
+ avecPertes,
443
+ { ...de, type_energie: 'gaz naturel' },
444
+ { cdimref: 8 }
445
+ );
446
+ const sansPertes = { ...di(), qp0: 0 };
447
+ calc_generateur_combustion_ch(
448
+ {},
449
+ sansPertes,
450
+ { ...de, type_energie: 'gaz naturel' },
451
+ { cdimref: 8 }
452
+ );
453
+ // le terme 0.45 × QP0 réduit bien le rendement
454
+ expect(avecPertes.rg).toBeLessThan(sansPertes.rg);
455
+ });
323
456
  });
package/README.md CHANGED
@@ -370,57 +370,57 @@ 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.6.2`** · branche `main` · généré le 2026-09-18
373
+ > **Version `1.8.1`** · à publier · branche `main` · généré le 2026-10-01
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
- <td align="center"><strong>89 996</strong><br/><sub>DPE analysés</sub></td>
380
- <td align="center"><strong>59 497</strong><br/><sub>DPE conformes</sub></td>
381
- <td align="center"><strong>66,11 %</strong><br/><sub>réussite globale</sub></td>
379
+ <td align="center"><strong>89 990</strong><br/><sub>DPE analysés</sub></td>
380
+ <td align="center"><strong>59 630</strong><br/><sub>DPE conformes</sub></td>
381
+ <td align="center"><strong>66,26 %</strong><br/><sub>réussite globale</sub></td>
382
382
  </tr>
383
383
  </table>
384
384
 
385
385
  | | Corpus | Réussite | | DPE conformes |
386
386
  | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
387
- | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **45,97 %** | `█████████░░░░░░░░░░░` | 4 597 / 10 000 |
388
- | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **91,55 %** | `██████████████████░░` | 9 155 / 10 000 |
389
- | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **88,14 %** | `██████████████████░░` | 8 812 / 9 998 |
390
- | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **87,93 %** | `██████████████████░░` | 8 793 / 10 000 |
391
- | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **73,76 %** | `███████████████░░░░░` | 7 375 / 9 999 |
392
- | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **69,47 %** | `██████████████░░░░░░` | 6 947 / 10 000 |
393
- | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,27 %** | `████████████░░░░░░░░` | 6 226 / 9 999 |
394
- | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **48,37 %** | `██████████░░░░░░░░░░` | 4 837 / 10 000 |
395
- | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **27,55 %** | `██████░░░░░░░░░░░░░░` | 2 755 / 10 000 |
387
+ | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,11 %** | `█████████░░░░░░░░░░░` | 4 611 / 10 000 |
388
+ | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **91,69 %** | `██████████████████░░` | 9 169 / 10 000 |
389
+ | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **88,19 %** | `██████████████████░░` | 8 812 / 9 992 |
390
+ | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,02 %** | `██████████████████░░` | 8 802 / 10 000 |
391
+ | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **74,08 %** | `███████████████░░░░░` | 7 407 / 9 999 |
392
+ | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **69,67 %** | `██████████████░░░░░░` | 6 967 / 10 000 |
393
+ | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,42 %** | `████████████░░░░░░░░` | 6 241 / 9 999 |
394
+ | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **48,82 %** | `██████████░░░░░░░░░░` | 4 882 / 10 000 |
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>
398
398
 
399
399
  #### Temps d’exécution
400
400
 
401
- > Durée de l’appel à `calcul_3cl` par DPE, sur 89 956 calculs.
401
+ > Durée de l’appel à `calcul_3cl` par DPE, sur 89 950 calculs.
402
402
  > La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure.
403
403
 
404
404
  <table>
405
405
  <tr>
406
- <td align="center"><strong>5,61 ms</strong><br/><sub>moyenne</sub></td>
407
- <td align="center"><strong>5,03 ms</strong><br/><sub>médiane</sub></td>
406
+ <td align="center"><strong>4,88 ms</strong><br/><sub>moyenne</sub></td>
407
+ <td align="center"><strong>4,26 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>705,8 ms</strong><br/><sub>max</sub></td>
409
+ <td align="center"><strong>891,4 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,50 ms | 4,09 ms | 0,20 ms | 34,8 ms | 8,12 ms | 11,3 ms |
416
- | **Appartement · chauffage individuel (2025)** | 3,71 ms | 3,46 ms | 1,32 ms | 26,1 ms | 6,22 ms | 7,99 ms |
417
- | **Logement individuel (2025)** | 5,30 ms | 4,84 ms | 1,52 ms | 42,5 ms | 9,73 ms | 13,2 ms |
418
- | **Maison individuelle (2025)** | 6,54 ms | 6,02 ms | 1,71 ms | 54,5 ms | 11,6 ms | 16,4 ms |
419
- | **Immeuble · chauffage individuel** | 7,17 ms | 6,22 ms | 0,11 ms | 137,7 ms | 14,3 ms | 22,3 ms |
420
- | **Appartement · chauffage collectif (2025)** | 3,98 ms | 3,76 ms | 1,22 ms | 37,6 ms | 6,35 ms | 8,00 ms |
421
- | **Immeuble · chauffage collectif** | 6,40 ms | 5,58 ms | 0,15 ms | 101,0 ms | 12,1 ms | 21,2 ms |
422
- | **Immeuble · chauffage mixte** | 6,01 ms | 5,01 ms | 0,22 ms | 705,8 ms | 12,4 ms | 19,8 ms |
423
- | **Individuel généré depuis l'immeuble (2026)** | 6,89 ms | 6,27 ms | 1,52 ms | 65,1 ms | 13,0 ms | 19,7 ms |
415
+ | **Généraliste** | 3,65 ms | 3,28 ms | 0,21 ms | 29,7 ms | 6,67 ms | 8,87 ms |
416
+ | **Appartement · chauffage individuel (2025)** | 3,54 ms | 3,23 ms | 1,09 ms | 24,6 ms | 6,11 ms | 8,02 ms |
417
+ | **Logement individuel (2025)** | 4,89 ms | 4,37 ms | 1,41 ms | 39,4 ms | 9,35 ms | 12,7 ms |
418
+ | **Maison individuelle (2025)** | 5,30 ms | 4,73 ms | 1,69 ms | 39,6 ms | 9,54 ms | 13,0 ms |
419
+ | **Immeuble · chauffage individuel** | 5,64 ms | 4,78 ms | 0,11 ms | 76,2 ms | 11,4 ms | 17,8 ms |
420
+ | **Appartement · chauffage collectif (2025)** | 3,92 ms | 3,56 ms | 1,18 ms | 32,4 ms | 6,59 ms | 9,44 ms |
421
+ | **Immeuble · chauffage collectif** | 5,56 ms | 4,75 ms | 0,13 ms | 140,4 ms | 10,7 ms | 18,7 ms |
422
+ | **Immeuble · chauffage mixte** | 5,41 ms | 4,41 ms | 0,19 ms | 891,4 ms | 11,0 ms | 18,0 ms |
423
+ | **Individuel généré depuis l'immeuble (2026)** | 6,04 ms | 5,26 ms | 1,65 ms | 64,0 ms | 11,5 ms | 17,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
package/engine.js CHANGED
@@ -33,7 +33,7 @@ import getFicheTechnique from './ficheTechnique.js';
33
33
  import { ProductionENR } from './16.2_production_enr.js';
34
34
  import DpeSanitizerService from './dpe-sanitizer.service.js';
35
35
 
36
- const LIB_VERSION = '1.8.0';
36
+ const LIB_VERSION = '1.8.2';
37
37
 
38
38
  const dpeSanitizerService = new DpeSanitizerService();
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -28,9 +28,10 @@
28
28
  "prepare": "husky install",
29
29
  "assets:sync": "node scripts/synchronize_assets.js",
30
30
  "version:update": "node scripts/update_version.js",
31
- "reports:charts": "node scripts/generate_corpus_history_chart.js",
32
31
  "reports:preview": "npm run http-server dist/reports/corpus -o",
33
- "reports:readme": "node scripts/generate_corpus_readme.js && npm run reports:charts"
32
+ "reports:readme": "node scripts/generate_corpus_readme.js",
33
+ "reports:stamp": "node scripts/stamp_corpus_release.js",
34
+ "reports:version": "node scripts/corpus_version.js"
34
35
  },
35
36
  "repository": {
36
37
  "type": "git",
@@ -53,6 +54,7 @@
53
54
  "@commitlint/cz-commitlint": "^19.2.0",
54
55
  "@duckdb/node-api": "^1.3.2-alpha.25",
55
56
  "@semantic-release/changelog": "^6.0.3",
57
+ "@semantic-release/commit-analyzer": "^13.0.1",
56
58
  "@semantic-release/exec": "^7.1.0",
57
59
  "@semantic-release/git": "^10.0.1",
58
60
  "@semantic-release/github": "^12.0.2",
@@ -71,6 +73,7 @@
71
73
  "fast-csv": "^5.0.2",
72
74
  "http-server": "^14.1.1",
73
75
  "husky": "^9.0.11",
76
+ "js-yaml": "^4.3.2",
74
77
  "jscpd": "^3.5.10",
75
78
  "piscina": "^5.1.2",
76
79
  "prettier": "^3.2.5",
package/utils.spec.js CHANGED
@@ -23,7 +23,12 @@ vi.mock('./tv.js', () => ({
23
23
  { code: 'abc', val: 'r4' }
24
24
  ],
25
25
  // Table dédiée à tvMatchOptimized (version optimisée).
26
- table_opt: [{ a: 'brique', b: '1|2', c: '≥50', d: 'isolation par exterieur', e: 'zone 3' }]
26
+ table_opt: [{ a: 'brique', b: '1|2', c: '≥50', d: 'isolation par exterieur', e: 'zone 3' }],
27
+ // Table dédiée à la compatibilité des deux matchers sur un libellé contenant « + » (#206).
28
+ table_plus: [
29
+ { conf: 'production volume habitable + pièces alimentées non contiguës', val: 'p0' },
30
+ { conf: 'production volume habitable + pièces alimentées contiguës', val: 'p1' }
31
+ ]
27
32
  }
28
33
  }));
29
34
 
@@ -431,6 +436,28 @@ describe('Utils unit tests', () => {
431
436
  });
432
437
  });
433
438
 
439
+ // -------------------------------------------------------------------------------------------
440
+ // Libellé contenant un « + » littéral (ex. rendement_distribution_ecs, cf. issue #206) : le
441
+ // motif ancré « ^…+…$ » doit sélectionner la même ligne dans les deux versions du matcher.
442
+ describe('tv / libellé avec « + » : compatibilité des deux matchers', () => {
443
+ const motifAncre = '^production volume habitable + pièces alimentées contiguës$';
444
+
445
+ test.each([
446
+ ['historique', unset_tv_match_optimized_version],
447
+ ['optimisée', set_tv_match_optimized_version]
448
+ ])('version %s : le motif ancré sélectionne la bonne ligne', (_, setVersion) => {
449
+ setVersion();
450
+ expect(tv('table_plus', { conf: motifAncre }).val).toBe('p1');
451
+ });
452
+
453
+ test('version optimisée : l’échappement regex « [+] » ne matche aucune ligne', () => {
454
+ set_tv_match_optimized_version();
455
+ expect(
456
+ tv('table_plus', { conf: 'production volume habitable [+] pièces alimentées contiguës' })
457
+ ).toBeNull();
458
+ });
459
+ });
460
+
434
461
  // -------------------------------------------------------------------------------------------
435
462
  describe('tv / cache', () => {
436
463
  // Règle : `tv()` mémoïse chaque couple (table, matcher), borné par une éviction FIFO.