PD-284 — Agent Developer : Module seal-types¶
Livrable : src/types/seal.ts¶
Résumé¶
Ce module définit l'ensemble des types TypeScript pour la story PD-284 (UX scellement urgent et suivi temps réel). Il couvre :
- Les branded types
SealIdetDocumentId(pas destringnu — invariant du code contract) - L'union littérale exhaustive
SealStatedes 7 états PD-80 - Les types d'événements SSE avec champs conditionnels par état (§5.12)
- Les types de configuration, dégradation, artefacts expert
- Les regex de validation pour chaque champ format (§5.6)
Décisions architecturales¶
architectural_decisions:
- decision: "Branded types via intersection `string & { readonly __brand: 'X' }`"
rationale: "Empêche l'inversion silencieuse SealId/DocumentId à la compilation. Pattern standard TypeScript sans runtime overhead."
alternatives_considered:
- "Opaque types via module-level Symbol — trop complexe pour l'usage"
- "Newtype via class wrapper — overhead runtime inutile"
trade_offs:
- "Nécessite des fonctions factory `asSealId()` / `asDocumentId()` pour créer les valeurs"
- "Pas de validation runtime dans le branded type lui-même (validation Zod séparée)"
Code source¶
// src/types/seal.ts
// PD-284 — Types TypeScript : états, événements SSE, payloads, branded types
// =============================================================================
// Branded types (INV contract : pas de string nu pour seal_id / document_id)
// =============================================================================
export type SealId = string & { readonly __brand: 'SealId' };
export type DocumentId = string & { readonly __brand: 'DocumentId' };
export function asSealId(value: string): SealId {
return value as SealId;
}
export function asDocumentId(value: string): DocumentId {
return value as DocumentId;
}
// =============================================================================
// SealState — Union littérale exhaustive des 7 états PD-80 (§5.7)
// =============================================================================
export const SEAL_STATES = [
'RECEIVED',
'QUEUED_PRIORITY',
'TSA_PENDING',
'TSA_SEALED',
'ANCHOR_PENDING',
'SEALED',
'FAILED_TIMEOUT',
] as const;
export type SealState = (typeof SEAL_STATES)[number];
export const TERMINAL_STATES: readonly SealState[] = [
'SEALED',
'FAILED_TIMEOUT',
] as const;
// =============================================================================
// DegradationFlag — Valeurs contractuelles (§5.3)
// =============================================================================
export const DEGRADATION_FLAGS = ['none', 'delayed', 'critical'] as const;
export type DegradationFlag = (typeof DEGRADATION_FLAGS)[number];
// =============================================================================
// Types dédiés avec regex de validation (§5.6)
// =============================================================================
export const SEAL_VALIDATION_PATTERNS = {
UUID_V4: /^[0-9a-fA-F-]{36}$/,
HASH_DOCUMENT: /^[a-f0-9]{64}$/,
TSA_TOKEN_REF: /^[A-Za-z0-9:_-]{1,256}$/,
MERKLE_ROOT: /^[a-f0-9]{64}$/,
MERKLE_PROOF_ELEMENT: /^[a-f0-9]{64}$/,
BLOCKCHAIN_TX_HASH: /^0x[a-f0-9]{64}$/,
PROOF_PACKAGE_URL: /^https:\/\/.+$/,
EVENT_ID: /^[1-9][0-9]{0,18}$/,
SEQUENCE_NUMBER: /^[1-9][0-9]{0,18}$/,
} as const;
// =============================================================================
// Typed wrappers pour champs format §5.6
// =============================================================================
/** SHA-256 hex, 64 chars, lowercase (§5.6) */
export type HashDocument = string & { readonly __brand: 'HashDocument' };
/** ASCII alphanumérique + `:_-`, 1..256 chars (§5.6) — artefact SENSIBLE */
export type TsaTokenRef = string & { readonly __brand: 'TsaTokenRef' };
/** Hex 64 chars lowercase (§5.6) */
export type MerkleRoot = string & { readonly __brand: 'MerkleRoot' };
/** Hex 64 chars lowercase par élément (§5.6) */
export type MerkleProofElement = string & { readonly __brand: 'MerkleProofElement' };
/** Hex préfixé 0x, 66 chars total (§5.6) */
export type BlockchainTxHash = string & { readonly __brand: 'BlockchainTxHash' };
/** URL HTTPS, 1..2048 chars (§5.6) */
export type ProofPackageUrl = string & { readonly __brand: 'ProofPackageUrl' };
/** ISO-8601 UTC timestamp (§5.6) */
export type TsaTimestamp = string & { readonly __brand: 'TsaTimestamp' };
/** Position dans la queue d'attente (entier positif) */
export type PositionInQueue = number & { readonly __brand: 'PositionInQueue' };
/** Raison d'échec du scellement (état FAILED_TIMEOUT) */
export type FailureReason = string & { readonly __brand: 'FailureReason' };
// =============================================================================
// AccountType — Enum pour l'éligibilité bouton urgent (§5.6)
// =============================================================================
export const ACCOUNT_TYPES = ['minor', 'standard', 'premium', 'enterprise'] as const;
export type AccountType = (typeof ACCOUNT_TYPES)[number];
// =============================================================================
// Événement SSE — Champs communs + champs conditionnels par état (§5.12)
// =============================================================================
/** Champs communs à TOUS les événements SSE */
interface SealEventBase {
event_id: number;
sequence_number: number;
seal_id: SealId;
status: SealState;
timestamp: string;
degradation_flag?: DegradationFlag;
document_id: DocumentId;
}
/** RECEIVED : hash_document obligatoire */
interface SealEventReceived extends SealEventBase {
status: 'RECEIVED';
hash_document: HashDocument;
}
/** QUEUED_PRIORITY : hash_document + position_in_queue obligatoires */
interface SealEventQueuedPriority extends SealEventBase {
status: 'QUEUED_PRIORITY';
hash_document: HashDocument;
position_in_queue: PositionInQueue;
}
/** TSA_PENDING : hash_document obligatoire */
interface SealEventTsaPending extends SealEventBase {
status: 'TSA_PENDING';
hash_document: HashDocument;
}
/** TSA_SEALED : hash_document + tsa_token_ref + tsa_timestamp obligatoires */
interface SealEventTsaSealed extends SealEventBase {
status: 'TSA_SEALED';
hash_document: HashDocument;
tsa_token_ref: TsaTokenRef;
tsa_timestamp: TsaTimestamp;
}
/** ANCHOR_PENDING : hash_document + merkle_root + merkle_proof[] obligatoires */
interface SealEventAnchorPending extends SealEventBase {
status: 'ANCHOR_PENDING';
hash_document: HashDocument;
merkle_root: MerkleRoot;
merkle_proof: MerkleProofElement[];
}
/** SEALED : tous les artefacts probatoires obligatoires */
interface SealEventSealed extends SealEventBase {
status: 'SEALED';
hash_document: HashDocument;
merkle_root: MerkleRoot;
merkle_proof: MerkleProofElement[];
blockchain_tx_hash: BlockchainTxHash;
proof_package_url: ProofPackageUrl;
}
/** FAILED_TIMEOUT : hash_document + failure_reason obligatoires */
interface SealEventFailedTimeout extends SealEventBase {
status: 'FAILED_TIMEOUT';
hash_document: HashDocument;
failure_reason: FailureReason;
}
/** Union discriminée par `status` — couvre exhaustivement les 7 états §5.12 */
export type SealEvent =
| SealEventReceived
| SealEventQueuedPriority
| SealEventTsaPending
| SealEventTsaSealed
| SealEventAnchorPending
| SealEventSealed
| SealEventFailedTimeout;
// =============================================================================
// SealStatusResponse — Réponse GET /seals/{id}/status
// =============================================================================
export type SealStatusResponse = SealEvent;
// =============================================================================
// ExpertFields — Champs affichables en mode expert par état (§5.4)
// =============================================================================
export interface ExpertFields {
hash_document?: HashDocument;
tsa_token_ref?: TsaTokenRef;
tsa_timestamp?: TsaTimestamp;
merkle_root?: MerkleRoot;
merkle_proof?: MerkleProofElement[];
blockchain_tx_hash?: BlockchainTxHash;
proof_package_url?: ProofPackageUrl;
}
/** Map définissant quels champs expert sont disponibles par état */
export const EXPERT_FIELDS_BY_STATE: Record<SealState, readonly (keyof ExpertFields)[]> = {
RECEIVED: ['hash_document'],
QUEUED_PRIORITY: ['hash_document'],
TSA_PENDING: ['hash_document'],
TSA_SEALED: ['hash_document', 'tsa_token_ref', 'tsa_timestamp'],
ANCHOR_PENDING: ['hash_document', 'merkle_root', 'merkle_proof'],
SEALED: ['hash_document', 'merkle_root', 'merkle_proof', 'blockchain_tx_hash', 'proof_package_url'],
FAILED_TIMEOUT: ['hash_document'],
};
// =============================================================================
// SealConfig — Paramètres numériques contractuels (§5.8)
// =============================================================================
export interface SealConfig {
/** Max SSE reconnect attempts before polling failover (default: 3) */
sse_reconnect_max_attempts_before_polling: number;
/** Base delay for SSE reconnect backoff in seconds (default: 1) */
sse_reconnect_base_delay: number;
/** Backoff multiplier (default: 2) */
sse_reconnect_backoff_factor: number;
/** Max cumulative backoff delay in seconds before immediate failover (default: 30) */
sse_reconnect_max_cumulative_delay: number;
/** Grace window in ms for sequence reordering (default: 200) */
sequence_reorder_grace_window: number;
/** Polling interval in seconds after SSE failover (default: 5) */
polling_interval_after_sse_failover: number;
/** Push notification title max length (default: 50) */
push_title_max_length: number;
/** Push notification body max length (default: 100) */
push_body_max_length: number;
}
export const DEFAULT_SEAL_CONFIG: Readonly<SealConfig> = {
sse_reconnect_max_attempts_before_polling: 3,
sse_reconnect_base_delay: 1,
sse_reconnect_backoff_factor: 2,
sse_reconnect_max_cumulative_delay: 30,
sequence_reorder_grace_window: 200,
polling_interval_after_sse_failover: 5,
push_title_max_length: 50,
push_body_max_length: 100,
};
// Bornes de clamp (§5.8 — hors bornes → clamp)
export const SEAL_CONFIG_BOUNDS: Record<keyof SealConfig, { min: number; max: number }> = {
sse_reconnect_max_attempts_before_polling: { min: 1, max: 10 },
sse_reconnect_base_delay: { min: 0.5, max: 5 },
sse_reconnect_backoff_factor: { min: 1, max: 3 },
sse_reconnect_max_cumulative_delay: { min: 5, max: 120 },
sequence_reorder_grace_window: { min: 50, max: 1000 },
polling_interval_after_sse_failover: { min: 2, max: 60 },
push_title_max_length: { min: 1, max: 50 },
push_body_max_length: { min: 1, max: 100 },
};
// =============================================================================
// Transport mode — État du canal de communication
// =============================================================================
export type TransportMode = 'SSE' | 'POLLING' | 'DISCONNECTED';
// =============================================================================
// Tooltips contractuels (INV-284-04 — texte exact, non modifiable)
// =============================================================================
export const TOOLTIP_QUOTA_EXHAUSTED = 'Quota épuisé ce mois' as const;
export const TOOLTIP_URGENT_ACTIVE = 'Scellement urgent déjà en cours' as const;
// =============================================================================
// Telemetry event types (erreurs contrôlées — §3)
// =============================================================================
export interface SealTelemetryEvent {
event_type: string;
seal_id: SealId | null;
timestamp: string;
details: Record<string, unknown>;
}
// =============================================================================
// Deep-link schema (SEC-01 — whitelist interne uniquement)
// =============================================================================
export const SEAL_DEEP_LINK_SCHEME = 'probatiovault' as const;
export const SEAL_DEEP_LINK_PATTERN = /^probatiovault:\/\/seal\/[0-9a-fA-F-]{36}$/ as const;
// =============================================================================
// Étapes visuelles de la carte de progression (mapping §2.1b du plan)
// =============================================================================
export const VISUAL_STEPS = [
'Capture',
'Horodatage TSA',
'Arbre Merkle',
'Blockchain',
'Scellé',
] as const;
export type VisualStep = (typeof VISUAL_STEPS)[number];
export const STATE_TO_VISUAL_STEP: Record<Exclude<SealState, 'FAILED_TIMEOUT'>, { step: VisualStep; index: number; completed: boolean }> = {
RECEIVED: { step: 'Capture', index: 0, completed: false },
QUEUED_PRIORITY: { step: 'Capture', index: 0, completed: false },
TSA_PENDING: { step: 'Horodatage TSA', index: 1, completed: false },
TSA_SEALED: { step: 'Horodatage TSA', index: 1, completed: true },
ANCHOR_PENDING: { step: 'Blockchain', index: 3, completed: false },
SEALED: { step: 'Scellé', index: 4, completed: true },
};
Matrice de couverture invariants¶
| Invariant contract | Mécanisme dans ce module | Vérifié par |
|---|---|---|
| SealState est une union littérale exhaustive des 7 états PD-80 | SEAL_STATES tuple + SealState union type | Compilation TS — ajout/suppression d'état → erreur exhaustiveness |
| SealId et DocumentId sont des branded types | Intersection string & { __brand } | Compilation TS — impossible de passer un string nu |
| Tout champ format §5.6 a un type dédié avec regex | Branded types + SEAL_VALIDATION_PATTERNS | Consommateurs (C4, C6, C10) utilisent les regex pour validation Zod |
| failure_reason (FAILED_TIMEOUT) typé | FailureReason branded type, champ obligatoire dans SealEventFailedTimeout | TS compile-time — champ non optionnel |
| tsa_timestamp (TSA_SEALED) typé | TsaTimestamp branded type, champ obligatoire dans SealEventTsaSealed | TS compile-time — champ non optionnel |
| position_in_queue (QUEUED_PRIORITY) typé | PositionInQueue branded type, champ obligatoire dans SealEventQueuedPriority | TS compile-time — champ non optionnel |
Vérification des interdits (forbidden)¶
| Interdit | Conformité |
|---|---|
| Utiliser string nu pour seal_id ou document_id | SealId et DocumentId sont des branded types — un string nu ne compile pas |
| Ajouter un état non listé dans §5.7 | SEAL_STATES est un tuple as const — tout ajout requiert modification explicite |
| Rendre optionnel un champ obligatoire par état (§5.12) | L'union discriminée SealEvent déclare les champs additionnels sans ? par variant |
Hypothèses¶
Aucune hypothèse supplémentaire au-delà de celles documentées dans le plan.
Fichiers hors périmètre identifiés¶
Aucun fichier hors périmètre nécessaire pour ce module. Le module seal-types est autonome (pas de dépendance interne).