Aller au contenu

PD-86 — Plan d'implémentation

Metadata

  • Story ID : PD-86
  • Epic : PD-185 — B2C-MINEURS
  • Titre : Détection de contenu sensible (IA locale)
  • Projet : ProbatioVault-app (iOS / React Native Expo)
  • Date : 2026-02-24
  • Version : 1.0.0

Phase 0 — Go/No-Go

Hypothèses critiques vérifiées

Hypothèse Vérification Statut Action si KO
Viewer document existant (MediaPreviewScreen.tsx) Fichier présent, gère déchiffrement in-memory AES-256-GCM, rendu image/vidéo/PDF OK
Pattern zeroization existant (crypto/zeroize.ts) SecureBuffer.destroy(), withZeroize(), double-pass fill(0) OK
Stockage chiffré disponible (expo-secure-store, AsyncStorage) Deux couches : SecureStore pour clés, AsyncStorage chiffré pour données OK
Screenshot protection existante (PD-248) expo-screen-capture intégré, pattern screenshotProtection.ts OK Étendre aux nouveaux écrans
Profil utilisateur chargé via API settingsApi.tsgetProfile(), hook useSettings() OK isMinor à ajouter au type UserProfile — dépend du backend B2C-MINEURS
Infrastructure ML locale Aucune infrastructure ML existante NOUVEAU Installer ONNX Runtime React Native ou équivalent
App Switcher protection AppDelegate.swift + useAutoLock.ts gère background OK Ajouter redaction overlay spécifique
Concurrency patterns async/await + cleanup active flag dans useEffect OK Ajouter mutex/PQueue pour sérialisation
Stores Zustand useAuthStore, useVaultStore, useSecurityStore, useAppStore OK Créer useSensitiveDetectionStore

Dépendance critique : isMinor

Le flag isMinor n'existe pas encore dans le profil utilisateur côté app. Il provient du backend B2C-MINEURS (PD-84). Approche retenue : stub local avec TODO tracé, lecture du flag depuis la réponse API profil quand disponible. Le stub retourne false par défaut (comportement adulte = moins restrictif = safe par défaut pour les tests).

// STUB — TODO(PD-84): remplacer par lecture profil serveur
function getIsMinor(): boolean {
  const profile = useAuthStore.getState().userProfile;
  return profile?.isMinor ?? false; // false = adulte par défaut
}

Dépendance critique : modèle ML

Aucun modèle ML n'est encore bundlé. La spec (Q1) indique : "Décision d'implémentation. Exigence contractuelle : modèle hors ligne avec benchmark documenté avant mise en production."

Approche retenue : Architecture modulaire avec interface ContentClassifier abstraite. Implémentation initiale avec ONNX Runtime Mobile via un native module React Native. Le modèle concret (MobileNet NSFW, NudeNet, ou custom) sera choisi après benchmark et documenté avant Gate 8. Le plan prévoit un MockClassifier pour les tests.


1. Découpage en composants

Architecture modulaire

src/sensitive-detection/
├── types.ts                          # Types partagés (Category, AnalysisStatus, etc.)
├── config.ts                         # Constantes (seuils par défaut, bornes, timeout)
├── engine/
│   ├── ContentClassifier.ts          # Interface abstraite du classifieur
│   ├── OnnxClassifier.ts             # Implémentation ONNX Runtime
│   ├── MockClassifier.ts             # Mock pour tests
│   ├── ImageAnalyzer.ts              # Pipeline analyse image
│   ├── VideoAnalyzer.ts              # Pipeline analyse vidéo (12 frames)
│   ├── PdfAnalyzer.ts                # Pipeline analyse PDF (5 pages)
│   └── EngineWarmup.ts               # Warm-up et lifecycle moteur
├── store/
│   ├── useSensitiveDetectionStore.ts # Store Zustand (settings + verdicts)
│   └── verdictCache.ts               # Cache chiffré des verdicts
├── gate/
│   ├── ViewerGate.ts                 # Logique de précédence (6 règles)
│   ├── ViewerGateModal.tsx           # Composant UI modal de consentement
│   └── MinorConfirmation.tsx         # Composant confirmation additionnelle mineur
├── services/
│   ├── DetectionService.ts           # Orchestrateur principal (mutex, idempotence)
│   ├── ThresholdEvaluator.ts         # Évaluation isSensitive + réévaluation seuils
│   ├── SamplingStrategy.ts           # Stratégies d'échantillonnage (vidéo/PDF)
│   └── DetectionLogger.ts            # Logger whitelist-only
├── security/
│   ├── BufferZeroizer.ts             # Wrapper zeroization try/finally
│   ├── iOSPrivacyGuard.ts           # App Switcher, Spotlight, QuickLook
│   └── NetworkGuard.ts               # Assertion zero-network (dev/test)
└── hooks/
    ├── useSensitiveDetection.ts      # Hook principal pour le viewer
    └── useSensitiveSettings.ts       # Hook gestion préférences

Composants et responsabilités

