Aller au contenu

PD-284 — Plan d'implémentation

1. Découpage en composants

# Composant Responsabilité Fichiers Dépendances internes
C1 seal-types Types TypeScript : états, événements SSE, payloads, branded types src/types/seal.ts
C2 seal-state-machine Machine d'états monotone avec table de transitions, validation, log src/seal/state-machine.ts C1
C3 seal-sse-client Client SSE (EventSource fetch-based), reconnexion backoff, failover polling, reprise SSE depuis polling src/seal/sse-client.ts C1
C4 seal-event-processor Déduplication event_id (cache FIFO N=100), ordonnancement sequence_number (fenêtre grâce 200ms), resync GET src/seal/event-processor.ts C1, C2
C5 seal-store Zustand store : état scellement actif, progression, dégradation, mode transport, telemetry src/store/useSealStore.ts C1, C2, C4
C6 seal-api Client API : POST /seals/urgent, GET /seals/{id}/status, validation Zod src/seal/api-client.ts C1
C7 seal-orchestrator Coordination flux A complet : POST → GET status → SSE, gestion cycle de vie src/seal/orchestrator.ts C3, C4, C5, C6
C8 urgent-button Bouton scellement urgent : visibilité, enabled/disabled, tooltips contractuels src/components/seal/UrgentSealButton.tsx C5, C6
C9 seal-progress-card Carte de progression 5 étapes, badges dégradation, badge hors-ligne, affichage position_in_queue en état QUEUED_PRIORITY, affichage failure_reason en état FAILED_TIMEOUT src/components/seal/SealProgressCard.tsx C5
C10 expert-panel Panneau mode expert : affichage conditionnel artefacts par état src/components/seal/ExpertPanel.tsx C1, C5
C11 seal-notifications Notifications de clôture (succès/échec) + deep-link vers détail src/seal/notifications.ts C1, existant notificationService
C12 seal-secure-storage Persistance sécurisée des artefacts sensibles (SecureStore), purge logout/delete src/seal/secure-storage.ts C1
C13 seal-telemetry Journalisation structurée : erreurs contrôlées, dédup, gaps, transitions src/seal/telemetry.ts C1
C14 seal-detail-screen Écran détail scellement (intègre C8, C9, C10) src/screens/vault/SealDetailScreen.tsx C8, C9, C10, C5
C15 seal-navigation Routes deep-link + navigation vers SealDetailScreen src/navigation/seal-linking.ts C14

2. Flux techniques

2.1 Flux A — Déclenchement urgent (POST → GET → SSE)

┌─────────┐     ┌──────────┐     ┌──────────┐     ┌───────────┐     ┌──────────┐
│ UI:      │     │ C6:      │     │ C6:      │     │ C7:       │     │ C3:      │
│ Button   │────>│ POST     │────>│ GET      │────>│ init      │────>│ SSE open │
│ pressed  │     │ /seals/  │     │ /status  │     │ store     │     │          │
│          │     │ urgent   │     │          │     │ (état     │     │          │
└─────────┘     └──────────┘     └──────────┘     │  initial) │     └──────────┘
                     │                │            └───────────┘          │
                     │ seal_id        │ état canonique                   │ events
                     ▼                ▼                                  ▼
                 feedback         SealStore                          C4: process
                 chargement       initialisé                        → SealStore

Séquence stricte : POST → attente seal_id → GET /seals/{seal_id}/status → init store avec état GET → ouverture SSE. Aucune étape ne peut être sautée.

Échec GET status post-POST (réserve R-02) : Si GET /seals/{id}/status échoue après un POST réussi, le client : 1. Affiche un toast d'erreur contrôlée (5s, non bloquant) 2. Initialise la carte en état RECEIVED (état conservateur — premier état de la machine) 3. Ouvre le SSE normalement — le prochain événement SSE recalera l'état réel 4. Émet un événement telemetry get_status_failed_after_post

2.1b Mapping états → étapes visuelles

État backend (§5.7) Étape visuelle (C9) Index Comportement carte
RECEIVED Capture 1 Étape 1 active (spinner)
QUEUED_PRIORITY Capture 1 Étape 1 active + affichage position_in_queue
TSA_PENDING Horodatage TSA 2 Étape 1 complétée, étape 2 active
TSA_SEALED Horodatage TSA 2 Étape 2 complétée (checkmark)
ANCHOR_PENDING Arbre Merkle / Blockchain 3-4 Étapes 3-4 actives
SEALED Scellé 5 Toutes complétées, message succès
FAILED_TIMEOUT (overlay) Overlay erreur sur étape courante + failure_reason + message contact support

