PD-284 — Agent Developer — Module seal-event-processor¶
1. Module livré¶
| Attribut | Valeur |
|---|---|
| Module | seal-event-processor (C4) |
| Fichier | src/seal/event-processor.ts |
| Interfaces exportées | SealEventProcessor, EventDeduplicationCache, SealEventProcessorCallbacks, ProcessEventResult |
| Dépendances internes | src/types/seal.ts (C1), src/seal/state-machine.ts (C2), src/seal/telemetry.ts (C13) |
| Dépendance externe | zod (validation payload SSE) |
2. Architecture¶
2.1 Vue d'ensemble¶
Le SealEventProcessor est le point d'entrée unique pour traiter les événements SSE bruts avant application au store Zustand (C5). Il implémente le pipeline de traitement décrit au §2.3 du plan :
Événement SSE brut
│
├─ seal_id ≠ actif → ignorer + telemetry
├─ event_id déjà vu (cache FIFO 100) → ignorer silencieusement
├─ Validation Zod payload → ignorer + toast 5s + telemetry
├─ sequence_number gap → fenêtre grâce 200ms → resync GET
├─ Transition non autorisée → ignorer + toast 5s + telemetry
└─ OK → callback onEventApplied → store update
2.2 Composants internes¶
EventDeduplicationCache¶
Ring buffer FIFO de taille N=100 pour les event_id. Politique d'éviction : les event_id les plus anciens sont éjectés en premier (FIFO strict, pas LRU — invariant contract).
checkAndAdd(eventId): retournetruesi doublon,falsesinon (et ajoute)- Recherche linéaire O(N=100) — acceptable pour la taille du cache
- Écriture en index modulo circulaire
SealEventProcessor¶
Classe principale avec lifecycle management :
- Construction : reçoit
activeSealId,initialState,callbacks, et optionnellement unconfigpartiel - processRawEvent(rawPayload) : point d'entrée principal, retourne un
ProcessEventResultdiscriminé - resyncFromServer(state, sequence) : recalibrage après GET /status en cas de gap
- dispose() : nettoyage des timers et ressources
3. Invariants respectés¶
| Invariant | Mécanisme implémenté |
|---|---|
| Cache FIFO N=100 (pas LRU) | EventDeduplicationCache — ring buffer avec index modulo, pas de Map/Set avec delete |
Doublon event_id ignoré silencieusement (pas de toast) | logDeduplication() en debug uniquement, pas d'appel onControlledError |
Fenêtre de grâce 200ms pour sequence_number désordonnés | setTimeout + performance.now() pour mesure (pas Date.now() — invariant contract) |
| Gap persistant après 200ms → GET resync immédiat | onGraceWindowExpired() → callbacks.onResyncRequired() |
| INV-284-06 : transition non autorisée → rejet + toast erreur contrôlée | canTransition() de C2 + logTransitionRejected() + onControlledError() |
Événement avec seal_id ≠ seal actif → ignoré + telemetry | Guard en première étape du pipeline |
| Validation Zod du payload avant application | Schemas par état avec champs obligatoires (§5.12) |
Interdits respectés (forbidden)¶
| Interdit | Vérification |
|---|---|
| LRU au lieu de FIFO | Ring buffer avec writeIndex % size, pas de Map/LRU |
| État spéculatif en cas de gap | pendingEvents buffer sans application, resync GET sur gap persistant |
| Ignorer la validation Zod | schema.safeParse() systématique avant application |
Toast pour doublon event_id | Pas d'appel onControlledError sur doublon — logDeduplication debug-only |
Date.now() pour fenêtre de grâce | performance.now() pour graceStartTime |
4. Schemas Zod¶
Chaque état SSE (§5.12) a un schema dédié qui étend le schema de base :
| État | Champs additionnels requis |
|---|---|
RECEIVED | hash_document |
QUEUED_PRIORITY | hash_document, position_in_queue |
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, failure_reason |
Tous les champs utilisent les regex de validation de SEAL_VALIDATION_PATTERNS (§5.6).
5. Gestion des erreurs¶
| Situation | Action | Toast | Telemetry |
|---|---|---|---|
seal_id mismatch | Ignorer | Non | logControlledError("seal_id_mismatch") |
event_id dupliqué | Ignorer silencieusement | Non (jamais) | logDeduplication() debug-only |
| Payload non-objet | Ignorer | Oui (5s) | logControlledError("sse_event_invalid") |
status inconnu | Ignorer | Oui (5s) | logControlledError("sse_event_invalid") |
| Zod validation failed | Ignorer | Oui (5s) | logControlledError("sse_event_invalid") |
| Transition interdite | Ignorer | Oui (5s) | logTransitionRejected() |
Gap sequence_number | Buffer + grace 200ms | Non | logSequenceGap() si gap persistant |
| Même état (idempotent) | Ignorer (dedup implicite) | Non | Non |
| Processor disposed | Ignorer | Non | Non |
6. Fenêtre de grâce — Comportement détaillé¶
- Événement reçu avec
sequence_number > lastApplied + 1: l'événement est bufferisé danspendingEvents - Si aucun timer actif : démarrage du timer
graceWindowMs(défaut 200ms, configurable §5.8) - Si l'événement manquant arrive avant expiration : flush de tous les événements en ordre
- Si le timer expire avec gap toujours présent :
- Log
logSequenceGap() - Appel
callbacks.onResyncRequired(sealId) - Vidage de
pendingEvents(le resync recalibrera l'état complet)
Le timer utilise performance.now() (pas Date.now()) conformément à l'interdit du code contract.
7. Resync serveur¶
Après un GET /seals/{id}/status de resynchronisation, l'orchestrateur (C7) appelle resyncFromServer(serverState, serverSequence) :
- Recale l'état interne du processor
- Remet à zéro le compteur de séquence
- Vide les événements bufferisés (la réponse GET fait foi)
- Annule tout timer de grâce actif
8. Matrice de couverture des tests¶
| Test-ID | Couverture module | Point d'observation |
|---|---|---|
| TC-NOM-16 | Cache FIFO event_id, déduplication silencieuse | processRawEvent() retourne deduplicated |
| TC-NOM-17 | Gap sequence → grace window → resync | onResyncRequired callback appelé |
| TC-NOM-18 | Validation Zod payload par état | processRawEvent() retourne applied pour payload valide |
| TC-ERR-03 | Payload SSE invalide → toast + telemetry | onControlledError callback + logControlledError |
| TC-ERR-04 | Transition interdite → toast + telemetry | onControlledError + logTransitionRejected |
| TC-ERR-05 | seal_id incohérent → ignoré | processRawEvent() retourne ignored_seal_mismatch |
| TC-ERR-06 | Transition depuis terminal → rejet | transition_rejected pour SEALED → * |
| TC-ERR-09 | Gap séquence multiple → resync immédiat (après grace) | onResyncRequired sans spéculation locale |
| TC-NEG-01 | seal_id non UUID → ignoré | invalid_payload |
| TC-NEG-03 | État backend inconnu → erreur contrôlée | invalid_payload pour status=FOO |
| TC-NEG-04 | Transition inverse forcée → rejet | transition_rejected |
| TC-NEG-07 | Tempête de doublons (>100) → FIFO conforme | Seul le premier appliqué, les suivants deduplicated |
| TC-NEG-08 | Réception désordonnée → tri correct / resync | buffered_reorder puis flush ou resync |
9. Décisions architecturales¶
architectural_decisions:
- decision: "Ring buffer tableau fixe pour cache FIFO event_id"
rationale: "Array pré-alloué avec index modulo — O(1) écriture, O(N) lookup. N=100 rend le lookup négligeable vs overhead Map/Set."
alternatives_considered:
- "Set avec delete du plus ancien (nécessite tracking ordre — overhead)"
- "Map avec compteur (surconsommation mémoire pour 100 entrées)"
trade_offs: "Lookup linéaire O(100) acceptable pour la taille. Pas de hashmap overhead."
- decision: "ProcessEventResult union discriminée plutôt qu'exceptions"
rationale: "Le traitement d'événements SSE ne doit jamais crasher. Retourner un résultat typé permet au caller de réagir sans try/catch."
alternatives_considered:
- "Throw + catch par le caller (risque de crash si oubli)"
- "Callback pour chaque cas (API verbeuse)"
trade_offs: "Le caller doit inspecter le résultat mais ne risque pas de crash non géré."
10. Hypothèses¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| H-EP-01 | event_id est un entier positif (pas un string) | Types incompatibles — nécessiterait conversion |
| H-EP-02 | Le sequence_number est strictement croissant pour un seal_id donné | Si non monotone, la détection de gap est incorrecte |
| H-EP-03 | Le resync GET retourne un sequence_number fiable pour recalibrer le processor | Si absent → impossible de recaler lastAppliedSequence |
| H-EP-04 | Les événements SSE arrivent parsés en JSON (le parsing text/event-stream est géré par C3) | Si raw text → nécessite parsing supplémentaire |
11. Dépendances non implémentées (stubs inter-PD)¶
Aucun stub inter-PD dans ce module. Toutes les dépendances sont intra-PD-284 : - C1 (types/seal.ts) : implémenté - C2 (state-machine.ts) : implémenté - C13 (telemetry.ts) : implémenté - zod : dépendance npm installée
12. Fichiers modifiés¶
| Fichier | Action | Raison |
|---|---|---|
src/seal/event-processor.ts | Créé | Module principal C4 |