Aller au contenu

PD-278 — Décomposition multi-agents (étape 6a)

Phase 0 — Validation préalable

H-06 : geo_copy_count ✅ VALIDÉ

  • geo_copy_count INTEGER NOT NULL DEFAULT 0 existe dans DocumentSecure entity (ajouté par PD-279, DONE).
  • La spec PD-278 §3 définit copies = "nombre de copies durables sur domaines de panne distincts, fourni par la couche stockage" — sémantiquement identique à geo_copy_count.

H-01 : Stack technique ✅ VALIDÉ

  • NestJS + TypeORM + PostgreSQL confirmé.
  • Schéma : vault_secure (documents), vault_integrity (journal).

V-01 : Enum DB 5 valeurs ✅ CONFIRMÉ

  • DocumentStatus enum TypeScript = {PENDING, SEALED, EXPIRED, RESTITUTED}.
  • Constantes hors enum : DESTROYED, RECONCILIATION_FAILED.
  • PD-278 ajoutera DIP → l'enum PostgreSQL contiendra au moins 5 valeurs.
  • Le test TC-INV-01 doit vérifier DIP ∈ enum_range, pas |enum| = 4.

V-03 : ALTER TYPE hors transaction ✅ CONFIRMÉ

  • Pattern PD-279 : ALTER TYPE vault_secure.document_status ADD VALUE IF NOT EXISTS 'RESTITUTED' — hors bloc BEGIN/COMMIT.

H-08 : BYPASSRLS retention_service → à vérifier implémentation mais documenté comme pattern PD-16.

TLA+ : ✅ DIP déjà dans le modèle formel

  • DocStates == {"SIP", "AIP", "DIP", "ELIMINATED"} — DIP est déjà présent dans NF_Z42_013.tla.
  • RealStates == {"SIP", "AIP", "DIP", "ELIMINATED", "RESTITUTED", "DESTROYED", "RECONCILIATION_FAILED"} — DIP aussi.
  • CA-10 est déjà satisfait au niveau TLA+. L'action CommunicateAIP et ReturnDIP existent.

Note migration timestamp

  • Le timestamp 1741400000000 est déjà pris par PD-280 (AddProofVerificationJobs).
  • Le fichier de migration PD-278 doit utiliser un timestamp supérieur, par exemple 1741500000000.

Découpage en tâches agents

Vue d'ensemble

# Agent ID Module Fichier(s) cible(s) Dépendances
1 agent-migration C1 src/database/migrations/1741500000000-PD278-AddDipState.ts Aucune
2 agent-entity C2 src/modules/documents/entities/document-secure.entity.ts + src/modules/documents/entities/dissemination-attestation.entity.ts Agent 1
3 agent-state-machine C3 src/modules/destruction/services/document-state-machine.service.ts Agent 2
4 agent-config C4 src/modules/documents/config/dissemination.config.ts Aucune
5 agent-dto C5 src/modules/documents/dto/create-dissemination.dto.ts + src/modules/documents/dto/dissemination-response.dto.ts + src/modules/documents/dto/dissemination-error.dto.ts Agent 4
6 agent-audit C6 src/modules/audit/types/audit-action.types.ts Aucune
7 agent-journal C7 src/modules/integrity/enums/journal-event-type.enum.ts Aucune
8 agent-rate-limit C8 src/modules/documents/guards/dissemination-rate-limit.guard.ts Agents 4, 6
9 agent-core C9 src/modules/documents/services/dissemination.service.ts Agents 2, 3, 4, 5, 6, 7
10 agent-controller C10 src/modules/documents/controllers/dissemination.controller.ts Agents 5, 8, 9
11 agent-filter C11 src/modules/documents/filters/dissemination-audit-exception.filter.ts Agent 6
12 agent-module src/modules/documents/documents.module.ts Agents 2, 4, 8, 9, 10, 11
13 agent-formal C12 ProbatioVault-doc/docs/normes/nf-z42-013/formal/NF_Z42_013.tla Aucune
14 agent-tests C13 src/modules/documents/**/*.spec.ts Tous