Règle : FAILED_TIMEOUT n'est PAS une 6e étape mais un overlay rouge sur la dernière étape active au moment de l'échec.

2.2 Flux B — Suivi temps réel SSE + failover

                    ┌─────────────────────────────────────┐
                    │            C3: SSE Client            │
                    │                                      │
    ┌──────┐  event │  ┌──────────┐    ┌──────────────┐   │
    │ SSE  │───────>│  │ C4:      │───>│ C5: Store    │   │
    │server│        │  │ dedup +  │    │ update state │   │
    │      │        │  │ reorder  │    │              │   │
    └──────┘        │  └──────────┘    └──────────────┘   │
                    │                                      │
     SSE fail ×3    │  ┌──────────────────────────┐       │
    (1s,2s,4s)──────│─>│ Failover → polling 5s    │       │
                    │  │ + tentative SSE parallèle │       │
                    │  │   à chaque cycle polling  │       │
                    │  └──────────────────────────┘       │
                    │                                      │
     SSE reconnect  │  ┌──────────────────────────┐       │
    succès ─────────│─>│ Retour SSE + arrêt poll   │       │
                    │  └──────────────────────────┘       │
                    └─────────────────────────────────────┘

Backoff exponentiel : delay = base × factor^attempt avec base=1s, factor=2, max_attempts=3. Plafond cumulé max_cumulative_delay=30s. Si cumulé atteint avant max_attempts → failover immédiat.

2.3 Flux C — Traitement événement SSE

Événement reçu
  ├─ seal_id ≠ actif ? → ignorer + log telemetry
  ├─ event_id déjà vu (cache FIFO 100) ? → ignorer silencieusement
  ├─ Validation Zod payload ?
  │   └─ échec → ignorer + toast erreur contrôlée 5s + telemetry
  ├─ sequence_number gap ?
  │   ├─ attente fenêtre grâce 200ms
  │   └─ gap persistant → GET /seals/{id}/status resync
  ├─ Transition autorisée (C2) ?
  │   └─ non → ignorer + toast erreur contrôlée 5s + telemetry
  └─ OK → update store → render UI
       ├─ badge dégradation (si degradation_flag)
       ├─ artefacts expert (si état suffisant)
       └─ notification (si état terminal)

2.4 Flux D — Évaluation bouton urgent

GET détail document (données serveur)
  ├─ account_type = minor → bouton ABSENT (INV-284-02)
  └─ account_type ≠ minor → bouton VISIBLE (INV-284-01)
       ├─ urgent_quota_remaining = 0 → DISABLED + tooltip "Quota épuisé ce mois"
       ├─ has_active_urgent_seal = true → DISABLED + tooltip "Scellement urgent déjà en cours"
       │   (Réserve R-03 : valeur par défaut false + telemetry si champ absent —
       │    acceptable car PD-80 est idempotent sur POST /seals/urgent)
       └─ sinon → ENABLED

2bis. Diagrammes Mermaid

Graphe de dépendances des composants

graph TD
    C1["C1: seal-types<br/><code>src/types/seal.ts</code>"]

    C2["C2: seal-state-machine<br/><code>src/seal/state-machine.ts</code>"]
    C3["C3: seal-sse-client<br/><code>src/seal/sse-client.ts</code>"]
    C4["C4: seal-event-processor<br/><code>src/seal/event-processor.ts</code>"]
    C5["C5: seal-store<br/><code>src/store/useSealStore.ts</code>"]
    C6["C6: seal-api<br/><code>src/seal/api-client.ts</code>"]
    C7["C7: seal-orchestrator<br/><code>src/seal/orchestrator.ts</code>"]
    C8["C8: urgent-button<br/><code>src/components/seal/UrgentSealButton.tsx</code>"]
    C9["C9: seal-progress-card<br/><code>src/components/seal/SealProgressCard.tsx</code>"]
    C10["C10: expert-panel<br/><code>src/components/seal/ExpertPanel.tsx</code>"]
    C11["C11: seal-notifications<br/><code>src/seal/notifications.ts</code>"]
    C12["C12: seal-secure-storage<br/><code>src/seal/secure-storage.ts</code>"]
    C13["C13: seal-telemetry<br/><code>src/seal/telemetry.ts</code>"]
    C14["C14: seal-detail-screen<br/><code>src/screens/vault/SealDetailScreen.tsx</code>"]
    C15["C15: seal-navigation<br/><code>src/navigation/seal-linking.ts</code>"]

    C2 --> C1
    C3 --> C1
    C4 --> C1
    C4 --> C2
    C5 --> C1
    C5 --> C2
    C5 --> C4
    C6 --> C1
    C7 --> C3
    C7 --> C4
    C7 --> C5
    C7 --> C6
    C8 --> C5
    C8 --> C6
    C9 --> C5
    C10 --> C1
    C10 --> C5
    C11 --> C1
    C12 --> C1
    C13 --> C1
    C14 --> C5
    C14 --> C8
    C14 --> C9
    C14 --> C10
    C15 --> C14

