Aller au contenu

PD-262 — Rapport de confrontation (Étape 5)

Ce rapport est produit par l'orchestrateur Claude avant la gate PMO 5 (AMBIGUITY). Il confronte les documents produits pour identifier convergences, divergences et zones d'ombre.

1. Sources confrontées

Document Étape Identifiant
Spécification 1 PD-262-specification.md
Tests contractuels 2 PD-262-tests.md
Plan d'implémentation 4 PD-262-plan.md
Code Contracts 4 PD-262-code-contracts.yaml

2. Convergences

CON-01 — Modèle d'état à 3 états : Les 4 documents s'accordent sur MONITORED → TAMPERED_SESSION → LOCKED_PERSISTENT avec interdiction de retour. La machine à états est identique dans la spec (§3.2, §5), les tests (TC-NOM-11, TC-INV-08, TC-NR-01), le plan (§1 C1 StateMachine, §2 FT3) et les code contracts (CC-262-T1 invariants).

CON-02 — Fail-closed systématique : INV-262-01 est implémenté de manière cohérente : détection positive, erreur détecteur, timeout budget, corruption données, absence lockout (hors premier lancement) → tous mènent à tampered=true / LOCKED_PERSISTENT. Couvert dans spec (§4, §6), tests (TC-NOM-09, TC-ERR-02, TC-ERR-08, TC-ERR-09), plan (§3 mapping INV-01), contracts (CC-262-T1, CC-262-T3).

CON-03 — Persistance Keychain exclusive : Unanimité sur l'utilisation de kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly pour le lockout_persistent_flag. Interdit en UserDefaults/fichier. Spec (§3.6), tests (TC-NOM-08, CA-16), plan (C1 LockoutPersistence), contracts (CC-262-T1 forbidden).

CON-04 — Purge sélective avec frontière explicite : Les 4 documents distinguent caches crypto (purgés) vs blobs persistants chiffrés (conservés). Spec (§3.1 définitions, INV-262-05), tests (TC-NOM-12, CA-15), plan (C4 tamperingPurge), contracts (CC-262-T4 invariants + forbidden).

CON-05 — Retry purge borné : Max 3 tentatives, intervalle 1s, abandon avec log et lockout maintenu. Spec (§3.4, F3 étape 7, ERR-05), tests (TC-ERR-05, TC-ERR-11, CA-19), plan (C4), contracts (CC-262-T4).

CON-06 — Gating environnement compile-time : Flag ANTI_TAMPERING_QA_ENABLED est compile-time uniquement, non mutable runtime. Spec (INV-262-07), tests (TC-NOM-06, TC-ERR-06), plan (C1, C3, C10), contracts (CC-262-T1, CC-262-T9).

CON-07 — Audit best effort non bloquant : Payload allowlist stricte §3.3, fire-and-forget, échec réseau sans impact sur lockout/purge. Spec (INV-262-06, INV-262-11), tests (TC-NOM-05, TC-NOM-07, TC-ERR-04), plan (C5, FT4), contracts (CC-262-T5).

CON-08 — Reconstruction boot fail-closed : Absence/corruption lockout + absence first_launch_clean + app initialisée = LOCKED_PERSISTENT. Cohérent dans spec (§3.6, F6, ERR-08/09/10), tests (TC-ERR-08/09/10, CA-17/18), plan (FT6, §6 ERR-262-008/009/010), contracts (CC-262-T1).

CON-09 — Native authority : Décision lockout prise côté Swift avant retour Promise JS. JS ne peut ni inhiber ni retarder. Spec (INV-262-02), tests (TC-INV-02, TC-NEG-03), plan (C1, §7.1), contracts (CC-262-T1 invariant + forbidden).

CON-10 — Couverture INV/CA complète : Les 12 invariants et 19 critères d'acceptation sont tous mappés dans le plan (§3, §4) et les code contracts (validation section). Chaque TC-* est assigné à au moins un composant.