Séquencement des phases

Phase 1 — Fondations (parallélisable)
  ├── Agent 1 (migration)        ─┐
  ├── Agent 4 (config)           ─┤ Indépendants entre eux
  ├── Agent 5 (dto)              ─┤
  ├── Agent 6 (audit types)      ─┤
  ├── Agent 7 (journal types)    ─┤
  └── Agent 13 (TLA+ formel)    ─┘

Phase 2 — Entities + State machine (séquentiel après Phase 1)
  ├── Agent 2 (entity)           ← dépend Agent 1
  └── Agent 3 (state machine)    ← dépend Agent 2

Phase 3 — Core métier (après Phase 2, partiellement parallélisable)
  ├── Agent 8 (rate-limit)       ← dépend Agents 4, 6
  ├── Agent 9 (service core)     ← dépend Agents 2, 3, 4, 5, 6, 7
  └── Agent 11 (filter)          ← dépend Agent 6

Phase 4 — Assemblage (après Phase 3)
  ├── Agent 10 (controller)      ← dépend Agents 5, 8, 9
  └── Agent 12 (module)          ← dépend Agents 2, 4, 8, 9, 10, 11

Phase 5 — Tests (après Phase 4)
  └── Agent 14 (tests)           ← dépend Tous

Tâche 1 : agent-migration (C1)

Module : dip-migration Fichier : src/database/migrations/1741500000000-PD278-AddDipState.ts

Mission : Créer la migration DDL pour l'état DIP.

Séquence DDL (up) : 1. ALTER TYPE vault_secure.document_status ADD VALUE IF NOT EXISTS 'DIP'HORS transaction 2. ALTER TABLE vault_secure.documents ADD COLUMN IF NOT EXISTS disseminated_at TIMESTAMPTZ NULL 3. ALTER TABLE vault_secure.documents ADD COLUMN IF NOT EXISTS disseminated_by UUID NULL 4. ALTER TABLE vault_secure.documents ADD COLUMN IF NOT EXISTS dissemination_returned_at TIMESTAMPTZ NULL 5. ALTER TABLE vault_secure.documents ADD COLUMN IF NOT EXISTS dissemination_package_id UUID NULL 6. ALTER TABLE vault_secure.documents ADD COLUMN IF NOT EXISTS motif_communication VARCHAR(1024) NULL 7. ALTER TABLE vault_secure.documents ADD COLUMN IF NOT EXISTS retention_due BOOLEAN NOT NULL DEFAULT false 8. CHECK constraint temporel : chk_dip_temporal_orderdissemination_returned_at IS NULL OR disseminated_at IS NULL OR dissemination_returned_at >= disseminated_at 9. Trigger WORM motif_communication : trg_motif_communication_worm via trg_motif_communication_immutable() 10. Index partiel : idx_documents_dip_status ON vault_secure.documents(status) WHERE status = 'DIP' 11. Index partiel : idx_documents_retention_due ON vault_secure.documents(retention_due) WHERE retention_due = true 12. Index : idx_documents_dissemination_package ON vault_secure.documents(dissemination_package_id) WHERE dissemination_package_id IS NOT NULL 13. Table vault_secure.dissemination_attestations (non partitionnée pour simplifier — dette technique documentée avec story destination) 14. Index attestations : idx_attestations_actor, idx_attestations_issued_at, idx_attestations_package

Down migration : - Guard : COUNT(*) WHERE status = 'DIP' > 0 → throw Error - DROP table dissemination_attestations - DROP trigger + function - DROP CHECK constraint - DROP colonnes - DROP indexes - Ne pas retirer la valeur enum (PostgreSQL ne supporte pas DROP VALUE)

Pattern de référence : 1741300000000-PD279-AddRestitutedStatus.ts