Diagramme de sequence — Flux A (declenchement urgent : POST -> GET -> SSE)

sequenceDiagram
    participant UI as UI (C8: UrgentSealButton)
    participant Orch as C7: seal-orchestrator
    participant API as C6: seal-api
    participant Backend as PD-80 Backend
    participant Store as C5: seal-store
    participant SSE as C3: seal-sse-client
    participant Proc as C4: seal-event-processor

    UI->>Orch: onPress (debounce 2s)
    Orch->>API: POST /seals/urgent
    API->>Backend: HTTP POST
    Backend-->>API: 201 { seal_id }
    API-->>Orch: seal_id

    Orch->>API: GET /seals/{seal_id}/status
    API->>Backend: HTTP GET
    alt GET reussit
        Backend-->>API: 200 { state, artefacts... }
        API-->>Orch: etat canonique
        Orch->>Store: init(state)
    else GET echoue (R-02)
        Backend-->>API: 5xx / timeout
        API-->>Orch: erreur
        Orch->>Store: init(RECEIVED)
        Note over Orch: Toast erreur 5s + telemetry
    end

    Orch->>SSE: open(seal_id)
    SSE->>Backend: EventSource /seals/{seal_id}/events

    loop Evenements SSE
        Backend-->>SSE: event { event_id, sequence_number, state, ... }
        SSE->>Proc: onEvent(event)
        Proc->>Proc: dedup event_id (cache FIFO 100)
        Proc->>Proc: check sequence_number gaps
        Proc->>Store: transition(new_state)
        Store-->>UI: re-render (C9: SealProgressCard)
    end

Diagramme de sequence — Failover SSE -> Polling (Flux B)

