Aller au contenu

PD-286 — Rapport de confrontation (Étape 8 — Gate CLOSURE)

Confrontation pré-Gate 8 sur l'acceptabilité finale de l'export probatoire multi-volumes. Sources : spécification, tests, plan, acceptabilité + 10 livrables agents (C1..C10).

1. Sources confrontées

# Document Étape Rôle
S PD-286-specification.md 1 Contrat fonctionnel + invariants INV-286-01..11
T PD-286-tests.md 2 26 scénarios + matrice couverture
P PD-286-plan.md 4 Plan d'implémentation + mapping invariants→mécanismes
A PD-286-acceptability.md 7 Verdict acceptabilité (auto + LLM)
C1 livrable export-partitioner 6b 39 tests, 100 % branch coverage
C2 livrable export-multi-dto 6b DTO + validators class-validator
C3 livrable export-manifest-multi 6b 41 tests, manifest partiel + root
C4 livrable export-state-machine-backend 6b Entity + state machine + worker EXPIRED
C5 livrable export-controller-multi 6b Controller + service + routing policy
C6 livrable export-audit-multi 6b Façade audit, 48 tests
C7 livrable app-export-orchestrator-multi 6b Orchestrator app, 11 tests PD-286
C8 livrable app-volume-verifier 6b 14 tests, recompute SHA3-256(JCS)
C9 livrable app-pvproof-assembler-multi 6b 36 tests, .pvproof unique
C10 livrable app-export-api-client 6b Zod, 51 tests

2. Convergences

2.1 Bornes contractuelles partagées

  • VOLUME_MAX_BYTES = 805_306_368 (768 MiB) et MAX_TOTAL_EXPORT_BYTES = 10_737_418_240 (10 GiB) cohérents partout (S §4 INV-286-01/02 ; P §1.1 ; C1 §2.2 ; C3 §2.2 ; C5 §2 ; C10 §3.2).
  • TTL bornes [1h, 72h] défaut 24h pour SIGNED_URL_TTL et EXPORT_SESSION_TTL (S §5.2 ; P §9.1 ; C4 §6 worker).

2.2 Algorithme cryptographique

  • Hash partiel : integrityHash = SHA3-256(JCS_RFC8785(manifest \ {integrityHash})) cohérent côté backend C3 et côté app C8 (vector regression test C3 + recomputation C8). Réutilisation de IntegrityHashComputer PD-85 confirmée par C3.
  • Hash racine : manifestRootHash = SHA3-256(JCS({exportId, totalVolumes, volumes:[{volumeIndex, integrityHash, estimatedBytes}]})) (P §3 ; C3 §2.2 ; C10 schemas valide la regex).
  • Lib JCS unique côté backend : JsonCanonicalizeService (PD-37) — aucun import npm canonicalize interdit (vérifié C3 §4 forbiddens, C8 §3 forbiddens).

2.3 Matrice d'états et transitions

  • Spec §4 (transitions autorisées + interdictions terminales) reprise byte-identical par C4 backend (ALLOWED_TRANSITIONS) et C7 app (ExportStateMulti). Trigger PG prevent_forbidden_export_transition ajoute defense-in-depth côté DB (C4 migration).
  • Terminaux COMPLETED/FAILED/EXPIRED sans transition sortante (INV-286-11) — appliqué par isTerminal() C4 + prepareRetry() C7 + test "EXPIRED is terminal".

2.4 Rétrocompatibilité PD-85 (INV-286-05)

  • Branche legacy single-volume conservée (P §2.1 ; C5 executeLegacySingle ; C9 shape Legacy byte-identical ; C10 router parseExportApiResponse accepte single-volume).
  • Sérialisation conditionnelle via champs ?: TypeScript (omission undefined plutôt que null) — décision D1 C2.
  • Tests PD-85 préservés : 158 tests non-régression backend (A), 19/19 schemas + 14/14 api-client legacy (C10), 4/4 pvproof-assembler PD-85 (C9), 76 tests state-machine PD-283 (C7).

2.5 Invariants INV-286-06 (continuité volumeIndex)

  • Validation triplée : C1 partitioner (assertion runtime), C2 DTO (IsValidVolumesShape), C3 manifest root (validateVolumes), C10 Zod (superRefine), C7 tri défensif (sort ASC).

