Aller au contenu

PD-278 — Agent Developer : dip-migration

Module

dip-migration — Migration DDL pour l'ajout de l'etat DIP au cycle de vie documentaire.

Fichier produit

src/database/migrations/1741500000000-PD278-AddDipState.ts

Resume des operations

up()

Phase Operation Invariant Idempotent
1 ALTER TYPE vault_secure.document_status ADD VALUE IF NOT EXISTS 'DIP' INV-278-01 Oui (IF NOT EXISTS)
2 Ajout 6 colonnes : disseminated_at, disseminated_by, dissemination_returned_at, dissemination_package_id, motif_communication, retention_due CA-04, CA-12, INV-278-14 Oui (IF NOT EXISTS)
3 CHECK constraint chk_dip_temporal_order : returned_at >= disseminated_at INV-278-13 Non (constraint named)
4 Trigger WORM trg_motif_communication_worm bloque UPDATE motif_communication apres insertion INV-278-06 Oui (CREATE OR REPLACE + DROP IF EXISTS)
5 3 index partiels : idx_documents_dip_status, idx_documents_retention_due, idx_documents_dissemination_package Performance Oui (IF NOT EXISTS)
6 Table vault_secure.dissemination_attestations partitionnee RANGE(issued_at), 2 partitions (2025, 2026) INV-278-05 Oui (IF NOT EXISTS)
7 3 index attestations : idx_attestations_actor, idx_attestations_issued_at, idx_attestations_package Performance Oui (IF NOT EXISTS)
8 Trigger append-only trg_dissemination_attestations_worm bloque UPDATE et DELETE sur attestations INV-278-05, spec SS5.13 Oui (CREATE OR REPLACE + DROP IF EXISTS)
9 REVOKE TRUNCATE ON vault_secure.dissemination_attestations FROM PUBLIC Defense en profondeur Oui (idempotent)

down()

  • Garde bloquante : COUNT(*) WHERE status = 'DIP' > 0 → exception avec message explicite
  • Drop en ordre inverse : trigger WORM attestations → index attestations → partitions (CASCADE) → table attestations (CASCADE) → index documents → trigger/function WORM motif → CHECK constraint → colonnes
  • L'enum DIP reste dans le type PostgreSQL (DROP VALUE n'existe pas)

Invariants couverts

Invariant Mecanisme Verification
INV-278-01 ALTER TYPE ADD VALUE IF NOT EXISTS 'DIP' SELECT unnest(enum_range(NULL::vault_secure.document_status)) contient DIP
INV-278-05 CREATE TABLE vault_secure.dissemination_attestations PARTITION BY RANGE (issued_at) + trigger append-only + REVOKE TRUNCATE Table existe, 2 partitions 2025/2026, UPDATE/DELETE rejetes, TRUNCATE revoque
INV-278-06 Trigger trg_motif_communication_worm + function trg_motif_communication_immutable() UPDATE motif apres INSERT → restrict_violation
INV-278-13 CHECK chk_dip_temporal_order INSERT avec returned_at < disseminated_at → rejet
INV-278-14 Colonne retention_due BOOLEAN NOT NULL DEFAULT false Colonne existe, default false
Down guard COUNT(*) WHERE status = 'DIP' > 0 → exception Down avec DIP actif → erreur

Interdits respectes

Interdit Verification
ALTER TYPE ADD VALUE dans BEGIN/COMMIT queryRunner.query() direct, pas de startTransaction() autour
Subquery dans WHERE d'index partiel (REX PD-55) Predicats simples uniquement (WHERE status = 'DIP', WHERE retention_due = true, WHERE ... IS NOT NULL)
DROP TYPE document_status Absent du down() — commentaire explicite
Math.random() Aucun appel — identifiants generes par gen_random_uuid() PostgreSQL

Decisions architecturales

Decision Justification Alternatives considerees Trade-offs
ALTER TYPE hors transaction + colonnes idempotentes IF NOT EXISTS Pattern etabli PD-250/PD-279, PostgreSQL contraint ALTER TYPE ADD VALUE hors tx Nouvelle table de mapping status, Type VARCHAR au lieu d'enum Enum natif = performance + integrite, mais non supprimable
Table dediee dissemination_attestations avec partitioning RANGE(issued_at) Schema riche SS5.13, conservation 10 ans, partitioning = archivage + performance requetes Reutilisation integrity_journal_entries, JSONB dans documents Table dediee = surface plus large mais requetes attestation independantes et archivage par annee

Patterns reutilises

  • PD-250 : Pattern ALTER TYPE ADD VALUE IF NOT EXISTS hors transaction
  • PD-279 : Pattern down migration guard COUNT(*) WHERE status = ... > 0
  • PD-272 : Pattern trigger WORM CREATE OR REPLACE FUNCTION + BEFORE UPDATE
  • PD-251/PD-237/PD-275 : Pattern REVOKE TRUNCATE sur tables append-only
  • PD-272 : Pattern trigger append-only BEFORE UPDATE OR DELETE pour attestations
  • PD-250 : Pattern COMMENT ON pour documentation DDL

Hypotheses

ID Hypothese Impact
H-07 retention_due est un champ stocke, mis a jour par le retention orchestrator La colonne est creee ici ; l'ecriture est hors perimetre PD-278
H-09 Attestation stockee dans table dediee avec partitioning Table creee avec 2 partitions initiales (2025, 2026)

Schema attestations (SS5.13)

vault_secure.dissemination_attestations (PARTITION BY RANGE issued_at)
  ├── id                        UUID PK (gen_random_uuid)
  ├── attestation_id            UUID NOT NULL UNIQUE (per partition)
  ├── request_id                UUID NOT NULL
  ├── document_ids              UUID[] NOT NULL
  ├── actor_id                  UUID NOT NULL
  ├── issued_at                 TIMESTAMPTZ NOT NULL DEFAULT NOW()
  ├── motif_communication       VARCHAR(1024) NULL
  ├── dissemination_package_id  UUID NULL
  ├── hash_evidence             TEXT NOT NULL
  ├── signature_ref             TEXT NULL (STUB: PD-37)
  └── created_at                TIMESTAMPTZ NOT NULL DEFAULT NOW()

Protections:
  ├── TRIGGER trg_dissemination_attestations_worm (BEFORE UPDATE OR DELETE → restrict_violation)
  └── REVOKE TRUNCATE FROM PUBLIC

Note : PRIMARY KEY et UNIQUE incluent issued_at pour la compatibilite avec le partitioning PostgreSQL (la colonne de partition doit faire partie de la cle primaire).

Stubs documentes

Stub Story destination Champ
signature_ref PD-37 — HSM signature probante dissemination_attestations.signature_ref

Matrice de couverture tests

Test ID Couverture par cette migration
TC-INV-01 DIP dans enum_range (Phase 1)
TC-INV-05 Table attestations avec partitioning + append-only (Phases 6, 8, 9)
TC-INV-06 Trigger WORM motif_communication (Phase 4)
TC-INV-13 CHECK constraint temporal order (Phase 3)
TC-NR-06 Down migration bloquee si DIP actif
TC-NOM-06 Trigger WORM bloque UPDATE motif (Phase 4)
TC-NOM-07 Colonne dissemination_package_id nullable (Phase 2)