# Composant Responsabilité Dépendances
C1 types.ts Définition des types Category, AnalysisStatus, SensitiveAnalysisResult, SensitiveDetectionSettings, DocumentSensitiveVerdict, DocumentSensitivePreference, ViewerGateDecision, InMemoryHandle Aucune
C2 config.ts Constantes : seuils par défaut, bornes [0.50, 0.95], timeout 5000ms, max frames 12, max pages 5, TTL cache, taille max handle 64MB C1
C3 ContentClassifier.ts Interface abstraite classify(input: ClassifierInput): Promise<ClassifierOutput>, contrat d'inférence C1
C4 OnnxClassifier.ts Implémentation ONNX : chargement modèle, inférence, extraction scores par catégorie C3, native module
C5 MockClassifier.ts Mock déterministe pour tests unitaires/intégration C3
C6 ImageAnalyzer.ts Préprocessing image (downscale), appel classifieur, extraction résultat C3, C14
C7 VideoAnalyzer.ts Extraction 12 frames (uniforme + 2 premières secondes), appel classifieur par frame, agrégation max(score) C3, C12, C14
C8 PdfAnalyzer.ts Rendu mémoire des pages échantillonnées, appel classifieur, agrégation max(score) C3, C12, C14
C9 EngineWarmup.ts Warm-up modèle à l'ouverture du coffre, lazy fallback au premier document C4
C10 useSensitiveDetectionStore.ts Store Zustand : settings, verdicts, preferences, actions CRUD avec persistance chiffrée C1, expo-secure-store
C11 verdictCache.ts CRUD chiffré des verdicts, invalidation par modelVersion, politique TTL/purge C1, C10
C12 SamplingStrategy.ts Algorithmes d'échantillonnage vidéo (12 frames) et PDF (5 pages déterministes) C2
C13 ThresholdEvaluator.ts Calcul isSensitive depuis rawScores + seuils effectifs (avec clamp mineur), réévaluation sans ré-inférence C1, C2
C14 BufferZeroizer.ts Wrapper withSafeBuffer<T>(buffer, fn): Promise<T> — try/finally + zeroization best-effort crypto/zeroize.ts existant
C15 DetectionService.ts Orchestrateur : mutex par (documentId, modelVersion), timeout 5000ms, dispatch vers analyzers, idempotence, cache check/write C4-C8, C10-C14
C16 ViewerGate.ts Évaluation des 6 règles de précédence contractuelle, retourne GateDecision C1, C10, C13
C17 ViewerGateModal.tsx UI : modal consentement avec boutons SHOW_ANYWAY / KEEP_HIDDEN / CANCEL C16
C18 MinorConfirmation.tsx UI : confirmation additionnelle pour mineur (double validation) C17
C19 DetectionLogger.ts Logger avec whitelist stricte de champs, rejet silencieux des champs blacklistés C1
C20 iOSPrivacyGuard.ts Redaction App Switcher, exclusion Spotlight, contrôle QuickLook expo-screen-capture, native modules
C21 NetworkGuard.ts Assertion en dev/test : aucun appel réseau durant pipeline de détection
C22 useSensitiveDetection.ts Hook principal intégré au viewer : orchestration détection + gate + modal C15, C16, C17
C23 useSensitiveSettings.ts Hook pour écran paramètres : activation/désactivation, seuils, préférences par document C10, C13

2. Flux techniques

2.1 Flux nominal : ouverture d'un document (image)

1. Utilisateur ouvre un document dans le viewer (MediaPreviewScreen)
2. Hook useSensitiveDetection(documentId, documentType) déclenché
3. Vérification : enabled ?
   ├── NON → rendu direct (aucune classification, aucun modal)
4. Vérification cache : verdict existant pour (documentId, currentModelVersion) ?
   ├── OUI + thresholdsVersion == current → utiliser verdict caché
   ├── OUI + thresholdsVersion != current + rawScores disponibles
   │     → ThresholdEvaluator.reevaluate(rawScores, newThresholds, isMinor)
   │     → mettre à jour verdict cache
   ├── OUI + modelVersion != current → invalider, reclassifier
5. Si pas de verdict valide → DetectionService.analyze()
   5a. Mutex acquire (documentId, modelVersion)
   │   ├── Déjà en cours → attendre résultat existant
   5b. Déchiffrement contenu → InMemoryHandle (via crypto existant)
   │   ├── Taille > 64MB → OUT_OF_MEMORY
   5c. BufferZeroizer.withSafeBuffer(handle, async (buffer) => {
   │     │
   │     5d. Dispatch selon documentType :
   │     │   ├── IMAGE → ImageAnalyzer.analyze(buffer)
   │     │   ├── VIDEO → VideoAnalyzer.analyze(buffer)
   │     │   └── PDF   → PdfAnalyzer.analyze(buffer)
   │     │
   │     5e. Timeout race (5000ms)
   │     │   ├── Dépassé → TIMEOUT
   │     │
   │     5f. Extraction rawScores par catégorie whitelist
   │     │
   │     5g. ThresholdEvaluator.evaluate(rawScores, effectiveThresholds)
   │     │   → isSensitive, detectedCategories, roundedScores
   │     │
   │     5h. Persistance verdict chiffré dans cache
   │   })  ← zeroization buffer en finally
   5i. Mutex release
6. ViewerGate.evaluate(verdict, settings, preference, context)
   Règles de précédence (ordre contractuel) :
   R1. enabled=false → pas de modal, rendu direct
   R2. status ∈ {erreurs} → modal prudent obligatoire
   R3. status=SUCCESS && isSensitive=true → modal obligatoire
   R4. isMinor=true → suppressWarning ignoré
   R5. !isMinor && isSensitive && suppressWarning → modal omis
   R6. KEEP_HIDDEN/CANCEL → aucun rendu
7. Si modal requis → ViewerGateModal affiché
   ├── SHOW_ANYWAY :
   │   ├── isMinor ? → MinorConfirmation (double validation)
   │   └── rendu contenu
   ├── KEEP_HIDDEN → retour, contenu non rendu
   └── CANCEL → retour, contenu non rendu

2.2 Flux vidéo : échantillonnage 12 frames

1. VideoAnalyzer reçoit buffer vidéo déchiffré
2. SamplingStrategy.videoFrames(duration) retourne 12 indices :
   - 2 frames dans les 2 premières secondes (obligatoire)
   - 10 frames uniformément réparties sur le reste
   - Total plafonné à 12
3. Pour chaque frame :
   a. Extraction frame à l'indice → buffer mémoire temporaire
   b. Downscale agressif (résolution modèle)
   c. ContentClassifier.classify(frameBuffer)
   d. Zeroization du buffer frame
4. Agrégation : pour chaque catégorie, score = max(scores_frames)
5. Retour SensitiveAnalysisResult avec samplingMetadata.videoFramesAnalyzed

2.3 Flux PDF : échantillonnage déterministe 5 pages

1. PdfAnalyzer reçoit buffer PDF déchiffré
2. Lecture totalPages (via PDFKit ou react-native-pdf)
3. SamplingStrategy.pdfPages(totalPages) retourne indices :
   - Si totalPages <= 5 → toutes les pages
   - Si totalPages > 5 :
     a. page 1 (première)
     b. page N (dernière)
     c. 3 pages intérieures aux quartiles : floor(N/4), floor(N/2), floor(3N/4)
     d. Déduplication + complétion si collision
4. Pour chaque page index :
   a. Rendu page en mémoire (PdfRenderer ou équivalent)
   b. Downscale résolution modèle
   c. ContentClassifier.classify(pageBuffer)
   d. Zeroization du buffer page
5. Agrégation : max(scores_pages) par catégorie
6. Zéro persistance disque — tout en mémoire
7. Retour avec samplingMetadata.pdfPagesAnalyzed et pdfPageIndexes

2.4 Flux warm-up moteur

1. Événement "ouverture du coffre" (unlock vault)
2. EngineWarmup.start() :
   a. Chargement modèle ONNX en mémoire
   b. Inférence dummy (tensor vide) pour préchauffer
   c. État : READY