2.6 Anti-patterns appliqués

  • crypto.randomUUID() exclusivement (C5) — learning 2026-02-21.
  • Anti-catch-absorb (C5 try/throw + dataSource.transaction ; C6 fail-closed sans .catch()) — learning 2026-03-08.
  • Branded types ExportId, ExportSessionUserId, VolumeIndex, TotalVolumes (C4, C10) — learning 2026-03-04.
  • Anti-énumération codes erreur uniformes (C6 ExportAuditRejectionReason enum fermée + UPPER_SNAKE) — learning 2026-03-08.

3. Divergences

⚠️ Conflits rendus visibles, sans lissage.

DIV-01 — Sonar QG SKIP non conforme à la procédure

  • Source A (acceptability §Phase 1.5) : « Quality Gate : SKIP (Docker indisponible, sonar-scanner non installé — capability probe: docker=unavailable). Mitigation : ESLint + TSC couvrent les règles Sonar critiques. »
  • Source B (procedures.md, étape 7) : « Phase 1.5 (Sonar local) est BLOQUANTE. Si Sonar QG ERROR -> STOP avant reviews LLM. La derogation ESLint+tsc comme substitution n'est PLUS acceptee. Si sonar-scanner n'est pas installe, l'orchestrateur DOIT l'installer avant de continuer. »
  • Impact : violation de la procédure Article V boucle d'acceptabilité ; le verdict A « ACCEPTABLE avec réserves » est techniquement non valide tant que Sonar n'a pas été exécuté localement (brew install sonar-scanner n'est pas mentionné).

DIV-02 — TypeScript : « 0 error PD-286 » contredit par C2/C3/C5

  • Source A (acceptability §Phase 1) : « TypeScript | OK | 0 error PD-286 (1 error pré-existante merkle/v2 hors scope). »
  • Source B (C2 §11 + C3 §7 + C5 §5) : trois livrables convergent pour identifier deux erreurs résiduelles : complaint-file-response.dto.ts:134 (attribuée explicitement à C2 PD-286) et merkle-proof-v2.controller.ts:16. C2 dit « unique erreur résiduelle hors périmètre C2 », mais C3 et C5 la situent dans complaint-file-response.dto.ts qui appartient à C2.
  • Impact : si l'erreur TS dans le DTO C2 est réelle, l'acceptability sous-déclare l'état. À investiguer avant Gate 8 — soit l'erreur a été corrigée depuis (à reconfirmer par npx tsc --noEmit | grep complaint-file-response), soit l'acceptability est inexacte.

DIV-03 — Audit : C5 n'utilise pas ExportAuditService (C6 livré)

  • Source A (C5 §4 décision D2) : « C6 (export-audit-multi) est en Wave 4 et n'est pas encore livré [...] j'utilise directement auditLogService.log() (PD-37) [...]. C6 pourra ultérieurement consolider en ExportAuditService sans changer la sémantique. »
  • Source B (C6 §1) : « Statut : implémenté + testé (48 tests). »
  • Source C (C6 §8.2) : « L'implémentation actuelle de ExportService [...] appelle directement auditLogService.log() avec un metadata ad hoc. Cette pratique est désormais à proscrire [...]. Diff attendu en step 6c. »
  • Impact : C6 livré mais non câblé ; les invariants protégés par C6 (validation signedUrl/manifest interdits dans metadata, énumération fermée des reasonCode, regex hash lowercase) ne sont pas appliqués sur le chemin de production. Le metadata de C5 contient potentiellement des champs non normalisés. Migration step 6c non réalisée.

DIV-04 — Endpoint REST de finalisation COMPLETED manquant

  • Source A (spec §5.4 step 5 + diagramme séquence) : « App → Audit: append {exportId, status: COMPLETED} ».
  • Source B (plan §2.2) : « ExportAudit (côté serveur via callback) : appendEnd(exportId, status=COMPLETED) ».
  • Source C (C6 §8.5) : « Aujourd'hui aucun endpoint REST n'expose appendFinal côté backend [...] story enfant à arbitrer avec le PMO. »
  • Source D (C7 livrable) : aucune mention d'appel REST POST /exports/:id/finalize. L'orchestrator app passe à READY localement sans signaler le COMPLETED au backend.
  • Impact : transition ASSEMBLING → COMPLETED côté backend jamais déclenchée en production. Seul EXPIRED est géré (par ExpiredExportWorker). L'audit WORM final status=COMPLETED (CA-286-07, INV-286-09) n'est pas émis. CA-286-07 implémentable en théorie, non implémenté en pratique sur le chemin nominal.

DIV-05 — integrityHashes non persistés côté ExportSession

  • Source A (C6 §8.4 open question) : « Aujourd'hui les integrityHashes ne sont pas persistés sur ExportSession — ils sont seulement émis dans le payload audit START. Pour permettre au worker EXPIRED de réémettre integrityHashes au moment du FINAL, il faut [...] ajouter une colonne integrity_hashes JSONB. »
  • Source B (C4 entity) : pas de colonne integrity_hashes dans ExportSession.
  • Source C (C6 §8.3) : appendFinal requiert integrityHashes (cardinalité = totalVolumes), sinon le builder lève cardinality must equal totalVolumes.
  • Impact : ExpiredExportWorker (C4) ne peut pas émettre un appendFinal valide pour les sessions EXPIRED multi-volumes. Soit l'audit FINAL est skippé (violation INV-286-09), soit le worker throw et la transition échoue. Non couvert par les tests d'intégration ; détectable uniquement en runtime sur multi-volume EXPIRED.

DIV-06 — Lib JCS partagée @probatiovault/jcs inexistante

  • Source A (plan H-PD286-PLAN-01) : « Suite CI roundtrip dédiée : génère 100 manifests aléatoires (incl. accents, emojis, floats edge) côté backend, calcule hash, transmet à un test app jest qui recalcule et compare. Bloque le merge si divergence. Réutilise lib partagée (publication interne npm @probatiovault/jcs). »
  • Source B (C8 §6.3 dette) : « @probatiovault/jcs n'existe pas comme package npm interne. Le canonicalizer PD-54 fait office de lib interne mais est typé Event. »
  • Source C (C3 §6 + C8 §6.1) : vector regression interne à backend uniquement ; aucune suite roundtrip backend↔app. C8 reporte la responsabilité « à la suite intégration plus large ».
  • Impact : la mitigation contractuelle de l'hypothèse cross-runtime H-PD286-PLAN-01 (RFC 8785 byte-identical Node.js ↔ React Native + Hermes) n'est pas en place. Risque INV-286-07 : faux négatifs d'intégrité sur manifests Unicode/floats edge. Non détectable par les tests unitaires actuels.

DIV-07 — TC-NR-02 (consommateur PD-85) non couvert E2E

  • Source A (plan §5) : « TC-NR-02 (régression conteneur PD-85) | OK | Vérifier consommateur PD-85 valide encore ». Test marqué Integration.
  • Source B (C9 §4.2) : « TC-NR-02 (régression conteneur PD-85) | OK partiel | [...] vérification consommateur PD-85 e2e relève de C11 (suite tests d'intégration) ».
  • Source C (acceptability) : aucune mention TC-NR-02 dans la liste des tests passants.
  • Impact : H-PD286-PLAN-04 (« le conteneur .pvproof tolère l'ajout de champs volumes_count et assembled_from[] ») non vérifiée par un test passant en CI. Risque casse silencieuse des consommateurs externes du .pvproof PD-85.

DIV-08 — Service de bundling S3 hors-périmètre, jamais référencé

  • Source A (C5 §4 décision D3 + H-C5-03) : « ExportVolumeDto.signedUrl contient une seule URL — implique un objet bundle par volume côté storage. La création effective du bundle [...] est hors-périmètre du module export (probable story future). C5 sait juste signer la clé prévue : convention déterministe exports/{exportId}/v{volumeIndex}.bundle. »
  • Source B (spec, plan) : aucune mention d'un service de bundling, ni d'une dépendance externe pour produire les blobs S3 par volume.
  • Source C (acceptability) : pas de mention.
  • Impact : en production, les signedUrl retournées par C5 pointent vers des objets S3 inexistants tant qu'aucun service ne produit le bundle. Toute tentative de download volume → 404 → state FAILED côté app. Story dépendante non identifiée, non chiffrée, non planifiée.

DIV-09 — Bornes TTL : clamp vs fail-closed

  • Source A (spec §5.2) : « Si non-respect bornes config: démarrage service refusé (fail-closed). »
  • Source B (C5 H-C5-05) : « EXPORT_SESSION_TTL_HOURS env var, si fournie hors bornes [1, 72] [...] est clampée silencieusement par C5. La spec §5.2 demande "démarrage service refusé (fail-closed)". Différence assumée : C5 clamp, C4 worker clamp aussi. À trancher en revue. »
  • Impact : violation contractuelle volontaire reportée à la revue. Une valeur EXPORT_SESSION_TTL_HOURS=100 (hors borne 72) ne fait pas crasher le service ; elle est silencieusement ramenée à 72h. Conséquence opérationnelle : misconfiguration non détectée.

DIV-10 — Volume count abuse non mitigé contractuellement

  • Source A (acceptability §Review Sécurité 7c MAJEUR) : « L'endpoint POST /exports/complaint-file ne limite pas le nombre de preuves atomiques > VOLUME_MAX_BYTES — un attaquant pourrait demander 13 volumes dédiés de 768 MB chacun. Mitigation : MAX_TOTAL_EXPORT_BYTES (10 GB) + MAX_PROOF_IDS (500) + rate limiting existant. »
  • Source B (spec §4 invariants) : pas de borne contractuelle sur totalVolumes (cf. §5.1 « min 1, pas de borne max fixe contractuelle »).
  • Source C (C2 §6 V2) : « Pas de borne max sur totalVolumes (cf. plan §9 vigilance). C2 ne pose pas de borne arbitraire pour rester aligné avec le contrat. »
  • Impact : la mitigation citée par l'acceptability (MAX_TOTAL_EXPORT_BYTES) borne le total bytes mais pas le nombre de volumes — un attaquant créant 14 preuves de 766 MB obtient totalSize ≈ 10.5 GB > MAX_TOTAL → rejet 413 OK ; mais 13 preuves de 768 MB exact → totalSize = 9.984 GB ≤ MAX_TOTAL → accepté → 13 volumes dédiés émis. Mitigation incomplète.

DIV-11 — Couverture totale tests backend annoncée vs détaillée

  • Source A (acceptability §Prérequis) : « Tests CI : 380/380 PASS (26 suites, npx jest --testPathPattern=export) [...] 222 tests PD-286 + 158 tests PD-85 non-régression. »
  • Source B (somme livrables backend) : C1 (39) + C3 (41) + C6 (48) = 128 tests PD-286 nouveaux explicitement déclarés. C4, C5 ne donnent pas de chiffre net. Écart de ~94 tests à attribuer entre C2/C4/C5 et tests d'intégration.
  • Source C (livrables app) : 289 tests app (C7 §4 « 289 / 289 (13 suites) ») non comptabilisés dans les 380/380 backend.
  • Impact : décompte difficile à reconstituer ; pas de divergence factuelle bloquante mais traçabilité dégradée. À reconfirmer par exécution npx jest --testPathPattern=export --listTests.

4. Zones d'ombre

ZO-01 — Tests E2E réels en staging

L'acceptability cite « C11 en staging » comme couverture des tests d'intégration E2E (TC-NOM-01 500MB, TC-NOM-02 2GB, TC-ERR-04 corruption, TC-ERR-05 TTL). Aucun rapport d'exécution de ces tests E2E n'est fourni dans les documents. Statut « ACCEPTABLE avec réserves » s'appuie sur les tests unitaires/intégration locaux uniquement.

ZO-02 — Performance / SLA temporels en conditions réelles

Spec §5.2 : « Contexte perf de référence: réseau mobile 4G standard, appareil classe iPhone 12 équivalent. » Aucun benchmark mesuré sur un export 2 GB en 4G n'est rapporté. H-PD286-PLAN-06 (« téléchargement séquentiel acceptable côté UX ») non validé.

ZO-03 — Q-286-02 (taxonomie codes erreur) non résolue

Spec §10.2 Q-286-02 : « Validation de la nomenclature (EXPORT_TOTAL_LIMIT_EXCEEDED, PROOF_TOO_LARGE, etc.). » C5 §2 utilise effectivement ces codes mais aucun arbitrage produit n'est documenté. Risque divergence avec consommateurs aval (front, télémétrie).

ZO-04 — Q-286-03 (rétention post-FAILED/EXPIRED) non chiffrée

Spec §10.2 Q-286-03 + plan §9.1 : « politique post-FAILED/EXPIRED non chiffrée par produit. Plan retient la politique PD-85 existante par défaut. » Aucune confirmation produit/RGPD dans l'acceptability.

ZO-05 — Q-286-04 (TTL prod) non confirmées

Spec §10.2 Q-286-04 : « Confirmation des bornes contractuelles proposées (24h défaut, 1h min, 72h max). » Plan retient les défauts spec mais aucune validation prod déployement.

ZO-06 — Stratégie de signed URL revocation absente

Acceptability §Review Sécurité 7c MAJEUR : « Les signed URLs ont un TTL configurable mais pas de revocation mechanism — risque si session compromise pendant le téléchargement multi-volumes. Mitigation : TTL borné [1, 30] min (INV-85-04), S3 bucket policy. » Conflit borne : C4 TTL est [1h, 72h] (cf. spec §5.2 et constants C4 §6), pas [1, 30] min. La mitigation citée s'appuie sur INV-85-04 PD-85, pas sur la borne PD-286 — divergence de référence à clarifier.

ZO-07 — Cohérence migration 1745000000000-PD286-ExportMultiVolumes.ts

C4 §2 livre la migration mais aucune mention dans l'acceptability d'une exécution npm run typeorm migration:run ou d'un test sur DB cible. La table export_sessions, le type ENUM et les triggers n'ont peut-être jamais été appliqués en environnement de test.

ZO-08 — Drift potentiel matrice C4 backend vs C7 app

Plan §2ter et C4 §9 imposent une matrice identique entre backend et app pour les transitions communes. Aucun test croisé n'est rapporté pour vérifier que ExportStateMachine.listAllowedTransitions() (C4) === ExportStateMulti.ALLOWED_TRANSITIONS (C7). Drift latent.

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 du rework (5 blocages identifiés, par ordre de criticité)

  1. DIV-04 (CA-286-07 non couvert nominal) — l'audit final WORM status=COMPLETED n'est jamais émis sur le chemin nominal multi-volume. Violation directe de l'invariant INV-286-09 (Art. III tracabilité). Bloque l'acceptation Gate 8 sans correctif (endpoint POST /exports/:id/finalize ou équivalent).
  2. DIV-03 (C6 livré mais non câblé) — l'audit C5 contourne ExportAuditService. Les forbiddens du contract C6 (signedUrl/manifest dans metadata, codes uniformes) ne sont pas appliqués en production. Migration step 6c indispensable.
  3. DIV-05 (integrityHashes non persistés)ExpiredExportWorker ne peut pas émettre un appendFinal valide pour les sessions EXPIRED multi-volume. Migration DB à compléter.
  4. DIV-01 (Sonar SKIP) — non-conformité Article V (acceptabilité). brew install sonar-scanner puis exécution effective avant Gate 8.
  5. DIV-08 (service de bundling S3 absent) — sans bundling, les signedUrl pointent vers du néant en prod ; toute story de production de bundle doit être identifiée et chiffrée avant que PD-286 puisse être considéré comme déployable.

Points secondaires à clarifier avant Gate 8

  • DIV-02 : reconfirmer l'état TypeScript sur complaint-file-response.dto.ts:134.
  • DIV-06 : créer le package @probatiovault/jcs ou documenter explicitement la dette + suite roundtrip CI.
  • DIV-07 : exécuter TC-NR-02 contre un consommateur PD-85 réel.
  • DIV-09 : trancher clamp vs fail-closed sur les bornes TTL.
  • DIV-10 : ajouter une borne max sur totalVolumes ou un MAX_DEDICATED_VOLUMES côté C5.
  • ZO-01, ZO-02, ZO-07 : exécuter et rapporter les tests E2E + benchmark perf + migration DB sur environnement de test.