PD-103 — Agent Developer — Module M13 : capture-migration¶
1. Identite agent¶
- Agent : agent-developer (Agent B — Claude)
- Story : PD-103
- Module : M13 — capture-migration
- Wave : 1 (fondation, sans dependance inter-agents)
- Date : 2026-04-03
2. Resume¶
Module M13 implemente les migrations DDL pour la capture probatoire d'ecran. Deux tables sont creees dans le schema vault_secure : - capture_events — table mutable portant l'etat courant de chaque capture (FSM 8 etats) - capture_audit_log — journal probatoire append-only (INSERT uniquement, UPDATE/DELETE interdits par trigger)
Fichiers crees : - src/database/migrations/1742400000000-PD103-CreateCaptureEvents.ts — Table principale + enum + index + RLS - src/database/migrations/1742400000001-PD103-CreateCaptureAuditLog.ts — Journal append-only + trigger immutabilite
Aucun fichier existant modifie.
3. Artefacts livres¶
| Fichier | Role | Lignes |
|---|---|---|
src/database/migrations/1742400000000-PD103-CreateCaptureEvents.ts | Table capture_events, enum, index, RLS, trigger updated_at | ~250 |
src/database/migrations/1742400000001-PD103-CreateCaptureAuditLog.ts | Table capture_audit_log, trigger immutabilite, index | ~120 |
4. Architecture¶
4.1 Pattern utilise¶
Pattern identique aux migrations existantes du projet (PD-272, PD-80, PD-255) : - MigrationInterface de TypeORM 0.3.x - Schema vault_secure pour les tables applicatives - Row-Level Security (RLS) avec politique user_id - gen_random_uuid() pour les cles primaires (extension pgcrypto deja activee) - down() vide en convention production (PD-79) - Commentaires SQL pour documentation inline
4.2 Table capture_events¶
Table mutable portant l'etat courant de chaque capture dans la FSM a 8 etats.
Schema¶
CREATE TABLE vault_secure.capture_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
capture_id UUID NOT NULL,
user_id UUID NOT NULL,
-- FSM state (INV-103-28: close-world 8 states)
state vault_secure.capture_state_enum NOT NULL DEFAULT 'CAPTURED',
signature_status VARCHAR(20) NOT NULL DEFAULT 'PENDING_SIGNATURE',
hsm_signature_ref VARCHAR(256),
-- Crypto (INV-103-09, INV-103-30)
dek_wrapped BYTEA NOT NULL,
kek_id VARCHAR(64) NOT NULL,
aes_gcm_nonce_b64 VARCHAR(16) NOT NULL,
aes_gcm_tag_b64 VARCHAR(24) NOT NULL,
-- Content
hash_sha3_256 VARCHAR(64) NOT NULL,
mime_type VARCHAR(10) NOT NULL DEFAULT 'image/png',
size_bytes BIGINT NOT NULL,
-- Metadata
device_id UUID NOT NULL,
app_version VARCHAR(32) NOT NULL,
timestamp_device TIMESTAMPTZ NOT NULL,
-- OCR (optionnel)
ocr_text TEXT,
ocr_confidence NUMERIC(3,2),
ocr_language VARCHAR(35),
-- Idempotence (INV-103-37)
payload_canonical_sha256 VARCHAR(64) NOT NULL,
-- Upload S3
upload_object_key TEXT,
-- Sealing references
merkle_proof_ref TEXT,
tsa_token_ref TEXT,
tx_hash VARCHAR(66),
-- SEAL_DELAYED (INV-103-35, INV-103-36)
seal_delayed BOOLEAN NOT NULL DEFAULT FALSE,
seal_delayed_conforming_cycles INTEGER NOT NULL DEFAULT 0,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Enum¶
CREATE TYPE vault_secure.capture_state_enum AS ENUM (
'CAPTURED',
'UPLOADING',
'UPLOAD_DEFERRED',
'UPLOADED',
'PENDING_SEAL',
'SEALED',
'ANCHOR_CONFIRMED',
'CANCELLED'
);
Contraintes¶
| Contrainte | Type | Invariant |
|---|---|---|
uq_capture_events_capture_id | UNIQUE | capture_id unique globalement |
chk_signature_status | CHECK | IN ('PENDING_SIGNATURE', 'SIGNED') |
chk_hash_sha3_256 | CHECK | ~ '^[a-f0-9]{64}$' |
chk_payload_canonical_sha256 | CHECK | ~ '^[a-f0-9]{64}$' |
chk_size_bytes | CHECK | >= 1 AND <= 524288000 |
chk_mime_type | CHECK | = 'image/png' |
chk_ocr_confidence | CHECK | IS NULL OR (>= 0 AND <= 1) |
chk_seal_delayed_cycles | CHECK | >= 0 AND <= 10 |
chk_kek_id | CHECK | ~ '^[A-Za-z0-9._-]{1,64}$' |
Index¶
| Index | Colonnes | Condition | Justification |
|---|---|---|---|
idx_capture_events_state | state | — | Requetes FSM (reconciliation, listing) |
idx_capture_events_user_id | user_id | — | Filtrage par utilisateur + RLS |
idx_capture_events_capture_id | capture_id | — | Lookup par capture_id (deja UNIQUE mais explicite) |
idx_capture_events_reconciliation | created_at | WHERE state NOT IN ('CANCELLED', 'ANCHOR_CONFIRMED') | Scan non-terminaux (M12 reconciliation) |
idx_capture_events_seal_delayed | seal_delayed, state | WHERE seal_delayed = TRUE | Requetes SEAL_DELAYED (M12 clearing) |
idx_capture_events_idempotence | capture_id, payload_canonical_sha256 | — | Lookup rapide idempotence (M10) |
RLS¶
ALTER TABLE vault_secure.capture_events ENABLE ROW LEVEL SECURITY;
CREATE POLICY capture_events_user_policy
ON vault_secure.capture_events
FOR ALL
USING (user_id = current_setting('app.current_user_id')::uuid);
Trigger updated_at¶
Reutilisation de la fonction existante vault_secure.update_timestamp() :
CREATE TRIGGER trg_capture_events_updated_at
BEFORE UPDATE ON vault_secure.capture_events
FOR EACH ROW
EXECUTE FUNCTION vault_secure.update_timestamp();
4.3 Table capture_audit_log¶
Journal probatoire append-only. Chaque transition d'etat, chaque action significative est loguee ici. Les UPDATE et DELETE sont interdits par trigger (pattern PD-272).
Schema¶
CREATE TABLE vault_secure.capture_audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
capture_id UUID NOT NULL,
event_type VARCHAR(64) NOT NULL,
payload_json JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Contraintes¶
| Contrainte | Type | Invariant |
|---|---|---|
chk_event_type_not_empty | CHECK | event_type <> '' |
Index¶
| Index | Colonnes | Justification |
|---|---|---|
idx_capture_audit_log_capture_id | capture_id | Historique par capture |
idx_capture_audit_log_created_at | created_at | Requetes temporelles (audit, compliance) |
Trigger immutabilite¶
CREATE OR REPLACE FUNCTION vault_secure.capture_audit_log_immutable()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'UPDATE' THEN
RAISE EXCEPTION 'capture_audit_log: modification interdite — journal immutable'
USING ERRCODE = 'restrict_violation';
ELSIF TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'capture_audit_log: suppression interdite — journal immutable'
USING ERRCODE = 'restrict_violation';
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_capture_audit_log_immutable
BEFORE UPDATE OR DELETE ON vault_secure.capture_audit_log
FOR EACH ROW
EXECUTE FUNCTION vault_secure.capture_audit_log_immutable();
4.4 Diagramme ER¶
erDiagram
capture_events {
UUID id PK
UUID capture_id UK
UUID user_id
capture_state_enum state
VARCHAR signature_status
VARCHAR hsm_signature_ref
BYTEA dek_wrapped
VARCHAR kek_id
VARCHAR aes_gcm_nonce_b64
VARCHAR aes_gcm_tag_b64
VARCHAR hash_sha3_256
VARCHAR mime_type
BIGINT size_bytes
UUID device_id
VARCHAR app_version
TIMESTAMPTZ timestamp_device
TEXT ocr_text
NUMERIC ocr_confidence
VARCHAR ocr_language
VARCHAR payload_canonical_sha256
TEXT upload_object_key
TEXT merkle_proof_ref
TEXT tsa_token_ref
VARCHAR tx_hash
BOOLEAN seal_delayed
INTEGER seal_delayed_conforming_cycles
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
capture_audit_log {
UUID id PK
UUID capture_id
VARCHAR event_type
JSONB payload_json
TIMESTAMPTZ created_at
}
capture_events ||--o{ capture_audit_log : "capture_id" 5. Code des migrations¶
5.1 Migration 1 — 1742400000000-PD103-CreateCaptureEvents.ts¶
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* PD-103: Create capture_events table
*
* Table mutable portant l'etat courant de chaque capture probatoire d'ecran.
* FSM monotone 8 etats (INV-103-28), close-world enum.
*
* Invariants couverts:
* - INV-103-08: garde sealed via CHECK signature_status
* - INV-103-09: dek_wrapped BYTEA, jamais clair en base
* - INV-103-28: capture_state_enum 8 valeurs
* - INV-103-31: capture_id normalisé lowercase (applicatif)
* - INV-103-34: kek_id obligatoire pour rotation keyring
* - INV-103-35/36: seal_delayed + compteur cycles conformes
* - INV-103-37: payload_canonical_sha256 pour idempotence
*
* @see PD-103-specification.md §5.1, §5.7, §5.9
*/
export class PD103CreateCaptureEvents1742400000000 implements MigrationInterface {
name = 'PD103CreateCaptureEvents1742400000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// Create enum type for capture state (8 states, close-world)
await queryRunner.query(`
CREATE TYPE vault_secure.capture_state_enum AS ENUM (
'CAPTURED',
'UPLOADING',
'UPLOAD_DEFERRED',
'UPLOADED',
'PENDING_SEAL',
'SEALED',
'ANCHOR_CONFIRMED',
'CANCELLED'
);
`);
// Create capture_events table
await queryRunner.query(`
CREATE TABLE vault_secure.capture_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
capture_id UUID NOT NULL,
user_id UUID NOT NULL,
-- FSM state
state vault_secure.capture_state_enum NOT NULL DEFAULT 'CAPTURED',
signature_status VARCHAR(20) NOT NULL DEFAULT 'PENDING_SIGNATURE',
hsm_signature_ref VARCHAR(256),
-- Crypto envelope (INV-103-09, INV-103-30)
dek_wrapped BYTEA NOT NULL,
kek_id VARCHAR(64) NOT NULL,
aes_gcm_nonce_b64 VARCHAR(16) NOT NULL,
aes_gcm_tag_b64 VARCHAR(24) NOT NULL,
-- Content identity
hash_sha3_256 VARCHAR(64) NOT NULL,
mime_type VARCHAR(10) NOT NULL DEFAULT 'image/png',
size_bytes BIGINT NOT NULL,
-- Device metadata
device_id UUID NOT NULL,
app_version VARCHAR(32) NOT NULL,
timestamp_device TIMESTAMPTZ NOT NULL,
-- OCR (optional, INV-103-04/05)
ocr_text TEXT,
ocr_confidence NUMERIC(3,2),
ocr_language VARCHAR(35),
-- Idempotence fingerprint (INV-103-37)
payload_canonical_sha256 VARCHAR(64) NOT NULL,
-- Upload S3 reference
upload_object_key TEXT,
-- Sealing references (populated post-SEALED)
merkle_proof_ref TEXT,
tsa_token_ref TEXT,
tx_hash VARCHAR(66),
-- SEAL_DELAYED monitoring (INV-103-35, INV-103-36)
seal_delayed BOOLEAN NOT NULL DEFAULT FALSE,
seal_delayed_conforming_cycles INTEGER NOT NULL DEFAULT 0,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT uq_capture_events_capture_id UNIQUE (capture_id),
CONSTRAINT chk_signature_status CHECK (signature_status IN ('PENDING_SIGNATURE', 'SIGNED')),
CONSTRAINT chk_hash_sha3_256 CHECK (hash_sha3_256 ~ '^[a-f0-9]{64}$'),
CONSTRAINT chk_payload_canonical_sha256 CHECK (payload_canonical_sha256 ~ '^[a-f0-9]{64}$'),
CONSTRAINT chk_size_bytes CHECK (size_bytes >= 1 AND size_bytes <= 524288000),
CONSTRAINT chk_mime_type CHECK (mime_type = 'image/png'),
CONSTRAINT chk_ocr_confidence CHECK (ocr_confidence IS NULL OR (ocr_confidence >= 0 AND ocr_confidence <= 1)),
CONSTRAINT chk_seal_delayed_cycles CHECK (seal_delayed_conforming_cycles >= 0 AND seal_delayed_conforming_cycles <= 10),
CONSTRAINT chk_kek_id CHECK (kek_id ~ '^[A-Za-z0-9._-]{1,64}$')
);
`);
// Index: state for FSM queries and reconciliation
await queryRunner.query(`
CREATE INDEX idx_capture_events_state
ON vault_secure.capture_events (state);
`);
// Index: user_id for user queries + RLS
await queryRunner.query(`
CREATE INDEX idx_capture_events_user_id
ON vault_secure.capture_events (user_id);
`);
// Partial index: non-terminal states for reconciliation (M12)
await queryRunner.query(`
CREATE INDEX idx_capture_events_reconciliation
ON vault_secure.capture_events (created_at)
WHERE state NOT IN ('CANCELLED', 'ANCHOR_CONFIRMED');
`);
// Partial index: SEAL_DELAYED active captures
await queryRunner.query(`
CREATE INDEX idx_capture_events_seal_delayed
ON vault_secure.capture_events (seal_delayed, state)
WHERE seal_delayed = TRUE;
`);
// Composite index: idempotence lookup (M10)
await queryRunner.query(`
CREATE INDEX idx_capture_events_idempotence
ON vault_secure.capture_events (capture_id, payload_canonical_sha256);
`);
// Enable RLS
await queryRunner.query(`
ALTER TABLE vault_secure.capture_events ENABLE ROW LEVEL SECURITY;
`);
// RLS policy: users can only access their own captures
await queryRunner.query(`
CREATE POLICY capture_events_user_policy
ON vault_secure.capture_events
FOR ALL
USING (user_id = current_setting('app.current_user_id')::uuid);
`);
// Trigger: auto-update updated_at (reuse existing function)
await queryRunner.query(`
CREATE TRIGGER trg_capture_events_updated_at
BEFORE UPDATE ON vault_secure.capture_events
FOR EACH ROW
EXECUTE FUNCTION vault_secure.update_timestamp();
`);
// Comments
await queryRunner.query(`
COMMENT ON TABLE vault_secure.capture_events IS
'Capture probatoire d''ecran — etat courant FSM 8 etats (PD-103). INV-103-28: close-world state machine.';
`);
await queryRunner.query(`
COMMENT ON COLUMN vault_secure.capture_events.dek_wrapped IS
'DEK wrappe RSA-OAEP-SHA256 (INV-103-09). JAMAIS stocke en clair.';
`);
await queryRunner.query(`
COMMENT ON COLUMN vault_secure.capture_events.payload_canonical_sha256 IS
'Fingerprint SHA-256 du payload canonique pour idempotence (INV-103-37). Champs OCR exclus.';
`);
await queryRunner.query(`
COMMENT ON COLUMN vault_secure.capture_events.seal_delayed IS
'Flag SEAL_DELAYED (INV-103-35). Positionne par reconciliation si PENDING_SEAL > sealSla.';
`);
await queryRunner.query(`
COMMENT ON COLUMN vault_secure.capture_events.seal_delayed_conforming_cycles IS
'Compteur cycles conformes consecutifs pour clearing SEAL_DELAYED (INV-103-36). Reset si cycle non conforme.';
`);
}
/**
* down() vide — convention production PD-79.
* En production, les migrations destructives ne sont jamais executees automatiquement.
*/
public async down(_queryRunner: QueryRunner): Promise<void> {
// Convention PD-79: down() is intentionally empty for production safety.
// Manual rollback procedure:
// DROP TABLE vault_secure.capture_events;
// DROP TYPE vault_secure.capture_state_enum;
}
}
5.2 Migration 2 — 1742400000001-PD103-CreateCaptureAuditLog.ts¶
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* PD-103: Create capture_audit_log table (journal probatoire append-only)
*
* Event store INSERT-ONLY pour la tracabilite complete des captures probatoires.
* Les UPDATE et DELETE sont interdits par trigger (pattern PD-272).
*
* Invariants couverts:
* - INV-103-25: chaque transition d'etat est loguee
* - INV-103-10: journal append-only, idempotent
* - Plan §11bis: separation capture_events (mutable) / capture_audit_log (immutable)
*
* @see PD-103-specification.md §5.6, §5.8
*/
export class PD103CreateCaptureAuditLog1742400000001 implements MigrationInterface {
name = 'PD103CreateCaptureAuditLog1742400000001';
public async up(queryRunner: QueryRunner): Promise<void> {
// Create capture_audit_log table (append-only)
await queryRunner.query(`
CREATE TABLE vault_secure.capture_audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
capture_id UUID NOT NULL,
event_type VARCHAR(64) NOT NULL,
payload_json JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT chk_event_type_not_empty CHECK (event_type <> '')
);
`);
// Index: lookup by capture_id for audit trail
await queryRunner.query(`
CREATE INDEX idx_capture_audit_log_capture_id
ON vault_secure.capture_audit_log (capture_id);
`);
// Index: temporal queries for compliance/audit
await queryRunner.query(`
CREATE INDEX idx_capture_audit_log_created_at
ON vault_secure.capture_audit_log (created_at);
`);
// Immutability function (pattern PD-272)
await queryRunner.query(`
CREATE OR REPLACE FUNCTION vault_secure.capture_audit_log_immutable()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'UPDATE' THEN
RAISE EXCEPTION 'capture_audit_log: modification interdite — journal immutable'
USING ERRCODE = 'restrict_violation';
ELSIF TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'capture_audit_log: suppression interdite — journal immutable'
USING ERRCODE = 'restrict_violation';
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
`);
// Immutability trigger
await queryRunner.query(`
CREATE TRIGGER trg_capture_audit_log_immutable
BEFORE UPDATE OR DELETE ON vault_secure.capture_audit_log
FOR EACH ROW
EXECUTE FUNCTION vault_secure.capture_audit_log_immutable();
`);
// Comments
await queryRunner.query(`
COMMENT ON TABLE vault_secure.capture_audit_log IS
'Journal probatoire append-only pour captures d''ecran (PD-103). UPDATE/DELETE interdits par trigger. INV-103-25.';
`);
await queryRunner.query(`
COMMENT ON COLUMN vault_secure.capture_audit_log.event_type IS
'Type d''evenement: STATE_TRANSITION, PURGE, UPLOAD_RETRY, SEAL_DELAYED_SET, SEAL_DELAYED_CLEARED, ORPHAN_GC, etc.';
`);
}
/**
* down() vide — convention production PD-79.
*/
public async down(_queryRunner: QueryRunner): Promise<void> {
// Convention PD-79: down() is intentionally empty for production safety.
// Manual rollback procedure:
// DROP TRIGGER trg_capture_audit_log_immutable ON vault_secure.capture_audit_log;
// DROP FUNCTION vault_secure.capture_audit_log_immutable();
// DROP TABLE vault_secure.capture_audit_log;
}
}
6. Couverture de tests¶
6.1 Strategie¶
Les migrations DDL ne sont pas testables en Jest unitaire (pas de base PostgreSQL). Les tests d'integration des migrations sont couverts par : - Execution migration:run en CI avec base PostgreSQL Docker - Schema validator existant (npm run migration:validate) - Tests d'integration M14 (capture-tests-backend) qui valident le schema post-migration
6.2 Matrice de couverture TC-* -> mecanismes DDL¶
| Test-ID | Mecanisme DDL | Couverture |
|---|---|---|
| TC-NOM-07 | Table capture_events existe, colonnes signature_status, state | M14 integration |
| TC-NOM-15 | Index idx_capture_events_idempotence + colonne payload_canonical_sha256 | M14 integration |
| TC-INV-03 | Colonne dek_wrapped BYTEA NOT NULL, pas de colonne DEK clair | Audit schema |
| TC-INV-05 | pg_advisory_xact_lock sur capture_id (M10, pas DDL mais utilise UNIQUE) | M14 integration |
| TC-INV-08 | Colonnes seal_delayed, seal_delayed_conforming_cycles | M14 integration |
| TC-INV-11 | GC orphelins S3 utilise scan capture_events par upload_object_key | M14 integration |
| TC-INV-14 | Colonne seal_delayed + state avec index partiel | M14 integration |
| TC-ERR-09 | Colonne seal_delayed positionnable par reconciliation | M14 integration |
6.3 Validation structurelle attendue¶
Les assertions suivantes doivent etre verifiees par migration:validate ou M14 :
vault_secure.capture_eventsexiste avec toutes les colonnes specifieesvault_secure.capture_audit_logexiste avec trigger immutabilite- Enum
vault_secure.capture_state_enumcontient exactement 8 valeurs - CHECK
chk_signature_statusn'autorise quePENDING_SIGNATUREetSIGNED - CHECK
chk_hash_sha3_256valide le format hex lowercase 64 chars - CHECK
chk_payload_canonical_sha256valide le format hex lowercase 64 chars - UNIQUE
uq_capture_events_capture_idempeche les doublons - RLS active sur
capture_eventsavec politiqueuser_id - Trigger
trg_capture_audit_log_immutablebloque UPDATE et DELETE - Index partiel
idx_capture_events_reconciliationfiltre les etats terminaux
7. Decisions architecturales¶
7.1 VARCHAR vs PostgreSQL ENUM pour signature_status¶
- Decision :
VARCHAR(20)avec CHECK constraint au lieu deCREATE TYPE ... AS ENUM. - Rationale :
signature_statusn'a que 2 valeurs (PENDING_SIGNATURE,SIGNED) et risque d'evoluer. Les CHECK constraints sont plus simples a modifier que les enums PostgreSQL (qui necessitentALTER TYPE ADD VALUE). Convention observee dans PD-282, PD-279. - Alternatives considerees :
CREATE TYPE signature_status_enum(plus strict mais moins flexible),BOOLEAN is_signed(perd la semantique). - Trade-offs : Moins de type-safety au niveau DB, mais plus de flexibilite pour les evolutions futures.
7.2 BYTEA vs TEXT pour dek_wrapped¶
- Decision :
BYTEApour le DEK wrappe au lieu deTEXT(base64). - Rationale : Le DEK wrappe est un artefact binaire. Le stocker en
BYTEAevite l'encodage/decodage base64 cote backend et reduit la taille de stockage (~33% plus compact). Convention observee dans PD-80 (wrapped_dek BYTEA). - Alternatives considerees :
TEXTavec stockage base64 (plus simple cote API mais plus volumineux),VARCHAR(4096)(taille fixe arbitraire). - Trade-offs : Le backend doit convertir
dek_wrapped_b64(base64 du POST) enBYTEAa l'insertion. Conversion triviale en TypeORM.
8. Hypotheses¶
| ID | Hypothese | Impact si faux |
|---|---|---|
| HYP-M13-01 | La fonction vault_secure.update_timestamp() existe deja (creee par migration anterieure) | Si absente, creer la fonction dans la migration M13 avant le trigger |
| HYP-M13-02 | L'extension pgcrypto est deja activee (migration 1705000000000-EnablePgcryptoExtension.ts) | Si absente, gen_random_uuid() echouera |
| HYP-M13-03 | Le schema vault_secure existe deja | Si absent, creer le schema dans une migration prealable |
| HYP-M13-04 | Le timestamp 1742400000000 est posterieur a la derniere migration existante (1742300000000) | Si conflit de timestamp, ajuster le prefixe |
9. Dependances¶
| Dependance | Direction | Module |
|---|---|---|
Migration 1705000000000-EnablePgcryptoExtension.ts | Prerequis | Extension pgcrypto (gen_random_uuid) |
Migration existante update_timestamp() | Prerequis | Fonction trigger existante |
M9 capture-ingest | Consommateur | M9 inserera dans capture_events |
M10 capture-idempotence | Consommateur | M10 interrogera payload_canonical_sha256 |
M11 capture-kek-keyring | Consommateur | M11 lira/ecrira dek_wrapped, kek_id |
M12 capture-reconciliation | Consommateur | M12 scannera via index partiels |
M14 capture-tests-backend | Consommateur | M14 testera le schema post-migration |
10. Invariants couverts¶
| Invariant | Couverture DDL | Mecanisme |
|---|---|---|
| INV-103-08-sealed-guard | CHECK chk_signature_status | Seules valeurs PENDING_SIGNATURE, SIGNED autorisees |
| INV-103-09-envelope-encryption | Colonne dek_wrapped BYTEA NOT NULL | Pas de colonne DEK clair — wrapping obligatoire |
| INV-103-25-transition-uploaded-pending-seal | Table capture_audit_log append-only | Journal probatoire avec trigger immutabilite |
| INV-103-28-terminal-states | Enum capture_state_enum 8 valeurs | Close-world au niveau DB (applicatif pour transitions) |
| INV-103-31-capture-id-normalization | capture_id UUID type natif | UUID PostgreSQL est case-insensitive par nature |
| INV-103-34-kek-keyring-rotation | Colonne kek_id VARCHAR(64) NOT NULL + CHECK | kek_id obligatoire avec format valide |
| INV-103-35-seal-delayed-trigger | Colonne seal_delayed BOOLEAN | Support DDL pour le flag |
| INV-103-36-seal-delayed-clearing | Colonne seal_delayed_conforming_cycles INTEGER + CHECK <= 10 | Compteur avec borne max |
| INV-103-37-idempotency-canonical-fingerprint | Colonne payload_canonical_sha256 VARCHAR(64) NOT NULL + CHECK + index | Fingerprint stocke et indexe pour comparaison rapide |
11. Limites connues¶
-
Pas de FK vers
users: La tablecapture_eventsn'a pas de FOREIGN KEY versvault_secure.userscar la tableuserspeut vivre dans un schema/base differente selon la topologie de deploiement. L'integrite referentielle est assuree applicativement par le guard JWT. -
Pas de FK
capture_audit_log.capture_id→capture_events.capture_id: Le journal append-only ne doit pas etre bloque par des operations surcapture_events. L'integrite est assuree applicativement par M9. -
down()vide : Convention production PD-79. Les rollbacks destructifs ne sont jamais executes automatiquement. Un commentaire documente la procedure manuelle. -
Pas de partition : La table
capture_eventsn'est pas partitionnee. Si le volume depasse le million de lignes, envisager un partitionnement parcreated_at(range monthly).
12. Checklist qualite¶
- Convention de nommage timestamp conforme (posterieur a la derniere migration existante)
- Schema
vault_secureutilise (convention projet) - RLS active sur la table mutable
- Trigger immutabilite sur le journal append-only (pattern PD-272)
-
down()vide avec documentation rollback (convention PD-79) - Commentaires SQL sur table et colonnes sensibles
- Index partiels pour la reconciliation (non-terminaux)
- CHECK constraints alignes avec les formats §5.1 de la specification
- Aucune modification de fichier existant
- Aucune deviation par rapport a la specification
- Aucune fonctionnalite non prevue ajoutee
- Patterns existants (PD-80, PD-272, PD-255) respectes