PD-101 — REX (Retour d'experience)¶
Resume¶
| Champ | Valeur |
|---|---|
| Story | PD-101 — Implementer upload document avec progress |
| Projet | app (ProbatioVault-app) |
| Domaine | mobile-ios |
| Date debut | 2026-03-10 |
| Date fin | 2026-03-10 |
| Duree | ~1 journee |
| Gate 3 | NON_CONFORME v1 (6.5), GO v2 (8.75) |
| Gate 5 | RESERVE v1 (7.625) |
| Gate 8 | GO v1 (8.125) |
| Tests | 121/121, 88% coverage |
| Fichiers | 31 fichiers, 4579 insertions |
Points forts¶
-
Strategie de chiffrement FILE-LEVEL : Le choix de chiffrer le fichier entier avant tout decoupage multipart a elimine le risque de reutilisation de nonce GCM. Un nonce unique par
doc_idsuffit, car les chunks uploades sont des morceaux du ciphertext et non du plaintext. Ce pattern est plus simple et plus sur que le chiffrement chunk-level. -
Convergence rapide Gate 3 (v1 -> v2, delta +2.25) : L'ecart v1 (6.5) a v2 (8.75) represente la plus forte convergence en une seule iteration. Les corrections ciblaient des ambiguites structurelles (transcodage iOS, hash avant/apres chiffrement, seuil multipart SI base 10) et non des bugs de fond, ce qui explique la rapidite du redressement.
-
Zero faux positif sur les forbidden patterns : Les 6 checks de securite critiques (Math.random absent, plaintext absent, chunk-level absent, secrets absent, createVerify absent, nonce reutilisation absent) sont tous passes du premier coup. Les learnings injectes depuis PD-282, PD-265 et PD-283 ont prevenu les erreurs recurrentes.
-
Machine a etats stricte avec branded types : L'utilisation de branded types (
DocId,Sha3Hash) et d'une machine a etats explicite dans le Zustand store (transitions autorisees/interdites) a permis de capter les erreurs de types a la compilation. Le pattern est directement issu du learning "refined/branded types obligatoires pour UUID semantiquement distincts". -
Gate 8 GO en v1 : Premiere iteration Gate 8 avec GO direct (8.125/10), 0 BLOQUANT, 0 MAJEUR. La maturite du workflow de gouvernance et l'injection proactive des learnings ont reduit le nombre d'iterations necessaires.
Ecarts et resolutions¶
Gate 3 v1 — NON_CONFORME (6.5/10)¶
| Ecart | Cause | Resolution |
|---|---|---|
| Testability 5.5 | Ambiguite sur le hash : avant ou apres chiffrement ? Quel fichier exact est hashe en cas de transcodage iOS ? | Spec v2 : hash SHA3-256 calcule sur le "fichier soumis" (recu par l'app), recalcul post-chiffrement pour validation fonctionnelle (INV-05) |
| Completeness 6.0 | Seuil multipart non defini (MB binaire vs SI base 10), transcodage iOS non traite, format preuve consentement absent | Spec v2 : seuil 10 000 000 bytes (SI base 10), detection transcodage avec ios_transcoded flag, format JSON preuve consentement (ss3.4) |
Gate 5 v1 — RESERVE (7.625/10)¶
| Ecart | Cause | Resolution |
|---|---|---|
| Risk mitigation 7.0 | OOM pour fichiers volumineux (500 MB = ~1 GB RAM avec plaintext + ciphertext) | Adresse en implementation : monitoring memoire dans C3, warning UX au-dela de 200 MB |
| Feasibility 7.5 | ESM compatibility non validee pour @noble/ciphers, audit fail-closed non prouve dans le plan | Valide en implementation : @noble/ciphers fonctionne en ESM, audit via uploadAudit.ts avec deny-list |
Les reserves Gate 5 ont ete acceptees par decision humaine car elles concernaient des risques d'implementation (pas de spec) et ont ete adressees pendant l'etape 6.
Gate 8 v1 — GO (8.125/10)¶
| Ecart | Criticite | Resolution |
|---|---|---|
S-01 : getAuthHeaders() stub sans JWT | RESERVE | Intentionnellement differe a PD-28 (session management). Backend fail-closed (401). |
E-01 : writeTempArtifact ecrit en clair | MINEUR (reclasse) | Faux positif — fonction orpheline jamais appelee dans le flux production. A supprimer dans une future story de nettoyage. |
S-02 : currentAbortController jamais assigne | MINEUR | Cancel backend atomique + S3 lifecycle mitigent. Impact limite a la bande passante d'une requete supplementaire. |
| 15 MINEURS au total | MINEUR | Acceptes — majorite lies a des couvertures de test hors scope mobile (DB inspection, device physique) ou a des edge cases negligeables. |
Learnings techniques¶
L1 — Chiffrement FILE-LEVEL elimine le risque nonce-reuse GCM¶
Pattern : Chiffrer le fichier entier en une seule operation AES-256-GCM avec un nonce unique, puis decouper le ciphertext en chunks pour l'upload multipart.
Pourquoi : Le chiffrement chunk-level (un nonce par chunk avec la meme cle) cree une reutilisation de (key, nonce) qui brise la securite GCM. Le FILE-LEVEL est plus simple et elimine ce risque par construction.
Applicable : Toute story d'upload chiffre sur mobile ou desktop.
L2 — 35 erreurs ESLint au premier commit : extraction de helpers¶
Pattern : Les modules generes par LLM depassent souvent la complexite cognitive ESLint (> 15). Extraire des fonctions helpers (calculateEta, buildChunkRanges, mapHttpErrorToMessage) reduit la complexite sans changer la logique.
Regles les plus frequentes : complexity (cognitive), @typescript-eslint/no-unused-vars (imports), react/prefer-read-only-props, import/no-nodejs-modules (Buffer).
Applicable : Injecter un resume des rules ESLint frequentes dans les prompts agents step 6b (cf. learning PD-265).
L3 — jest.mock avec imports statiques : ordre de declaration¶
Pattern : 7 tests echouaient au premier run. Cause principale : jest.mock() doit etre declare avant tout import du module sous test quand les mocks utilisent des factory functions. Avec les imports statiques ESM, l'ordre d'evaluation n'est pas garanti.
Solution : Utiliser jest.mock('module', () => ({ ... })) en haut de fichier, avant les imports, et eviter les references a des variables hoistees.
Applicable : Tout test React Native avec mocks de modules natifs.
L4 — Timer interleaving dans les tests de retry avec backoff¶
Pattern : Les tests de retry avec jest.useFakeTimers() echouent si les timers ne sont pas avances dans le bon ordre. Le backoff exponentiel (1s/2s/4s) necessite d'avancer le timer de la bonne duree a chaque iteration.
Solution : Utiliser jest.advanceTimersByTime(delay) avec des valeurs precises correspondant au backoff attendu, et intercaler des await Promise.resolve() pour laisser les microtasks s'executer.
L5 — Progress window eviction pour calcul ETA stable¶
Pattern : Le calcul d'ETA basculait entre des valeurs instables car la fenetre de mesure de vitesse n'etait pas bornee. En limitant la fenetre a N echantillons recents (sliding window), l'ETA devient stable.
Solution : Fenetre glissante de 5 echantillons dans uploadProgress.ts, eviction du plus ancien a chaque nouveau sample.
L6 — writeTempArtifact : faux positif en review code¶
Pattern : Un reviewer LLM a signale writeTempArtifact comme BLOQUANT (ecriture en clair sans envelope encryption). L'analyse a montre que la fonction est exportee mais jamais appelee dans le flux production — c'est du code orphelin genere par l'agent.
Learning : Les reviewers LLM ne distinguent pas "exporte" de "appele". L'orchestrateur doit verifier les appels reels (grep callsites) avant de valider un ecart BLOQUANT.
L7 — Stub auth headers trace vers story destination¶
Pattern : getAuthHeaders() retourne un objet vide au lieu d'un JWT. Ecart S-01 classe RESERVE mais immediatement attenue car (1) le backend rejette 401 (fail-closed), (2) la story de destination PD-28 est explicitement tracee.
Learning : Le pattern "stub inter-PD avec story destination" (issu PD-250/PD-251) fonctionne — la criticite S-01 est restee RESERVE au lieu de BLOQUANT grace a la tracabilite.
Metriques workflow¶
| Metrique | Valeur |
|---|---|
| Iterations Gate 3 | 2 (v1 NON_CONFORME 6.5, v2 GO 8.75) |
| Iterations Gate 5 | 1 (RESERVE 7.625) |
| Iterations Gate 8 | 1 (GO 8.125) |
| Nombre de tests | 121 |
| Coverage | 88.39% statements, 89.73% lines |
| ESLint errors (PD-101) | 0 (35 corrigees avant commit) |
| Sonar | SKIPPED (credentials Vault expirees) |
| Pipeline | lint OK, knip FAILED (pre-existant) |
| Fichiers crees/modifies | 31 fichiers, 4579 insertions |
| Composants | 13 modules (C1-C13) |
| Art. II derogations | 3 (Gate 3, Gate 5, Gate 8 — prompts > 30KB) |
Ameliorations process identifiees¶
P1 — Renouveler les credentials SonarQube dans Vault¶
Constat : Sonar Quality Gate skippee (HTTP 401 — credentials expirees). L'attenuation ESLint+tsc ne remplace pas Sonar pour les rules de securite avancees (S2245, detection injection, etc.).
Action : Renouveler le token SonarQube dans Vault (kv/ci/sonarqube). Ajouter un check d'expiration dans le script de pre-flight du workflow.
P2 — Injecter les ESLint rules frequentes dans les prompts agents step 6b¶
Constat : 35 erreurs ESLint au premier commit, principalement complexity, no-unused-vars, prefer-read-only-props, import/no-nodejs-modules. Chaque correction est triviale mais couteuse en temps cumule.
Action : Ajouter une section "ESLint rules critiques" dans le template de prompt step 6b, incluant les 5 rules les plus frequentes avec exemples (cf. learning PD-265 deja documente, a appliquer systematiquement).
P3 — Documenter le pattern FILE-LEVEL vs chunk-level dans les learnings constitutionnels¶
Constat : Le chiffrement FILE-LEVEL est un choix architectural critique qui a simplifie toute l'implementation. Ce pattern devrait etre reference dans les learnings pour toute future story d'upload chiffre.
Action : Ajouter un learning dans .claude/rules/learnings.md : "Pour tout upload chiffre AES-GCM, privilegier le chiffrement FILE-LEVEL (un nonce par fichier) au chunk-level (un nonce par chunk) pour eliminer le risque de nonce-reuse."
Risques residuels¶
| Risque | Criticite | Mitigation |
|---|---|---|
S-01 : auth JWT absente (getAuthHeaders stub) | RESERVE | Backend fail-closed (401). Tracee PD-28. Aucun upload ne peut aboutir en production sans cette story. |
| OOM sur fichiers > 200 MB | Moyen | Warning UX au-dela de 200 MB. Monitoring memoire dans uploadCrypto.ts. Test perf sur device reel requis avant production. |
| Sonar SKIPPED | Moyen | Pipeline CI executera Sonar apres merge. Credentials a renouveler. |
| knip FAILED (pre-existant) | Faible | Duplicate exports dans auditQueue/auditService — hors scope PD-101. A traiter dans une story de nettoyage. |
writeTempArtifact orphelin | Faible | Fonction jamais appelee. A supprimer ou proteger dans une future story. |
currentAbortController jamais assigne | Faible | Cancel backend atomique + S3 lifecycle policy (7 jours) couvrent le risque. |
Recommandations¶
Pour les futures stories d'upload mobile¶
- Privilegier le chiffrement FILE-LEVEL pour AES-GCM. Ne jamais chiffrer chunk par chunk avec la meme cle — le risque de nonce-reuse est structurel et non attenuable.
- Tester le backoff avec fake timers des le premier commit. Les tests de retry sont fragiles si les timers ne sont pas avances avec precision.
- Valider la compatibilite ESM des libraries crypto (
@noble/ciphers,@noble/hashes) avant la Gate 5. L'ecosysteme React Native/Hermes a des contraintes specifiques.
Pour les futures stories du module crypto¶
- Appliquer le roundtrip sign-verify (learning PD-282) : tout module crypto avec
encrypt/decryptousign/verifydoit avoir un test de roundtrip complet. PD-101 l'applique via INV-05 (hash recalcul post-chiffrement). - Utiliser des branded types pour tous les identifiants cryptographiques (
DocId,Sha3Hash,NonceBytes). Le compilateur TypeScript est la meilleure barriere contre les inversions de parametres.
Pour le workflow de gouvernance¶
- Renouveler les credentials Sonar avant la prochaine story. Le skip Sonar cree un angle mort sur les vulnerabilites de securite avancees.
- Resoudre le knip FAILED pre-existant (duplicate exports) pour eviter la confusion entre erreurs pre-existantes et regressions introduites par une story.