@mostajs/recette 0.2.0 → 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 +43 -0
- package/package.json +1 -1
- package/src/recette.js +58 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
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
|
+
|
|
28
|
+
## 0.2.1 — 2026-09-10
|
|
29
|
+
|
|
30
|
+
Correction née d'une panne **en production**, invisible depuis les essais.
|
|
31
|
+
|
|
32
|
+
### Corrigé
|
|
33
|
+
|
|
34
|
+
- **Le contenu du dossier revenait en CHAÎNE** de plusieurs dialectes SQL (sqljs le premier) là
|
|
35
|
+
où un dépôt en mémoire rend l'objet. L'enveloppe exportée transportait donc une chaîne, le
|
|
36
|
+
destinataire recalculait l'empreinte sur autre chose, et **refusait tout dossier** — avec un
|
|
37
|
+
message parlant de falsification. Le moteur normalise désormais à la lecture : `dossiers.of()`
|
|
38
|
+
et l'export rendent toujours un objet.
|
|
39
|
+
- **`dossiers.exporter()` vérifie l'empreinte avant de signer.** Un document dont l'empreinte ne
|
|
40
|
+
recouvre plus le contenu relu est refusé ici, en nommant la cause, plutôt que d'aller se faire
|
|
41
|
+
rejeter chez le destinataire qui, lui, n'aura que « signature ou empreinte invalide » à offrir.
|
|
42
|
+
|
|
43
|
+
### Ajouté aux essais
|
|
44
|
+
|
|
45
|
+
- **T-REC-14** monte un dépôt qui imite sqljs — écriture d'objet, lecture de texte. C'est le cas
|
|
46
|
+
qui manquait : quatorze essais verts et rien ne fonctionnait en ligne.
|
|
47
|
+
|
|
5
48
|
## 0.2.0 — 2026-09-10
|
|
6
49
|
|
|
7
50
|
Le sceau savait fermer un pli ; il ne savait pas l'ouvrir. C'est ce qui manquait pour que QaTrax
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/recette",
|
|
3
|
-
"version": "0.
|
|
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
|
@@ -31,6 +31,17 @@ const rang = (e) => PARCOURS.indexOf(e);
|
|
|
31
31
|
const need = (v, msg) => { if (v === undefined || v === null || v === '') throw new Error(msg); return v; };
|
|
32
32
|
const asDate = (d) => (d ? new Date(d) : null);
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Le CONTENU d'un dossier, quel que soit ce que le stockage a bien voulu en faire.
|
|
36
|
+
*
|
|
37
|
+
* Un champ `json` revient en OBJET d'un dépôt en mémoire, et en CHAÎNE de plusieurs dialectes
|
|
38
|
+
* SQL — sqljs le premier. Sans cette normalisation, l'enveloppe exportée transportait une
|
|
39
|
+
* chaîne là où le destinataire attend un objet, et l'empreinte recalculée à l'arrivée ne
|
|
40
|
+
* pouvait plus correspondre : tout dossier était refusé, pour une raison invisible depuis les
|
|
41
|
+
* essais, qui n'utilisent que des dépôts en mémoire.
|
|
42
|
+
*/
|
|
43
|
+
const contenuDe = (d) => (typeof d?.contenu === 'string' ? JSON.parse(d.contenu) : d?.contenu);
|
|
44
|
+
|
|
34
45
|
export function createRecette({ repositories, ports = {}, clock = () => new Date(), numeroteur } = {}) {
|
|
35
46
|
const repo = need(repositories, 'repositories requis');
|
|
36
47
|
for (const c of ['campagnes', 'points', 'feuillets', 'releves', 'anomalies', 'dossiers', 'certificats']) {
|
|
@@ -250,6 +261,29 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
250
261
|
},
|
|
251
262
|
};
|
|
252
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
|
+
|
|
253
287
|
const releves = {
|
|
254
288
|
/**
|
|
255
289
|
* Consigner une observation.
|
|
@@ -274,6 +308,8 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
274
308
|
},
|
|
275
309
|
of: (campagneId) => repo.releves.find((r) => r.campagneId === campagneId),
|
|
276
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)),
|
|
277
313
|
};
|
|
278
314
|
|
|
279
315
|
const anomalies = {
|
|
@@ -357,14 +393,16 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
357
393
|
if (obstacles.length && !force) throw new Error(`figement refusé — ${obstacles.join(' · ')}`);
|
|
358
394
|
|
|
359
395
|
const fs = await feuillets.of(campagneId);
|
|
360
|
-
const
|
|
396
|
+
const tous = await releves.of(campagneId);
|
|
397
|
+
const rs = derniers(tous);
|
|
361
398
|
const as = await anomalies.of(campagneId);
|
|
362
399
|
const contenu = {
|
|
363
400
|
campagne: { no: c.no, releaseRef: c.releaseRef, projet: c.projet, perimetre: c.perimetre, instanceUrl: c.instanceUrl },
|
|
364
401
|
feuillets: fs.map((f) => ({ role: f.role, langue: f.langue, state: f.state, planVersion: f.planVersion })),
|
|
365
402
|
releves: rs.map((r) => ({ ref: r.ref, issue: r.issue, at: r.at, source: r.source, confiance: r.confiance, texte: r.texte })),
|
|
366
403
|
anomalies: as.map((a) => ({ titre: a.titre, graviteProposee: a.graviteProposee, graviteQualifiee: a.graviteQualifiee, state: a.state, bugRef: a.bugRef, motifEcart: a.motifEcart })),
|
|
367
|
-
compteurs:
|
|
404
|
+
compteurs: compter(rs),
|
|
405
|
+
relevesRemplaces: tous.length - rs.length,
|
|
368
406
|
obstaclesAuFigement: obstacles, // consignés même en `force` : un dossier dit ce qu'il vaut
|
|
369
407
|
};
|
|
370
408
|
const empreinte = createHash('sha256').update(JSON.stringify(contenu)).digest('hex');
|
|
@@ -392,12 +430,21 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
392
430
|
const sceau = ports.sceau;
|
|
393
431
|
if (!sceau?.signer) throw new Error("aucun sceau configuré : un export non signé ne prouve rien, il est refusé");
|
|
394
432
|
|
|
433
|
+
// ON VÉRIFIE AVANT DE SIGNER. Exporter un document dont l'empreinte ne recouvre plus le
|
|
434
|
+
// contenu, c'est fabriquer un refus chez le destinataire — et un refus qu'il mettra sur le
|
|
435
|
+
// compte d'une falsification. Mieux vaut refuser ici, en nommant la cause.
|
|
436
|
+
const contenu = contenuDe(d);
|
|
437
|
+
const verif = createHash('sha256').update(JSON.stringify(contenu)).digest('hex');
|
|
438
|
+
if (verif !== d.empreinte) {
|
|
439
|
+
throw new Error(`empreinte du dossier ${d.no} incohérente avec son contenu relu (${verif.slice(0, 12)}… ≠ ${String(d.empreinte).slice(0, 12)}…) : le stockage n'a pas rendu ce qui lui a été confié`);
|
|
440
|
+
}
|
|
441
|
+
|
|
395
442
|
const enveloppe = {
|
|
396
443
|
type: 'mostajs-recette-dossier/1',
|
|
397
444
|
emisLe: clock().toISOString(),
|
|
398
445
|
campagne: { no: c.no, releaseRef: c.releaseRef },
|
|
399
446
|
dossier: { no: d.no, empreinte: d.empreinte, figeLe: d.figeLe },
|
|
400
|
-
contenu
|
|
447
|
+
contenu,
|
|
401
448
|
};
|
|
402
449
|
const charge = JSON.stringify(enveloppe);
|
|
403
450
|
const signature = await sceau.signer(charge);
|
|
@@ -416,7 +463,12 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
416
463
|
const d = need(await dossiers.of(campagneId), 'aucun dossier pour cette campagne');
|
|
417
464
|
return repo.dossiers.update(d.id, { depotRef: need(depotRef, 'depotRef requis') });
|
|
418
465
|
},
|
|
419
|
-
of: async (campagneId) =>
|
|
466
|
+
of: async (campagneId) => {
|
|
467
|
+
const d = (await repo.dossiers.find((x) => x.campagneId === campagneId))[0] || null;
|
|
468
|
+
// On rend TOUJOURS un objet : l'appelant n'a pas à connaître le dialecte SQL sous-jacent
|
|
469
|
+
// pour relire le contenu du dossier qu'il vient de figer.
|
|
470
|
+
return d ? { ...d, contenu: contenuDe(d) } : null;
|
|
471
|
+
},
|
|
420
472
|
};
|
|
421
473
|
|
|
422
474
|
const certificats = {
|
|
@@ -474,12 +526,12 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
474
526
|
async function etat(campagneId) {
|
|
475
527
|
const c = await getCampagne(campagneId);
|
|
476
528
|
const fs = await feuillets.of(campagneId);
|
|
477
|
-
const rs = await releves.of(campagneId);
|
|
529
|
+
const rs = derniers(await releves.of(campagneId));
|
|
478
530
|
const as = await anomalies.of(campagneId);
|
|
479
531
|
return {
|
|
480
532
|
campagne: { no: c.no, state: c.state, releaseRef: c.releaseRef },
|
|
481
533
|
feuillets: { remis: fs.length, rendus: fs.filter((f) => f.state !== 'remis').length },
|
|
482
|
-
releves:
|
|
534
|
+
releves: compter(rs),
|
|
483
535
|
anomalies: { declarees: as.filter((a) => a.state === 'declaree').length, bloquantes: as.filter((a) => a.graviteQualifiee === 'bloquante').length, total: as.length },
|
|
484
536
|
obstacles: await obstaclesAuDepot(campagneId),
|
|
485
537
|
};
|