Aller au contenu

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).

11. Conformité CLAUDE.md

  • Jamais setState({}) — toujours set() avec champs spécifiques
  • Pas de données sensibles dans le store (DEK, clés, tokens)
  • Clé de stockage versionnée : com.probatiovault.capture-store.v1
  • Tests nommés avec IDs TC-* contractuels
  • Pattern Zustand existant respecté (useSecurityStore pour persist, useSealStore pour selectors)