Aller au contenu

PD-287 — Rapport de confrontation (Étape 5 — Gate 5 Plan)

Ce rapport est produit par l'orchestrateur Claude avant la Gate 5 PMO. Il confronte la spécification, les tests, le plan d'implémentation et les code contracts pour identifier convergences, divergences et zones d'ombre.

1. Sources confrontées

  • PD-287-specification.md (étape 1, post Gate 3 GO) — 25 invariants, 35 données contractuelles, 7 flux nominaux, FSM à 5 états, 20 cas d'erreur, 31 critères d'acceptation, hypothèses H-287-01..08, points Q-287-01..07.
  • PD-287-tests.md (étape 2, post Gate 3 GO) — matrice de couverture INV/CA, 19 tests nominaux, 20 tests d'erreur, 14 tests d'invariants, 5 non-régression, 14 adversariaux.
  • PD-287-plan.md (étape 4) — 14 composants (C1..C14), 4 waves, 28 points de vigilance, ADR-287-01/02/03, hypothèses H-plan-01..09.
  • PD-287-code-contracts.yaml (étape 4) — 11 modules, interfaces, invariants, forbidden patterns, dependency graph, cross_module_protections, feature flags, constants.

2. Convergences

  • FSM : les 5 états PENDING_ACTIVATION / ACTIVE / OTP_BLOCKED / REVOKED / EXPIRED et les transitions listées §5.5 spec sont reprises intégralement par le plan (ShareStateMachine C1) et par les contracts (invariant INV-287-06/07 module sharing-api). Trigger PG prevent_forbidden_state_transition ajouté par le plan en défense en profondeur ; pas de contradiction.
  • Terminalité REVOKED/EXPIRED : spec INV-287-10, tests TC-INV-02 + TC-NEG-12, plan §3 mapping + trigger PG. Couverture cohérente.
  • Anti-enumeration 404 générique destinataire : spec INV-287-12, tests TC-INV-06 + TC-ERR-05 + TC-NEG-02, plan GenericErrorMapper C1 + jitter latence, contracts module sharing-api (forbidden "différenciation de message"). Convergents sur le principe.
  • Audit append-only + hash-chain + ancrage Merkle : spec INV-287-13, tests TC-NOM-14 + TC-ERR-14 + TC-INV-14, plan C5 + C6 + ADR-287-03 (mono-instance via pg_advisory_lock), contracts module audit-journal (forbidden DELETE/UPDATE + anti-catch-absorb). Pipeline bout-en-bout cohérent.
  • Révocation non rétroactive : spec INV-287-23, tests TC-NOM-14, plan §3 (immuabilité DB via trigger), contracts module audit-journal. Convergent.
  • Lock + idempotence + rate-limit + réconciliation + clearing : spec INV-287-20, tests TC-NOM-05/06/15/16/18 + TC-ERR-04/08/12/13/19/20 + TC-NEG-01/09/10/13, plan §2.1 + C7 + C1, contracts module sharing-api. Couverture complète.
  • Atomicité DB + audit synchrone bloquant : spec INV-287-21, tests TC-INV-04/05 + TC-ERR-14, plan §2.1 (audit dans la même transaction SQL) + §3, contracts module audit-journal (forbidden "append asynchrone post-response"). Convergent sur fail-closed.
  • Trust-store obligatoire fail-closed au bootstrap : spec INV-287-17, tests TC-INV-07 + TC-ERR-16 + TC-NEG-08, plan C9 + point de vigilance #25, contracts module trust-store (forbidden "insecure"/"trust-all"/fallback CA system). Convergent.
  • Secrets temporaires crypto chiffrés au repos : spec INV-287-16, tests TC-INV-08 + TC-NEG-06, plan C4 + KMS envelope, contracts module crypto-pre (forbidden DEK en clair, log envelope). Convergent.
  • PRE obligatoire, pas de fallback KMS direct : spec INV-287-02, tests TC-NOM-07 + TC-ERR-15 + TC-INV-09, plan §2.3 + ESLint rule custom, contracts module crypto-pre (forbidden "fallback non-PRE"). Convergent.
  • Identité éphémère unique par lien : spec INV-287-05, tests TC-NOM-12, plan §2.1 étape 7 + contrainte UNIQUE DB, contracts module crypto-pre. Convergent.
  • Purge proactive artefacts temporaires : spec INV-287-19, tests TC-NOM-17 + TC-NEG-04, plan C8 (RetentionWorker.purgeOnBoot() + hook onTerminal()), contracts module retention-worker. Convergent.
  • Rétention RGPD J+90 post-terminal + audit conservé : spec INV-287-15 + §5.11, tests TC-NOM-19 + TC-INV-12, plan C8, contracts module retention-worker (invariant journal conservé). Convergent.
  • Seuil OTP cumulé 50 → REVOKED + notification : spec CA-287-31 + INV-287-20 + ERR-287-20, tests TC-NOM-18 + TC-ERR-20, plan §2.2 + C2 + C11, contracts modules otp + notifications. Convergent.
  • Feature flag export_zip_enabled default false, 404 générique si désactivé : spec Q-287-06 + F-287-04, tests TC-NOM-08 sous condition, plan C10 + point #20, contracts feature_flags + module export-zip. Convergent.
  • Guard cross-module ShareProofGuard scope ProofsModule : spec §5.8, plan C12 + §2ter, contracts cross_module_protections (forbidden "guard global"). Convergent.
  • SLA révocation P95 ≤ 2000ms non configurable MVP : spec §5.3 + INV-287-08, plan constante REVOCATION_P95_SLA_MS=2000, contracts module sharing-api. Convergent.
  • Charge nominale contractuelle tests perf (1 lien actif, 10 RPS, staging mono-instance) : spec §5.2 + CA-287-08, tests TC-INV-08 + préconditions §3, plan TC-NOM-09b split perf (k6 N≥200), contracts module tests. Convergent.
  • Anti-catch-absorb (learning 2026-03-08) : contracts module audit-journal (forbidden .catch(() => logger.error(...))), plan §6 (règle ESLint audit-must-throw). Propagation explicite du learning transverse.
  • crypto.randomUUID() / crypto.randomInt() obligatoires (learning 2026-02-21) : contracts modules sharing-api + otp + recipient-session (forbidden Math.random()). Propagation explicite.