Contrats : - ❌ ALTER TYPE ADD VALUE dans un bloc BEGIN/COMMIT - ❌ Subquery dans clause WHERE d'un index partiel - ❌ Math.random() pour tout identifiant - ✅ IF NOT EXISTS pour idempotence


Tâche 2 : agent-entity (C2)

Module : dip-entity-extension + dip-attestation Fichiers : - src/modules/documents/entities/document-secure.entity.ts (modification) - src/modules/documents/entities/dissemination-attestation.entity.ts (création)

Mission : Étendre l'entity DocumentSecure et créer l'entity DisseminationAttestation.

Modifications DocumentSecure : 1. Ajouter DIP = 'DIP' dans l'enum DocumentStatus 2. Ajouter les colonnes TypeORM : - @Column({ type: 'timestamptz', nullable: true }) disseminated_at: Date | null - @Column({ type: 'uuid', nullable: true }) disseminated_by: string | null - @Column({ type: 'timestamptz', nullable: true }) dissemination_returned_at: Date | null - @Column({ type: 'uuid', nullable: true }) dissemination_package_id: string | null - @Column({ type: 'varchar', length: 1024, nullable: true }) motif_communication: string | null - @Column({ type: 'boolean', default: false }) retention_due: boolean

Création DisseminationAttestation : - Entity TypeORM conforme au schéma §5.13 de la spec - Champs : id, attestation_id, request_id, document_ids (UUID[]), actor_id, issued_at, motif_communication, dissemination_package_id, hash_evidence, signature_ref, created_at

Contrats : - ❌ Modifier les colonnes existantes de DocumentSecure - ❌ Supprimer/renommer des colonnes PD-279 - ❌ UPDATE sur hash_evidence ou signature_ref (WORM)


Tâche 3 : agent-state-machine (C3)

Module : dip-state-machine Fichier : src/modules/destruction/services/document-state-machine.service.ts (modification)

Mission : Ajouter les transitions DIP dans la machine à états.

Modifications : - ALLOWED_TRANSITIONS : ajouter DIP dans la valeur du set de SEALED : new Set([DocumentStatus.EXPIRED, DocumentStatus.RESTITUTED, DocumentStatus.DIP]) - Ajouter nouvelle entrée : [DocumentStatus.DIP, new Set([DocumentStatus.SEALED])]

Pattern existant :

const ALLOWED_TRANSITIONS: ReadonlyMap<string, ReadonlySet<string>> = new Map([
  [DocumentStatus.PENDING, new Set([DocumentStatus.SEALED])],
  [DocumentStatus.SEALED, new Set([DocumentStatus.EXPIRED, DocumentStatus.RESTITUTED, DocumentStatus.DIP])],  // +DIP
  [DocumentStatus.RESTITUTED, new Set([DocumentStatus.SEALED])],
  [DocumentStatus.DIP, new Set([DocumentStatus.SEALED])],  // NOUVEAU
  [DocumentStatus.EXPIRED, new Set([DESTROYED, RECONCILIATION_FAILED])],
  [DESTROYED, new Set<string>()],
  [RECONCILIATION_FAILED, new Set<string>()],
]);

Contrats : - ❌ Modifier les transitions existantes PD-250/PD-279 - ❌ Ajouter une transition sortante de EXPIRED - ❌ Transition implicite timer/cron pour DIP → SEALED


Tâche 4 : agent-config (C4)

Module : dip-config Fichier : src/modules/documents/config/dissemination.config.ts (création)

Mission : Créer la configuration DIP avec validation Joi.

Bornes : | Paramètre | Env var | Default | Min | Max | |---|---|---:|---:|---:| | minCopies | DIP_MIN_COPIES | 2 | 2 | 10 | | maxPackageSize | DIP_MAX_PACKAGE_SIZE | 100 | 1 | 100 | | slaTargetSealedToDipMs | SLA_TARGET_SEALED_TO_DIP_MS | 2000 | 200 | 5000 | | slaTargetDipToSealedMs | SLA_TARGET_DIP_TO_SEALED_MS | 1500 | 200 | 5000 | | slaHardTimeoutMs | SLA_HARD_TIMEOUT_MS | 5000 | 1000 | 30000 | | rateLimitPerMinute | DIP_RATE_LIMIT_PER_MIN | 60 | 1 | 1000 | | quotaPerDay | DIP_QUOTA_PER_DAY | 1000 | 1 | 100000 |

