PD-86 — Plan d'implémentation
- 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.ts → getProfile(), 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 && isSensitive → gateRequired=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 isMinor → min(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 | useSensitiveDetection → ImageAnalyzer → ViewerGate 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 TIMEOUT → ViewerGate R2 → modal prudent | C15, C16, C17 | Modal "non analysé" affiché ; statut horodaté persisté | Faible |
| CA-86-09 | OnnxClassifier catch MODEL_UNAVAILABLE → ViewerGate 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
- Aucun crash : tous les chemins d'erreur sont encapsulés dans try/catch au niveau
DetectionService - Zeroization garantie :
BufferZeroizer.withSafeBuffer() nettoie en finally, indépendamment de l'erreur - Politique prudente prime :
ViewerGate R2 prioritaire sur R5 (suppressWarning) - Contrôle utilisateur conservé : boutons "Afficher quand même" / "Garder masqué" toujours présents
- Zéro réseau : aucune erreur ne déclenche d'envoi de contenu ou de télémétrie externe
- Log whitelist : seuls
eventName, documentId, analysisStatus, durationMs, errorCode sont loggés - 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
- 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. - Stub
isMinor : Dépend de PD-84 (B2C-MINEURS backend). Stub retourne false. TODO tracé. - 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.