Aller au contenu

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) : retourne true si doublon, false sinon (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 un config partiel
  • processRawEvent(rawPayload) : point d'entrée principal, retourne un ProcessEventResult discriminé
  • 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é

  1. Événement reçu avec sequence_number > lastApplied + 1 : l'événement est bufferisé dans pendingEvents
  2. Si aucun timer actif : démarrage du timer graceWindowMs (défaut 200ms, configurable §5.8)
  3. Si l'événement manquant arrive avant expiration : flush de tous les événements en ordre
  4. Si le timer expire avec gap toujours présent :
  5. Log logSequenceGap()
  6. Appel callbacks.onResyncRequired(sealId)
  7. 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