Pattern : restitution.config.ts — interface + Joi schema + registerAs('dissemination', ...) + fail-fast au démarrage.

Contrats : - ❌ Valeurs magiques en dur dans le code - ❌ Clamping silencieux de valeurs hors bornes


Tâche 5 : agent-dto (C5)

Module : dip-dto Fichiers : - src/modules/documents/dto/create-dissemination.dto.ts (création) - src/modules/documents/dto/dissemination-response.dto.ts (création) - src/modules/documents/dto/dissemination-error.dto.ts (création)

Mission : Créer les DTOs de requête/réponse pour les endpoints DIP.

CreateDisseminationDto : - documents: string[]@IsArray(), @ArrayMinSize(1), @ArrayMaxSize(100), @IsUUID('4', { each: true }) - motif_communication?: string@IsOptional(), @IsString(), @MaxLength(1024) - Validation interdite en entrée : dissemination_package_id, disseminated_at, dissemination_returned_at - Utiliser @ApiProperty() avec descriptions/exemples

DisseminationResponseDto : - attestation_id, document_ids, disseminated_at, dissemination_package_id, etc.

DisseminationErrorDto : - Enum de codes d'erreur contractuels : E-400-ID-FORMAT, E-400-READONLY-FIELD, E-401-AUTH, E-403-ROLE, E-403-RLS, E-409-STATE, E-409-CONFLICT, E-409-TEMPORAL-ORDER, E-409-RETENTION-DUE, E-422-GUARD-COPIES, E-422-PACKAGE-SIZE, E-422-MOTIF-LENGTH, E-429-RATE-LIMIT, E-500-ATTESTATION, E-500-AUDIT, E-503-REDIS

Contrats : - ❌ Accepter des timestamps client - ❌ Accepter dissemination_package_id en entrée


Tâche 6 : agent-audit (C6)

Module : dip-audit-types Fichier : src/modules/audit/types/audit-action.types.ts (modification)

Mission : Ajouter 3 nouveaux types d'action audit.

Ajouts dans AuditActionType :

DOCUMENT_DISSEMINATED = 'document.disseminated',
DOCUMENT_RETURNED_FROM_DISSEMINATION = 'document.returned_from_dissemination',
DOCUMENT_DISSEMINATION_DENIED = 'document.dissemination_denied',

Nommage : Suivre le pattern existant domain.action (ex: document.restitute).

Contrats : - ❌ Modifier ou supprimer des AuditActionType existants


Tâche 7 : agent-journal (C7)

Module : dip-journal-types Fichier : src/modules/integrity/enums/journal-event-type.enum.ts (modification)

Mission : Ajouter 2 nouveaux types d'événement journal.

Ajouts dans JournalEventType :

DIP_DISSEMINATED = 'DIP_DISSEMINATED',
DIP_RETURNED = 'DIP_RETURNED',

Contrats : - ❌ Modifier ou supprimer des JournalEventType existants


Tâche 8 : agent-rate-limit (C8)

Module : dip-rate-limit Fichier : src/modules/documents/guards/dissemination-rate-limit.guard.ts (création)

Mission : Créer le guard rate-limit + quota journalier DIP.

Mécanisme : - Clé rate-limit : dip:rate:{actorId} — Redis INCR + EXPIRE 60s - Clé quota journalier : dip:quota:{actorId}:{YYYY-MM-DD} — Redis INCR + EXPIRE 86400s - Si Redis indisponible → fail-closed 503 E-503-REDIS - Rejet 429 avec Retry-After header

Pattern : LegalRateLimitGuard (PD-81).