3. Si ouverture document avant warm-up terminé :
   a. Lazy fallback : chargement synchrone au premier analyze()
   b. Pas de rupture de flux — juste latence accrue au premier document

2.5 Flux changement de seuils

1. Utilisateur modifie un seuil via useSensitiveSettings
2. Validation bornes [0.50, 0.95]
3. Si isMinor → clamp aux valeurs par défaut (pas de relaxation)
4. Incrémentation thresholdsVersion
5. Au prochain accès viewer pour un document avec verdict caché :
   a. thresholdsVersionAtDecision != current
   b. rawScores disponibles → ThresholdEvaluator.reevaluate() sans ré-inférence
   c. rawScores absents → reclassification complète requise
   d. Mise à jour verdict cache avec nouveau thresholdsVersionAtDecision

2.6 Flux erreur (tous types)

1. Erreur détectée (TIMEOUT, MODEL_UNAVAILABLE, CORRUPTED_INPUT, etc.)
2. BufferZeroizer : finally → zeroization du buffer
3. Verdict partiel persisté : { status: <error_code>, analyzedAt, modelVersion }
4. DetectionLogger : log whitelist-only (eventName, documentId, status, durationMs, errorCode)
5. ViewerGate : R2 (erreur) → modal prudent obligatoire
   - Ignore suppressWarning (politique prudente prime)
   - Affiche message adapté au code erreur
   - Options : "Afficher quand même" / "Garder masqué"
6. Si isMinor && erreur → confirmation additionnelle avant affichage

2.7 Diagrammes Mermaid

Graphe de dépendances des composants

graph TD
    subgraph Types & Config
        C1[C1 types.ts]
        C2[C2 config.ts]
    end

    subgraph Engine
        C3[C3 ContentClassifier]
        C4[C4 OnnxClassifier]
        C5[C5 MockClassifier]
        C6[C6 ImageAnalyzer]
        C7[C7 VideoAnalyzer]
        C8[C8 PdfAnalyzer]
        C9[C9 EngineWarmup]
    end

    subgraph Store
        C10[C10 useSensitiveDetectionStore]
        C11[C11 verdictCache]
    end

    subgraph Services
        C12[C12 SamplingStrategy]
        C13[C13 ThresholdEvaluator]
        C14[C14 BufferZeroizer]
        C15[C15 DetectionService]
        C19[C19 DetectionLogger]
    end

    subgraph Gate & UI
        C16[C16 ViewerGate]
        C17[C17 ViewerGateModal]
        C18[C18 MinorConfirmation]
    end

    subgraph Security
        C20[C20 iOSPrivacyGuard]
        C21[C21 NetworkGuard]
    end

    subgraph Hooks
        C22[C22 useSensitiveDetection]
        C23[C23 useSensitiveSettings]
    end

    C2 --> C1
    C3 --> C1
    C4 --> C3
    C5 --> C3
    C6 --> C3
    C6 --> C14
    C7 --> C3
    C7 --> C12
    C7 --> C14
    C8 --> C3
    C8 --> C12
    C8 --> C14
    C9 --> C4
    C11 --> C1
    C11 --> C10
    C12 --> C2
    C13 --> C1
    C13 --> C2
    C15 --> C4
    C15 --> C5
    C15 --> C6
    C15 --> C7
    C15 --> C8
    C15 --> C10
    C15 --> C11
    C15 --> C12
    C15 --> C13
    C15 --> C14
    C16 --> C1
    C16 --> C10
    C16 --> C13
    C17 --> C16
    C18 --> C17
    C22 --> C15
    C22 --> C16
    C22 --> C17
    C23 --> C10
    C23 --> C13

Diagramme de séquence — Flux nominal (ouverture document image)

sequenceDiagram
    participant User as Utilisateur
    participant Viewer as MediaPreviewScreen
    participant Hook as useSensitiveDetection (C22)
    participant Cache as verdictCache (C11)
    participant DS as DetectionService (C15)
    participant BZ as BufferZeroizer (C14)
    participant IA as ImageAnalyzer (C6)
    participant Clf as ContentClassifier (C3/C4)
    participant TE as ThresholdEvaluator (C13)
    participant VG as ViewerGate (C16)
    participant Modal as ViewerGateModal (C17)

    User->>Viewer: Ouvre un document image
    Viewer->>Hook: useSensitiveDetection(documentId, IMAGE)
    Hook->>Hook: Vérifier enabled?

    alt enabled = false
        Hook-->>Viewer: Rendu direct (R1)
    else enabled = true
        Hook->>Cache: get(documentId, modelVersion)

        alt Verdict valide en cache
            Cache-->>Hook: verdict existant
        else Pas de verdict valide
            Hook->>DS: analyze(documentId, IMAGE)
            DS->>DS: Mutex acquire (documentId, modelVersion)
            DS->>BZ: withSafeBuffer(handle, fn)
            BZ->>IA: analyze(buffer)
            IA->>Clf: classify(input)
            Clf-->>IA: rawScores par catégorie
            IA-->>BZ: SensitiveAnalysisResult
            BZ->>BZ: finally: zeroization buffer
            BZ-->>DS: résultat
            DS->>TE: evaluate(rawScores, thresholds)
            TE-->>DS: isSensitive, detectedCategories
            DS->>Cache: persist(verdict chiffré)
            DS->>DS: Mutex release
            DS-->>Hook: verdict
        end

        Hook->>VG: evaluate(verdict, settings, preference)

        alt isSensitive = true (R3)
            VG-->>Hook: gateRequired = true
            Hook->>Modal: Afficher modal consentement

            alt SHOW_ANYWAY
                Modal-->>Viewer: Rendu contenu
            else KEEP_HIDDEN / CANCEL
                Modal-->>Viewer: Retour, contenu non rendu
            end
        else isSensitive = false
            VG-->>Hook: gateRequired = false
            Hook-->>Viewer: Rendu direct
        end
    end

Diagramme de séquence — Flux erreur (timeout / modèle indisponible)

