@mostajs/recette 0.3.0 → 0.4.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,33 @@
2
2
 
3
3
  **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
4
 
5
+ ## 0.4.0 — 2026-10-23
6
+
7
+ ### Corrigé
8
+
9
+ - **Le feu s'allumait sans certificat.** `campagnes.avancer` menait de « validation » à « figee »,
10
+ « certifiee » puis « exploitation » à qui cliquait trois fois — constaté le 23/10/2026 sur
11
+ REC-2026-0001, sans un point, ni dossier, ni certificat. Avancer à la main s'arrête désormais à
12
+ « validation » ; les trois étapes suivantes ne s'atteignent que par leur acte (`dossiers.figer`,
13
+ `certificats.recevoir`, `autoriserExploitation`). **T-REC-16**.
14
+ - Une campagne arrivée en « exploitation » **sans certificat** peut être abandonnée : elle n'a jamais
15
+ été autorisée. Avec certificat, elle ne s'abandonne toujours pas. **T-REC-17**.
16
+
17
+ ### Ajouté
18
+
19
+ - **`campagnes.reculer(id, { by, motif })`** — revenir d'une étape (validation → recette →
20
+ verification → candidate) tant que le dossier n'est pas figé ; l'historique garde le retour et son
21
+ motif. **T-REC-17**.
22
+ - **`rapportEtape(campagneId, { declaration: 'partiel' | 'urgent', sceller, note })`** — le rapport
23
+ d'étape SIGNÉ (`mostajs-recette-rapport/1`) : relevés qui font foi, avec l'exigence, l'action,
24
+ l'attendu, la remarque et l'heure ; anomalies ; compteurs. Il ne fige rien et ne demande aucun PV :
25
+ il sert à CORRIGER pendant que la recette continue. `declaration: 'fini'` accompagne le dossier
26
+ final. `pieces` décrit les pièces jointes et captures d'écran (cible, nom, type, taille, SHA-256)
27
+ sous la signature ; leur contenu voyage à côté. **T-REC-18**.
28
+
29
+ > **Rupture** : `campagnes.avancer(id, 'figee' | 'certifiee' | 'exploitation')` lève désormais une
30
+ > erreur. Un appelant qui s'en servait doit passer par l'acte correspondant.
31
+
5
32
  ## 0.3.0 — 2026-10-23
6
33
 
7
34
  ### Corrigé
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/recette",
3
- "version": "0.3.0",
3
+ "version": "0.4.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
@@ -62,7 +62,11 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
62
62
  if (vers === 'abandonnee') {
63
63
  // On abandonne de partout, sauf une fois l'exploitation autorisée : à ce moment le logiciel
64
64
  // est en production, et « abandonner la recette » ne veut plus rien dire.
65
- if (campagne.state === 'exploitation') throw new Error("l'exploitation est autorisée : la campagne ne s'abandonne plus");
65
+ // 0.4.0 — SAUF si elle y est arrivée SANS certificat : avant cette version, `campagnes.avancer` menait de
66
+ // « validation » à « exploitation » par trois clics (constaté le 23/10/2026, REC-2026-0001, 0 point). Une telle
67
+ // campagne n'a jamais été autorisée ; la garder irrécupérable serait garder l'erreur.
68
+ if (campagne.state === 'exploitation' && (await certificats.of(campagne.id))) throw new Error("l'exploitation est autorisée : la campagne ne s'abandonne plus");
69
+ if (campagne.state === 'abandonnee') throw new Error('la campagne est déjà abandonnée');
66
70
  } else {
67
71
  const de = rang(campagne.state), a = rang(vers);
68
72
  if (a < 0) throw new Error(`étape inconnue : ${vers}`);
@@ -96,7 +100,37 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
96
100
  },
97
101
  get: getCampagne,
98
102
  list: (filtre = () => true) => repo.campagnes.find(filtre),
99
- avancer: async (id, vers, opts) => avancer(await getCampagne(id), vers, opts),
103
+ /**
104
+ * AVANCER À LA MAIN — seulement les étapes de TRAVAIL : verification, recette, validation (et l'abandon).
105
+ *
106
+ * 0.4.0 : « figee », « certifiee » et « exploitation » ne se franchissent QUE par leur acte — figer le dossier,
107
+ * recevoir le certificat, autoriser l'exploitation. Avant, ce verbe public les accordait à qui cliquait trois fois,
108
+ * et le feu « dérivé du certificat » s'allumait sans certificat.
109
+ */
110
+ avancer: async (id, vers, opts) => {
111
+ const ACTE = { figee: '« figer le dossier »', certifiee: '« recevoir le certificat »', exploitation: '« autoriser l’exploitation » (avec un certificat qui la permet)' };
112
+ if (ACTE[vers]) throw new Error(`« ${vers} » ne s'atteint pas en avançant : il faut ${ACTE[vers]}`);
113
+ return avancer(await getCampagne(id), vers, opts);
114
+ },
115
+ /**
116
+ * REVENIR D'UNE ÉTAPE (0.4.0) — tant que le dossier n'est pas figé : validation → recette → verification → candidate.
117
+ * Une recette relancée parce qu'une correction est arrivée n'est pas une faute ; l'historique garde le retour et son motif.
118
+ */
119
+ async reculer(id, { by = null, motif = '' } = {}) {
120
+ const c = await getCampagne(id);
121
+ const de = rang(c.state);
122
+ if (!['verification', 'recette', 'validation'].includes(c.state)) {
123
+ throw new Error(`on ne revient pas en arrière depuis « ${c.state} »${rang(c.state) >= rang('figee') ? ' : le dossier est figé, ouvrez une nouvelle campagne' : ''}`);
124
+ }
125
+ const vers = PARCOURS[de - 1];
126
+ const at = clock();
127
+ const out = await repo.campagnes.update(c.id, {
128
+ state: vers,
129
+ stateHistory: [...(c.stateHistory || []), { from: c.state, to: vers, at, by, reason: `retour : ${motif || 'sans motif'}` }],
130
+ });
131
+ await trace('recette.campagne.reculer', by, { campagneId: c.id, from: c.state, to: vers, motif });
132
+ return out;
133
+ },
100
134
  };