Différences avec le pattern : - Ajouter le quota journalier (double compteur Redis) - Scope controller uniquement (pas APP_GUARD global) - Audit 429 délégué au filter C11 (pas dans le guard)

Contrats : - ❌ Enregistrer en APP_GUARD global - ❌ Fail-open si Redis down - ❌ Rate-limit par document_id


Tâche 9 : agent-core (C9)

Module : dip-service Fichier : src/modules/documents/services/dissemination.service.ts (création)

Mission : Implémenter le service core DIP — orchestration SEALED↔DIP.

Méthode disseminate(dto, actorId) : 1. Validation cardinalité (1..maxPackageSize) 2. queryRunner = dataSource.createQueryRunner()connect()startTransaction() 3. SET LOCAL app.current_user_id = actorId (RLS context) 4. SELECT * FROM vault_secure.documents WHERE id = ANY($1) ORDER BY id ASC FOR UPDATE// LOCK-ORDER: document_id ASC (§5.9) 5. Vérification tous trouvés (sinon E-404/E-403-RLS) 6. Pour chaque document — gardes séquentielles : - état = SEALED ? (sinon E-409-STATE) - geo_copy_count >= minCopies ? (sinon E-422-GUARD-COPIES) - retention_due = false ? (sinon E-409-RETENTION-DUE) - Si un échoue → ROLLBACK global (INV-278-11) 7. package_id = documents.length > 1 ? randomUUID() : null 8. UPDATE status=DIP + colonnes DIP 9. INSERT attestation dans vault_secure.dissemination_attestations (même tx) 10. INSERT outbox audit DOCUMENT_DISSEMINATED (même tx) via appendJournalEntryInTransaction 11. commitTransaction()AuditLogService.logAsync() (post-commit)

Méthode returnFromDissemination(documentId, actorId) : 1. QueryRunner + transaction 2. SET LOCAL RLS 3. SELECT FOR UPDATE — // LOCK-ORDER: document_id ASC (§5.9) 4. Guard état = DIP 5. UPDATE status=SEALED, dissemination_returned_at = GREATEST(NOW(), disseminated_at) 6. INSERT outbox audit DOCUMENT_RETURNED (même tx) 7. Commit + logAsync post-commit

Invariants critiques : - crypto.randomUUID() pour package_id, attestation_id, request_id (REX PD-63) - hash_evidence = SHA-256 des document_ids + actor_id + issued_at - signature_ref = stub // STUB: PD-37 — HSM signature probante

Pattern : RestitutionService (PD-279)

Contrats : - ❌ Promise.all sur les gardes - ❌ Mutation contenu documentaire en DIP - ❌ Attestation/audit hors de la transaction ACID - ❌ Timeout implicite DIP → SEALED - ❌ Math.random() - ❌ Cache applicatif pour retention_due


Tâche 10 : agent-controller (C10)

Module : dip-controller Fichier : src/modules/documents/controllers/dissemination.controller.ts (création)

Mission : Créer le controller REST pour les 2 endpoints DIP.

Endpoints : 1. POST /api/v1/documents/disseminations - Guards : @UseGuards(JwtAuthGuard, AuthorizationGuard, DisseminationRateLimitGuard) - Roles : @Roles('PA', 'SA', 'auditor') - Filter : @UseFilters(DisseminationAuditExceptionFilter) - Body : CreateDisseminationDto - Return : 201 Created + DisseminationResponseDto

  1. POST /api/v1/documents/:id/dissemination-return
  2. Guards : @UseGuards(JwtAuthGuard, AuthorizationGuard)
  3. Roles : @Roles('PA', 'SA', 'auditor', 'retention_service')
  4. Filter : @UseFilters(DisseminationAuditExceptionFilter)
  5. Param : @Param('id', ParseUUIDPipe) id: string
  6. Body : vide
  7. Return : 200 OK

Pattern : RestitutionController (PD-279)