sequenceDiagram
    participant Hook as useSensitiveDetection (C22)
    participant DS as DetectionService (C15)
    participant BZ as BufferZeroizer (C14)
    participant Analyzer as Analyzer (C6/C7/C8)
    participant VG as ViewerGate (C16)
    participant Modal as ViewerGateModal (C17)
    participant MC as MinorConfirmation (C18)

    Hook->>DS: analyze(documentId, type)
    DS->>DS: Mutex acquire
    DS->>BZ: withSafeBuffer(handle, fn)

    alt TIMEOUT (Promise.race > 5000ms)
        BZ->>BZ: finally: zeroization buffer
        DS-->>DS: verdict partiel {status: TIMEOUT}
    else MODEL_UNAVAILABLE
        BZ->>BZ: finally: zeroization buffer
        DS-->>DS: verdict partiel {status: MODEL_UNAVAILABLE}
    end

    DS->>DS: Persist verdict partiel chiffré
    DS->>DS: Mutex release
    DS-->>Hook: verdict (status erreur)

    Hook->>VG: evaluate(verdict, settings, preference)
    Note over VG: R2: erreur → modal prudent obligatoire<br/>(prime sur suppressWarning R5)
    VG-->>Hook: gateRequired = true

    Hook->>Modal: Afficher modal prudent

    alt isMinor = true
        Modal->>MC: Confirmation additionnelle requise
        MC-->>Modal: Choix utilisateur
    end

    alt SHOW_ANYWAY
        Modal-->>Hook: Rendu contenu
    else KEEP_HIDDEN / CANCEL
        Modal-->>Hook: Contenu non rendu
    end

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-86-01 Classification locale uniquement, zéro réseau NetworkGuard assertion en dev/test ; aucun import de fetch/axios dans le module ; architecture offline-only C21, architecture NetworkGuard intercepte tout appel réseau durant pipeline ; revue de code : aucun import réseau dans src/sensitive-detection/ Faible : assertion active en dev/test
INV-86-02 Détection uniquement au viewer Hook useSensitiveDetection appelé exclusivement depuis MediaPreviewScreen ; aucun déclenchement à l'import C22, MediaPreviewScreen Test TC-86-17 : import sans ouverture → aucun événement d'analyse Faible
INV-86-03 Fichier original jamais modifié Pipeline opère sur copie déchiffrée en mémoire (InMemoryHandle) ; aucune écriture sur le fichier source C14, C15 Test TC-86-05 : hash probatoire identique avant/après Faible : pattern existant dans l'app
INV-86-04 Seules catégories whitelistées Type Category = union litérale VIOLENCE | NUDITE | SEXUEL | CHOQUANT ; ThresholdEvaluator filtre toute catégorie hors type C1, C13 Test TC-86-14 : catégorie hors whitelist ignorée ; TypeScript compile-time enforcement Faible : enforcement statique
INV-86-05 Modal obligatoire si SUCCESS + isSensitive ViewerGate règle R3 : si status=SUCCESS && isSensitivegateRequired=true C16, C17 Tests TC-86-01/02/03 : modal avant tout rendu Faible
INV-86-06 Mineur : confirmation explicite additionnelle ViewerGate : requiresExplicitConfirmation = isMinor && (isSensitive \|\| isErrorStatus) ; MinorConfirmation composant dédié C16, C18 Tests TC-86-13, TC-86-27 : double confirmation Moyen : dépend du stub isMinor (PD-84)
INV-86-07 Cache sans média dérivé verdictCache stocke uniquement les champs whitelist (documentId, scores, status, etc.) ; type vérifié à la compilation C11 Test TC-86-06 : inspection cache, aucun pixel/embedding Faible : enforcement par type
INV-86-08 Cache local chiffré Persistance via expo-secure-store pour clé de chiffrement + AsyncStorage chiffré AES-256-GCM pour données C10, C11 Test TC-86-06 : données cache illisibles hors contexte app Faible : pattern existant
INV-86-09 Politique prudente sur erreur (prime sur suppressWarning) ViewerGate règle R2 (priorité > R5) : tout status erreur → modal prudent obligatoire, indépendamment de suppressWarning C16 Tests TC-86-08/09/18/19/20/26 : modal prudent sur chaque code erreur Faible
INV-86-10 Zeroization best-effort des buffers BufferZeroizer.withSafeBuffer() utilise try/finally sur toutes les allocations ; appelle SecureBuffer.destroy() existant C14 Test TC-86-16 : instrumentation mémoire, cleanup sur tous les chemins Moyen : best-effort JS, GC peut retenir copies
INV-86-11 Aucun fichier temporaire en clair Pipeline 100% in-memory ; InMemoryHandle opaque ; PDF rendu en mémoire sans écriture disque C6-C8, C14 Tests TC-86-07, TC-86-25 : inventaire FS avant/après Faible
INV-86-12 Logs whitelist stricte DetectionLogger : whitelist explicite de champs ; tout champ hors whitelist silencieusement ignoré C19 Test TC-86-15 : scan logs complet, aucun champ blacklisté Faible
INV-86-13 Invalidation cache si modelVersion change verdictCache.get() compare modelVersion ; mismatch → null → reclassification C11 Test TC-86-12 : changement version → nouveau verdict Faible
INV-86-14 enabled=false désactive classification useSensitiveDetection vérifie settings.enabled en premier ; court-circuit total C22 Test TC-86-10 : aucun verdict créé quand désactivé Faible
INV-86-15 Préférence "ne plus prévenir" réversible DocumentSensitivePreference.suppressWarning toggle dans store ; effet immédiat C10, C23 Test TC-86-11 : SUPPRESSED → WARN → modal réapparaît Faible
INV-86-16 Mineur : suppressWarning ignoré ViewerGate règle R4 : if (isMinor) → suppressWarning = false C16 Test TC-86-27 : modal maintenu malgré suppressWarning Faible
INV-86-17 Mineur : seuils plafonnés aux défauts ThresholdEvaluator.effectiveThresholds() : si isMinormin(userThreshold, defaultThreshold) par catégorie C13 Tests TC-86-28, TC-86-30 : seuil effectif = défaut si tentative relaxation Faible
INV-86-18 enabled=false → aucun modal même si verdict sensible en cache ViewerGate règle R1 (priorité maximale) : enabled=false → skip complet C16 Test TC-86-29 : rendu direct sans modal Faible
INV-86-19 Idempotence + sérialisation par clé DetectionService : mutex par (documentId, modelVersion) via Map<string, Promise> ; deuxième demande attend le résultat C15 Tests TC-86-31/32 : une seule inférence, pas de doublon Moyen : race conditions subtiles
INV-86-20 Réévaluation isSensitive depuis rawScores si seuils changent ThresholdEvaluator.reevaluate(rawScores, newThresholds) ; si rawScores absents → flag reclassification C13 Test TC-86-30 : pas de ré-inférence si rawScores disponibles Faible

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

