PD-253 — Décomposition (Étape 6a)¶
Story : PD-253 — Export bulk réversible avec métadonnées et preuves probatoires Auteur : Claude (orchestrateur ProbatioVault) Date : 2026-03-12 Statut : Approuvé Gate 5 — Prêt pour étape 6b
1. Stratégie de parallélisation¶
Principe directeur¶
Le module BulkExportModule est un nouveau module NestJS distinct, ce qui permet une parallélisation maximale sans risque de régression sur les modules existants. La stratégie retenue est by_level : les agents travaillent en parallèle sur les tâches sans dépendances inter-agents au sein d'un même niveau, et les niveaux se succèdent séquentiellement.
Analyse des dépendances¶
Le graphe de dépendances des 12 code contracts (CC-253-01 à CC-253-12) fait apparaître 6 niveaux naturels :
-
Niveau 0 — Fondations : CC-253-01 (entité/migration) et CC-253-02 (configuration) sont indépendants. Ils définissent le socle sur lequel tous les autres s'appuient. CC-253-01 est le prérequis de la quasi-totalité des services.
-
Niveau 1 — DTOs et périmètre : CC-253-03 (DTOs) et CC-253-06 (ExportScopeService) dépendent de CC-253-01 mais sont indépendants entre eux. Ils peuvent être développés en parallèle.
-
Niveau 2 — Services cœur : CC-253-04 (Quota + StateMachine), CC-253-08 (BagIt + DualManifest), CC-253-09 (DestructionLog) et CC-253-12 (Cleanup + Scheduler) dépendent de CC-253-01 mais sont mutuellement indépendants — 4 agents en parallèle.
-
Niveau 3 — Services dépendants : CC-253-10 (ExportDownloadService) dépend de CC-253-04 et CC-253-05 ; CC-253-11 (ExportSigningService) dépend de CC-253-08.
-
Niveau 4 — Orchestration : CC-253-05 (BulkExportService + Controller) dépend de CC-253-01, CC-253-03, CC-253-04 — séquentiel obligatoire.
-
Niveau 5 — Processor : CC-253-07 (BulkExportProcessor) est le nœud intégrateur qui orchestre tous les services — dernière tâche de code avant les tests.
Justification des choix¶
- La séparation agent-config (niveau 0) / agent-developer (niveaux 1-5) est justifiée par la nature des tâches : infrastructure DB et configuration vs logique métier.
- La branche par niveau (
git strategy: branch_per_level) garantit que les merges sont propres et que les conflits sont localisés. - L'ajout d'une phase tests (niveau 6) en parallèle maximise le throughput en évitant que les agents QA attendent la fin des agents développeurs.
- Le BulkExportProcessor (niveau 5) est développé en dernier car il orchestre CC-253-06 (scope), CC-253-08 (bagit), CC-253-09 (destruction-log), CC-253-10 (download), CC-253-11 (signing), CC-253-12 (cleanup) — aucun de ces services ne peut être mocké sans risquer de rater des invariants critiques (INV-253-01, INV-253-12).
2. DAG des dépendances¶
Niveau 0 (sans dépendances)
├── CC-253-01 : Entité BulkExport + migration DDL
│ └── produit : BulkExport entity, BulkExportStatus, BulkExportScope,
│ BulkExportFailureReason enums, branded types BulkExportId/UserId
└── CC-253-02 : Configuration BulkExportModule
└── produit : BulkExportConfig, valeurs SLA (TTL, retention, timeout, maxGB)
Niveau 1 (dépend de CC-253-01)
├── CC-253-03 : DTOs requête/réponse
│ ├── dépend : CC-253-01 (enums BulkExportStatus, BulkExportScope, BulkExportFailureReason)
│ └── produit : CreateBulkExportDto, BulkExportResponseDto, ConfirmDownloadResponseDto
└── CC-253-06 : ExportScopeService
├── dépend : CC-253-01 (BulkExport entity, BulkExportScope)
└── produit : resolveDocuments(), buildDocumentEntry(), DocumentExportEntry interface
Niveau 2 (dépend de CC-253-01)
├── CC-253-04 : ExportQuotaService + ExportStateMachineService
│ ├── dépend : CC-253-01 (BulkExport entity, BulkExportStatus, branded types)
│ └── produit : hasActiveExport(), transition(), isTerminal()
├── CC-253-08 : BagItAssemblerService + DualManifestService
│ ├── dépend : CC-253-01 (BulkExport entity, BulkExportScope)
│ └── produit : assemble(), generate(), verify(), structure BagIt RFC 8493
├── CC-253-09 : DestructionLogService
│ ├── dépend : CC-253-01 (BulkExportId branded type)
│ └── produit : buildLog(), DestructionLog interface (ECT-02)
└── CC-253-12 : ExportCleanupService + ExportExpiryScheduler
├── dépend : CC-253-04 (transition, pour expireExport())
└── produit : purgeStaleTemp(), expireExport(), scheduler CRON 15min
Niveau 3 (dépend niveaux 1 et 2)
├── CC-253-10 : ExportDownloadService
│ ├── dépend : CC-253-04 (StateMachine pour confirmDownload), CC-253-05 (BulkExportService)
│ └── produit : generateSignedUrl(), confirmDownload()
└── CC-253-11 : ExportSigningService
├── dépend : CC-253-08 (BagItPackage interface)
└── produit : signPackage(), export.sig optionnel (niveau strong)
Niveau 4 (dépend niveaux 1 et 2)
└── CC-253-05 : BulkExportService + BulkExportController
├── dépend : CC-253-01 (entity), CC-253-03 (DTOs), CC-253-04 (quota, statemachine)
└── produit : createExport(), getExport(), cancelExport(), confirmDownload()
+ endpoints REST POST/GET/DELETE
Niveau 5 (dépend de tout)
└── CC-253-07 : BulkExportProcessor (BullMQ worker)
├── dépend : CC-253-05 (BulkExportService), CC-253-06 (ExportScopeService),
│ CC-253-08 (BagItAssemblerService), CC-253-09 (DestructionLogService),
│ CC-253-10 (ExportDownloadService), CC-253-11 (ExportSigningService),
│ CC-253-12 (ExportCleanupService)
└── produit : process(), orchestration complète du workflow d'assemblage asynchrone
Niveau 6 (parallèle — tests + module barrel)
├── Tests unitaires BulkExportService (dépend CC-253-05)
├── Tests unitaires ExportStateMachineService (dépend CC-253-04)
├── Tests E2E BulkExport endpoints (dépend CC-253-05 + CC-253-07)
└── BulkExportModule barrel + registration (dépend de tous)
3. Niveaux de parallélisation¶
Niveau 0 (fondations — sans dépendances)¶
Ces deux tâches peuvent démarrer immédiatement et en parallèle. Elles produisent les contrats de types consommés par tous les agents suivants.
- Task 1 — CC-253-01 : Entité BulkExport + migration DDL
- Agent:
agent-config - Branche:
feature/PD-253-l0-entities-migration - Fichiers:
src/modules/bulk-export/entities/bulk-export.entity.tssrc/modules/bulk-export/enums/bulk-export-status.enum.tssrc/modules/bulk-export/enums/bulk-export-scope.enum.tssrc/modules/bulk-export/enums/bulk-export-failure-reason.enum.tssrc/modules/bulk-export/interfaces/bulk-export-scope-params.interface.tssrc/database/migrations/1742000000000-PD253-CreateBulkExports.ts
- Invariants couverts: INV-253-13 (contrainte CHECK état), INV-253-14 (colonne atomique)
- Contraintes critiques:
- Branded types
BulkExportIdetUserIdobligatoires (learning PD-282) - Migration idempotente :
CREATE TABLE IF NOT EXISTSuniquement - Aucun
ALTER TYPE ADD VALUE— stratégieVARCHAR + CHECKpour éviter le trap PostgreSQL (learning PD-282, PD-279) - Aucune modification de table existante
Math.random()interdit —crypto.randomUUID()uniquement (learning PD-63)
- Branded types
-
Dependencies: []
-
Task 2 — CC-253-02 : Configuration BulkExportModule
- Agent:
agent-config - Branche:
feature/PD-253-l0-config - Fichiers:
src/modules/bulk-export/config/bulk-export.config.tssrc/config/config.schema.ts(extension Joi uniquement)
- Invariants couverts: INV-253-07 (quota SLA), SLA §5.2/§5.3
- Paramètres et bornes Joi:
- Contraintes critiques:
- Rejet strict (pas de clamp) si hors bornes —
Joi.number().min(x).max(y)sans valeur par défaut forcée allowUnknown: false, abortEarly: false- Nom de queue sans ':' (learning BullMQ v5 PD-55) — PAS
getRepeatableJobs()/removeRepeatableByKey()(deprecated BullMQ v5) - Valeurs codées en dur interdites — toujours via
ConfigService
- Rejet strict (pas de clamp) si hors bornes —
- Dependencies: []
Niveau 1 (dépend du niveau 0)¶
Ces deux tâches peuvent démarrer dès que CC-253-01 est mergée. Elles sont indépendantes entre elles.
- Task 3 — CC-253-03 : DTOs requête/réponse
- Agent:
agent-developer - Branche:
feature/PD-253-l1-dtos - Fichiers:
src/modules/bulk-export/dto/create-bulk-export.dto.tssrc/modules/bulk-export/dto/bulk-export-response.dto.tssrc/modules/bulk-export/dto/confirm-download-response.dto.ts
- Invariants couverts: INV-253-07 (validation scope case-sensitive), INV-253-13 (réponse status typée)
- Contrats:
CreateBulkExportDto:scopeavec@IsEnum(BulkExportScope)(case-sensitive, rejet si minuscule — TC-NEG-01)from/to:@IsISO8601()+ validationfrom <= todans le servicedocument_idssi scopeSELECTION:@IsArray @ArrayMinSize(1) @IsUUID('4', {each: true}) @ArrayUnique()BulkExportResponseDto:failure_reasonnullable, typeBulkExportFailureReason | null- Champ
package_s3_keyinterdit dans la réponse (chemin S3 interne — anti-énumération) - String libre pour
failure_reasoninterdite — enum typé uniquement
-
Dependencies: [CC-253-01]
-
Task 4 — CC-253-06 : ExportScopeService
- Agent:
agent-developer - Branche:
feature/PD-253-l1-export-scope - Fichiers:
src/modules/bulk-export/services/export-scope.service.tssrc/modules/bulk-export/interfaces/document-export-entry.interface.ts
- Invariants couverts: INV-253-01 (exhaustivité GLOBAL), INV-253-09 (soft-deleted inclus, détruits exclus)
- Contrats:
resolveDocuments(export: BulkExport): Promise<DocumentExportEntry[]>buildDocumentEntry(documentId, userId): Promise<DocumentExportEntry | null>- Scope
GLOBAL: sélectionne TOUS lesDocumentSecureduuser_idavecstatus NOT IN ('DESTROYED')— y comprisEXPIRED(soft-deleted). Comptage source stocké dans job metadata pour INV-253-01. - Scope
VAULT: filtre survault_id(champscope_params.vault_id) - Scope
PERIOD: filtre surcreated_at BETWEEN from AND to - Scope
SELECTION: filtre surid IN (document_ids)avec vérification ownership stricte - Comptage source == comptage export éligibles avant génération package. Mismatch →
FAILEDavecSCOPE_RESOLUTION_FAILED - Inclusion de documents d'un autre
user_id: INTERDIT - Lecture du contenu S3 des documents : INTERDITE (Zero-Knowledge)
- Dependencies: [CC-253-01]
Niveau 2 (dépend de CC-253-01)¶
Quatre agents peuvent travailler en parallèle. Ces services cœur sont mutuellement indépendants.
- Task 5 — CC-253-04 : ExportQuotaService + ExportStateMachineService
- Agent:
agent-developer - Branche:
feature/PD-253-l2-quota-statemachine - Fichiers:
src/modules/bulk-export/services/export-quota.service.tssrc/modules/bulk-export/services/export-state-machine.service.tssrc/modules/bulk-export/exceptions/bulk-export.exceptions.ts
- Invariants couverts: INV-253-07 (quota 1 actif/user), INV-253-13 (transitions explicites), INV-253-14 (atomicité)
- Contrats:
ExportQuotaService.hasActiveExport(userId: UserId): Promise<boolean>— COUNT(*) WHEREuser_id = ? AND status IN ('REQUESTED','ASSEMBLING','READY_FOR_DOWNLOAD')ExportStateMachineService.transition(export: BulkExport, to: BulkExportStatus): void— throwsBulkExportTransitionExceptionsi interditeExportStateMachineService.isTerminal(status: BulkExportStatus): boolean- Matrice transitions conforme spec §5.4 : REQUESTED→{ASSEMBLING,CANCELLED}, ASSEMBLING→{READY_FOR_DOWNLOAD,FAILED,FAILED_TIMEOUT,CANCELLED}, READY_FOR_DOWNLOAD→{DOWNLOADED,EXPIRED}
- États terminaux : DOWNLOADED, EXPIRED, FAILED, FAILED_TIMEOUT, CANCELLED — toute sortie INTERDITE
- Libération quota implicite — COUNT filtre les terminaux, pas de slot explicite (ECT-06)
- Tout UPDATE de statut sans passage par
ExportStateMachineService: INTERDIT - Transition directe
REQUESTED → READY_FOR_DOWNLOAD: INTERDITE
-
Dependencies: [CC-253-01]
-
Task 6 — CC-253-08 : BagItAssemblerService + DualManifestService
- Agent:
agent-developer - Branche:
feature/PD-253-l2-bagit-manifest - Fichiers:
src/modules/bulk-export/services/bagit-assembler.service.tssrc/modules/bulk-export/services/dual-manifest.service.tssrc/modules/bulk-export/interfaces/bagit-package.interfaces.ts
- Invariants couverts: INV-253-02 (ProofEnvelope complète), INV-253-03 (dual-hash), INV-253-04 (dual-manifest), INV-253-05 (intégrité package)
- Structure BagIt produite:
{export_id}/ bagit.txt (BagIt declaration RFC 8493) bag-info.txt (métadonnées package: org, date, taille) manifest-sha256.txt (hash SHA-256 de chaque fichier data/) manifest-sha3.txt (hash SHA3-256 de chaque fichier data/) data/ {document_id}/ content.enc (binaire chiffré WORM — copie depuis S3) metadata.json (métadonnées document + ProofEnvelope) metadata/ export-manifest.json (résumé export: scope, dates, comptages) destruction-log.json (ECT-02 — documents détruits exclus) export.sig (optionnel — niveau strong) - Contrats BagItAssemblerService:
assemble(entries: DocumentExportEntry[], export: BulkExport): Promise<BagItPackage>plaintext_hashetciphertext_hash: regex^[a-f0-9]{64}$vérifiée fonctionnellement (validation fonctionnelle ≠ format — learning PD-283). Si invalide → exportFAILED.blockchain_anchor.statusconservé tel quel — jamais "upgradé" versanchored(INV-253-08)is_deletedetdeleted_atdansmetadata.jsonsi applicable- Encodage UTF-8 sans BOM pour tous les JSON
- Contrats DualManifestService:
generate(packageDir: string): Promise<{ sha256Manifest: string, sha3Manifest: string }>verify(packageDir: string): Promise<{ valid: boolean, errors: string[] }>- SHA-256 :
crypto.createHash('sha256').update(fileBuffer).digest('hex') - SHA3-256 :
sha3_256(fileBuffer)viajs-sha3(dépendance existante) - Format ligne :
<64 chars hex> <path relatif>(2 espaces, séparateur/, sans..) - Tri des fichiers : alphabétique pour reproductibilité
- Exclus des manifests :
manifest-sha256.txt,manifest-sha3.txt,bagit.txt,bag-info.txt,export.sig export.sigdans les manifests : INTERDIT (il signe les manifests, pas l'inverse)path.join()pour les chemins : INTERDIT — utiliserpath.posix.join()
-
Dependencies: [CC-253-01]
-
Task 7 — CC-253-09 : DestructionLogService
- Agent:
agent-developer - Branche:
feature/PD-253-l2-destruction-log - Fichiers:
src/modules/bulk-export/services/destruction-log.service.tssrc/modules/bulk-export/interfaces/destruction-log.interfaces.ts
- Invariants couverts: INV-253-09 (traçabilité destruction légale)
- Schéma DestructionLog (ECT-02):
interface DestructionLogEntry { document_id: string; // UUID v4 destroyed_at: string; // RFC3339 UTC (PD-250) destruction_act_hash: string; // hex SHA3-256 64 chars destruction_batch_id: string; // UUID v4, référence batch PD-250 destruction_reason: string; // "RETENTION_EXPIRED" | "LEGAL_REQUEST" | "USER_REQUEST" bordereau_id: string; // UUID v4, référence bordereau PD-250 (INV-250-03) } interface DestructionLog { schema_version: "1.0"; export_id: string; // UUID v4 generated_at: string; // RFC3339 UTC total_destroyed: number; entries: DestructionLogEntry[]; } - Contrats:
buildLog(exportId, destroyedEntries: DocumentExportEntry[]): Promise<DestructionLog>destruction_act_hash: lu depuisdestruction_bordereau.hash(PD-250), regex^[a-f0-9]{64}$vérifiée fonctionnellement (validation fonctionnelle obligatoire — learning PD-283)- Si
destruction_act_hashinvalide → exportFAILEDavecPROOF_ENVELOPE_INCOMPLETE total_destroyed==entries.length(cohérence interne vérifiée)bordereau_idobligatoire — omettre est INTERDIT (INV-250-03 PD-250)- Données personnelles directes (nom, prénom, email) dans les entrées : INTERDITES
- Encodage UTF-8 sans BOM
-
Dependencies: [CC-253-01]
-
Task 8 — CC-253-12 : ExportCleanupService + ExportExpiryScheduler
- Agent:
agent-developer - Branche:
feature/PD-253-l2-cleanup-scheduler - Fichiers:
src/modules/bulk-export/services/export-cleanup.service.tssrc/modules/bulk-export/schedulers/export-expiry.scheduler.ts
- Invariants couverts: INV-253-12 (no-residuals — purgeStale + finally)
- Contrats:
ExportCleanupService.purgeStaleTemp(exportId: BulkExportId): Promise<void>— supprime/tmp/bulk-export-{exportId}/ExportCleanupService.expireExport(exportId: BulkExportId): Promise<void>— transition→ EXPIRED+ purge S3ExportExpiryScheduler: CRON toutes les 15 min, sélectionnestatus = 'READY_FOR_DOWNLOAD'ETexpires_at <= NOW()expireExport: supprime l'objet S3 viaS3Service.deleteObject(packageS3Key)avant de transitionner en DB — retry 3x si S3 échoue (pas de blocage de l'expiration DB)- Audit
BULK_EXPORT_EXPIREDémis après transition purgeStaleTempdansfinallyuniquement : INTERDIT — DOIT être appelé en début de job aussi (learning PD-283, INV-253-12)DOWNLOADED → EXPIRED: INTERDITE — seulREADY_FOR_DOWNLOADtransite versEXPIRED(ECT-05)- L'état
DOWNLOADEDest conservé en DB après purge physique S3 (correction E-02)
- Note: Cette tâche dépend fonctionnellement de CC-253-04 (ExportStateMachineService) pour
expireExport(). L'agent doit importer le service via injection NestJS standard. - Dependencies: [CC-253-01, CC-253-04]
Niveau 3 (dépend des niveaux précédents)¶
Ces deux tâches peuvent démarrer en parallèle dès que leurs dépendances respectives sont disponibles.
- Task 9 — CC-253-10 : ExportDownloadService
- Agent:
agent-developer - Branche:
feature/PD-253-l3-export-download - Fichiers:
src/modules/bulk-export/services/export-download.service.ts
- Invariants couverts: INV-253-06 (signature optionnelle), ECT-01 (confirm-download endpoint)
- Contrats:
generateSignedUrl(export: BulkExport): Promise<{ url: string, expiresAt: Date }>confirmDownload(userId: UserId, exportId: BulkExportId): Promise<void>generateSignedUrl: URL signée TTL configurable (download_url_ttl_h) via S3Service présignatureconfirmDownload: idempotent — si déjàDOWNLOADED, retourne200sans re-audit (ECT-01)- Précondition : état
READY_FOR_DOWNLOADuniquement pour confirmDownload - Si état !=
READY_FOR_DOWNLOADet !=DOWNLOADED:409 EXPORT_NOT_DOWNLOADABLE - Validation existence objet S3 avant génération URL signée (
package_s3_keyfonctionnellement vérifié — learning PD-283) 410 DOWNLOAD_URL_EXPIREDsi URL signée expirée410 EXPORT_EXPIREDsi étatEXPIRED
-
Dependencies: [CC-253-04, CC-253-05]
-
Task 10 — CC-253-11 : ExportSigningService
- Agent:
agent-developer - Branche:
feature/PD-253-l3-export-signing - Fichiers:
src/modules/bulk-export/services/export-signing.service.ts
- Invariants couverts: INV-253-06 (signature HSM optionnelle — niveau strong)
- Contrats:
signPackage(packageDir: string, exportId: BulkExportId): Promise<Buffer | null>- Signature uniquement si
probative_level = 'strong'— null retourné si standard - Périmètre signé : digest SHA3-256 du package final (PC-253-01 — option contractuelle provisoire)
export.sigNE DOIT PAS être inclus dans les manifests (BagItAssemblerService)- Si HSM indisponible : log WARNING, retourner null — réversibilité niveau
standardconservée (INV-253-06, H-253-06) - Consomme
HsmService.sign()existant (PD-37) — STUB tracé :// STUB: PD-37 — signature HSM export package - TSA optionnelle sur
export.sigviaTsaService.timestamp()— STUB tracé :// STUB: PD-39 — horodatage TSA sur export.sig crypto.createVerify().update(hash): INTERDIT si mode raw ECDSA (double-hash — learning PD-282)
- Dependencies: [CC-253-08]
Niveau 4 (dépend des niveaux 1 et 2)¶
Tâche séquentielle unique — BulkExportService est le point d'orchestration synchrone.
- Task 11 — CC-253-05 : BulkExportService + BulkExportController
- Agent:
agent-developer - Branche:
feature/PD-253-l4-service-controller - Fichiers:
src/modules/bulk-export/services/bulk-export.service.tssrc/modules/bulk-export/controllers/bulk-export.controller.ts
- Invariants couverts: INV-253-10 (audit fail-closed), INV-253-14 (atomicité sync/async), INV-253-07 (quota)
- Endpoints REST:
- Contrats BulkExportService:
createExport(userId, dto): Promise<BulkExportResponseDto>— transaction DB unique : (1) vérification quota, (2) émission audit, (3) INSERTbulk_exports. Publication BullMQ post-commit (séparation INV-253-14).getExport(userId, exportId): Promise<BulkExportResponseDto>— vérification ownership ou403 FORBIDDEN_EXPORT_ACCESScancelExport(userId, exportId): Promise<void>— vérification ownership + état non-terminal. SiASSEMBLINGetready_atnon null (package déjà produit) :409avec étatREADY_FOR_DOWNLOADinchangéconfirmDownload(userId, exportId): Promise<void>— délégation à ExportDownloadService
- Pattern fail-closed audit (learning PD-85, INV-253-10):
// CORRECT — fail-closed obligatoire await queryRunner.startTransaction(); try { await this.auditService.log({ ... }); // throws si indisponible const saved = await queryRunner.manager.save(BulkExport, entity); await queryRunner.commitTransaction(); await this.queue.add('assemble', { exportId: saved.id }); // post-commit return toDto(saved); } catch (err) { await queryRunner.rollbackTransaction(); if (err instanceof AuditUnavailableException) throw new AuditWriteFailedException(); throw err; } - Contrats BulkExportController:
OidcJwtAuthGuardobligatoire sur TOUS les endpoints — endpoint sans guard : INTERDIT- Vérification ownership sur tous les endpoints avec
:id 409 EXPORT_ALREADY_ACTIVEsi quota dépassé à la création409 EXPORT_NOT_CANCELLABLEsi annulation sur état terminal- Exposer
package_s3_keydans une réponse : INTERDIT ValidationPipeavec class-validator + class-transformer
- Interdit:
.catch(() => logger.error())sur appel audit — anti-catch-absorb (learning PD-85)- Publication BullMQ avant
commitTransaction() Math.random()pour tout identifiant- Rate limiting global (doit être par
userId)
- Dependencies: [CC-253-01, CC-253-03, CC-253-04]
Niveau 5 (dépend de tout)¶
Tâche séquentielle finale — le processor est le nœud intégrateur de tous les services.
- Task 12 — CC-253-07 : BulkExportProcessor (BullMQ worker)
- Agent:
agent-developer - Branche:
feature/PD-253-l5-processor - Fichiers:
src/modules/bulk-export/processors/bulk-export.processor.ts
- Invariants couverts: INV-253-01 (exhaustivité), INV-253-02 (ProofEnvelope), INV-253-08 (pending anchor), INV-253-09 (soft-deleted/destroyed), INV-253-11 (chiffrement), INV-253-12 (no-residuals)
- Contrats:
@Processor('pv-jobs-bulk-export')— nom de queue sans ':' (CC-253-02)BulkExportJobPayload:{ exportId: BulkExportId }- Séquence obligatoire:
ExportCleanupService.purgeStaleTemp(exportId)— au démarrage avant tout traitement (learning PD-283)- Transition
REQUESTED → ASSEMBLING+ timestampassembling_started_at ExportScopeService.resolveDocuments(export)— liste documents éligibles- Pour chaque document :
buildDocumentEntry()— ProofEnvelope absente →FAILED(PROOF_ENVELOPE_INCOMPLETE). Anchorpending→ inclus tel quel (INV-253-08) BagItAssemblerService.assemble(entries, export)— construction BagItDualManifestService.generate(packageDir)— dual manifest SHA-256 + SHA3-256- Si
probative_level = 'strong':ExportSigningService.signPackage()— optionnel - Upload S3 staging via
S3Service(multipart obligatoire pour packages > 5GB via@aws-sdk/lib-storage Upload— H-253-11) - Transition
ASSEMBLING → READY_FOR_DOWNLOAD+expires_at = now() + retention_ttl_h finally { await ExportCleanupService.purgeStaleTemp(exportId) }— nettoyage (INV-253-12)- Timeout BullMQ =
BULK_EXPORT_JOB_TIMEOUT_H * 3600 * 1000ms →FAILED_TIMEOUTsi dépassé - Audit à chaque transition :
finally { await audit(transition) }— fail-closed (learning PD-85) - Répertoire temp :
/tmp/bulk-export-{exportId}/ - STUB notification disponibilité :
// STUB: PC-253-03 — notification canal externe (email/push)
- Interdit:
- Déchiffrement du contenu des documents (Zero-Knowledge)
Math.random()pour tout identifiant de corrélation —crypto.randomUUID()uniquement- Fire-and-forget sur les transitions d'état
- Accès à des documents d'un autre
user_id PutObjectCommandpour packages > 5GB — multipart obligatoire (H-253-11)
- Dependencies: [CC-253-05, CC-253-06, CC-253-08, CC-253-09, CC-253-10, CC-253-11, CC-253-12]
4. Tâches additionnelles (tests + module)¶
Ces tâches constituent le niveau 6 et peuvent s'exécuter en parallèle dès que leurs dépendances respectives sont disponibles.
- Task 13 — Tests unitaires BulkExportService
- Agent:
agent-qa-unit-integration - Branche:
feature/PD-253-l6-tests-service - Fichiers:
src/modules/bulk-export/__tests__/bulk-export.service.spec.ts
- Tests contractuels couverts: TC-ERR-04 (quota 409), TC-ERR-07 (audit fail-closed 500), TC-NOM-13 (annulation depuis REQUESTED), TC-NOM-14 (annulation depuis ASSEMBLING), TC-SEC-01 (accès inter-utilisateur 403)
- Tests additionnels (non-contractuels): atomicité queryRunner (crash pre-commit → rollback), publication BullMQ post-commit uniquement
- Traçabilité: chaque test contractuel porte l'identifiant TC-* dans son nom ou commentaire. Les tests additionnels portent
// NON-CONTRACTUAL -
Dependencies: [Task 11]
-
Task 14 — Tests unitaires ExportStateMachineService
- Agent:
agent-qa-unit-integration - Branche:
feature/PD-253-l6-tests-statemachine - Fichiers:
src/modules/bulk-export/__tests__/export-state-machine.service.spec.ts
- Tests contractuels couverts: TC-NOM-09 (matrice transitions autorisées + interdites), TC-NOM-10 (atomicité chaos), TC-NEG-06 (transition terminale FAILED→ASSEMBLING rejetée)
- Tests additionnels: toutes les transitions de la matrice §5.4 — couverture exhaustive de la machine à états (chaque arc du DAG d'états)
-
Dependencies: [Task 5]
-
Task 15 — Tests E2E BulkExport endpoints
- Agent:
agent-qa-unit-integration - Branche:
feature/PD-253-l6-tests-e2e - Fichiers:
test/bulk-export.e2e-spec.ts
- Tests contractuels couverts:
- TC-NOM-01 (Global nominal — exhaustivité READY_FOR_DOWNLOAD)
- TC-NOM-02 (ProofEnvelope complète)
- TC-NOM-03 (dual-hash + chaîne Merkle)
- TC-NOM-04 (dual manifest présent)
- TC-NOM-05 (niveau standard sans export.sig)
- TC-NOM-07 (anchor pending conservé)
- TC-NOM-08 (soft-deleted inclus, destroyed exclus + destruction-log.json)
- TC-NOM-11 (expiration TTL → EXPIRED + purge)
- TC-NOM-15 (confirm-download → DOWNLOADED — ajout Gate 5, ECT-01)
- TC-ERR-01 (400 scope invalide)
- TC-ERR-02 (401 non authentifié)
- TC-ERR-03 (403 scope inter-tenant)
- TC-ERR-05 (413 taille max dépassée)
- TC-ERR-06 (ProofEnvelope incomplète → FAILED asynchrone)
- TC-ERR-08 (500 assemblage échoué)
- TC-ERR-09 (FAILED_TIMEOUT)
- TC-ERR-10 (410 URL expirée)
- TC-ERR-11 (410 export expiré)
- TC-ERR-12 (corruption package détectée par manifest)
- TC-INV-11 (chiffrement artefacts temporaires)
- TC-INV-12 (no-residuals succès/échec/crash)
- TC-SEC-01 (403 FORBIDDEN_EXPORT_ACCESS inter-utilisateur)
- TC-NEG-01 à TC-NEG-08 (tests négatifs adversariaux)
- Note: TC-NOM-12 (performance P95) conditionnel à la disponibilité d'un environnement de référence instrumenté (H-253-04 — non bloquant).
-
Dependencies: [Task 11, Task 12]
-
Task 16 — BulkExportModule (barrel + registration)
- Agent:
agent-developer - Branche:
feature/PD-253-l6-module-barrel - Fichiers:
src/modules/bulk-export/bulk-export.module.tssrc/modules/bulk-export/index.ts
- Contrats:
- Déclaration NestJS de tous les providers, controllers, imports (BullModule, TypeOrmModule, AuditModule, StorageModule)
- Enregistrement de la queue BullMQ
pv-jobs-bulk-exportviaBullModule.registerQueue({ name: 'pv-jobs-bulk-export' }) - Export des services nécessaires aux modules consommateurs
- Registration dans
AppModuleviaimports
- Dependencies: [All tasks 1-12]
5. Bloc parallelization (YAML)¶
parallelization:
story: PD-253
strategy: by_level
levels:
- level: 0
name: "Fondations"
description: "Entité DB, migration DDL, configuration SLA — socle de tous les services"
tasks: [1, 2]
task_names:
- "CC-253-01 : Entité BulkExport + migration DDL"
- "CC-253-02 : Configuration BulkExportModule"
agents: [agent-config, agent-config]
branches:
- feature/PD-253-l0-entities-migration
- feature/PD-253-l0-config
estimated_time: "1h"
blocking_for: [1, 2, 3, 4, 5, 6, 7]
- level: 1
name: "DTOs et périmètre de sélection"
description: "DTOs API + ExportScopeService — interfaces publiques et résolution des 4 granularités"
tasks: [3, 4]
task_names:
- "CC-253-03 : DTOs requête/réponse"
- "CC-253-06 : ExportScopeService"
agents: [agent-developer, agent-developer]
branches:
- feature/PD-253-l1-dtos
- feature/PD-253-l1-export-scope
estimated_time: "1.5h"
blocking_for: [8, 9]
- level: 2
name: "Services cœur"
description: "Quota, machine à états, BagIt, manifests, destruction-log, cleanup — 4 agents en parallèle"
tasks: [5, 6, 7, 8]
task_names:
- "CC-253-04 : ExportQuotaService + ExportStateMachineService"
- "CC-253-08 : BagItAssemblerService + DualManifestService"
- "CC-253-09 : DestructionLogService"
- "CC-253-12 : ExportCleanupService + ExportExpiryScheduler"
agents: [agent-developer, agent-developer, agent-developer, agent-developer]
branches:
- feature/PD-253-l2-quota-statemachine
- feature/PD-253-l2-bagit-manifest
- feature/PD-253-l2-destruction-log
- feature/PD-253-l2-cleanup-scheduler
estimated_time: "2h"
blocking_for: [9, 10, 11]
- level: 3
name: "Services dépendants"
description: "ExportDownloadService (confirm-download ECT-01) + ExportSigningService (niveau strong)"
tasks: [9, 10]
task_names:
- "CC-253-10 : ExportDownloadService"
- "CC-253-11 : ExportSigningService"
agents: [agent-developer, agent-developer]
branches:
- feature/PD-253-l3-export-download
- feature/PD-253-l3-export-signing
estimated_time: "1.5h"
blocking_for: [11]
- level: 4
name: "Orchestration synchrone"
description: "BulkExportService + BulkExportController — point d'entrée REST et pattern fail-closed audit"
tasks: [11]
task_names:
- "CC-253-05 : BulkExportService + BulkExportController"
agents: [agent-developer]
branches:
- feature/PD-253-l4-service-controller
estimated_time: "2h"
blocking_for: [12]
- level: 5
name: "Processor asynchrone"
description: "BulkExportProcessor — nœud intégrateur BullMQ, orchestration complète de l'assemblage"
tasks: [12]
task_names:
- "CC-253-07 : BulkExportProcessor"
agents: [agent-developer]
branches:
- feature/PD-253-l5-processor
estimated_time: "2h"
blocking_for: [13, 14, 15]
- level: 6
name: "Tests + module barrel"
description: "Tests unitaires, E2E et module NestJS final — 4 agents en parallèle"
tasks: [13, 14, 15, 16]
task_names:
- "Tests unitaires BulkExportService"
- "Tests unitaires ExportStateMachineService"
- "Tests E2E BulkExport endpoints"
- "BulkExportModule barrel + registration"
agents:
[
agent-qa-unit-integration,
agent-qa-unit-integration,
agent-qa-unit-integration,
agent-developer,
]
branches:
- feature/PD-253-l6-tests-service
- feature/PD-253-l6-tests-statemachine
- feature/PD-253-l6-tests-e2e
- feature/PD-253-l6-module-barrel
estimated_time: "3h"
total_sequential_time: "20h"
total_parallel_time: "13h"
speedup_factor: 1.54
git_strategy: branch_per_level
merge_target: develop
merge_order: "level-by-level — merge suivant uniquement après CI vert sur level précédent"
6. Contraintes techniques rappelées¶
Les contraintes suivantes sont OBLIGATOIRES pour tous les agents. Elles découlent des learnings des REX précédents et sont non négociables.
1. S3 multipart (H-253-11)¶
BulkExportProcessor (CC-253-07) DOIT utiliser @aws-sdk/lib-storage Upload pour les packages > 5 GB, PAS PutObjectCommand. La limite PutObjectCommand est 5 GB (S3 spec) ; les exports peuvent atteindre 100 GB. Si S3Service.uploadFile() est un stub existant, il DOIT être étendu pour déléguer l'upload multipart. Criticité : MAJEUR (bloque les exports de gros coffres).
// CORRECT — multipart obligatoire pour > 5GB
import { Upload } from '@aws-sdk/lib-storage';
const upload = new Upload({
client: this.s3Client,
params: { Bucket: stagingBucket, Key: packageS3Key, Body: fileStream },
});
await upload.done();
2. failure_reason en VARCHAR + CHECK (ECT-03)¶
La colonne failure_reason est VARCHAR(50) avec contrainte CHECK (failure_reason IN (...) OR failure_reason IS NULL). Cette stratégie évite le piège ALTER TYPE ADD VALUE + commitTransaction() obligatoire de PostgreSQL (learning PD-282, PD-279). Aucun ALTER TYPE ADD VALUE ne doit apparaître dans la migration.
3. Branded types obligatoires pour UUID (learning PD-282)¶
BulkExportId et UserId sont des branded types nominaux. Tout service qui accepte un exportId DOIT typer le paramètre en BulkExportId, pas en string. Inversion silencieuse impossible au compile-time.
type BulkExportId = string & { readonly __brand: 'BulkExportId' };
type UserId = string & { readonly __brand: 'UserId' };
4. purgeStale() au démarrage du processor (learning PD-283)¶
ExportCleanupService.purgeStaleTemp(exportId) DOIT être appelé AU DÉMARRAGE du processor (avant tout traitement), ET dans le bloc finally. Un crash applicatif (OOM, kill signal) bypass le finally — sans purge au démarrage, les résidus du run précédent persistent jusqu'au prochain run.
// CORRECT — double nettoyage
async process(job: Job<BulkExportJobPayload>) {
await this.cleanupService.purgeStaleTemp(job.data.exportId); // démarrage
try {
// ... traitement ...
} finally {
await this.cleanupService.purgeStaleTemp(job.data.exportId); // toujours
}
}
5. Anti-catch-absorb — audit fail-closed (learning PD-85)¶
Tout appel auditService.log() dans le module bulk-export est en dehors de tout catch absorbant. Pattern obligatoire : finally { await audit() } ou re-throw après audit.
// INTERDIT — absorbe l'échec d'audit
await this.auditService.log(event).catch((err) => this.logger.error(err));
// CORRECT — fail-closed (propagation de l'erreur)
await this.auditService.log(event); // throws → 500 AUDIT_WRITE_FAILED
6. confirm-download endpoint (ECT-01)¶
POST /bulk-exports/:id/confirm-download est le seul déclencheur de la transition READY_FOR_DOWNLOAD → DOWNLOADED. Ce mécanisme est retenu car l'architecture S3 présignée ne fournit pas de webhook fiable de téléchargement réussi. L'endpoint est idempotent : si déjà DOWNLOADED, retourne 200 sans re-audit.
7. destruction-log.json — 6 champs obligatoires (ECT-02)¶
Chaque entrée de destruction-log.json DOIT contenir exactement ces 6 champs : document_id, destroyed_at, destruction_act_hash, destruction_batch_id, destruction_reason, bordereau_id. Omettre bordereau_id est une violation de INV-250-03 (PD-250). La validation fonctionnelle de destruction_act_hash (regex + existence en DB) est obligatoire — la validation de format seul est insuffisante (learning PD-283).
8. crypto.randomUUID() obligatoire (learning PD-63)¶
Tous les identifiants générés utilisent import { randomUUID } from 'node:crypto'. Math.random() est flaggé Sonar S2245.
9. Validation fonctionnelle des champs de sécurité (learning PD-283)¶
Format != fonctionnel. Les champs plaintext_hash, ciphertext_hash, destruction_act_hash et package_s3_key doivent être validés DEUX fois : - Validation format (regex ^[a-f0-9]{64}$ ou UUID v4) - Validation fonctionnelle (référencé dans Merkle pour ciphertext_hash, existence en DB pour destruction_act_hash, existence S3 pour package_s3_key)
10. path.posix.join() pour les chemins manifests (CC-253-08)¶
path.join() utilise le séparateur OS (\ sur Windows). Les manifests BagIt DOIVENT utiliser / comme séparateur. Utiliser path.posix.join() uniquement pour construire les chemins relatifs dans les fichiers manifest-sha256.txt et manifest-sha3.txt.
11. BullMQ v5 — API dépréciée (learning PD-55)¶
Ne JAMAIS utiliser getRepeatableJobs() / removeRepeatableByKey() (deprecated BullMQ v5). Utiliser getJobSchedulers() / removeJobScheduler(). Nom de queue sans ':' obligatoire.
Annexe — Mapping Tests contractuels → Tâches agents¶
| Test ID | Invariant couvert | Tâche responsable |
|---|---|---|
| TC-NOM-01 | INV-253-01 | Task 12 (Processor — exhaustivité), Task 15 (E2E) |
| TC-NOM-02 | INV-253-02 | Task 6 (BagIt — ProofEnvelope), Task 15 (E2E) |
| TC-NOM-03 | INV-253-03 | Task 6 (dual-hash + Merkle), Task 15 (E2E) |
| TC-NOM-04 | INV-253-04 | Task 6 (DualManifest), Task 15 (E2E) |
| TC-NOM-05 | INV-253-06 | Task 10 (ExportSigningService), Task 15 (E2E) |
| TC-NOM-06 | INV-253-06 | Task 10 (ExportSigningService), Task 15 (E2E) |
| TC-NOM-07 | INV-253-08 | Task 6 (BagIt — pending anchor), Task 15 (E2E) |
| TC-NOM-08 | INV-253-09 | Task 4 (ExportScope), Task 7 (DestructionLog), Task 15 (E2E) |
| TC-NOM-09 | INV-253-13 | Task 5 (StateMachine), Task 14 (tests unitaires) |
| TC-NOM-10 | INV-253-14 | Task 11 (BulkExportService — atomicité), Task 13 (tests unitaires) |
| TC-NOM-11 | CA-253-09 | Task 8 (ExportExpiryScheduler), Task 15 (E2E) |
| TC-NOM-12 | CA-253-12 | Task 12 (Processor — async isolé) — conditionnel env P95 |
| TC-NOM-13 | INV-253-13 | Task 11 (cancelExport), Task 13 (tests unitaires) |
| TC-NOM-14 | INV-253-13 | Task 11 (cancelExport — idempotence ASSEMBLING), Task 13 |
| TC-NOM-15 | ECT-01 | Task 9 (ExportDownloadService), Task 15 (E2E) |
| TC-SEC-01 | §5.7 accès inter-user | Task 11 (ownership check), Task 13 (tests unitaires) |
| TC-ERR-04 | INV-253-07 | Task 5 (ExportQuotaService), Task 13 (tests unitaires) |
| TC-ERR-07 | INV-253-10 | Task 11 (audit fail-closed), Task 13 (tests unitaires) |
| TC-ERR-12 | INV-253-05 | Task 6 (DualManifest.verify), Task 15 (E2E) |
| TC-INV-11 | INV-253-11 | Task 12 (Processor — Zero-Knowledge), Task 15 (E2E) |
| TC-INV-12 | INV-253-12 | Task 8 (ExportCleanup), Task 12 (Processor — purgeStale), Task 15 |
Références¶
- Spec :
PD-253-specification.md(v2) - Tests :
PD-253-tests.md(v2) - Plan :
PD-253-plan.md(v1.0, Gate 5 approuvé) - Gate 3 :
PD-253-verdict-step3-v2.yaml(RESERVE — ECT-01/02/03 adressés dans le plan) - Gate 5 :
PD-253-verdict-step5-v1.yaml - Dépendances : PD-85 (ExportEngine), PD-282 (ProofEnvelope eIDAS), PD-39 (TSA), PD-37 (HSM), PD-250 (destruction bordereaux)
- Standards : NF Z42-013:2020 §13.1, ISO 14641, RGPD Art.20, BagIt RFC 8493
- Learnings injectés : PD-83, PD-85, PD-250, PD-262, PD-265, PD-279, PD-282, PD-283