sequenceDiagram
    participant SSE as C3: seal-sse-client
    participant Backend as PD-80 Backend
    participant Proc as C4: seal-event-processor
    participant Store as C5: seal-store
    participant Telem as C13: seal-telemetry

    SSE->>Backend: EventSource (connexion SSE)
    Backend--xSSE: connexion perdue

    SSE->>SSE: retry #1 (delay 1s)
    SSE->>Backend: reconnexion SSE
    Backend--xSSE: echec

    SSE->>SSE: retry #2 (delay 2s)
    SSE->>Backend: reconnexion SSE
    Backend--xSSE: echec

    SSE->>SSE: retry #3 (delay 4s)
    SSE->>Backend: reconnexion SSE
    Backend--xSSE: echec

    SSE->>Telem: sse_failover_polling
    Note over SSE: Basculement en mode polling

    loop Polling 5s + tentative SSE parallele
        SSE->>Backend: GET /seals/{seal_id}/status
        Backend-->>SSE: 200 { state }
        SSE->>Proc: onPollResult(state)
        Proc->>Store: update(state)
        SSE->>Backend: tentative SSE parallele
        alt SSE reconnecte
            Backend-->>SSE: EventSource OK
            SSE->>Telem: sse_restored
            Note over SSE: Arret polling, retour SSE
        else SSE echoue encore
            Note over SSE: Continue polling
        end
    end

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-284-01 Bouton visible pour account_type != minor Rendu conditionnel dans UrgentSealButton : if (accountType === 'minor') return null; return <Button ... /> C8 Snapshot test + test unitaire par account_type Faible
INV-284-02 Aucun bouton pour minor Même guard accountType === 'minor'return null C8 Test unitaire : aucun élément rendu pour minor Faible
INV-284-03 Bouton enabled ssi quota > 0 AND !has_active_urgent_seal Props disabled calculé : disabled={quota <= 0 \|\| hasActiveUrgent} C8 Tests unitaires pour chaque combinaison (2x2 matrice) Moyen — R-03 : valeur par défaut false si champ absent
INV-284-04 Tooltips contractuels exacts Constantes i18n extraites, texte vérifié en test C8 Test snapshot sur texte exact tooltip Faible
INV-284-05 Pas de calcul local de seuil dégradation Store consomme degradation_flag brut, pas de timer/seuil local C5, C9 Review code : aucun Date.now() ni comparaison de durée dans C9 Faible
INV-284-06 Transitions conformes à PD-80 Table ALLOWED_TRANSITIONS dans SealStateMachine, rejet sinon C2 Tests unitaires exhaustifs de la table de transitions Faible
INV-284-07 Chaque état déclare transitions sortantes ALLOWED_TRANSITIONS est un Record<SealState, readonly SealState[]> complet — tout état est une clé C2 TypeScript exhaustiveness check + tests Faible
INV-284-08 SEALED et FAILED_TIMEOUT terminaux ALLOWED_TRANSITIONS['SEALED'] = [], ALLOWED_TRANSITIONS['FAILED_TIMEOUT'] = []. FAILED_TIMEOUT affiche failure_reason dans C9 (message terminal) et C10 (mode expert). C2, C9, C10 Test : toute transition depuis terminal → rejet + affichage failure_reason Faible
INV-284-09 Badge Hors ligne uniquement perte réseau réelle Hook useNetworkStatus() basé sur @react-native-community/netinfo ; badge conditionnel isConnected === false (pas échec SSE) C9, hook Test : badge absent si SSE échoue mais réseau OK Moyen — dépend fiabilité NetInfo
INV-284-10 Artefacts sensibles jamais en clair hors SecureStore SealSecureStorage utilise expo-secure-store avec kSecAttrAccessibleWhenUnlockedThisDeviceOnly C12 Test d'intégration : vérif AsyncStorage ne contient pas de clés sensibles Moyen
INV-284-11 Artefacts publics non classés sensibles hash_document, merkle_root, blockchain_tx_hash dans le state Zustand (mémoire), pas dans SecureStore C5 Review code : ces champs absents de C12 Faible
INV-284-12 Deep-link valide vers détail dans notifications buildSealDeepLink(sealId) génère URL interne ; notificationService l'inclut dans le payload C11, C15 Test : notification contient deep-link parseable, navigation résout vers SealDetailScreen Moyen
INV-284-13 Aucune contrainte inter-module backend Vérification périmètre : aucun endpoint hors POST /seals/urgent, GET /seals/{id}/status, SSE stream C6 Review code + traçage réseau en test d'intégration Faible

4. Mapping critères d'acceptation → mécanismes

Critère ID Mécanisme(s) Composant Observable Risque
CA-284-01 Guard accountType === 'minor'return null C8 Test : aucun rendu bouton pour minor Faible
CA-284-02 Guard accountType !== 'minor' → rendu bouton (enabled ou disabled) C8 Test : bouton toujours présent si non-minor Faible
CA-284-03 disabled={quota <= 0} + tooltip constante TOOLTIP_QUOTA_EXHAUSTED C8 Test : disabled=true + texte tooltip exact Faible
CA-284-04 disabled={hasActiveUrgent} + tooltip constante TOOLTIP_URGENT_ACTIVE C8 Test : disabled=true + texte tooltip exact R-03
CA-284-05 Séquence orchestrateur : POSTGET status → init store → SSE open C7 Test d'intégration : ordre des appels réseau mockés R-02
CA-284-06 SealStateMachine.transition() rejette toute transition non autorisée C2, C4 Tests unitaires table complète Faible
CA-284-07 Cache FIFO ring buffer (tableau circulaire) de taille N=100 dans EventProcessor. Éviction FIFO par index modulo. C4 Test : doublon ignoré, pas de rerender Faible
CA-284-08 Fenêtre grâce 200ms + détection gap → GET /status C4 Test : gap → resync GET observé Moyen
CA-284-09 Backoff 1s/2s/4s dans SSEClient, compteur tentatives, failover à max_attempts C3 Test : mesure des délais entre tentatives Moyen
CA-284-10 NetInfo.addEventListener pour badge, SSE fail != offline C9, hook Test : badge absent si réseau OK mais SSE échoue Moyen
CA-284-11 Store expose degradationFlag brut du serveur, composant mappe vers badge C5, C9 Test : badge correspond exactement au flag recu Faible
CA-284-12 buildSealDeepLink(sealId) + notificationService.scheduleLocal() a SEALED C11 Test : notification avec deep-link valide émise Moyen
CA-284-13 Même mécanisme que CA-284-12 a FAILED_TIMEOUT C11 Test : notification échec avec deep-link Moyen
CA-284-14 Architecture React optimisée : useMemo, sélecteurs Zustand granulaires, pas de re-render cascadant C9, C5 Test perf : instrumentation t_event → t_render <= 100ms P95 Élevé
CA-284-15 Guard if (!modeExpert) return null dans ExpertPanel C10 Test : panneau absent si préférence false Faible
CA-284-16 Map EXPERT_FIELDS_BY_STATE détermine quels champs afficher par état C10 Test : champs apparaissent uniquement aux états contractuels Faible
CA-284-17 SealSecureStorage pour tsa_token_ref, clés session SSE, tokens auth C12 Test : inspection AsyncStorage vide d'artefacts sensibles Moyen