Critère ID Mécanisme(s) Composant Observable Risque
CA-86-01 useSensitiveDetectionImageAnalyzerViewerGate R3 → ViewerGateModal C6, C16, C17, C22 Modal visible avant premier pixel du média image Faible
CA-86-02 VideoAnalyzer (12 frames, agrégation max) → ViewerGate R3 → modal C7, C12, C16, C17 Modal avant rendu vidéo ; samplingMetadata.videoFramesAnalyzed <= 12 Faible
CA-86-03 PdfAnalyzer (5 pages, déterministe) → ViewerGate R3 → modal C8, C12, C16, C17 Modal avant toute page PDF Faible
CA-86-04 NetworkGuard + architecture offline-only + aucun import réseau C21 Capture réseau complète : zéro flux sortant contenu/scores Faible
CA-86-05 Pipeline in-memory sur copie déchiffrée ; fichier source jamais touché C14, C15 Hash probatoire identique avant/après Faible
CA-86-06 verdictCache type-checked : uniquement champs whitelist C11 Inspection entrée cache : champs conformes, aucun pixel/embedding Faible
CA-86-07 Pipeline 100% in-memory + BufferZeroizer + PDF rendu mémoire C6-C8, C14 Inventaire FS : aucun fichier temporaire média post-analyse Faible
CA-86-08 DetectionService timeout race 5000ms → status TIMEOUTViewerGate R2 → modal prudent C15, C16, C17 Modal "non analysé" affiché ; statut horodaté persisté Faible
CA-86-09 OnnxClassifier catch MODEL_UNAVAILABLEViewerGate R2 → modal prudent C4, C16, C17 Modal prudent ; flux non bloqué ; pas de crash Faible
CA-86-10 useSensitiveDetection court-circuit si enabled=false C22 Aucun nouveau verdict ; reprise à réactivation Faible
CA-86-11 DocumentSensitivePreference.suppressWarning toggle + ViewerGate R4/R5 C10, C16, C23 Effet par document ; réversible ; contraintes mineur/prudence respectées Faible
CA-86-12 verdictCache.get() compare modelVersion → mismatch = invalidation C11, C15 Nouvelle classification exécutée ; verdict V2 persisté Faible
CA-86-13 BufferZeroizer.withSafeBuffer() try/finally sur tous les chemins C14 Buffers libérés/zeroized sur succès, erreur, timeout, annulation Moyen
CA-86-14 ViewerGate R4 (isMinor → suppressWarning=false) + MinorConfirmation C16, C18 Modal maintenu + double confirmation obligatoire Faible
CA-86-15 ThresholdEvaluator.effectiveThresholds() clamp mineur C13 Rejet/clamp valeurs > défaut si isMinor=true Faible
CA-86-16 ThresholdEvaluator.reevaluate(rawScores, newThresholds) C13 Pas de ré-inférence ; thresholdsVersionAtDecision mis à jour Faible
CA-86-17 DetectionService mutex par (documentId, modelVersion) + écriture idempotente C15 Une seule inférence effective ; pas de doublon persistant Moyen
CA-86-18 SamplingStrategy.pdfPages(totalPages) déterministe C12 pdfPagesAnalyzed <= 5 ; première + dernière + 3 intérieures uniformes ; stable Faible
CA-86-19 ViewerGate R1 (enabled=false → skip complet, même si verdict sensible en cache) C16 Rendu direct sans modal Faible

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

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau de test
TC-86-01 INV-86-05, CA-86-01 ImageAnalyzer + ViewerGate R3 + ViewerGateModal Modal visible avant rendu image Integration
TC-86-02 INV-86-05, CA-86-02 VideoAnalyzer (12 frames, max) + ViewerGate R3 Modal avant rendu vidéo ; 12 frames max Integration
TC-86-03 INV-86-05, CA-86-03 PdfAnalyzer (5 pages) + ViewerGate R3 Modal avant toute page ; 5 pages max Integration
TC-86-04 INV-86-01, CA-86-04 NetworkGuard + architecture offline Capture réseau : zéro flux sortant Integration/Sec
TC-86-05 INV-86-03, CA-86-05 Pipeline in-memory, fichier source non touché Hash probatoire identique Unit
TC-86-06 INV-86-07, INV-86-08, CA-86-06 verdictCache type-checked + chiffrement Champs cache conformes ; données chiffrées Unit
TC-86-07 INV-86-11, CA-86-07 Pipeline mémoire + BufferZeroizer Aucun fichier temporaire média Integration
TC-86-08 INV-86-09, CA-86-08 DetectionService timeout 5000ms + ViewerGate R2 Modal prudent "non analysé" ; statut horodaté Unit + Integration
TC-86-09 INV-86-09, CA-86-09 OnnxClassifier catch MODEL_UNAVAILABLE + ViewerGate R2 Modal prudent ; pas de crash Unit + Integration
TC-86-10 INV-86-14, CA-86-10 useSensitiveDetection court-circuit enabled=false Aucun nouveau verdict ; reprise à réactivation Unit
TC-86-11 INV-86-15, CA-86-11 Preference toggle + ViewerGate R5 Modal supprimé/réactivé par document Unit
TC-86-12 INV-86-13, CA-86-12 verdictCache.get() modelVersion check Invalidation + reclassification Unit
TC-86-13 INV-86-06 ViewerGate requiresExplicitConfirmation + MinorConfirmation Double confirmation mineur Integration
TC-86-14 INV-86-04 ThresholdEvaluator + type Category Catégories hors whitelist ignorées Unit
TC-86-15 INV-86-12 DetectionLogger whitelist/blacklist Logs conformes ; aucun champ blacklisté Unit
TC-86-16 INV-86-10, CA-86-13 BufferZeroizer.withSafeBuffer try/finally Cleanup sur succès/erreur/timeout/annulation Unit
TC-86-17 INV-86-02 useSensitiveDetection uniquement dans viewer Aucune analyse à l'import Integration
TC-86-18 INV-86-09, INV-86-01 UNSUPPORTED_FORMAT → ViewerGate R2 + NetworkGuard Modal prudent ; pas de crash ; zéro réseau Unit + Integration
TC-86-19 INV-86-09, INV-86-01 CORRUPTED_INPUT → ViewerGate R2 + NetworkGuard Modal prudent non culpabilisant ; zéro réseau Unit + Integration
TC-86-20 INV-86-09, INV-86-01 INTERNAL_ERROR → ViewerGate R2 + NetworkGuard Contenu masqué par défaut ; contrôle utilisateur Unit + Integration
TC-86-21 Contrat gate viewer ViewerGateModal KEEP_HIDDEN / CANCEL Aucun rendu contenu dans les deux cas Unit
TC-86-22 Perf warm-up EngineWarmup.start() au coffre + lazy fallback Warm-up déclenché ; fallback sans rupture Integration
TC-86-23 Perf P95 Pipeline complet sur device cible P95(durationMs) <= 700ms ; UI non bloquée Perf
TC-86-24 Perf vidéo VideoAnalyzer + SamplingStrategy 12 frames max ; pas de décodage intégral Unit + Perf
TC-86-25 INV-86-11, CA-86-18 PdfAnalyzer in-memory ; SamplingStrategy.pdfPages Aucun artefact disque ; pages conformes Integration
TC-86-26 INV-86-09, CA-86-18 PdfAnalyzer erreur + SamplingStrategy déterministe Modal prudent ; stratégie stable Unit + Integration
TC-86-27 INV-86-06, INV-86-16, CA-86-14 ViewerGate R4 (isMinor, suppressWarning ignoré) + MinorConfirmation Modal maintenu ; double confirmation Unit + Integration
TC-86-28 INV-86-17, CA-86-15 ThresholdEvaluator.effectiveThresholds clamp Rejet/clamp seuils > défauts pour mineur Unit
TC-86-29 INV-86-18, CA-86-19 ViewerGate R1 (enabled=false, verdict sensible en cache) Rendu direct sans modal Unit
TC-86-30 INV-86-20, INV-86-17, CA-86-16 ThresholdEvaluator.reevaluate depuis rawScores Pas de ré-inférence ; verdict mis à jour Unit
TC-86-31 INV-86-19, CA-86-17 DetectionService mutex (documentId, modelVersion) Une seule inférence ; deuxième attend Unit
TC-86-32 INV-86-19, CA-86-17 verdictCache écriture idempotente Pas de doublon persistant Unit
TC-86-33 Modèle de données (seuils) useSensitiveSettings validation bornes [0.50, 0.95] Rejet hors bornes ; acceptation aux bornes Unit
TC-86-34 Source isMinor Context.isMinor depuis profil authentifié serveur Patch local impossible ; garde-fous maintenus Integration/Sec