3. Divergences

⚠️ Les conflits ne doivent JAMAIS être lissés. Chaque divergence est rendue visible.

DIV-01 — Transition directe MONITORED → LOCKED_PERSISTENT dans le plan - Spec (§5, machine à états checklist) : MONITORED → LOCKED_PERSISTENT : INTERDITE (doit passer par purge/lockout séquence). - Plan (§2 FT1, FT5, FT6) : Au cold start, si lockout_persistent_flag=true → lockout immédiat. Si absence lockout + absence first_launch_clean → LOCKED_PERSISTENT. Ces flux ne passent pas explicitement par TAMPERED_SESSION. - Impact : La spec interdit formellement MONITORED → LOCKED_PERSISTENT directe. Or au boot, l'état reconstruit est LOCKED_PERSISTENT sans passer par TAMPERED_SESSION. Le plan traite cela comme une reconstruction d'état (pas une transition), mais la spec ne formalise pas cette distinction entre "transition" et "reconstruction". Risque d'écart d'interprétation dans l'implémentation de la state machine.

DIV-02 — device_id_pseudo : calcul côté natif vs TypeScript - Spec (§3.3) : device_id_pseudo = SHA-256(UIDevice.current.identifierForVendor.uuidString UTF-8), hex lowercase 64 chars. - Plan (C5 tamperingAudit.ts, §2 FT4) : Le calcul du device_id_pseudo est décrit dans le composant TypeScript C5 (SHA-256(identifierForVendor)). - Contracts (CC-262-T5) : device_id_pseudo = SHA-256(UIDevice.identifierForVendor.uuidString UTF-8) hex lowercase 64 chars listé dans les invariants de T5 (service TypeScript). - Contracts (CC-262-T1) : Le module natif T1 n'expose PAS identifierForVendor ni le hash — il expose uniquement performCheck(), getLockoutState(), isFirstLaunchClean(). - Impact : Le identifierForVendor est une API UIKit native. Le plan prévoit que C5 (TypeScript) calcule le hash, mais UIDevice.current.identifierForVendor n'est accessible que depuis le natif iOS. Soit le module natif T1 doit exposer une méthode supplémentaire pour obtenir cette valeur, soit le bridge T2 doit l'inclure. Le plan et les contracts ne prévoient aucun mécanisme pour remonter identifierForVendor du natif vers le TypeScript.

DIV-03 — Endpoint backend audit : STUB non tracé vers PD cible - Spec (§10.2 Q-03) : Politique de retry backend non définie — question ouverte. - Plan (§9.3) : STUB: PD-futur (endpoint audit backend) — story de destination marquée "à créer dans le backlog backend". - Contracts (CC-262-T5) : Aucune mention du stub. - CLAUDE.md (règle procédurale) : "Chaque stub inter-PD dans le code DOIT être documenté avec la story de destination exacte (ex: // STUB: PD-37). Les stubs sans story traçable sont signalés MAJEUR en Gate 8." - Impact : Le stub n'a pas de PD cible identifié. Selon les règles de gouvernance, ce sera signalé MAJEUR en Gate 8 s'il n'est pas résolu avant.

DIV-04 — Budget cold start : spec vs plan (ambiguïté budget vs timeout) - Spec (§3.4) : Délai max démarrage contrôle cold start (budget) = 1500ms P95. La colonne "Hors bornes" indique fail-closed si dépassement. - Plan (§6 ERR-262-002) : Timeout budget contrôle dépassé (>1500ms P95 cold, >1s lockout) — mélange deux budgets distincts dans un seul code erreur. - Tests (TC-ERR-02) : Teste uniquement le dépassement budget cold start >1500ms. Le dépassement du budget lockout <=1s (CA-04) n'a pas de TC-ERR dédié. - Impact : Le plan fusionne deux budgets temporels sous un même code erreur (ERR-262-002). Pas d'ambiguïté fonctionnelle, mais le diagnostic sera plus difficile si les deux budgets sont confondus. Mineur.