5. Mapping tests (TC-*) → mécanismes + observables

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau
TC-NOM-01 INV-284-01, CA-284-02 Guard account_type dans C8 Rendu bouton + état enabled + quota affiché Unit
TC-NOM-02 INV-284-02, CA-284-01 Guard minor → null dans C8 queryByTestId retourne null Unit
TC-NOM-03 INV-284-03, CA-284-03 Props disabled + tooltip dans C8 disabled=true + tooltip text Unit
TC-NOM-04 Flux A, CA-284-05 Séquence C7 orchestrateur Ordre appels mock : POST → GET → SSE Integration
TC-NOM-05 INV-284-06/07, CA-284-06 Table transitions C2 Log transitions : séquence complète sans rejet Unit
TC-NOM-06 INV-284-08, CA-284-12 Terminal SEALED dans C2 + notification C11 État terminal + notification émise + deep-link Integration
TC-NOM-07 INV-284-08, CA-284-13 Terminal FAILED_TIMEOUT dans C2 + notification C11 État terminal + message support + notification Integration
TC-NOM-08 INV-284-09, CA-284-10 Backoff C3 + NetInfo hook Pas de badge offline, polling actif après 3 échecs Integration
TC-NOM-09 Flux B Retour SSE depuis polling dans C3 SSE actif + polling stoppé Integration
TC-NOM-10 INV-284-05, CA-284-11 Store flag brut C5 + badge C9 Badge exact = flag serveur Unit
TC-NOM-11 Flux D, CA-284-15 Guard mode_expert dans C10 Panneau absent Unit
TC-NOM-12 Flux D, CA-284-16 Map EXPERT_FIELDS_BY_STATE dans C10 Champs progressifs par état Unit
TC-NOM-13 CA-284-14 Sélecteurs granulaires + useMemo Perf device : t_render - t_event P95 <= 100ms (hors CI). Test proxy CI : vérifier nombre de re-renders ≤ 2 par événement SSE via jest.fn() wrapper sur sélecteurs Zustand. Unit (proxy) + Perf (device)
TC-NOM-14 INV-284-09 NetInfo isConnected=false → badge Badge affiché + état conservé Unit
TC-NOM-15 CA-284-04, INV-284-03 Props disabled + tooltip urgent actif C8 disabled=true + tooltip exact Unit
TC-NOM-16 CA-284-07 Cache FIFO dans C4 Doublon ignoré, 1 seul render Unit
TC-NOM-17 CA-284-08 Fenêtre grâce + resync dans C4 Gap → GET status appelé Integration
TC-NOM-18 §5.12 Validation Zod par état dans C4 Payload valide → rendu OK Unit
TC-ERR-01 Flux A erreur 4xx C6 error handling + C7 abort Message échec, pas de carte Integration
TC-ERR-02 Flux A erreur 5xx C6 error handling + C7 abort Message technique, pas de carte Integration
TC-ERR-03 Événement SSE invalide Validation Zod C4 → toast 5s + telemetry C13 Toast affiché, état conservé Unit
TC-ERR-04 Transition interdite C2 rejet + C4 toast + C13 telemetry Rejet, dernier état valide conservé Unit
TC-ERR-05 seal_id incohérent Guard C4 sealId !== activeSealId Événement ignoré + log Unit
TC-ERR-06 Transition depuis terminal C2 ALLOWED_TRANSITIONS[terminal] = [] Rejet + telemetry Unit
TC-ERR-07 Donnée expert invalide Validation Zod champs expert C10 Champ masqué, carte intacte Unit
TC-ERR-08 Push impossible Fallback email (observable backend) Client constate absence push Integration
TC-ERR-09 Gap séquence multiple C4 resync GET immédiat Pas de spéculation locale Integration
TC-INV-09 INV-284-10 C12 SecureStore Inspection stockage : rien en clair Sec
TC-INV-10 INV-284-13 Traçage réseau Aucun endpoint hors contrat Integration
TC-INV-11 INV-284-11 Pas de SecureStore pour publics Review : hash/merkle/tx en state Zustand Unit
TC-NEG-01..10 §7 tests négatifs Validation Zod + guards C2/C4/C8 Rejet propre sans crash Unit
TC-NR-01..09 §6 non-régression Ensemble des mécanismes ci-dessus Campagne regression automatisée Unit+Integration