6. Gestion des erreurs

Traitements d'erreur par code

Code erreur Source Traitement dans DetectionService Traitement dans ViewerGate Persistance Message UI
MODEL_UNAVAILABLE OnnxClassifier : modèle absent du bundle ou corruption Catch → verdict partiel { status, analyzedAt } → release mutex R2 : modal prudent obligatoire { status: MODEL_UNAVAILABLE, analyzedAt } chiffré "Le module d'analyse n'est pas disponible. Ce contenu pourrait être sensible."
CORRUPTED_INPUT Analyzers : buffer invalide, décodage échoué Catch → verdict partiel → release mutex R2 : modal prudent { status: CORRUPTED_INPUT, analyzedAt } "Ce document semble endommagé. Par précaution, un avertissement est affiché."
UNSUPPORTED_FORMAT DetectionService : documentType non reconnu Catch → verdict partiel → release mutex R2 : modal prudent { status: UNSUPPORTED_FORMAT, analyzedAt } "Ce format n'est pas pris en charge par l'analyse de contenu."
TIMEOUT DetectionService : Promise.race dépasse 5000ms Abort signal → BufferZeroizer finally → verdict partiel R2 : modal prudent { status: TIMEOUT, analyzedAt, durationMs } "L'analyse n'a pas pu être terminée à temps. Ce contenu pourrait être sensible."
OUT_OF_MEMORY DetectionService : handle > 64MB ou allocation échouée Catch → BufferZeroizer finally → verdict partiel R2 : modal prudent { status: OUT_OF_MEMORY, analyzedAt } "Ce document est trop volumineux pour être analysé. Par précaution, un avertissement est affiché."
INTERNAL_ERROR Catch-all dans DetectionService Catch → BufferZeroizer finally → verdict partiel → log errorCode R2 : modal prudent, contenu masqué par défaut { status: INTERNAL_ERROR, analyzedAt } "Une erreur inattendue est survenue. Par précaution, le contenu est masqué."

Règles transverses erreur

  1. Aucun crash : tous les chemins d'erreur sont encapsulés dans try/catch au niveau DetectionService
  2. Zeroization garantie : BufferZeroizer.withSafeBuffer() nettoie en finally, indépendamment de l'erreur
  3. Politique prudente prime : ViewerGate R2 prioritaire sur R5 (suppressWarning)
  4. Contrôle utilisateur conservé : boutons "Afficher quand même" / "Garder masqué" toujours présents
  5. Zéro réseau : aucune erreur ne déclenche d'envoi de contenu ou de télémétrie externe
  6. Log whitelist : seuls eventName, documentId, analysisStatus, durationMs, errorCode sont loggés
  7. Mineur + erreur : ECT-v2-02 adressé — requiresExplicitConfirmation = isMinor && (isSensitive || isErrorStatus) — confirmation additionnelle aussi sur modal prudent pour mineur

7. Impacts sécurité

Architecture zero-knowledge

Risque Mitigation Composant Observable
Fuite de contenu via réseau Architecture offline-only ; aucun import réseau dans le module ; NetworkGuard en dev/test C21 Capture réseau complète : zéro flux
Contenu en clair sur disque Pipeline 100% in-memory ; InMemoryHandle non sérialisable ; PDF rendu mémoire C6-C8, C14 Inventaire FS : aucun fichier temporaire
Contenu résiduel en mémoire BufferZeroizer try/finally ; SecureBuffer.destroy() existant C14 Instrumentation mémoire : cleanup confirmé
Cache reconstructible Stockage uniquement documentId, scores numériques, metadata — aucun pixel/embedding C11 Inspection cache : champs conformes
Cache non chiffré Chiffrement AES-256-GCM via pattern existant AsyncStorage chiffré C10, C11 Tentative lecture directe : données illisibles
isMinor falsifié Source : profil authentifié serveur (B2C-MINEURS) ; aucun setter local Architecture Tentative patch local : valeur serveur conservée
Logs avec données sensibles DetectionLogger whitelist stricte ; champs hors liste silencieusement ignorés C19 Scan logs : aucun élément blacklisté
App Switcher snapshot iOSPrivacyGuard : overlay de redaction au passage en background quand modal actif C20 Screenshot en background : contenu masqué
Spotlight indexing Exclusion des métadonnées de classification de l'indexation Spotlight C20 Recherche Spotlight : aucun résultat de classification
QuickLook preview Désactivation/contrôle des aperçus QuickLook pour fichiers analysés C20 Preview QuickLook : non disponible pour fichiers du coffre

