Aller au contenu

PD-278 — Agent Developer Report: dip-state-machine

Module : dip-state-machine Agent : 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 :

[DocumentStatus.EXPIRED, new Set([DESTROYED as string, RECONCILIATION_FAILED as string])]
Les seules transitions sortantes de EXPIRED restent 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.