6. Gestion des erreurs

Situation Composant Comportement Code/Message Observable
POST /seals/urgent → 4xx C6, C7 Toast erreur métier 5s, pas de carte, pas de SSE Message backend (ex: QUOTA_EXCEEDED) Toast visible + telemetry
POST /seals/urgent → 5xx C6, C7 Toast erreur technique 5s, pas de carte Erreur serveur, réessayez Toast visible + telemetry
GET /seals/{id}/status échoue post-POST (R-02) C7 Toast 5s, init carte en RECEIVED, ouvre SSE get_status_failed_after_post Telemetry + carte en RECEIVED
Événement SSE mal formé C4 Ignorer, toast 5s, telemetry sse_event_invalid Dernier état conservé
Transition interdite (retour/saut) C2, C4 Ignorer, toast 5s, telemetry transition_rejected: FROM→TO Dernier état conservé
seal_id incohérent dans SSE C4 Ignorer, telemetry seal_id_mismatch Pas d'impact UI
event_id dupliqué C4 Ignorer silencieusement (debug telemetry uniquement) Pas de toast, pas de rerender
Gap sequence_number C4 Fenêtre grâce 200ms, puis GET resync sequence_gap_detected GET status appelé
SSE coupé (réseau OK) C3 Backoff 1s/2s/4s, puis polling 5s sse_failover_polling Pas de badge offline
Perte réseau hook NetInfo, C3, C9 Badge Hors ligne, conservation état, reconnexion auto Badge visible
Donnée expert invalide C10 Champ masqué, toast 5s expert_field_invalid Carte intacte
Push indisponible C11 Constat côté client, fallback email géré par PD-80 backend Notification locale si possible
degradation_flag inconnu C5, C9 Traiter comme none + telemetry silencieuse (pas de toast). Justification : un flag inconnu n'est PAS une erreur contrôlée §3 mais un cas conservateur — le système se comporte comme si aucune dégradation n'existait. unknown_degradation_flag Pas de badge, pas de toast

Définition « erreur contrôlée » : toast non bloquant auto-dismiss 5 secondes + émission telemetry structurée ({ event_type, seal_id, timestamp, details }). Pas d'interruption du flux UI. Pas d'état dans la machine d'états.


7. Impacts sécurité

