PD-278 — Agent Developer Report: dip-state-machine¶
Module :
dip-state-machineAgent : agent-developer Date : 2026-03-01 Fichier modifie :src/modules/destruction/services/document-state-machine.service.ts
1. Travail realise¶
1.1 Modification de la matrice ALLOWED_TRANSITIONS¶
Extension de la ReadonlyMap ALLOWED_TRANSITIONS avec deux nouvelles entrees :
| Modification | Detail |
|---|---|
| SEALED targets | Ajout de DocumentStatus.DIP aux cibles autorisees (existantes : EXPIRED, RESTITUTED) |
| Nouvelle entree DIP | [DocumentStatus.DIP, new Set([DocumentStatus.SEALED])] — seule transition sortante autorisee |
1.2 Mise a jour de la documentation JSDoc¶
La documentation de la matrice a ete enrichie avec : - Les transitions nominales PD-278 (SEALED -> DIP, DIP -> SEALED) - Les transitions interdites PD-278 (PENDING -> DIP, DIP -> EXPIRED, DIP -> PENDING, EXPIRED -> DIP) - Les references aux invariants (INV-278-08, INV-278-09)
1.3 Aucune modification des mecanismes existants¶
validateTransition(): logique inchangee (lookup dans la map + early returns terminaux)applyTransition(): logique inchangee (validation + UPDATE SQL atomique)InvalidStateTransitionError: classe inchangee- Exports
DESTROYED,RECONCILIATION_FAILED,ExtendedDocumentStatus: inchanges
2. Verification des invariants¶
INV-278-09 : SEALED -> DIP et DIP -> SEALED sont les seules nouvelles transitions¶
Conforme. La map ALLOWED_TRANSITIONS ajoute exactement : - DIP dans les cibles de SEALED (ligne 66) - [DIP, Set([SEALED])] comme nouvelle entree (ligne 67)
Aucune autre transition nouvelle n'est ajoutee.
INV-278-08 : EXPIRED reste terminal strict (aucune transition sortante applicative)¶
Conforme. L'entree EXPIRED reste :
DESTROYED et RECONCILIATION_FAILED (PD-250). DIP n'est PAS dans les cibles de EXPIRED. De plus, la logique validateTransition applique une garde explicite sur les etats terminaux (DESTROYED, RECONCILIATION_FAILED) avant meme de consulter la map. EXPIRED n'a pas cette garde car il a des transitions sortantes (vers DESTROYED/RECONCILIATION_FAILED), mais aucune vers DIP.
Transitions explicitement INTERDITES¶
| Transition interdite | Mecanisme de blocage |
|---|---|
PENDING -> DIP | PENDING n'a que SEALED dans ses cibles |
DIP -> EXPIRED | DIP n'a que SEALED dans ses cibles |
DIP -> PENDING | DIP n'a que SEALED dans ses cibles |
DIP -> DIP | Self-transition bloquee par garde current === target (ligne 99) |
EXPIRED -> DIP | EXPIRED n'a que DESTROYED et RECONCILIATION_FAILED dans ses cibles |
DESTROYED -> DIP | Garde explicite early return (ligne 111) |
RECONCILIATION_FAILED -> DIP | Garde explicite early return (ligne 103) |
RESTITUTED -> DIP | RESTITUTED n'a que SEALED dans ses cibles |
3. Verification des interdits (forbidden)¶
"Modifier les transitions existantes PD-250/PD-279"¶
Conforme. Toutes les transitions pre-existantes sont preservees sans modification :
| Transition existante | Statut |
|---|---|
PENDING -> SEALED | Inchangee |
SEALED -> EXPIRED | Inchangee |
SEALED -> RESTITUTED (PD-279) | Inchangee |
RESTITUTED -> SEALED (PD-279) | Inchangee |
EXPIRED -> DESTROYED (PD-250) | Inchangee |
EXPIRED -> RECONCILIATION_FAILED (PD-250) | Inchangee |
Preuve : les 93 tests existants (59 unit + 34 contractuels) passent tous apres la modification.
"Ajouter une transition sortante de EXPIRED"¶
Conforme. L'entree EXPIRED dans la map n'a pas ete modifiee. Ses cibles restent {DESTROYED, RECONCILIATION_FAILED}.
"Transition implicite timer/cron pour DIP -> SEALED"¶
Conforme. Aucun scheduler, cron, ou timer n'est ajoute. La transition DIP -> SEALED n'est possible que via appel explicite a validateTransition / applyTransition.
4. Non-regression¶
Tests executes¶
| Suite de tests | Resultat | Tests |
|---|---|---|
document-state-machine.service.spec.ts | 59/59 PASS | Transitions autorisees, interdites, self-transitions, exhaustive matrix 5x5, applyTransition SQL |
contractual/state-transitions.spec.ts | 34/34 PASS | PD-250 + PD-279 transitions, RESTITUTED, terminal states, matrix 6x6 |
| Total | 93/93 PASS | Aucune regression |
Note sur la couverture de la matrice exhaustive¶
Les tests existants couvrent une matrice 5x5 (sans DIP) et 6x6 (sans DIP). L'ajout de DIP dans la matrice exhaustive des tests est de la responsabilite du module dip-tests (agent-tests). Les tests existants prouvent que les transitions pre-PD-278 ne sont pas alterees.
5. Analyse technique¶
Pourquoi DocumentStatus.DIP est utilise directement (pas un const cast)¶
Le commit 7680095 (deja sur la branche) a ajoute DIP = 'DIP' dans l'enum DocumentStatus du fichier document-secure.entity.ts. Contrairement a DESTROYED et RECONCILIATION_FAILED (PD-250) qui sont des const ... as DocumentStatus car ils n'etaient pas dans l'enum TypeScript original, DIP est directement dans l'enum. La reference DocumentStatus.DIP est donc type-safe sans cast.
Pas de modification de l'interface DocumentStateMachineService¶
L'interface publique du service reste identique : - validateTransition(currentStatus, targetStatus): void - applyTransition(queryRunner, documentId, currentStatus, newStatus): Promise<void>
Les consommateurs existants (DocumentPurgeService, RestitutionService) ne necessitent aucune adaptation. Le nouveau DisseminationService (module dip-service) pourra appeler validateTransition(DocumentStatus.SEALED, DocumentStatus.DIP) et applyTransition(...) sans modification.
6. Hypotheses documentees¶
| ID | Hypothese | Impact |
|---|---|---|
| H-SM-01 | L'enum DocumentStatus contient deja DIP (commit 7680095 sur la branche) | Si absent, l'import DocumentStatus.DIP echoue a la compilation |
| H-SM-02 | Les etats DESTROYED et RECONCILIATION_FAILED restent des const cast (PD-250) et ne sont pas integres a l'enum TypeScript | Coherent avec le pattern existant — pas de modification |
7. Decision architecturale¶
architectural_decisions:
- decision: "Ajout de DIP via DocumentStatus.DIP natif (pas de const cast)"
rationale: "DIP est deja dans l'enum TypeScript (commit 7680095), contrairement a DESTROYED/RECONCILIATION_FAILED qui sont des extensions PD-250 hors enum"
alternatives_considered: ["const DIP = 'DIP' as DocumentStatus (pattern PD-250)"]
trade_offs: ["Reference directe = type-safe sans cast, mais depend du commit entity extension"]
8. Matrice de couverture tests¶
| Test ID contractuel | Couverture dans ce module | Fichier existant |
|---|---|---|
| TC-INV-08 (EXPIRED terminal strict) | Couvert par tests existants (93/93 pass) | document-state-machine.service.spec.ts L106-138, state-transitions.spec.ts L76-106 |
| TC-INV-09 (matrice transitions exhaustive) | Partiellement couvert (matrice sans DIP) — extension par module dip-tests | document-state-machine.service.spec.ts L216-246, state-transitions.spec.ts L259-303 |
| TC-ERR-06 (E-409-STATE transitions interdites) | Mecanisme valide par tests interdits existants | document-state-machine.service.spec.ts L63-99 |
| TC-NR-03 (PENDING -> SEALED inchange) | Couvert | state-transitions.spec.ts L195-199 |
| TC-NR-04 (SEALED -> EXPIRED inchange) | Couvert | state-transitions.spec.ts L200-204 |
| TC-NEG-04 (fuzz transitions interdites) | Partiellement couvert (sans DIP) — extension par module dip-tests | state-transitions.spec.ts L282-294 |
9. Fichiers hors perimetre identifies¶
Aucun fichier hors perimetre ne necessite de modification pour ce module. La machine a etats est auto-contenue — les consommateurs (DisseminationService) sont dans le perimetre d'autres agents.
10. Diff synthetique¶
--- a/src/modules/destruction/services/document-state-machine.service.ts
+++ b/src/modules/destruction/services/document-state-machine.service.ts
@@ -43,6 +43,7 @@
* - SEALED → EXPIRED
* - SEALED → RESTITUTED (PD-279 INV-279-07)
+ * - SEALED → DIP (PD-278 INV-278-09)
+ * - DIP → SEALED (PD-278 INV-278-09, retour explicite uniquement)
* - RESTITUTED → SEALED (PD-279 INV-279-07)
* - EXPIRED → DESTROYED
@@ -53,13 +54,20 @@
* - RESTITUTED → PENDING/EXPIRED/DESTROYED (INV-279-07)
+ * - PENDING → DIP (INV-278-09)
+ * - DIP → EXPIRED (INV-278-09)
+ * - DIP → PENDING (INV-278-09)
+ * - EXPIRED → DIP (INV-278-08, etat terminal strict applicatif)
*/
const ALLOWED_TRANSITIONS: ReadonlyMap<string, ReadonlySet<string>> = new Map([
[DocumentStatus.PENDING, new Set([DocumentStatus.SEALED])],
- [DocumentStatus.SEALED, new Set([DocumentStatus.EXPIRED, DocumentStatus.RESTITUTED])],
+ [DocumentStatus.SEALED, new Set([DocumentStatus.EXPIRED, DocumentStatus.RESTITUTED, DocumentStatus.DIP])],
+ [DocumentStatus.DIP, new Set([DocumentStatus.SEALED])],
[DocumentStatus.RESTITUTED, new Set([DocumentStatus.SEALED])],
[DocumentStatus.EXPIRED, new Set([DESTROYED as string, RECONCILIATION_FAILED as string])],
Lignes modifiees : 2 lignes modifiees, 8 lignes ajoutees (JSDoc), 1 ligne ajoutee (map entry). Aucune ligne supprimee.