101
135
 
102
136
  /** Les champs que l'import a le droit d'écrire. Tout le reste appartient à ValiTrax. */
@@ -537,5 +571,56 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
537
571
  };
538
572
  }
539
573
 
540
- return { campagnes, trame, points, feuillets, releves, anomalies, dossiers, certificats, autoriserExploitation, etat, PARCOURS };
574
+ /**
575
+ * LE RAPPORT D'ÉTAPE (0.4.0) — ce que le client déclare EN COURS de recette : « partiellement fini », ou « l'urgent est
576
+ * fini ». Il part vers QaTrax SIGNÉ (et scellé si son destinataire est configuré), comme le dossier ; mais il ne fige rien
577
+ * et ne demande aucun PV : il sert à CORRIGER — chaque `ko`, chaque `er`, chaque anomalie y devient un défaut à traiter
578
+ * pendant que la recette continue. Le dossier final, lui, reste l'affaire de `dossiers.figer` puis `exporter`.
579
+ */
580
+ // « fini » accompagne le dossier final : le dossier porte le PV, le rapport porte de quoi corriger (et les pièces).
581
+ const DECLARATIONS = ['partiel', 'urgent', 'fini'];
582
+ /**
583
+ * @param {Array} [pieces] les pièces jointes DÉCRITES — `{ cible: '<réf>' | 'anomalie:<n>', nom, type, taille, sha256 }`.
584
+ * Leur CONTENU voyage à côté de l'enveloppe ; la signature couvre leurs empreintes : une pièce échangée en chemin
585
+ * ne correspond plus.
586
+ */
587
+ async function rapportEtape(campagneId, { declaration, by = null, sceller = false, note = '', pieces = [] } = {}) {
588
+ const c = await getCampagne(campagneId);
589
+ if (!DECLARATIONS.includes(declaration)) throw new Error(`déclaration inconnue : ${declaration} (attendu : ${DECLARATIONS.join(' ou ')})`);
590
+ if (!['verification', 'recette', 'validation'].includes(c.state)) throw new Error(`un rapport d'étape s'envoie pendant la recette (la campagne est « ${c.state} »)`);
591
+ const sceau = ports.sceau;
592
+ if (!sceau?.signer) throw new Error("aucun sceau configuré : un rapport non signé ne prouve rien, il est refusé");
593
+ const fs = await feuillets.of(campagneId);
594
+ const rs = derniers(await releves.of(campagneId));
595
+ const as = await anomalies.of(campagneId);
596
+ const ps = await points.of(campagneId);
597
+ const parRef = new Map(ps.map((p) => [p.ref, p]));
598
+ const enveloppe = {
599
+ type: 'mostajs-recette-rapport/1',
600
+ emisLe: clock().toISOString(),
601
+ declaration, note: String(note || ''),
602
+ campagne: { no: c.no, releaseRef: c.releaseRef, projet: c.projet, instanceUrl: c.instanceUrl, state: c.state },
603
+ feuillets: fs.map((f) => ({ role: f.role, destinataire: f.destinataire || '', state: f.state })),
604
+ // Chaque relevé porte de quoi le CORRIGER : l'exigence, ce que le testeur devait faire et voir, sa remarque, l'heure.
605
+ releves: rs.map((r) => {
606
+ const p = parRef.get(r.ref);
607
+ return { ref: r.ref, issue: r.issue, at: r.at, texte: r.texte || '', specRef: p?.specRef || null,
608
+ action: p?.libelle || p?.titreTrame || '', attendu: p?.attendu || (p?.etapesTrame || []).map((e) => e.expected).filter(Boolean).join(' · ') };
609
+ }),
610
+ anomalies: as.map((a) => ({ titre: a.titre, description: a.description || '', graviteProposee: a.graviteProposee, graviteQualifiee: a.graviteQualifiee, state: a.state, declareePar: a.declareePar })),
611
+ compteurs: { ...compter(rs), retenus: ps.filter((p) => p.retenu).length },
612
+ pieces: (pieces || []).map((x) => ({ cible: String(x.cible || ''), nom: String(x.nom || ''), type: String(x.type || ''), taille: Number(x.taille) || 0, sha256: String(x.sha256 || '') })),
613
+ };
614
+ for (const x of enveloppe.pieces) if (!/^[0-9a-f]{64}$/.test(x.sha256)) throw new Error(`pièce « ${x.nom} » : empreinte SHA-256 absente ou invalide`);
615
+ const signature = await sceau.signer(JSON.stringify(enveloppe));
616
+ let corps = { enveloppe, signature };
617
+ if (sceller) {
618
+ if (!sceau.sceller) throw new Error("scellement demandé mais aucun destinataire configuré (clé publique du certificateur)");
619
+ corps = { type: 'mostajs-recette-scelle/1', scelle: await sceau.sceller(JSON.stringify(corps)) };
620
+ }
621
+ await trace('recette.rapport.etape', by, { campagneId, declaration });
622
+ return corps;
623
+ }
624
+
625
+ return { campagnes, trame, points, feuillets, releves, anomalies, dossiers, certificats, autoriserExploitation, rapportEtape, etat, PARCOURS };
541
626
  }