Risque Mitigation Composant
Artefacts sensibles en clair (INV-284-10) expo-secure-store avec kSecAttrAccessibleWhenUnlockedThisDeviceOnly. Purge a logout/deleteAccount/fermeture SSE. Purge proactive : purgeStaleSealArtifacts() appelée au démarrage de triggerUrgentSeal() (avant POST) pour nettoyer les résidus d'un crash précédent (learning PD-283/PD-262). C12, C7
Deep-link injection (SEC-01) Validation whitelist schéma URL interne (probatiovault://seal/{uuid}). Rejet de toute URL ne matchant pas le pattern. C15
Spam bouton urgent (SEC-02) Debounce 2s sur le handler onPress du bouton. Disable immédiat après premier tap (optimistic). C8
SSE token exposition Clé de session SSE (si utilisée) stockée en SecureStore, jamais en AsyncStorage C12
iCloud Keychain restore Attribut kSecAttrAccessibleWhenUnlockedThisDeviceOnly empêche la restauration cross-device C12
Purge artefacts sensibles Purge explicite sur : logout, deleteAccount, fermeture session SSE. Hook useEffect cleanup dans SealDetailScreen. C12, C14

8. Hypothèses techniques

ID Hypothèse Impact si faux Mitigation
HT-01 PD-80 expose SSE avec event_id et sequence_number par seal_id (H-284-01) Pas de déduplication ni réordonnancement possibles → progression non fiable Fallback polling uniquement (dégradation gracieuse)
HT-02 @react-native-community/netinfo (ou expo équivalent) fournit un signal fiable isConnected Badge offline incorrect (faux positifs/négatifs) Double-check avec un ping lightweight
HT-03 Expo SDK 54 supporte fetch en mode streaming (pour SSE custom) SSE impossible nativement → nécessite activation clause contractuelle CE-SSE-01 Clause CE-SSE-01 : si fetch streaming indisponible, lib native react-native-sse autorisée sous wrapper conforme à l'interface SSEClient (C3). Le wrapper DOIT exposer la même API que l'implémentation fetch-based. Sinon, fallback polling uniquement (dégradation gracieuse).
HT-04 expo-secure-store supporte kSecAttrAccessibleWhenUnlockedThisDeviceOnly ou équivalent Artefacts sensibles potentiellement restaurés via iCloud Utiliser la lib native react-native-keychain en remplacement
HT-05 Deep-linking Expo configuré et fonctionnel dans l'app Notifications sans redirection → UX dégradée Fallback : ouverture app sur écran d'accueil
HT-06 Le backend renvoie has_active_urgent_seal dans le GET détail document (H-284-03) Impossible d'appliquer INV-284-03. R-03 : valeur par défaut false (fail-open) acceptable car PD-80 idempotent Si champ absent → false + telemetry missing_has_active_urgent_seal
HT-07 Le backend envoie degradation_flag dans les événements SSE (H-284-02) INV-284-05 non vérifiable → pas de badge dégradation Progression sans badge, pas de calcul local
HT-08 SSE heartbeat TTL = 30s (H-284-02) Reconnexion trop précoce ou trop tardive Paramètre configurable côté client
HT-09 PD-80 est idempotent sur POST /seals/urgent (H-284-07). Un second POST pour le même document retourne le seal existant, pas de doublon. Double scellement urgent si hypothèse fausse. Test de vérification obligatoire lors de l'intégration PD-80 : envoyer 2× POST /seals/urgent avec le même document_id → vérifier que le seal_id retourné est identique. Si non idempotent → changer défaut has_active_urgent_seal en true (fail-closed).

9. Points de vigilance (risques, dette, pièges)

Réserves Gate 3 intégrées

Réserve Intégration dans le plan
R-01 (matrice CA croisées incorrectes) Pas d'impact sur l'implémentation — erreur documentaire dans la matrice de couverture des tests. Les mappings du §3 et §4 ci-dessus utilisent les bonnes correspondances INV-CA.
R-02 (échec GET status post-POST non couvert) Traité explicitement dans §2.1 (fallback RECEIVED + SSE) et §6 (ligne GET /seals/{id}/status échoue). Ajout du cas d'erreur avec stratégie conservatrice.
R-03 (fail-open has_active_urgent_seal par défaut false) Traité dans §2.4 : valeur par défaut false + telemetry obligatoire. Acceptable car PD-80 est idempotent sur POST (un second POST retourne le seal existant, pas de doublon).

Risques techniques

  1. SSE sur React Native : Pas de lib SSE standard dans Expo SDK 54. L'implémentation custom via fetch + ReadableStream doit gérer le parsing text/event-stream manuellement. Risque de bugs subtils sur le parsing multi-ligne. Prévoir des tests unitaires exhaustifs du parser SSE.

  2. Performance rendu P95 <= 100ms (CA-284-14) : Critique. Nécessite des sélecteurs Zustand granulaires (un sélecteur par champ affiché) et React.memo sur les sous-composants. Le test de performance est un test d'instrumentation sur device réel (iPhone 12+), pas simulable en CI.

  3. Cache FIFO 100 event_id : Attention au type — event_id est un entier, pas un string. Le cache doit être un tableau circulaire (ring buffer) pour l'éviction FIFO (pas de LRUCache, pas de Set avec delete aléatoire).

  4. Fenêtre de grâce 200ms : setTimeout en React Native peut dériver. Utiliser performance.now() pour mesurer la fenêtre, pas Date.now().

  5. Debounce bouton : Le disabled optimistic après premier tap évite le spam. Mais attention a remettre enabled si le POST échoue (sinon bouton bloqué définitivement).

Dette technique connue

  • Mode expert : La préférence mode_expert n'existe pas encore dans le store settings. Tâche explicite : ajouter mode_expert: boolean (défaut false) dans useSettingsStore existant ou à créer si absent. Fichier : src/store/useSettingsStore.ts. Persistance AsyncStorage. Cette tâche est rattachée au composant C10 (ExpertPanel) qui en dépend.
  • Notifications deep-link : Le service de notifications existant supporte déjà les deep-links. L'ajout de la route seal/:id dans la config Expo linking est minimal.

10. Hors périmètre

Élément Raison
Pipeline backend de scellement (orchestration, BullMQ, TSA, Merkle, ancrage) PD-80
Facturation / gestion des quotas côté serveur PD-80 / autre story
Monitoring opérationnel interne Backend ops
Android / Web iOS-only pour cette story
API d'administration (retry/resubmit manuels) Backend admin
Migration DDL Aucune modification de schéma — story front-only
Politique iOS hors foreground (locale vs push distante) Q-284-04 non résolu
Quota max par plan (standard/premium/enterprise) Q-284-05 non résolu
Tests E2E sur device réel (TC-NOM-13 perf) Nécessite iPhone 12+ physique, hors CI — exécution manuelle

11. Contraintes techniques

Contrainte Valeur Impact
Dépendance PD-80 STUB — endpoints non implémentés. Tests d'intégration avec mocks HTTP (msw ou jest.fn()). Intégration réelle lors de PD-80 DONE. Tous les TC-NOM/TC-ERR utilisent des mocks API conformes au contrat PD-80
Framework test Jest + React Native Testing Library (RNTL). Config existante dans le projet app. Unitaires et intégration dans Jest. Pas de Vitest (incompatible RN).
Compatibilité ESM/CJS Le projet app utilise Metro bundler (CJS natif). Les imports ESM-only nécessitent un wrapper CJS ou un polyfill Metro. ReadableStream est disponible nativement dans Hermes (RN 0.76+). SSE fetch-based ne nécessite pas de polyfill sur Hermes récent
Variables CI Aucune variable CI spécifique requise. Les tests d'intégration utilisent des mocks en mémoire (pas de serveur distant). CI standard : npm test
Expo SDK SDK 54 (hypothèse HT-03). Si fetch streaming non supporté → clause CE-SSE-01. Vérifier en pré-implémentation

12. Mécanismes cross-module (anciennement §11)

Aucune modification d'autres modules. PD-284 est une story front-only qui consomme les API définies par PD-80. Les endpoints utilisés (POST /seals/urgent, GET /seals/{id}/status, SSE stream) sont exposés par le backend PD-80, pas créés par PD-284.


12. Périmètre de test

Niveau de test In scope Hors scope (justification)
Unitaire Tous les composants C1-C15
Intégration Flux orchestrateur (C7 : POST→GET→SSE), failover SSE→polling (C3), resync gap (C4), notifications (C11)
E2E Infrastructure backend PD-80 non disponible côté app. Les tests d'intégration avec mocks API couvrent les flux complets.
Perf TC-NOM-13 proxy (re-render count ≤ 2/event) en CI TC-NOM-13 device (P95 <= 100ms) nécessite un iPhone 12+ physique. Exécution manuelle hors CI. Ticket de suivi : à créer post-Gate 8. Le test proxy CI garantit l'absence de régressions structurelles (re-renders excessifs).
Sécurité TC-INV-09 (stockage sécurisé), SEC-01 (deep-link), SEC-02 (debounce) Audit SecureStore exhaustif → story dédiée sécurité

Tous les niveaux unitaire et intégration sont couverts. Les exclusions E2E et Perf sont justifiées par l'indisponibilité d'infrastructure et de device physique en CI.