Politique TTL/purge (ECT-v2-05)

Pour répondre à l'écart Gate 3, le cache de verdicts inclut une politique de purge :

  • TTL par défaut : 90 jours après analyzedAt (configurable)
  • Purge au déverrouillage : vérification TTL des verdicts au déverrouillage du coffre
  • Purge complète : lors d'un clearAllOnLogout() (pattern existant dans cacheCleaner.ts)
  • Pas de purge en arrière-plan : uniquement à l'ouverture active du coffre
  • Impact : document avec verdict purgé → reclassification à la prochaine consultation

Journalisation

Conformément à INV-86-12, la politique de logging est stricte :

Whitelist autorisée : eventName, documentId, analysisStatus, durationMs, modelVersion, detectedCategories, thresholdsVersion, errorCode

Blacklist interdite : bytes média, chemins fichiers temporaires, miniatures, dimensions exactes corrélables, EXIF, noms de fichiers source, dump InMemoryHandle, rawScores (scores bruts = data sensible potentielle)


8. Hypothèses techniques

ID Hypothèse Impact si faux
H-01 ONNX Runtime Mobile est disponible pour React Native via un native module existant ou développable Choix alternatif : CoreML via un bridge natif Swift → React Native ; replanification du composant C4
H-02 Le modèle de classification (ex: MobileNet NSFW ou NudeNet) produit des scores par catégorie compatibles avec les 4 catégories whitelistées Mapping de catégories nécessaire ; couche d'adaptation dans OnnxClassifier
H-03 La taille du modèle est compatible avec la distribution mobile (< 50 MB compressé) Modèle quantifié requis (INT8) ; impact potentiel sur précision
H-04 L'extraction de frames vidéo in-memory est possible via un module React Native natif sans écriture disque Alternative : FFmpeg compilé statique avec output en mémoire ; complexité accrue
H-05 Le rendu de pages PDF en mémoire est possible via PDFKit (iOS natif) ou react-native-pdf sans écriture temporaire Alternative : implémentation native Swift avec bridge ; replanification C8
H-06 Le flag isMinor sera exposé dans la réponse API profil avant l'intégration finale Stub maintenu avec false par défaut ; fonctionnalité mineur testable uniquement avec mock
H-07 expo-secure-store permet de stocker la clé de chiffrement du cache de verdicts (< 2048 bytes) Alternative : dérivation HKDF depuis K_master existant
H-08 Le warm-up modèle est réalisable en < 2s sur iPhone 12 (A14) Lazy fallback seul ; warm-up supprimé si trop lent
H-09 L'agrégation max(score) sur 12 frames vidéo respecte le budget P95 700ms Réduction du nombre de frames ou downscale plus agressif
H-10 React Native permet l'interception fiable de l'App Switcher pour la redaction (via AppState + native module) Implémentation native Swift dans AppDelegate.swift

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

Risques techniques

Risque Probabilité Impact Mitigation
Performance ML sur device bas de gamme (iPhone 12 = baseline) Moyenne Élevé — dépassement budget 700ms Benchmark obligatoire pré-production (H-03) ; quantification INT8 ; downscale agressif
Zeroization JS best-effort (GC retient copies) Certaine Faible — limitation connue du runtime Documenté dans code ; migration Rust à terme (dette technique identifiée)
Race condition sur mutex in-memory (redémarrage app mid-analysis) Faible Faible — mutex perdu = reclassification au prochain accès Timeout sur mutex (5s aligné sur timeout inférence) ; cleanup au démarrage
Dérive horloge LocalTimestamp (ECT-v2-07) Faible Faible — TTL cache peut être incorrect de quelques secondes Tolérance ±60s sur comparaison TTL ; pas de dépendance critique à la précision horloge
Modèle absent du bundle en production (corrupted download) Faible Élevé — MODEL_UNAVAILABLE pour tous les utilisateurs Checksum du modèle au démarrage ; fallback gracieux (modal prudent)
PDF rendering OOM sur documents volumineux Moyenne Moyen — crash viewer Guard InMemoryHandle 64MB ; catch OUT_OF_MEMORY explicite ; downscale pages
Bridge natif React Native pour ONNX/PDF/vidéo Moyenne Élevé — complexité d'intégration Prototypage early (Sprint 0) ; modules natifs isolés testables indépendamment

Écarts Gate 3 adressés