Contrats : - ❌ GET endpoint qui modifie l'état - ❌ Bypass de ParseUUIDPipe - ❌ Enregistrement hors DocumentsModule


Tâche 11 : agent-filter (C11)

Module : dip-exception-filter Fichier : src/modules/documents/filters/dissemination-audit-exception.filter.ts (création)

Mission : Créer le filter pour audit SYNCHRONE des refus sécurité.

Capture : 401, 403, 429, 409 (si error_code === 'E-409-RETENTION-DUE')

Mécanisme (spec §5.8 — synchrone) : 1. QueryRunner dédié (pas le même que le service) 2. startTransaction()INSERT audit DOCUMENT_DISSEMINATION_DENIEDcommitTransaction() 3. Persistance garantie AVANT le retour de la réponse HTTP 4. En cas d'échec audit : log error mais ne pas masquer l'exception originale 5. Re-throw l'exception originale

Différence avec DepositAuditExceptionFilter : persistance SYNCHRONE via QueryRunner dédié au lieu de logAsync.

Contrats : - ❌ Swallow d'exceptions sans re-throw - ❌ logAsync pour les refus sécurité


Tâche 12 : agent-module (pas un agent séparé — intégré dans l'assemblage)

Fichier : src/modules/documents/documents.module.ts (modification)

Ajouts : - imports : ConfigModule.forFeature(disseminationConfig), TypeOrmModule.forFeature([..., DisseminationAttestation]) - controllers : DisseminationController - providers : DisseminationService, DisseminationRateLimitGuard, DisseminationAuditExceptionFilter - exports : DisseminationService (si nécessaire pour d'autres modules)


Tâche 13 : agent-formal (C12)

Module : dip-tla-formal Fichier : ProbatioVault-doc/docs/normes/nf-z42-013/formal/NF_Z42_013.tla

Observation : DIP est déjà présent dans le modèle TLA+. - DocStates == {"SIP", "AIP", "DIP", "ELIMINATED"} - Actions CommunicateAIP et ReturnDIP existent.

Mission : Vérifier que le modèle est complet et aligné avec la spec PD-278. - Confirmer que CommunicateAIP vérifie les gardes (copies >= MIN_COPIES) - Confirmer que ReturnDIP est explicite uniquement - Confirmer l'invariant DocStates ⊆ RealStates

CA-10 : potentiellement déjà satisfait — à valider par exécution TLC.


Tâche 14 : agent-tests (C13)

Module : dip-tests Fichiers : - src/modules/documents/services/dissemination.service.spec.ts — tests unitaires service - src/modules/documents/controllers/dissemination.controller.spec.ts — tests unitaires controller - src/modules/documents/guards/dissemination-rate-limit.guard.spec.ts — tests unitaires guard - src/modules/documents/filters/dissemination-audit-exception.filter.spec.ts — tests unitaires filter - src/modules/documents/__tests__/dissemination.integration.spec.ts — tests intégration DB

49 tests contractuels à couvrir : - TC-NOM-01 à TC-NOM-07 (7 nominaux) - TC-ERR-01 à TC-ERR-14 (14 erreurs) - TC-INV-01 à TC-INV-13 (13 invariants) - TC-NR-01 à TC-NR-06 (6 non-régression) - TC-NEG-01 à TC-NEG-07 (7 négatifs/adversariaux) - TC-FML-01, TC-FML-02 (2 formels)

Répartition : | Fichier | Tests | Niveau | |---|---|---| | dissemination.service.spec.ts | TC-NOM-01/02/03/06/07, TC-ERR-06/07/09/10/11/14, TC-INV-02/03/05/06/11/13 | Unit + mock DB | | dissemination.controller.spec.ts | TC-ERR-01/02/08A/08B, TC-NEG-01/02/03 | Unit | | dissemination-rate-limit.guard.spec.ts | TC-ERR-13 (rate-limit + fail-closed Redis) | Unit | | dissemination-audit-exception.filter.spec.ts | TC-INV-04 (audit refus synchrone) | Unit | | dissemination.integration.spec.ts | TC-NOM-01/02/03/04, TC-ERR-03/04/05/12, TC-INV-01/07/08/09/10/12, TC-NR-01/02/03/04/05/06, TC-NEG-04/05/06/07 | Integration DB |

Contrats : - ❌ Mocks qui masquent le comportement PostgreSQL réel pour les tests d'intégration - ❌ Tests dépendants d'un ordre non déterministe - ❌ Skip de tests concurrence ou atomicité - ✅ Framework Jest + ts-jest - ✅ Pattern .overrideGuard().useValue() pour tests unitaires controller


Matrice de couverture TC → Agent

TC Agent(s) responsable(s)
TC-INV-01 agent-migration (enum DB), agent-entity (enum TS), agent-tests
TC-INV-02 agent-core (gardes), agent-tests
TC-INV-03 agent-core (pas de timer), agent-tests
TC-INV-04 agent-core (outbox tx) + agent-filter (audit refus), agent-tests
TC-INV-05 agent-core (attestation), agent-tests
TC-INV-06 agent-migration (trigger WORM), agent-core (pas de mutation), agent-tests
TC-INV-07 agent-core (SET LOCAL RLS), agent-tests
TC-INV-08 agent-state-machine (EXPIRED terminal), agent-tests
TC-INV-09 agent-state-machine (matrice complète), agent-tests
TC-INV-10 agent-core (hash_evidence), agent-tests
TC-INV-11 agent-core (ROLLBACK global), agent-tests
TC-INV-12 agent-core (SELECT FOR UPDATE), agent-tests
TC-INV-13 agent-core (GREATEST + retention), agent-tests
TC-NOM-* agent-core + agent-controller, agent-tests
TC-ERR-* agent-dto + agent-core + agent-rate-limit + agent-filter, agent-tests
TC-NR-* agent-state-machine + agent-core, agent-tests
TC-NEG-* agent-dto + agent-core + agent-migration, agent-tests
TC-FML-01/02 agent-formal + agent-tests

Points de vigilance (rappel)

ID Risque Mitigation
V-01 Enum DB 5 valeurs vs spec 4 TC-INV-01 : vérifier DIP ∈ enum_range, pas |enum| = 4
V-02 Deadlock multi-modules // LOCK-ORDER: document_id ASC (§5.9) dans chaque SELECT FOR UPDATE
V-03 ALTER TYPE hors transaction queryRunner.query() sans transaction wrapping
V-04 Index partiels sans subquery WHERE status = 'DIP' — prédicat simple OK
V-05 Exception filter scope Guards scopés controller → filter controller les capture
V-07 crypto.randomUUID() obligatoire import { randomUUID } from 'node:crypto'
V-08 Stub geo_copy_count PD-279 DONE — colonne disponible
V-10 Down migration bloquée si DIP actif Guard COUNT(*) dans down()
NOUVEAU Timestamp migration 1741400000000 pris par PD-280 Utiliser 1741500000000
NOUVEAU TLA+ DIP déjà présent Valider alignment, pas besoin de modifier

Résumé exécutif

  • 14 tâches agents, 5 phases séquentielles avec parallélisme intra-phase
  • 13 fichiers à créer/modifier dans ProbatioVault-backend
  • 49 tests contractuels à implémenter
  • Phase 1 : 7 agents parallèles (fondations)
  • Phase 2 : 2 agents séquentiels (entities + state machine)
  • Phase 3 : 3 agents partiellement parallèles (core métier)
  • Phase 4 : 2 agents (assemblage controller + module)
  • Phase 5 : 1 agent (tests — le plus volumineux)
  • TLA+ : CA-10 potentiellement déjà satisfait — DIP est déjà dans DocStates
  • Patterns réutilisés : RestitutionService (PD-279), LegalRateLimitGuard (PD-81), DepositAuditExceptionFilter (PD-60), migrations enum (PD-250/PD-279)