DIV-05 — Tests adversariaux TC-INV-02 et TC-NEG-03 : niveau de test - Tests (§5.1, §7) : TC-INV-02 et TC-NEG-03 testent l'impossibilité pour JS de neutraliser le lockout natif. - Plan (§5, §11) : TC-INV-02 et TC-NEG-03 sont classés Sec (adversarial). Le plan liste Détox sur device réel pour E2E mais ne détaille pas comment simuler un hook JS adversarial en test unitaire avec un mock natif. - Contracts (CC-262-T10) : Exige Tests adversariaux TC-INV-02, TC-NEG-03 présents mais le mock natif (src/__mocks__/TamperingDetectorModule.ts) ne peut pas simuler une tentative de bypass JS du module natif puisque c'est précisément le mock qui remplace le natif. - Impact : Ces tests ne peuvent être exécutés de manière significative qu'en intégration sur device réel. En unitaire avec mock, ils testent le mock, pas la résistance native. Le plan ne clarifie pas cette limitation.

DIV-06 — Composant C2 bridge : requireNativeModule vs NativeModules - Contracts (CC-262-T2 forbidden) : "Utiliser NativeModules directement (utiliser requireNativeModule Expo pattern)". - Plan (§1 C2) : Ne spécifie pas quel pattern d'import utiliser — décrit uniquement les exports TypeScript. - Impact : Si le développeur utilise NativeModules au lieu de requireNativeModule, le contract T2 est violé. Le plan devrait préciser le pattern d'import. Mineur — la contrainte est dans le contract, le plan est moins prescriptif.

DIV-07 — Nombre de tâches : plan vs contracts - Contracts (header) : total_tasks: 10 — 10 tâches T1-T10. - Plan (§1) : 10 composants C1-C10 — correspondance 1:1. - Contracts (CC-262-T10 depends_on) : [T1, T2, T3, T4, T5, T6, T7, T8, T9] — dépend de toutes les tâches. - Plan (§11 périmètre de test) : Couverture minimale 80% s'applique au périmètre in scope. Pas de mention de 10 tâches explicitement. - Impact : Aucun — la correspondance est effective. Convergence confirmée.

4. Zones d'ombre

ZO-01 — Accès identifierForVendor depuis TypeScript : Ni le plan ni les contracts ne prévoient comment le service TypeScript C5 (tamperingAudit.ts) obtiendra UIDevice.current.identifierForVendor. Le module natif T1 n'expose pas cette donnée. Le bridge T2 ne la prévoit pas non plus. Cette donnée est nécessaire pour construire le device_id_pseudo du payload audit (§3.3). Connexe à DIV-02.

ZO-02 — Comportement si identifierForVendor retourne nil : La spec (H-01, §3.3) indique Si invalide → rejet événement. Le plan (HT-02) mentionne "si indisponible, device_id_pseudo vide et événement rejeté". Mais aucun test ne couvre ce cas (identifierForVendor = nil). Manque un TC-ERR dédié.

ZO-03 — Détox sur device réel : infrastructure CI : Le plan (§11) prévoit des tests E2E Détox sur device réel. Aucun document ne mentionne si l'infrastructure CI dispose de devices iOS réels ou de simulateurs farm. Les tests de performance (N>=100 exécutions, P95) nécessitent un device physique. La faisabilité CI n'est pas documentée.

ZO-04 — Concurrence cold start vs persistance lockout : Le plan (FT1) montre que initialize() vérifie d'abord getLockoutState() puis isFirstLaunchClean() puis performCheck(). Si ces appels sont asynchrones et que l'app commence à charger l'UI entre-temps, il y a une fenêtre où l'app pourrait brièvement afficher du contenu avant le lockout. Le hook C7 (useAntiTampering) utilise un guard navigation, mais le timing entre mount du hook et résolution des Promises natives n'est pas contractualisé.

