PD-278 — Agent Report: dip-service
Agent: agent-developer Module: dip-service Story: PD-278 — NF Z42-013: ajout contractuel de l'etat DIP Date: 2026-03-01
1. Fichiers modifies
| Fichier | Action | Justification |
src/modules/documents/services/dissemination.service.ts | Cree | Service core metier DIP — orchestration SEALED<->DIP |
2. Resume de l'implementation
DisseminationService
Service NestJS injectable implementant les deux flux metier de la communication DIP :
Flux F1 — disseminate(documentIds, actorId, motifCommunication) : SEALED -> DIP - Transaction ACID via QueryRunner explicite - SET LOCAL pour contexte RLS (INV-278-07) - SELECT FOR UPDATE ordonne par document_id ASC (LOCK-ORDER SS5.9, INV-278-12) - Gardes sequentielles par document (INV-278-02, INV-278-11) : - Etat = SEALED (E-409-STATE) - copies >= MIN_COPIES (E-422-GUARD-COPIES) - retention_due = false (E-409-RETENTION-DUE, INV-278-14) - UPDATE atomique: status=DIP, disseminated_at=NOW(), disseminated_by, package_id, motif - INSERT attestation dans vault_secure.dissemination_attestations (INV-278-05) - INSERT journal entry dans vault_integrity.integrity_journal_entries (INV-278-04, PD-251) - Post-commit: auditLogService.logAsync() (publication externe non-bloquante)
Flux F2 — returnFromDissemination(documentId, actorId) : DIP -> SEALED - Transaction ACID via QueryRunner - SET LOCAL pour contexte RLS - SELECT FOR UPDATE (LOCK-ORDER SS5.9) - Garde etat = DIP (E-409-STATE) - UPDATE atomique: status=SEALED, dissemination_returned_at=GREATEST(NOW(), disseminated_at) (INV-278-13) - Verification temporelle belt-and-suspenders (E-409-TEMPORAL-ORDER) - INSERT journal entry (INV-278-04) - Post-commit: auditLogService.logAsync()
3. Couverture des invariants
| Invariant | Mecanisme | Localisation |
| INV-278-02 | 6 gardes sequentielles (etat, copies, retention + auth/role/RLS via guards/SET LOCAL) | disseminate() lignes 112-178 |
| INV-278-04 | Journal entry INSERT synchrone dans meme tx que UPDATE status ; audit async post-commit | disseminate() lignes 251-268, 277-289 ; returnFromDissemination() lignes 405-416, 422-432 |
| INV-278-05 | 1 attestation par requete, INSERT dans vault_secure.dissemination_attestations dans meme tx | disseminate() lignes 208-249 |
| INV-278-06 | Aucune mutation contenu en DIP (WORM). motif_communication via COALESCE (preservation existant) | disseminate() UPDATE SQL ligne 195 |
| INV-278-11 | Package atomique — boucle sequentielle, 1 echec = ROLLBACK global (throw dans la boucle) | disseminate() lignes 144-178 |
| INV-278-12 | SELECT FOR UPDATE ordonne par document_id ASC (// LOCK-ORDER: document_id ASC SS5.9) | disseminate() lignes 115-130 ; returnFromDissemination() lignes 345-354 |
| INV-278-13 | returned_at = GREATEST(NOW(), disseminated_at) en SQL + check applicatif | returnFromDissemination() lignes 378-403 |
| INV-278-14 | Garde retention_due=false sur SEALED->DIP ; retention_service BYPASSRLS pour cloture DIP->SEALED | disseminate() lignes 170-177 ; SET LOCAL pour RLS |
4. Couverture des forbidden
| Interdit | Conformite | Justification |
| Promise.all sur les gardes | Conforme | Boucle for (const row of rows) sequentielle |
| Mutation du contenu documentaire en DIP | Conforme | Aucune mutation de encrypted_metadata, file_hash, ovh_path |
| Attestation ou audit hors transaction ACID | Conforme | INSERT attestation et journal dans meme queryRunner.tx |
| Timeout implicite DIP -> SEALED | Conforme | Pas de scheduler/cron — retour explicite uniquement |
| Math.random() pour identifiants | Conforme | randomUUID() de node:crypto pour package_id, attestation_id, request_id |
| Cache applicatif pour retention_due | Conforme | Lecture directe DB via SELECT FOR UPDATE |
5. Decisions architecturales
architectural_decisions:
- decision: "Journal entry comme garantie transactionnelle (pattern outbox)"
rationale: "Le codebase existant (RestitutionService) utilise appendJournalEntryInTransaction pour l'atomicite. La table audit_outbox n'existe pas — le journal integrity est l'outbox. auditLogService.logAsync() gere la publication post-commit."
alternatives_considered: ["Table audit_outbox dediee", "INSERT direct dans vault_secure.audit_log en tx"]
trade_offs: ["Alignement avec pattern existant PD-279. Le journal entry est la preuve transactionnelle, l'audit log est la publication externe."]
- decision: "COALESCE(motif_communication, $4) dans l'UPDATE SQL"
rationale: "Preserve la valeur existante si non-NULL (WORM INV-278-06). Seule la premiere ecriture persiste le motif. Le trigger DB trg_motif_communication_worm est la derniere ligne de defense."
alternatives_considered: ["SET motif_communication = $4 direct", "Verification applicative avant UPDATE"]
trade_offs: ["COALESCE = idempotent en SQL, mais necessite que le trigger bloque les cas edge"]
6. Dependances inter-agents
| Dependance | Agent responsable | Statut | Impact |
JournalEventType.DIP_DISSEMINATED / DIP_RETURNED | agent-journal (C7) | Non encore present dans enum | Service compile quand C7 est livre |
DisseminationAttestation entity | agent-entity (C2) | Non encore present | Table vault_secure.dissemination_attestations doit exister (migration C1) |
DocumentStateMachineService SEALED<->DIP transitions | agent-state-machine (C3) | Non encore present dans ALLOWED_TRANSITIONS | Service ne depend pas directement de la state machine (gardes internes) |
DisseminationConfig | agent-config (C4) | Present et complet | Config validee Joi, registerAs('dissemination') |
| DTOs (CreateDisseminationDto, error codes) | agent-dto (C5) | Present et complet | DisseminationError, DisseminationErrorCode utilises |
AuditActionType.DOCUMENT_DISSEMINATED/RETURNED/DENIED | agent-audit (C6) | Present et complet | 3 types disponibles |
7. Hypotheses documentees
| ID | Hypothese | Justification |
| H-DIP-SVC-01 | Le pattern outbox est le journal integrity (vault_integrity.integrity_journal_entries), pas une table audit_outbox dediee | Le codebase existant (RestitutionService) utilise ce pattern. Aucune table audit_outbox n'existe. |
| H-DIP-SVC-02 | SET LOCAL app.current_user_id est suffisant pour le contexte RLS DIP | Pattern identique a RestitutionService. Le retention_service utilise BYPASSRLS au niveau PostgreSQL (REX PD-16). |
| H-DIP-SVC-03 | Les UUID dans documents[] sont deja normalises lowercase par ParseUUIDPipe du controller | Le DTO utilise @IsUUID('4') et le controller ParseUUIDPipe. |
| H-DIP-SVC-04 | La colonne geo_copy_count est maintenue par la couche stockage (PD-279) | Phase 0 du plan — validation H-06. La colonne existe avec DEFAULT 0 (fail-safe). |
8. Points de vigilance
| ID | Point | Mitigation |
| V-SVC-01 | L'enum JournalEventType n'a pas encore DIP_DISSEMINATED/DIP_RETURNED | Dependance C7 (agent-journal). Compilation echouera tant que C7 n'est pas livre. |
| V-SVC-02 | La table vault_secure.dissemination_attestations n'existe pas encore | Dependance C1 (agent-migration). INSERT echouera a runtime sans la migration. |
| V-SVC-03 | signature_ref est NULL (STUB PD-37) | Le service inserere NULL pour signature_ref. Quand PD-37 sera livre, ajouter l'appel HSM ici. |
| V-SVC-04 | Deadlock possible si un autre module verrouille les documents dans un ordre different | Convention LOCK-ORDER document_id ASC (SS5.9) documentee. Commentaire dans le code. |
| V-SVC-05 | Fenetre de race retention_due (5 min max reconciliation) | La garde retention_due = false est lue dans le SELECT FOR UPDATE — source de verite DB. |
9. Matrice de couverture tests
Les tests contractuels suivants sont couverts par l'implementation du service :
| Test ID | Couvert par | Mecanisme dans le service |
| TC-NOM-01 | disseminate() | Gardes OK -> UPDATE + attestation + journal |
| TC-NOM-02 | disseminate() | Package 1 et 100 docs, SELECT FOR UPDATE ordonne |
| TC-NOM-03 | returnFromDissemination() | Retour explicite, GREATEST(NOW, disseminated_at) |
| TC-NOM-04 | (absence de scheduler) | Pas de cron/timer dans le service |
| TC-NOM-06 | disseminate() | motif persiste + present attestation + journal |
| TC-NOM-07 | disseminate() | Package_id NULL mono / UUID v4 multi |
| TC-ERR-06 | disseminate() garde etat | Rejet E-409-STATE |
| TC-ERR-07 | disseminate() garde copies | Rejet E-422-GUARD-COPIES |
| TC-ERR-09 | disseminate() try/catch attestation | Rejet E-500-ATTESTATION, ROLLBACK |
| TC-ERR-10 | appendJournalEntryInTransaction | Echec journal -> ROLLBACK |
| TC-ERR-11 | disseminate() boucle gardes | 1 doc invalide -> ROLLBACK global |
| TC-ERR-14 | disseminate() garde retention | Rejet E-409-RETENTION-DUE |
| TC-INV-05 | disseminate() INSERT attestation | 1 attestation par requete |
| TC-INV-11 | disseminate() boucle + ROLLBACK | Atomicite package |
| TC-INV-12 | SELECT FOR UPDATE | Concurrence geree |
| TC-INV-13 | returnFromDissemination() GREATEST | Ordre temporel garanti |
Verification des interfaces declarees dans le code contract dip-service :