Écart Adressé par Section du plan
ECT-v2-01 (précédence enabled=false vs prudence) ViewerGate R1 prioritaire ; enabled=false = skip complet y compris pour erreurs persistées en cache ; cas documenté dans flux §2.1 et tests unitaires dédiés §2.1, §3 (INV-86-18), §5 (TC-86-29)
ECT-v2-02 (requiresExplicitConfirmation exclut erreur mineur) Formule étendue : requiresExplicitConfirmation = isMinor && (isSensitive \|\| isErrorStatus) §6 (règle transverse #7), §3 (INV-86-06)
ECT-v2-03 (tests réseau plus stricts que spec) Plan aligne sur INV-86-01 = contenu en clair ; tests vérifient l'absence de contenu visuel ET scores (approche conservatrice retenue car coût marginal) §3 (INV-86-01), §5 (TC-86-04)
ECT-v2-04 (SEC-86-07 sans tests pas-à-pas) Tests unitaires individuels pour App Switcher (overlay redaction), Spotlight (exclusion metadata), QuickLook (désactivation) dans iOSPrivacyGuard §7, composant C20
ECT-v2-05 (pas de politique TTL/purge) Politique TTL 90 jours + purge au déverrouillage §7 (Politique TTL/purge)
ECT-v2-06 (zeroization best-effort) Documenté comme limitation runtime JS ; try/finally sur tous les chemins §3 (INV-86-10), §9
ECT-v2-07 (LocalTimestamp dérive) Tolérance ±60s sur TTL ; pas de dépendance critique §9
ECT-v2-08 (rawScores absents chemin négatif) ThresholdEvaluator.reevaluate() : si rawScores absents → flag reclassification §2.5, §3 (INV-86-20)
ECT-v2-09 (PDF cas limites totalPages ~ 5) SamplingStrategy.pdfPages() : algorithme explicite avec déduplication et complétion §2.3, composant C12
ECT-v2-10 (InMemoryHandle 64MB non testé) Guard explicite dans DetectionService : if (buffer.length > MAX_HANDLE_SIZE) → OUT_OF_MEMORY §6 (OUT_OF_MEMORY)
ECT-v2-11 (benchmark modèle absent du plan de test) H-01/H-03/H-09 documentent la nécessité ; benchmark obligatoire avant Gate 8 §8 (H-01, H-03, H-09)

Dette technique identifiée

  1. Zeroization JS : SecureBuffer.destroy() est best-effort en JavaScript (GC non déterministe). Migration vers module natif Rust (via react-native-rust-bridge) identifiée comme amélioration future.
  2. Stub isMinor : Dépend de PD-84 (B2C-MINEURS backend). Stub retourne false. TODO tracé.
  3. Modèle ML concret : Choix reporté après benchmark. Interface ContentClassifier abstraite permet le swap sans impact architecture.

10. Hors périmètre

Conformément à la section 2.2 de la spécification, sont explicitement exclus :

  • Modération automatique (suppression, blocage, signalement)
  • Qualification légale/morale du contenu
  • Analyse côté serveur / API externe
  • Analyse à l'upload
  • Contrôle parental externe (parental gate côté parent)
  • Classification de texte (OCR/NLP)
  • Persistance serveur des verdicts
  • Désactivation par catégorie individuelle (scope PD-86 = activation globale uniquement)
  • Pipeline de conversion/entraînement du modèle (hors périmètre app)
  • Choix définitif du modèle ML (décision d'implémentation, benchmark requis avant production)

11. Contraintes techniques

Dépendances inter-PD

Story Statut Nature de la dépendance
PD-97 (crypto zero-knowledge) DONE SecureBuffer, zeroization, K_master → réutilisé pour chiffrement cache
PD-248 (screenshot protection) DONE expo-screen-capture, pattern screenshotProtection.ts → étendu pour écrans de gate
PD-174 (auto-lock) DONE useAutoLock, cacheCleaner → purge verdicts intégrée au cleanup
PD-84 (B2C-MINEURS offre) TODO Flag isMinor dans profil API → STUB acceptable (false par défaut)
PD-98 (Keychain K_master) DONE Clé de dérivation pour chiffrement cache verdicts

Framework de test

  • Runner : Jest (existant dans le projet Expo)
  • Tests unitaires : Jest + mocks pour ContentClassifier, stores Zustand, ViewerGate
  • Tests d'intégration : React Native Testing Library pour composants UI (modal, confirmation)
  • Tests de sécurité : Jest + instrumentation mémoire (jest.spyOn sur SecureBuffer.destroy)
  • Tests de performance : Sur device réel (iPhone 12, A14, iOS 16+) — hors Jest
  • CI : Variables CI=true ; mock du modèle ML en CI (pas de ONNX Runtime en CI standard)

Compatibilité ESM/CJS

  • Expo/React Native : Metro bundler, ESM natif
  • Dépendances ESM-only identifiées : aucune nouvelle (ONNX Runtime React Native est un native module, pas un package JS pur)
  • ONNX Runtime : Module natif C++ avec bridge React Native — pas de conflit ESM/CJS

12. Checklist INV/CA (pré-Gate 5)

Mapping complet INV → Tâches → Tests

Invariant/CA Tâche couvrant Test couvrant
INV-86-01 C21 NetworkGuard + architecture offline TC-86-04, TC-86-18, TC-86-19, TC-86-20
INV-86-02 C22 useSensitiveDetection (viewer only) TC-86-17
INV-86-03 C14 BufferZeroizer (copie mémoire) TC-86-05
INV-86-04 C1 types.ts + C13 ThresholdEvaluator TC-86-14
INV-86-05 C16 ViewerGate R3 + C17 modal TC-86-01, TC-86-02, TC-86-03
INV-86-06 C16 ViewerGate R4 + C18 MinorConfirmation TC-86-13, TC-86-27
INV-86-07 C11 verdictCache (type-checked) TC-86-06
INV-86-08 C10/C11 chiffrement AES-256-GCM TC-86-06
INV-86-09 C16 ViewerGate R2 (erreurs → prudent) TC-86-08, TC-86-09, TC-86-18, TC-86-19, TC-86-20, TC-86-26
INV-86-10 C14 BufferZeroizer try/finally TC-86-16
INV-86-11 C6-C8 pipeline mémoire + C14 TC-86-07, TC-86-25
INV-86-12 C19 DetectionLogger whitelist TC-86-15
INV-86-13 C11 verdictCache modelVersion check TC-86-12
INV-86-14 C22 court-circuit enabled=false TC-86-10
INV-86-15 C10/C23 preference toggle TC-86-11
INV-86-16 C16 ViewerGate R4 (isMinor → suppress=false) TC-86-27
INV-86-17 C13 ThresholdEvaluator clamp mineur TC-86-28, TC-86-30
INV-86-18 C16 ViewerGate R1 (enabled=false → skip) TC-86-29
INV-86-19 C15 DetectionService mutex + idempotence TC-86-31, TC-86-32
INV-86-20 C13 ThresholdEvaluator.reevaluate TC-86-30
CA-86-01 C6 ImageAnalyzer + C16 + C17 TC-86-01
CA-86-02 C7 VideoAnalyzer + C16 + C17 TC-86-02
CA-86-03 C8 PdfAnalyzer + C16 + C17 TC-86-03
CA-86-04 C21 NetworkGuard TC-86-04
CA-86-05 C14 pipeline in-memory TC-86-05
CA-86-06 C11 verdictCache whitelist TC-86-06
CA-86-07 C14 BufferZeroizer + pipeline mémoire TC-86-07
CA-86-08 C15 timeout 5000ms + C16 R2 TC-86-08
CA-86-09 C4 MODEL_UNAVAILABLE + C16 R2 TC-86-09
CA-86-10 C22 enabled=false court-circuit TC-86-10
CA-86-11 C10/C23 preference + C16 R4/R5 TC-86-11
CA-86-12 C11 modelVersion invalidation TC-86-12
CA-86-13 C14 BufferZeroizer try/finally TC-86-16
CA-86-14 C16 R4 + C18 MinorConfirmation TC-86-27
CA-86-15 C13 clamp mineur TC-86-28
CA-86-16 C13 reevaluate rawScores TC-86-30
CA-86-17 C15 mutex + idempotence TC-86-31, TC-86-32
CA-86-18 C12 SamplingStrategy.pdfPages TC-86-03, TC-86-25, TC-86-26
CA-86-19 C16 R1 enabled=false TC-86-29

Vérification : 20/20 INV couverts. 19/19 CA couverts. Aucune ligne vide. Checklist complète.