PD-103 — Agent Developer — Module capture-store (M5)
1. Résumé
Module Zustand store pour la capture probatoire : gestion de l'état de la capture active en mémoire et persistance des captures différées en AsyncStorage pour reprise après restart.
2. Fichiers produits
| Fichier | Type | Lignes |
src/store/useCaptureStore.ts | Source | ~280 |
src/__tests__/capture/capture-store.test.ts | Test | ~430 |
3. Architecture du store
3.1 Données en mémoire (non persistées)
activeCapture — État complet de la capture en cours : captureId, state, metadata, hash, artefacts crypto (wrappés uniquement, jamais de DEK en clair), OCR, progression upload. isProcessing / errorMessage — État UI.
3.2 Données persistées (AsyncStorage)
deferredCaptures — Map captureId → DeferredCapturePayload pour les captures en UPLOAD_DEFERRED. Survit aux redémarrages de l'app. Utilise partialize pour ne persister que ce champ.
3.3 Clé de stockage
com.probatiovault.capture-store.v1
Versionnée dès le départ conformément à CLAUDE.md (com.probatiovault.*.v1).
4. Actions principales
| Action | Usage | Invariant |
initCapture(id, metadata) | M6 orchestrateur après capture | État initial CAPTURED |
updateState(state) | M6 après transition M1 validée | — |
setHash(hash) | M6 après calcul local SHA3-256 | INV-103-03 |
setCryptoArtifacts(...) | M6 après pipeline crypto M2 | INV-103-09 (wrappé, pas clair) |
setUploadObjectKey(key) | M6 après obtention URL pré-signée | — |
setOcr(result) | M6 après OCR M3 | INV-103-04 |
setUploadProgress(progress) | M4 → M8 UI progression | — |
deferCapture(payload) | M6 sur bascule UPLOAD_DEFERRED | INV-103-23 |
removeDeferred(captureId) | M6/M7 après reprise ou TTL | INV-103-24, INV-103-38 |
getExpiredDeferred(ttl?) | M7 watchdog TTL | INV-103-38 |
clearActiveCapture() | M6 après UPLOADED ou CANCELLED | INV-103-38 |
resetAll() | Logout / purge totale | — |
5. Sélecteurs granulaires
11 sélecteurs exports, un par champ. Pattern selectXxx(s: CaptureStore) conforme à useSealStore (PD-284).
Évite les re-renders en cascade (chaque composant ne souscrit qu'au champ nécessaire).
6. Couverture des invariants
| Invariant | Mécanisme store |
| INV-103-03 | setHash() trace le hash local avant upload |
| INV-103-09 | setCryptoArtifacts() stocke uniquement les références wrappées (pas de DEK) |
| INV-103-23 | deferCapture() persiste le payload et clear la capture active |
| INV-103-24 | removeDeferred() supprime après reprise réussie |
| INV-103-28 | selectIsTerminal détecte CANCELLED/ANCHOR_CONFIRMED |
| INV-103-38 | getExpiredDeferred() détecte TTL expiré ; clearActiveCapture() purge post-UPLOADED |
7. Matrice de couverture tests
| Test-ID | Fichier de test | Ligne(s) |
| TC-NOM-05 | src/__tests__/capture/capture-store.test.ts | ~180 |
| TC-NOM-06 | src/__tests__/capture/capture-store.test.ts | ~210 |
| TC-NOM-17 | src/__tests__/capture/capture-store.test.ts | ~250 |
| TC-ERR-11 | src/__tests__/capture/capture-store.test.ts | ~275 |
| TC-INV-10 | src/__tests__/capture/capture-store.test.ts | ~325 |
| NON-CONTRACTUAL (init, setters, selectors, UI, reset) | src/__tests__/capture/capture-store.test.ts | ~50-170, 360-430 |
8. Résultats des tests
Test Suites: 1 passed, 1 total
Tests: 37 passed, 37 total
Time: 0.97 s
9. Décisions architecturales
architectural_decisions:
- decision: "Persist uniquement deferredCaptures via partialize"
rationale: "L'état actif est éphémère (une seule capture à la fois). Seules les captures différées doivent survivre aux restart. Cela minimise les écritures AsyncStorage."
alternatives_considered:
- "Persister tout l'état (activeCapture inclus)"
- "Pas de persistance du tout (reprise impossible après restart)"
trade_offs: "Si l'app crash pendant UPLOADING sans avoir différé, la capture est perdue. Acceptable car M7 purgeStale nettoie les artefacts orphelins au redémarrage."
- decision: "getExpiredDeferred comme méthode synchrone du store (pas un selector)"
rationale: "Le calcul TTL dépend de Date.now() et ne doit pas être dans un selector Zustand (qui comparerait par référence et causerait des re-renders continus). M7 watchdog l'appelle périodiquement."
alternatives_considered:
- "Selector Zustand avec shallow compare"
- "Méthode externe hors store"
trade_offs: "L'action est sur le store mais ne mute pas l'état — pattern inhabituel mais cohérent avec le pattern get() de Zustand."
10. Hypothèses
| ID | Hypothèse | Impact si faux |
| H-M5-01 | M1 (state-machine) valide les transitions en amont de updateState(). Le store ne re-valide pas les gardes. | Si M6 appelle updateState sans passer par M1, des transitions invalides pourraient être stockées. |
| H-M5-02 | DeferredCapturePayload est sérialisable en JSON (pas de Uint8Array, pas de fonctions). | Vrai par construction : tous les champs sont des strings/numbers/objets plats. |
| H-M5-03 | Le mock AsyncStorage existant (src/__mocks__/@react-native-async-storage/async-storage.ts) est suffisant pour les tests unitaires. | Les tests de persist complet nécessiteraient un mock avec stockage réel (intégration). |