@mostajs/recette 0.2.1 → 0.3.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
4
 
5
+ ## 0.3.0 — 2026-10-23
6
+
7
+ ### Corrigé
8
+
9
+ - **Une ligne, une issue : le DERNIER relevé fait foi.** Le formulaire enregistre à chaque
10
+ changement, comme un questionnaire ; un testeur qui hésitait entre `ko` et `ok`, ou qui écrivait
11
+ sa remarque en plusieurs fois, laissait plusieurs relevés pour la même ligne, et tous étaient
12
+ comptés. Une hésitation devenait un `ko` de plus au dossier, une remarque tapée en trois fois
13
+ trois `ok` — et le verdict proposé au certificateur s'en trouvait faussé. Les compteurs du
14
+ dossier et de `etat()` ne retiennent plus que le dernier relevé de chaque (feuillet, ligne).
15
+ **T-REC-15** le verrouille.
16
+
17
+ ### Ajouté
18
+
19
+ - `releves.derniersDuFeuillet(feuilletId)` — le relevé qui fait foi pour chaque ligne, issue et
20
+ remarque comprises : ce qu'un formulaire réaffiche.
21
+ - Le dossier figé porte `relevesRemplaces` : combien de relevés ont été remplacés. L'historique
22
+ reste en base, rien n'est effacé.
23
+
24
+ > Le **contenu** d'un dossier figé change de forme (`releves` ne garde que les relevés qui font
25
+ > foi) : son empreinte n'est pas comparable à celle d'un dossier figé en 0.2.x. Les dossiers déjà
26
+ > figés ne sont pas touchés.
27
+
5
28
  ## 0.2.1 — 2026-09-10
6
29
 
7
30
  Correction née d'une panne **en production**, invisible depuis les essais.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/recette",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Validation & recette applicative : trame `mostajs-devtest/1` reçue signée du certificateur, points enrichis POUR LE CLIENT (l'import n'écrase jamais l'enrichissement), feuillets composés à l'image du manifeste PDF (ok/ko/er), anomalies déclarées puis qualifiées, dossier figé et EXPORTÉ SIGNÉ — aucun appel réseau —, certificat reçu et vérifié, feu d'exploitation DÉRIVÉ du certificat. Powers ValiTrax (valitrax.mostajs.dev).",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",
package/src/recette.js CHANGED
@@ -261,6 +261,29 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
261
261
  },
262
262
  };
263
263
 
264
+ /**
265
+ * UNE LIGNE, UNE ISSUE : le DERNIER relevé de chaque (feuillet, ligne) fait foi.
266
+ *
267
+ * Un relevé ne s'efface pas, il se remplace : le formulaire enregistre à chaque changement, comme
268
+ * un questionnaire, et le testeur qui hésite entre `ko` et `ok`, ou qui écrit sa remarque en
269
+ * plusieurs fois, laisse plusieurs relevés pour la même ligne. Les compter tous faisait d'une
270
+ * hésitation un `ko` de plus au dossier, et d'une remarque tapée en trois fois trois `ok` :
271
+ * le verdict proposé au certificateur s'en trouvait faussé. L'historique reste en base ; le
272
+ * dossier dit combien de relevés ont été remplacés.
273
+ */
274
+ const instant = (r) => +new Date(r.createdAt || r.at || 0);
275
+ function derniers(rs) {
276
+ const parLigne = new Map();
277
+ rs.forEach((r, i) => {
278
+ const cle = `${r.feuilletId}\u0000${r.ref}`;
279
+ const avant = parLigne.get(cle);
280
+ // À instant égal, l'ordre de lecture départage : le plus tard lu est le plus récent.
281
+ if (!avant || instant(r) >= instant(avant.r)) parLigne.set(cle, { r, i });
282
+ });
283
+ return [...parLigne.values()].sort((a, b) => a.i - b.i).map((x) => x.r);
284
+ }
285
+ const compter = (rs) => ({ ok: rs.filter((r) => r.issue === 'ok').length, ko: rs.filter((r) => r.issue === 'ko').length, er: rs.filter((r) => r.issue === 'er').length });
286
+
264
287
  const releves = {
265
288
  /**
266
289
  * Consigner une observation.
@@ -285,6 +308,8 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
285
308
  },
286
309
  of: (campagneId) => repo.releves.find((r) => r.campagneId === campagneId),
287
310
  duFeuillet: (feuilletId) => repo.releves.find((r) => r.feuilletId === feuilletId),
311
+ /** Le relevé QUI FAIT FOI pour chaque ligne d'un feuillet : le dernier consigné (voir `derniers`). */
312
+ derniersDuFeuillet: async (feuilletId) => derniers(await repo.releves.find((r) => r.feuilletId === feuilletId)),
288
313
  };
289
314
 
290
315
  const anomalies = {
@@ -368,14 +393,16 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
368
393
  if (obstacles.length && !force) throw new Error(`figement refusé — ${obstacles.join(' · ')}`);
369
394
 
370
395
  const fs = await feuillets.of(campagneId);
371
- const rs = await releves.of(campagneId);
396
+ const tous = await releves.of(campagneId);
397
+ const rs = derniers(tous);
372
398
  const as = await anomalies.of(campagneId);
373
399
  const contenu = {
374
400
  campagne: { no: c.no, releaseRef: c.releaseRef, projet: c.projet, perimetre: c.perimetre, instanceUrl: c.instanceUrl },
375
401
  feuillets: fs.map((f) => ({ role: f.role, langue: f.langue, state: f.state, planVersion: f.planVersion })),
376
402
  releves: rs.map((r) => ({ ref: r.ref, issue: r.issue, at: r.at, source: r.source, confiance: r.confiance, texte: r.texte })),
377
403
  anomalies: as.map((a) => ({ titre: a.titre, graviteProposee: a.graviteProposee, graviteQualifiee: a.graviteQualifiee, state: a.state, bugRef: a.bugRef, motifEcart: a.motifEcart })),
378
- compteurs: { ok: rs.filter((r) => r.issue === 'ok').length, ko: rs.filter((r) => r.issue === 'ko').length, er: rs.filter((r) => r.issue === 'er').length },
404
+ compteurs: compter(rs),
405
+ relevesRemplaces: tous.length - rs.length,
379
406
  obstaclesAuFigement: obstacles, // consignés même en `force` : un dossier dit ce qu'il vaut
380
407
  };
381
408
  const empreinte = createHash('sha256').update(JSON.stringify(contenu)).digest('hex');
@@ -499,12 +526,12 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
499
526
  async function etat(campagneId) {
500
527
  const c = await getCampagne(campagneId);
501
528
  const fs = await feuillets.of(campagneId);
502
- const rs = await releves.of(campagneId);
529
+ const rs = derniers(await releves.of(campagneId));
503
530
  const as = await anomalies.of(campagneId);
504
531
  return {
505
532
  campagne: { no: c.no, state: c.state, releaseRef: c.releaseRef },
506
533
  feuillets: { remis: fs.length, rendus: fs.filter((f) => f.state !== 'remis').length },
507
- releves: { ok: rs.filter((r) => r.issue === 'ok').length, ko: rs.filter((r) => r.issue === 'ko').length, er: rs.filter((r) => r.issue === 'er').length },
534
+ releves: compter(rs),
508
535
  anomalies: { declarees: as.filter((a) => a.state === 'declaree').length, bloquantes: as.filter((a) => a.graviteQualifiee === 'bloquante').length, total: as.length },
509
536
  obstacles: await obstaclesAuDepot(campagneId),
510
537
  };