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 : POST → GET 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¶
-
SSE sur React Native : Pas de lib SSE standard dans Expo SDK 54. L'implémentation custom via
fetch+ReadableStreamdoit gérer le parsingtext/event-streammanuellement. Risque de bugs subtils sur le parsing multi-ligne. Prévoir des tests unitaires exhaustifs du parser SSE. -
Performance rendu P95 <= 100ms (CA-284-14) : Critique. Nécessite des sélecteurs Zustand granulaires (un sélecteur par champ affiché) et
React.memosur les sous-composants. Le test de performance est un test d'instrumentation sur device réel (iPhone 12+), pas simulable en CI. -
Cache FIFO 100 event_id : Attention au type —
event_idest un entier, pas un string. Le cache doit être un tableau circulaire (ring buffer) pour l'éviction FIFO (pas deLRUCache, pas deSetavec delete aléatoire). -
Fenêtre de grâce 200ms :
setTimeouten React Native peut dériver. Utiliserperformance.now()pour mesurer la fenêtre, pasDate.now(). -
Debounce bouton : Le
disabledoptimistic après premier tap évite le spam. Mais attention a remettreenabledsi le POST échoue (sinon bouton bloqué définitivement).
Dette technique connue¶
- Mode expert : La préférence
mode_expertn'existe pas encore dans le store settings. Tâche explicite : ajoutermode_expert: boolean(défautfalse) dansuseSettingsStoreexistant 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/:iddans 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.