PD-103 — Agent Developer — Module M1 : capture-state-machine
1. Identite agent
- Agent : agent-developer (Agent B — Claude)
- Story : PD-103
- Module : M1 — capture-state-machine
- Wave : 1 (fondation, sans dependance inter-agents)
- Date : 2026-04-03
2. Resume
Module M1 implemente la machine d'etats monotone a 8 etats pour la capture probatoire d'ecran. Le module fournit la validation structurelle des transitions, les gardes metier contractuelles, le terminal enforcement et la journalisation des transitions.
Fichiers crees : - src/capture/state-machine.ts — Implementation FSM - src/__tests__/capture/state-machine.test.ts — 90 tests (contractuels + non-regression)
Fichier existant reutilise : - src/capture/types.ts — Types, etats, constantes, table de transitions (T1 capture-types)
3. Artefacts livres
| Fichier | Role | Lignes |
src/capture/state-machine.ts | FSM 8 etats, gardes, log, erreur typee | ~220 |
src/__tests__/capture/state-machine.test.ts | Tests contractuels + non-regression | ~460 |
4. Architecture
4.1 Pattern utilise
Pattern identique a src/seal/state-machine.ts (PD-284) : - Table ALLOWED_TRANSITIONS exhaustive (importee de types.ts) - Fonction pure canTransition(from, to) sans effet de bord - Fonction pure isTerminalCaptureState(state) - Classe CaptureStateMachine avec etat mutable + log immutable - Erreur typee InvalidCaptureTransitionError avec from, to, code
4.2 Enrichissements par rapport au pattern existant
| Enrichissement | Justification |
TransitionGuardContext | Interface structuree pour les gardes metier (INV-103-08, INV-103-20..29, INV-103-38) |
evaluateGuard(from, to, ctx) | Fonction pure evaluant les gardes metier par paire (from, to) |
canTransitionTo(nextState, ctx) | Methode d'instance combinant verification structurelle + gardes |
captureId dans le log | Chaque entree du journal porte le CaptureId pour correlation (observabilite §8) |
| Timestamp ISO 8601 UTC | Timestamps en format ISO au lieu de Date.now() pour alignement avec le contrat §5.1 |
| Code d'erreur differencie | TERMINAL_STATE vs INVALID_TRANSITION dans InvalidCaptureTransitionError.code |
4.3 Gardes de transition implementees
| Transition | Garde | Invariant |
CAPTURED -> UPLOADING | hashComputed + encryptionDone | INV-103-20 |
CAPTURED -> CANCELLED | userCancelled | INV-103-21 |
UPLOADING -> UPLOADED | s3UploadConfirmed + backendAck | INV-103-22 |
UPLOADING -> UPLOAD_DEFERRED | aucune | INV-103-23 |
UPLOADING -> CANCELLED | userCancelled | INV-103-29 |
UPLOAD_DEFERRED -> UPLOADING | jwtValid + freshPresignedUrl | INV-103-24 |
UPLOAD_DEFERRED -> CANCELLED | deferredTtlExpired | INV-103-38 |
UPLOADED -> PENDING_SEAL | aucune | INV-103-25 |
PENDING_SEAL -> SEALED | signatureStatus='SIGNED' + hsmSignatureRef != null | INV-103-08/26 |
SEALED -> ANCHOR_CONFIRMED | blockchainConfirmed | INV-103-27 |
4.4 Exports publics
// Classes
export class CaptureStateMachine { ... }
export class InvalidCaptureTransitionError extends Error { ... }
// Interface
export interface TransitionGuardContext { ... }
// Fonctions pures
export function canTransition(from, to): boolean
export function isTerminalCaptureState(state): boolean
export function evaluateGuard(from, to, ctx): string | null
4.5 Diagramme
stateDiagram-v2
[*] --> CAPTURED
CAPTURED --> UPLOADING : hash+encrypt OK
CAPTURED --> CANCELLED : user cancel
UPLOADING --> UPLOADED : S3+ACK OK
UPLOADING --> UPLOAD_DEFERRED : reseau KO
UPLOADING --> CANCELLED : user cancel
UPLOAD_DEFERRED --> UPLOADING : JWT+URL OK
UPLOAD_DEFERRED --> CANCELLED : TTL expire
UPLOADED --> PENDING_SEAL
PENDING_SEAL --> SEALED : SIGNED+hsm_ref
SEALED --> ANCHOR_CONFIRMED : blockchain OK
ANCHOR_CONFIRMED --> [*]
CANCELLED --> [*]
5. Couverture de tests
5.1 Metriques
| Metrique | Valeur | Seuil |
| Statements | 97.46% | >= 80% |
| Branches | 97.46% | >= 80% |
| Functions | 91.66% | >= 80% |
| Lines | 97.05% | >= 80% |
| Tests | 90 | — |
| Succes | 90/90 | 100% |
5.2 Matrice de couverture TC-* -> fichiers de test
| Test-ID | Fichier de test | Ligne | Description |
| TC-INV-02 | src/__tests__/capture/state-machine.test.ts | ~41 | CAPTURED -> UPLOADING refuse sans hash+chiffrement |
| TC-INV-10 | src/__tests__/capture/state-machine.test.ts | ~73 | Etats terminaux stricts — toute transition rejetee |
| TC-NOM-12 | src/__tests__/capture/state-machine.test.ts | ~73 | CANCELLED et ANCHOR_CONFIRMED terminaux |
| TC-ERR-02 | src/__tests__/capture/state-machine.test.ts | ~119 | CAPTURED -> CANCELLED annulation explicite |
| TC-ERR-08 | src/__tests__/capture/state-machine.test.ts | ~148 | Garde PENDING_SEAL -> SEALED cas positif et negatif |
| TC-NEG-13 | src/__tests__/capture/state-machine.test.ts | ~224 | Transitions skip directes interdites (31 paires) |
5.3 Tests additionnels (NON-CONTRACTUAL)
| Test | Description |
| Transitions autorisees exhaustives | 10 paires valides + canTransition pour toute la table |
| Gardes INV-103-22 | UPLOADING -> UPLOADED sans S3 / sans ACK |
| Gardes INV-103-24 | UPLOAD_DEFERRED -> UPLOADING sans JWT / sans URL |
| Gardes INV-103-27 | SEALED -> ANCHOR_CONFIRMED sans blockchain |
| Gardes INV-103-29 | UPLOADING -> CANCELLED sans annulation explicite |
| Gardes INV-103-38 | UPLOAD_DEFERRED -> CANCELLED sans TTL expire |
| Flux nominal complet | CAPTURED -> ANCHOR_CONFIRMED (5 transitions) |
| Flux differe | CAPTURED -> UPLOAD_DEFERRED -> UPLOADED (4 transitions) |
canTransitionTo | Verification sans mutation |
| Journal transitions | Observabilite : timestamps, captureId, raisons |
| InvalidCaptureTransitionError | Proprietes from/to/code |
| evaluateGuard isole | Tests unitaires des gardes individuelles |
| Monotonicite | Seul retour autorise = UPLOAD_DEFERRED -> UPLOADING |
6. Decisions architecturales
6.1 Gardes comme fonction pure separee
- Decision :
evaluateGuard() est une fonction pure exportee, separee de la classe FSM. - Rationale : Permet de tester les gardes isolement sans instancier la machine. Les consumers (M6 orchestrator) peuvent pre-valider une transition sans effet de bord.
- Alternatives considerees : Gardes inline dans
transition() (moins testable), gardes comme methodes privees (non exportable). - Trade-offs : Legere duplication du mapping (from, to) dans
evaluateGuard par rapport a la table ALLOWED_TRANSITIONS, mais meilleure lisibilite et testabilite.
6.2 Timestamp ISO 8601 au lieu de Date.now()
- Decision : Les entrees du log utilisent
new Date().toISOString() (ISO 8601 UTC) au lieu de Date.now() (millisecondes epoch). - Rationale : Alignement avec le format contractuel
timestamp_device (RFC3339 UTC) et l'interface CaptureTransitionLog de types.ts. Facilite la correlation avec les logs backend. - Alternatives considerees :
Date.now() (pattern PD-284), performance.now() (PD-284 learning). - Trade-offs : Microseconde de surcharge pour la serialisation ISO ; acceptable car les transitions sont evenementielles et non critiques en latence.
7. Hypotheses
| ID | Hypothese | Impact si faux |
| HYP-M1-01 | Les gardes sont evaluees cote mobile par l'orchestrateur (M6) qui fournit le TransitionGuardContext | Si le contexte n'est pas correctement peuple par M6, les gardes seront trop restrictives |
| HYP-M1-02 | La garde PENDING_SEAL -> SEALED (INV-103-08) est evaluee cote backend par le worker de scellement, pas cote mobile | Cote mobile, cette transition sera declenchee par resync SSE/polling sans evaluation locale de la garde |
8. Dependances
| Dependance | Direction | Module |
src/capture/types.ts | Import | T1 capture-types (prealable, livre) |
M5 useCaptureStore | Consommateur | M5 importera CaptureStateMachine pour gerer l'etat persistant |
M6 orchestrator | Consommateur | M6 instanciera CaptureStateMachine et fournira TransitionGuardContext |
9. Invariants couverts
| Invariant | Couverture | Mecanisme |
| INV-103-08-sealed-guard | Tests positif + negatif (TC-ERR-08) | Garde signatureStatus='SIGNED' + hsmSignatureRef |
| INV-103-20-transition-captured-uploading | Tests positif + negatif (TC-INV-02) | Garde hashComputed + encryptionDone |
| INV-103-21-transition-captured-cancelled | Test positif + negatif (TC-ERR-02) | Garde userCancelled |
| INV-103-22-transition-uploading-uploaded | Tests positif + negatif | Garde s3UploadConfirmed + backendAck |
| INV-103-23-transition-uploading-deferred | Test positif (pas de garde) | Transition structurelle |
| INV-103-24-transition-deferred-uploading | Tests positif + negatif | Garde jwtValid + freshPresignedUrl |
| INV-103-25-transition-uploaded-pending-seal | Test positif (pas de garde) | Transition structurelle |
| INV-103-26-transition-pending-seal-sealed | Tests positif + negatif (TC-ERR-08) | Delegue a INV-103-08 |
| INV-103-27-transition-sealed-anchor | Tests positif + negatif | Garde blockchainConfirmed |
| INV-103-28-terminal-states | Tests exhaustifs (TC-INV-10, TC-NOM-12) | ALLOWED_TRANSITIONS[terminal] = [] |
| INV-103-29-transition-uploading-cancelled | Tests positif + negatif | Garde userCancelled |
| INV-103-38-transition-deferred-cancelled | Tests positif + negatif | Garde deferredTtlExpired |
10. Limites connues
-
Pas de resync serveur : Contrairement a SealStateMachine (PD-284), pas de methode resync(). Le besoin n'est pas identifie dans la spec PD-103 pour le mobile. Si necessaire, a ajouter dans une iteration ulterieure.
-
Gardes evaluees localement : La garde PENDING_SEAL -> SEALED (INV-103-08) est implementee mais sera probablement evaluee cote backend. Cote mobile, la transition sera declenchee par SSE/polling sans re-evaluation locale de la garde.
-
Pas de persistence : La machine d'etats est en memoire. La persistence est deleguee a M5 (useCaptureStore) qui reconstituera l'etat au redemarrage.
11. Checklist qualite