ZO-05 — Cibles de purge exhaustives : La spec (Q-05) identifie explicitement ce point comme non résolu. Le plan (C4, §9.2) le reconnaît comme dette. Les tests (§9 règles non testables) le signalent comme MAJEUR. Mais aucun mécanisme de versionnement de la liste de cibles n'est prévu dans le plan ou les contracts. Risque de couverture incomplète à l'implémentation.

ZO-06 — Politique retry backend : La spec (Q-03) marque la politique de retry backend comme non définie. Le plan (§9.2, C5) implémente un fire-and-forget simple (1 tentative, pas de retry). Les contracts (CC-262-T5 forbidden) interdisent le retry bloquant mais n'imposent pas de retry non-bloquant. Le choix de 0 retry est implicite, pas contractualisé.

ZO-07 — Stockage causes secondaires en audit local : La spec (Q-04) ne contractualise pas le format des causes secondaires. Le plan (C5, §9.2) les stocke en log local "sans format garanti". Les contracts (CC-262-T5 outputs) prévoient secondaryReasons?: TamperingReasonCode[] dans l'interface mais pas de format de stockage local. Les tests (TC-ERR-07) vérifient l'unicité du lockout et la cause primaire mais pas le stockage effectif des secondaires.

ZO-08 — first_launch_clean : mécanisme d'initialisation : La spec (§3.1) définit le marqueur mais ne précise pas quel composant l'écrit ni à quel moment. Le plan (C1 LockoutPersistence) mentionne "gestion first_launch_clean" mais ne détaille pas l'écriture initiale. Les contracts (CC-262-T1) exposent isFirstLaunchClean() en lecture mais pas de méthode d'écriture. Question : qui écrit first_launch_clean=true lors de la première installation propre ?

ZO-09 — Interaction avec le processus de mise à jour de l'app : Aucun document ne traite le cas d'une mise à jour de l'app (App Store update). Le Keychain persiste entre les mises à jour — un lockout_persistent_flag=true resterait actif après une mise à jour. C'est probablement le comportement souhaité, mais ce n'est documenté dans aucune source.

ZO-10 — stopPeriodicCheck() : usage et contexte : Les contracts (CC-262-T3 outputs) exportent stopPeriodicCheck(): void. Ni la spec ni le plan ne mentionnent de cas d'usage pour arrêter le timer périodique. Si le timer est arrêté, INV-262-03 (trigger-coverage) pourrait être violé. Aucun test ne couvre ce cas.

5. Recommandation

  • Procéder — convergence confirmée, aucun conflit bloquant
  • Rework nécessaire — divergences à résoudre avant de continuer
  • Escalade — décision humaine requise sur un point structurant

Justification :

Les 4 documents sont globalement bien alignés (10 convergences majeures confirmées). Cependant, DIV-02/ZO-01 (accès identifierForVendor depuis TypeScript) est un blocage d'implémentation : le plan ne prévoit aucun mécanisme pour remonter cette donnée native vers le service TypeScript qui construit le payload audit. Sans correction, l'agent développeur ne pourra pas implémenter CC-262-T5 correctement.

Actions requises avant soumission Gate 5 :

  1. DIV-02/ZO-01 (BLOQUANT) : Ajouter dans le module natif T1 (ou le bridge T2) une méthode pour exposer le device_id_pseudo pré-calculé côté Swift, ou documenter explicitement que le calcul SHA-256 est fait côté natif et le résultat transmis via le bridge.

  2. DIV-03 (MAJEUR Gate 8) : Identifier la story PD cible pour le stub endpoint audit backend, ou documenter explicitement l'acceptation du risque MAJEUR en Gate 8.

  3. ZO-08 (à clarifier) : Préciser dans le plan quel composant écrit first_launch_clean=true et à quel moment du cycle de vie.

Les autres divergences et zones d'ombre sont mineures ou documentées comme dette acceptée (Q-03, Q-04, Q-05).