3. Divergences

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

  • DIV-01 : Extension du vocabulaire d'événements audit hors enum contractuel D-287-23.
  • Source A (spec) : D-287-23 définit strictement 6 types : LINK_CREATED|LINK_ACTIVATED|LINK_VIEWED|LINK_EXPORTED|LINK_REVOKED|LINK_EXPIRED.
  • Source B (plan §4 mapping CA-287-04 + point de vigilance #26 + §2.2) : introduit LINK_OTP_BLOCKED, LINK_REAUTH_REQUESTED, LINK_REAUTH_CONFIRMED. Deux options évoquées (extension enum vs table dérivée) sans décision tranchée.
  • Source C (tests TC-NOM-03 / TC-NOM-05 / TC-ERR-07 / TC-ERR-09) : ne référencent pas de type d'événement nouveau — observables décrits via "transition état" et "row notification" plutôt que via événement enum.
  • Impact : l'extension étend la chaîne de hash et la sémantique probatoire. Si l'enum contractuel D-287-23 ne bouge pas, la chaîne intègre des événements hors contrat → risque de ré-ouverture Gate 3 sur la spec (ratification enum élargi) ou bien table séparée qui casse la continuité de la hash-chain unique (INV-287-13). Décision attendue Gate 5.

  • DIV-02 : Format de livraison du document chiffré au destinataire.

  • Source A (spec §5.4 F-287-03 + diagramme séquence §5bis) : parle d'encrypted_doc_ref (référence/URL) dans la réponse SharingAPI → Recipient.
  • Source B (plan §2.3 étape 6 + ADR-287-01 + point de vigilance #2) : retient une livraison base64 inline en JSON jusqu'à 50 MB (constante MAX_INLINE_DOCUMENT_BYTES=52428800) ; au-delà streaming binaire application/octet-stream.
  • Source C (tests TC-NOM-07 + CA-287-12 matrice) : listent les 6 champs attendus mais ne tranchent pas entre encrypted_doc_ref et encrypted_document_b64 ; golden files non spécifiés.
  • Impact : divergence de contrat d'API. Un test TC-NOM-07 écrit contre encrypted_doc_ref échouera contre une implémentation encrypted_document_b64. Risque de taille mémoire Node.js sur documents proches de 50 MB (buffer base64 = +33% + sérialisation JSON). Arbitrage PO/architecture requis.

  • DIV-03 : Statut contractuel du chiffrement recipient_email / client_ip / user_agent au repos.

  • Source A (spec D-287-03/28/29 + INV-287-16) : INV-287-16 classe uniquement les "artefacts crypto temporaires (DEK envelope, rekey, fragment, clé éphémère, capsule)". Email/IP/UA sont en §5.11 (rétention) sans exigence de chiffrement au repos.
  • Source B (plan point de vigilance #15 + §7) : ajoute pgcrypto pgp_sym_encrypt sur email/IP/UA et recipient_email_sha256 indexé.
  • Source C (tests TC-NEG-06) : cherche "secrets temporaires en clair" — périmètre ambigu (couvre-t-il email/IP/UA ?).
  • Impact : surcoût DB (chiffrement + index hashé) non budgété par spec. Risque d'over-engineering si PO n'exige pas ce renforcement. À l'inverse, position défensive cohérente avec RGPD et conforme à l'esprit de l'INV-287-15. Décision attendue : ratifier l'extension (bump INV-287-16 ou nouvel invariant) ou rétrograder en pattern recommandé non bloquant.

  • DIV-04 : Endpoints reauth OTP (/reauth/request-otp, /reauth/confirm).

  • Source A (spec diagramme séquence §5bis) : ces endpoints sont présents dans le diagramme mais ne figurent ni dans §5.4 (flux nominaux), ni dans §6 (cas d'erreur), ni dans §5.2 (bornes) ni parmi les cas d'ERR-287-*.
  • Source B (plan §1 + §2.3 + point de vigilance #3) : les formalise avec rate-limit partagé et anti-enum identiques à l'activation ; introduit événements audit LINK_REAUTH_* (cf. DIV-01).
  • Source C (tests TC-ERR-09) : évoque "réauthentification OTP requise côté UX" sans test dédié d'endpoint. Aucun TC-NOM spécifique pour le flux reauth.
  • Impact : endpoints non couverts par un test intégration dédié ; quotas de resend otp_resend_limit_per_hour sont partagés entre activation initiale et reauth (spec §5.2 n'était-elle pas scopée activation initiale ?). Risque de contournement quota via reauth si non explicite. Décision Gate 5 : ajouter TC-NOM dédié reauth + confirmer partage de compteur.

  • DIV-05 : Testabilité de CA-287-27 (consentement RGPD affiché avant activation) vs stub front.

  • Source A (spec CA-287-27 + H-287-08) : "Information RGPD destinataire affichée avant activation : capture UI + trace consentement info".
  • Source B (plan §4 ligne CA-287-27 + H-plan-07 + hors périmètre) : "Page activation destinataire : bandeau RGPD obligatoire (front) + event audit LINK_VIEWED avec flag rgpd_info_shown=true" — front stubbé PD-287-FRONT.
  • Source C (tests TC-NOM-02) : "information RGPD affichée avant validation OTP et audit LINK_ACTIVATED présent" — observable UI attendu.
  • Impact : le backend seul ne peut pas observer "capture UI". Le plan propose un flag dans LINK_VIEWED, mais la spec associe le critère à LINK_ACTIVATED et à une observation UI. Si le front est stubbé PD-287-FRONT, TC-NOM-02 au niveau backend dégrade en "le flag est exposé dans la réponse API d'activation" — ce n'est pas ce que la spec décrit. Arbitrage : ré-écrire TC-NOM-02 en découpage backend (exposition de la contract API) + front (capture UI) ou rendre CA-287-27 explicite "hors périmètre MVP testable backend".

  • DIV-06 : Périmètre audité par INV-287-01 (zero-knowledge bornée).

  • Source A (spec INV-287-01) : "aucun endpoint, service, log ou dump mémoire listé dans le périmètre de la story" — périmètre non énuméré.
  • Source B (plan point de vigilance #24) : énumère C1/C2/C3/C4/C5/C6/C7/C8/C10/C12 + logs applicatifs + DB replicas. Exclut systèmes externes (HSM, KMS, Blockchain client).
  • Source C (tests TC-INV-11) : renvoie à "périmètre story" sans énumération ; plan propose protocole déterministe (GC×3 + heap snapshot + grep).
  • Impact : énumération faite unilatéralement par le plan. Si un reviewer Gate 5 (ou Gate 8) conteste le périmètre, le test peut être rejoué sur un scope différent. Décision attendue : ratifier l'énumération côté spec ou l'acter comme annexe test.

  • DIV-07 : Hypothèse "ownership preuve immuable durant la vie du lien".

  • Source A (spec) : ne mentionne pas de transfert d'ownership. D-287-02 owner_user_id est présent mais sans règle de mutabilité.
  • Source B (plan H-plan + point de vigilance #22) : documente hypothèse ownership immuable durant la vie du lien + évolution tracée PD-287-EXT-3 (trigger PG REVOKED auto si transfert).
  • Source C (tests) : aucun scénario de test portant sur un transfert d'ownership pendant la vie d'un lien.
  • Impact : zone grise assumée par le plan. Si l'écosystème ProbatioVault permet un transfert d'ownership ailleurs (epic autre que sharing), les liens de PD-287 restent "orphelins" côté FSM. Pas bloquant MVP car documenté ; mais pas non plus testé défensivement.

  • DIV-08 : 429 sur états FSM non "rate-limit" stricts (OTP_BLOCKED, REVOKED anti-abus).

  • Source A (spec INV-287-12) : 429 "réservé aux conditions de rate-limit/anti-abus, sans fuite d'existence de ressource".
  • Source A' (spec ERR-287-07 + ERR-287-20 + §5.4 F-287-02 + §5.5 table transitions) : impose néanmoins 429 + share_state=OTP_BLOCKED et 429 + transition REVOKED.
  • Source B (plan point de vigilance #1 + #13) : reconnaît la tension, retient 429 + jitter latence + masquage header X-OTP-Blocked-Until. Risque résiduel documenté.
  • Source C (tests TC-ERR-07 + TC-ERR-20) : valident la réponse 429 sans tester le fingerprinting par latence.
  • Impact : tension interne à la spec elle-même (déjà relevée Gate 3 écart #1). Le plan applique la règle littérale (429) mais à fine granularité, un test adversarial sophistiqué pourrait distinguer OTP_BLOCKED de RATE_LIMITED rate_limit=open_requests_per_ip_per_min via timing. Risque résiduel accepté mais à reconfronter Gate 8 avec métriques latence.

  • DIV-09 : Check seuil OTP cumulé : avant vs après incrément.

  • Source A (spec §5.2) : otp_failed_attempts_total_per_link_max=50 → à 50, transition REVOKED.
  • Source B (plan point de vigilance #18) : "check count >= 50 avant incrément (la 50e tentative échouée déclenche REVOKED, stockage max 49 ou 50 selon transaction)".
  • Source C (tests TC-ERR-20) : "otp_failed_attempts_total=49 + OTP invalide supplémentaire → 429 + REVOKED" — sémantique : la 50e tentative échouée est celle qui déclenche.
  • Impact : ambiguïté "check avant incrément" vs test "49+1=50 déclenche". Si le plan check avant incrément, à 49 il rejette (incrément à 50 puis REVOKED) ; à 50 il rejette sans incrémenter. Cohérent avec tests mais formulation plan ambiguë. Préciser dans le code : "incrémenter d'abord dans une transaction atomique, puis tester count >= 50 et transitionner".

  • DIV-10 : Rate-limit IP open_requests_per_ip_per_min=30 : couverture par tests.

  • Source A (spec §5.2) : paramètre défini comme protection anti-scan.
  • Source B (plan §7 + point #4) : utilisé pour "mitiger DoS via token volé" en amont.
  • Source C (tests) : aucun TC nominal ni adversarial spécifique pour ce quota IP (TC-NEG-09 parle de "rotation preuve/compte/IP" mais n'enforce pas le quota IP isolément).
  • Impact : quota documenté mais non testé isolément. Ajouter TC-NEG dédié au rate-limit IP (scan token aléatoire sans quota IP → 429).

4. Zones d'ombre

  • OTP TTL (OTP_CODE_TTL_SECONDS=300s) : introduite par le plan (constante + H-plan-03) suite à écart Gate 3 #10. Aucun test ne vérifie explicitement qu'un OTP émis à t0 est rejeté à t0+301s. Ajouter un TC-NOM-OTP-TTL dédié.
  • Événement audit technique de purge RGPD : spec §5.11 dit "traçable par événement technique non destructif" ; plan C8 + contracts module retention-worker (invariant "trace de purge"). Type d'événement non défini : LINK_PURGED ? Hors enum D-287-23. Tombe sous DIV-01.
  • Clearing_success_cycles_required=2 : spec §5.2 définit le paramètre, plan C7 l'évoque, aucun test (TC-NOM-16 parle de "retour HEALTHY seulement après clearing_success_cycles_required cycles" mais le mécanisme d'observation du statut HEALTHY n'est pas formalisé — y a-t-il un healthcheck exposé ? où est lu le statut ?).
  • max_consultations_per_link : spec §5.2 + ERR non défini ; plan point #12 "feature dormante MVP". Aucun test MVP (acceptable). Mais CA-287-12 ("consultation fournit document + artefacts") ne précise pas qu'elle peut être plafonnée → pas d'ambiguïté si feature OFF par défaut.
  • Format owner_key_ref : spec D-287 ne le définit pas dans §5.1 ; plan point #9 propose "HSM KeyHandle opaque ≤ 128 chars" comme extension. Non ratifié par spec → extension implicite.
  • Scope mémorisation warning "pas de DRM" : spec CA-287-26 dit "mémorisée" ; plan ADR-287-02 tranche "compte utilisateur serveur". Choix ferme côté plan, non tranché côté spec → à ratifier Gate 5.
  • Définition formelle de "trust-store incomplet" : spec INV-287-17 parle de "trust-store obligatoire non optionnel" ; plan Point #25 définit "racine plateforme manquante OU CRL inaccessible OU (si OCSP requis, stapling KO)". Définition unilatérale côté plan, à ratifier.
  • Worker MerkleAnchorWorker : comportement post-crash avant ancrage : plan ADR-287-03 dit "reprise depuis le dernier batch ancré confirmé" ; tests TC-NOM-16 couvrent la réconciliation mais ne simulent pas explicitement un crash worker Merkle. Scénario d'injection de crash à ajouter.
  • Extension enum D-287-23 vs hash-chain : cf. DIV-01. Zone d'ombre : si la décision Gate 5 est "table dérivée" au lieu d'enum étendu, quel impact sur la hash-chain probatoire (double chaîne → rupture INV-287-13) ?
  • Golden files tests TC-NR-04 (stabilité format export offline signé) : tests non-régression sur le manifeste JCS signé, mais aucun golden file versionné côté tests. À matérialiser côté agent-tests.
  • Observabilité SLO: spec §5.2 liste métriques attendues (link_generation_latency_p95_ms, revocation_effective_latency_p95_ms) ; plan mentionne métrique Prometheus share_revocation_latency_p95_ms mais ne détaille pas le pipeline (scraping, alerting, dashboard). Hors scope story ? À confirmer.

5. Recommandation

  • Procéder avec réserves majeures ratifiables — convergence globale forte sur invariants, flux et tests. 10 divergences identifiées, toutes anticipées par le plan via ADR ou Points de vigilance, mais 3 requièrent arbitrage formel avant Gate 5 GO :
  • DIV-01 (enum D-287-23 étendu) : décision nécessaire entre (a) extension contractuelle via bump spec, (b) table dérivée hors hash-chain, ou © abandon des événements reauth/OTP_BLOCKED au profit de champs dans les événements existants.
  • DIV-02 (format livraison document chiffré) : trancher formellement ADR-287-01 (base64 inline 50 MB) vs encrypted_doc_ref (URL présignée) → arbitrage architecture + impact test TC-NOM-07.
  • DIV-05 (testabilité CA-287-27) : découper ou redéfinir pour rendre le critère backend observable.
  • Les divergences DIV-03 (chiffrement email/IP/UA), DIV-06 (périmètre INV-287-01), DIV-07 (ownership immuable), DIV-08 (429 FSM), DIV-09 (sémantique check 50), DIV-10 (rate-limit IP) peuvent être traitées par précision au sein du dossier Gate 5 sans re-ouvrir la spec.
  • Zones d'ombre (OTP TTL, clearing healthcheck, golden files export, crash worker Merkle) → à couvrir par tests additionnels avant clôture, pas bloquant pour Gate 5.

Pas d'escalade humaine structurelle requise pour le moment — DIV-01/02/05 peuvent être tranchés dans le dossier de conformité Gate 5 par l'arbitre PMO.