Aller au contenu

PD-295 — Review de spécification (Gate 3)

Auditeur : revue contractuelle indépendante. Documents audités : PD-295-specification.md, PD-295-tests.md. Périmètre tests : ambiguïtés, contradictions, non-testabilité, hypothèses dangereuses, risques — sans conclure sur la couverture.


1) Ambiguïtés

Type : Ambiguïté
Référence : specification.md §3 (Définitions) + §5.4 (B3) + §5.1 D-295-12
Description : `reuse_score` est défini avec « max effective 0.9999 » (§3), et D-295-12 fixe le pattern `^0\.[0-9]{4}$` OU littéral `0.9999`. Or `tanh(x)` peut produire des valeurs intermédiaires arrondies à `1.0000` pour des `reuse_score_brut` élevés (ex : tanh(50/10) ≈ 0.99991 → arrondi 4 décimales = `0.9999`, mais tanh(100/10) ≈ 1.0000000 → arrondi = `1.0000`). Le comportement entre « clamp interdit » (§5.7) et « rejet si > max » (§5.7) n'est pas déterminé sur la frontière exacte avant vs après arrondi : rejette-t-on si `tanh(...)` brut > 0.9999 avant arrondi, ou si l'arrondi 4 décimales > 0.9999 ?
Impact : Règle de sérialisation non déterministe. TC-NOM-10 demande « Toute valeur > 0.9999 est rejetée (pas de clamp silencieux) » sans lever l'ambiguïté. Un implémenteur peut produire soit un rejet soit une valeur acceptée pour un même input selon l'ordre calcul/arrondi.
Gravité : Majeur
Type : Ambiguïté
Référence : specification.md §5.1 D-295-17 (« Interdits: email, téléphone, IBAN, adresse postale explicite »)
Description : La notion « adresse postale explicite » n'a ni regex, ni exemples, ni définition opérationnelle. Les tests TC-NOM-02/TC-ERR-14/TC-NEG-08 n'exercent pas ce pattern. Q-295-05 reconnaît explicitement qu'il manque la validation DPO/compliance.
Impact : Critère de rejet PII subjectif. Deux implémentations conformes peuvent diverger sur ce qui déclenche ERR-295-PII_DETECTED. Risque de faux négatifs RGPD.
Gravité : Majeur (déjà marqué non testable §9 tests)
Type : Ambiguïté
Référence : specification.md §5.6 étape 4 + §5.14 vecteurs V1/V2/V3
Description : §5.6 indique « B5 construit l'événement d'audit, canonicalise JCS, signe HMAC » sans spécifier la liste exhaustive et ordonnée des champs constitutifs de l'événement. Les vecteurs V1/V2/V3 montrent 3 structures de payload différentes (count/event/key_version/result_ids/source/story_id ; puis domain/event/project/story_id/timestamp ; puis event/learning_id/nb_domains/...) sans règle générique indiquant quels champs sont obligatoires par type d'événement.
Impact : La signature HMAC dépend byte à byte du payload canonicalisé. Sans schéma contractuel par `event`, deux implémentations conformes produiront des signatures différentes → CA-295-05 non reproductible en dehors des 3 cas littéraux des vecteurs.
Gravité : Bloquant
Type : Ambiguïté
Référence : specification.md §5.1 D-295-09 (« case-insensitive en lecture, sérialisé lowercase »)
Description : La clé HMAC est décrite `case-insensitive en lecture, sérialisé lowercase`. Or HMAC-SHA256 opère sur les 32 octets décodés, pas sur la représentation texte. L'expression « sérialisé lowercase » n'a aucun sens fonctionnel puisque la clé n'est jamais sérialisée dans le payload signé (INV-295-08 exclut `signature_hmac_sha256` mais la clé elle-même n'est pas un champ du payload).
Impact : Contrainte inutile ou indique un artefact de test (storage de la clé en trace ?) non documenté. Ambiguïté sur ce que « sérialisé lowercase » s'applique.
Gravité : Mineur
Type : Ambiguïté
Référence : specification.md §5.1 D-295-17 (« 2..4 lignes par section »)
Description : Le résumé clarification exige « 2..4 lignes par section » mais la notion de « ligne » n'est pas définie (ligne logique Markdown ? ligne après wrapping ? avec/sans lignes vides ?). Aucun test contrôle cette borne.
Impact : Critère non vérifiable automatiquement, risque de faux rejets.
Gravité : Mineur
Type : Ambiguïté
Référence : specification.md §5.12 « Clearing conditionnel » + §5.9
Description : Le drapeau `MEMORY_DEGRADED`/`MEMORY_HEALTHY` est mentionné §5.12 avec « après 2 cycles conformes consécutifs ». Aucun invariant INV-295-* ne le couvre, aucun état n'est intégré aux machines à états §5.9, et Q-295-04 confirme que la valeur `2 cycles` n'est pas encore validée. Le terme « cycle » n'est pas défini (une requête ? un flush ? un run B5 ?).
Impact : Règle non-testable en l'état. TC-NOM-16 l'évoque sans pouvoir préciser l'unité de cycle.
Gravité : Majeur

2) Contradictions

Type : Contradiction
Référence : specification.md §5.6 étape 5 + §5.6 étape 7 + INV-295-10
Description : §5.6.5 : « B5 écrit la trace signée dans data/sessions/*.jsonl » (avant le retour du bloc). §5.6.7 : « Si écriture trace échoue, B5 retourne un bloc vide et ERR-295-TRACE_WRITE_FAILED ». L'étape 4 précise que le payload est construit AVANT l'écriture. Or le diagramme de séquence §5bis montre `append OK` → retour bloc, `append FAIL` → `S0->>Log: append(injection_failed_fail_closed)`. Cette 2ᵉ écriture fail-closed est un nouvel append log qui peut lui aussi échouer : la spec ne dit pas quoi faire en cas d'échec de l'écriture fail-closed elle-même (récursion infinie ? continuer muet ?).
Impact : Boucle fail-closed non bornée. Contradiction entre §5.6 (texte, pas d'écriture secondaire) et §5bis (diagramme, écriture secondaire).
Gravité : Majeur
Type : Contradiction
Référence : specification.md §2 Exclu (« Mode dégradé autorisant une injection sans trace ») vs §5.12 « Clearing conditionnel : drapeau MEMORY_DEGRADED levé après erreur fail-closed »
Description : Le périmètre exclut explicitement tout « mode dégradé » mais §5.12 introduit bien un état dégradé `MEMORY_DEGRADED`. Contradiction terminologique sur la notion de « dégradé ».
Impact : Ambiguïté contractuelle sur l'existence d'un mode dégradé, même sans effet sur l'injection.
Gravité : Mineur
Type : Contradiction
Référence : specification.md §5.9.1 INV-295-LS-10 vs §5.9.1 INV-295-LS-01/INV-295-LS-04
Description : INV-295-LS-10 autorise `ARCHIVED -> STORY_ACTIVE` « uniquement via restauration manuelle auditée ». Mais un learning restauré en `STORY_ACTIVE` avec son `reuse_score` historique (souvent élevé car il a été promu puis archivé stale) sera à nouveau éligible immédiatement à `STORY_ACTIVE -> DOMAIN_ACTIVE` via INV-295-LS-01. La spec ne précise pas si le `reuse_score` doit être réinitialisé lors de la restauration, ni si un cooldown s'applique. Contradiction pratique avec l'esprit « restauration contrôlée » (INV-295-LS-10 justification).
Impact : Chemin contourné pour re-promouvoir un stale ancien en global en 2 transitions automatiques immédiates.
Gravité : Majeur
Type : Contradiction Spec↔Tests
Référence : specification.md §5.12 « Clearing : 2 cycles conformes consécutifs » vs tests.md TC-NOM-16
Description : TC-NOM-16 valide le clearing via « MEMORY_DEGRADED est levé puis MEMORY_HEALTHY après 2 cycles conformes » sans définir ce qu'est un cycle testé. Le test prétend observer une transition mesurable sans que la spec fournisse le déclencheur.
Impact : Test non exécutable de manière déterministe.
Gravité : Majeur
Type : Contradiction
Référence : specification.md §5.2 étape 5 « Le système journalise les erreurs de format sans bloquer les entrées valides » vs INV-295-06 (« Toute trace audit B1..B5 DOIT être signée HMAC-SHA256 »)
Description : §5.2 autorise des logs d'erreur non bloquants côté B1, mais INV-295-06 impose signature HMAC pour « toute trace audit B1..B5 ». Un log d'erreur de format est-il une « trace audit » au sens INV-295-06 ? Si oui, il DOIT être signé, ce qui ralentit le flux d'indexation. Si non, la frontière entre log opérationnel et trace audit n'est pas définie.
Impact : Catégorisation « trace audit » non contractualisée.
Gravité : Mineur

3) Règles non testables

Type : Non testable
Référence : specification.md INV-295-02 (« Le verbatim PO DOIT rester en mémoire volatile et être détruit en sortie de phase 1 bis »)
Description : Destruction mémoire non observable en boîte noire. Reconnu par §9 tests. TC-INV-02 propose une sonde « environnement QA uniquement » qui n'est pas partie prenante du système livré et n'est pas documentée dans la spec.
Impact : Invariant non auditable post-livraison, risque de persistance accidentelle via swap, core dump, logs Python verbose.
Gravité : Bloquant (RGPD)
Type : Non testable
Référence : specification.md INV-295-05 (« date > retention_until ») + H-295-04 (« dérive <= 500 ms »)
Description : La frontière stricte `>` s'applique à une comparaison de dates (D-295-13 = ISO date `YYYY-MM-DD`). Or H-295-04 parle de dérive en millisecondes. La résolution temporelle effective (jour vs instant) n'est pas définie : un job purge qui tourne à 23:59:59 le jour `retention_until` produit-il le même résultat que 00:00:01 le lendemain ?
Impact : Non testable sans fixer la granularité et le fuseau.
Gravité : Majeur
Type : Non testable
Référence : specification.md CA-295-16, CA-295-17, CA-295-18, CA-295-19
Description : Ces CA dépendent de mesures calendaires post-merge (T+30/T+60/T+90j, 30j glissants, 5 premières stories post-B5). Aucune de ces mesures ne peut être vérifiée au moment de Gate 8 → le CA méta (CA-295-15) ne prouve que l'existence des scripts. L'écart entre « critère d'acceptation contractuel » et « preuve lors du Gate » n'est pas assumé dans la spec. Les CA-295-16..19 sont inopérants en Gate 8 mais sont listés comme critères d'acceptation.
Impact : Critères non bloquants de facto, créent une illusion de couverture.
Gravité : Majeur
Type : Non testable
Référence : specification.md INV-295-04 (« traces query des sessions liées au story_id »)
Description : La purge inclut « les traces query des sessions liées au story_id ». Or §5.13 indique que la corrélation se fait par `story_id` entre logs sessions. Rien n'oblige le format de session à contenir `story_id` de manière indexée — Q-295-05 confirme l'absence de validation conformité. Vérifier une suppression complète demande un modèle d'interrogation non défini.
Impact : Complétude de purge non prouvable.
Gravité : Majeur
Type : Non testable
Référence : specification.md §5.12 « rate-limit 3 req / 5 min glissantes »
Description : Glissant vs fixe n'est pas précisé en implémentation (buckets ? sliding window log ? token bucket ?). Le test TC-ERR-06 pilote 4 requêtes, mais un comptage par buckets 5 min produit le même résultat qu'un sliding window log pour ce cas. Le comportement à la frontière (requête 4 à t+4min59s puis t+5min01s) n'est pas testable sans spécification de l'algorithme.
Impact : Règle ambiguë aux bords de fenêtre.
Gravité : Mineur

4) Incohérences de testabilité (Spec ↔ Tests)

Type : Incohérence Spec↔Tests
Référence : specification.md §5.14 vecteurs V1/V2/V3 vs tests.md TC-NOM-06
Description : TC-NOM-06 exécute la validation HMAC sur les vecteurs V1..V3 littéraux. Mais la spec ne fournit pas d'algorithme JCS clair pour produire `payload_canonique_jcs` depuis un objet non canonicalisé. L'implémenteur ne peut pas vérifier que son canonicalizer produit ces mêmes strings — il peut seulement constater que si on lui donne la string littérale, HMAC produit la signature attendue. Autrement dit, les vecteurs testent `HMAC(key, string)` mais pas `HMAC(key, JCS(object))`.
Impact : Le test du canonicalizer JCS RFC 8785 (INV-295-08) est absent. Un bug de canonicalisation (ordre, encodage UTF-8, échappement) ne sera pas détecté.
Gravité : Bloquant
Type : Incohérence Spec↔Tests
Référence : specification.md CA-295-07 + INV-295-10 vs tests.md TC-NOM-08
Description : TC-NOM-08 vérifie que la trace est écrite « avant le retour du bloc ». Cette observation temporelle est non triviale en boîte noire — rien dans les observables §8 tests ne permet de distinguer « écrit avant le retour » de « écrit après le retour » si les deux opérations sont synchrones dans le même processus. Le test ne spécifie pas la méthode d'observation.
Impact : Test non déterministe / non reproductible.
Gravité : Majeur
Type : Incohérence Spec↔Tests
Référence : specification.md §5.14 V1..V3 vs H-295-01
Description : V2 inclut `key_version: 12` (implicite dans V1 via champ), mais H-295-01 suppose que le chemin Vault `kv/pd-295/memory-audit-hmac` est provisionné. Rien ne dit si `key_version` est une version Vault réelle ou un champ métadonnée. TC-NOM-06 ne vérifie pas la cohérence `key_version ↔ clé Vault`.
Impact : Rotation de clé Vault non couverte par tests.
Gravité : Majeur
Type : Incohérence Spec↔Tests
Référence : tests.md TC-NOM-15 vs specification.md §5.12 (lock scope global par fichier)
Description : TC-NOM-15 parle de « lock stale simulé ». La simulation n'est pas décrite — créer manuellement un fichier `.lock` avec mtime > 60s ? La spec §5.12 ne définit pas le fichier de lock, sa nomenclature, son emplacement. Un implémenteur ne peut pas reproduire la simulation.
Impact : Test non reproductible entre implémentations.
Gravité : Majeur
Type : Incohérence Spec↔Tests
Référence : specification.md INV-295-11 vs TC-NEG-15
Description : TC-NEG-15 teste cardinalité ≠ 5/3/3 mais la spec ne définit pas le comportement si une source n'a pas assez d'éléments (ex : <5 learnings disponibles). B5 doit-il échouer, retourner moins, compléter avec des placeholders ? §5.6 est muet, §5.7 fixe top_k min=max=5/3/3.
Impact : Cas limite « corpus insuffisant » non défini. TC-NEG-15 peut être faux positif ou faux négatif selon l'interprétation.
Gravité : Bloquant

5) Hypothèses dangereuses

Type : Hypothèse dangereuse
Référence : specification.md §5.12 « idempotency_bucket 5 min » + clé `{story_id, step, operation, timestamp_bucket_5min}`
Description : La clé d'idempotence inclut un `timestamp_bucket_5min`, ce qui signifie que deux requêtes parfaitement identiques envoyées à t=4min59s et t=5min01s auront des clés différentes et seront toutes deux traitées. L'idempotence n'est donc pas une vraie idempotence mais une déduplication par fenêtre. Rien n'avertit de ce piège.
Impact : Fausse sécurité contre les replays près des bords de fenêtre.
Gravité : Majeur
Type : Hypothèse dangereuse
Référence : specification.md INV-295-14 + §5.12 lock scope « global par fichier »
Description : `flock` est un advisory lock POSIX local à un hôte. La spec parle de « concurrence multi-instance » en justification. Si les instances `/gov` peuvent tourner sur plusieurs hôtes (cmux/ringbearers distribués), `flock` n'est pas un lock distribué. Aucune hypothèse H-295-* ne restreint l'exécution à un seul hôte.
Impact : Race condition inter-hôtes possible sur `learnings*.jsonl`.
Gravité : Bloquant
Type : Hypothèse dangereuse
Référence : specification.md §5.3 étape 1 « Step 0 collecte 4 réponses PO en mémoire volatile uniquement »
Description : « Mémoire volatile » en Python n'empêche ni le swap disque, ni les core dumps, ni le passage par la stdin/logs du subprocess `claude -p` qui peut écrire le prompt complet dans `terminal.log` (cf. learning CLAUDE.md « Injection terminal.log »). Aucune précaution n'est prise pour éviter la fuite via subprocess.
Impact : INV-295-01/02 potentiellement violé par architecture existante d'injection.
Gravité : Bloquant
Type : Hypothèse dangereuse
Référence : specification.md H-295-04 « dérive horloge <= 500 ms »
Description : La tolérance 500 ms n'est pas vérifiée au runtime : rien ne détecte une dérive > 500 ms. L'invariant INV-295-14 ne mentionne pas de check NTP. TC-NEG-10 teste le fail-closed sur dérive mais la spec ne fournit pas le mécanisme de détection.
Impact : Hypothèse non gardée. Violation silencieuse possible.
Gravité : Majeur
Type : Hypothèse dangereuse
Référence : specification.md §5.7 `clock_drift_max 500 ms` (fail-closed trace) vs §5.1 D-295-14 horodatage en secondes
Description : La dérive max est 500 ms mais l'horodatage trace est en secondes entières (`^...T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$`). Deux écritures consécutives peuvent donc porter le même timestamp, rendant l'ordre non reconstructible à partir des logs signés.
Impact : Ordonnancement des traces non déterministe, complique l'audit chaîné.
Gravité : Majeur
Type : Hypothèse dangereuse
Référence : specification.md §5.2 FAISS dimension 768 + embeddings `nomic-embed-text`
Description : FAISS est un index en mémoire/fichier sans contrôle de concurrence natif. §5.12 couvre `learnings*.jsonl` via flock, mais pas les fichiers d'index FAISS. Une écriture concurrente B1 (veille) et B3 (scoring) peut corrompre l'index.
Impact : Corruption silencieuse d'index.
Gravité : Majeur

5bis) Cohérence des diagrammes

Type : Incohérence diagramme ↔ invariants
Référence : specification.md §5bis diagramme `learning.scope` + INV-295-LS-*
Description : Le diagramme utilise un pseudo-état `TRANSITION_REFUSED`. Cela masque le fait qu'une transition interdite n'est pas un changement d'état — INV-295-15 est pourtant explicite : « état avant/après identique ». Le diagramme contredit INV-295-15 en laissant croire à un nœud intermédiaire.
Impact : Lecture ambiguë par un implémenteur.
Gravité : Majeur
Type : Incohérence diagramme ↔ invariants
Référence : specification.md §5bis diagramme `clarification.lifecycle_state`
Description : Même problème : `C_REFUSED` est un nœud Mermaid, alors qu'il ne doit pas être un état persisté. De plus, les transitions `REJECTED -> REJECTED` et `PURGED -> PURGED` (« -> * INTERDITE ») sont des self-loops qui ne correspondent à aucune transition réelle : elles ajoutent du bruit sans valeur.
Impact : Diagramme ambigu, self-loops trompeurs.
Gravité : Mineur
Type : Incohérence diagramme ↔ transformations
Référence : specification.md §5bis diagramme de séquence B5
Description : Le diagramme montre `B5->>B5: sig = HMAC_SHA256(canonical, hex_to_bytes(key_hex))` mais ne montre pas les champs exacts qui composent `payload` avant `canonical = JCS(payload)`. Absence de format d'entrée/sortie spécifié pour l'étape de construction du payload, conformément au critère 5bis de la revue.
Impact : L'implémenteur ne peut pas reproduire la trace signée depuis le diagramme (cf. ambiguïté §1 sur schéma d'événement).
Gravité : Majeur
Type : Incohérence diagramme ↔ spec texte
Référence : specification.md §5.6 vs §5bis séquence B5 (appels parallèles `par...and...end`)
Description : Le texte §5.6 étape 2 dit « B5 exécute les 3 recherches (learnings, veille, clarifications) » sans mentionner de parallélisme, tandis que le diagramme indique explicitement un bloc `par...and...and` (3 recherches en parallèle). Rien n'indique si le parallélisme est obligatoire, recommandé ou simple commodité de dessin.
Impact : Contrainte d'implémentation non contractuelle.
Gravité : Mineur

6) Risques sécurité / conformité

Type : Risque sécu/conformité
Référence : specification.md INV-295-01/02/04 (RGPD) + architecture d'injection existante
Description : La spec ne traite pas du chemin de fuite par subprocess `claude -p` (prompt complet peut être écrit dans `terminal.log`, cf. learning projet). Un verbatim PO passé à B2 pour synthèse via Ollama ou claude peut persister dans les logs subprocess.
Impact : Violation RGPD possible en dépit du respect formel des invariants.
Gravité : Bloquant
Type : Risque sécu/conformité
Référence : specification.md INV-295-06 + stockage clé Vault
Description : La clé HMAC Vault est un secret symétrique : quiconque y a accès peut forger une trace et la faire passer pour authentique. La spec n'impose pas de rotation (H-295-01 suppose juste la présence), ni de séparation de rôle (lecture signature ≠ lecture vérification — ce qui demanderait des signatures asymétriques). Pour une « chaîne probatoire » (INV-295-06 justification), HMAC-SHA256 est un choix discutable.
Impact : Non-répudiation faible. Un attaquant interne peut forger des événements historiques si la clé fuite.
Gravité : Majeur
Type : Risque sécu/conformité
Référence : specification.md §5.3 purge + ERR-295-PURGE_VERIFICATION_FAILED
Description : La « vérification 100% succès » de la purge (TC-NOM-04) n'inclut pas les sauvegardes, snapshots, backups distants, ni les index FAISS en cours d'utilisation par un autre processus (cf. risque concurrence §5 précédent). Le droit à l'oubli RGPD exige une effectivité y compris sur backups.
Impact : Fausse preuve de purge.
Gravité : Majeur
Type : Risque sécu/conformité
Référence : specification.md ERR-295-HMAC_VERIFICATION_FAILED (« Trace rejetée + alerte »)
Description : Rejeter une trace invalide en lecture ne supprime ni ne déplace la ligne falsifiée. Aucune procédure de quarantaine n'est définie. Un attaquant ayant accès au fichier session peut insérer des lignes qui seront toujours rejetées à la lecture mais qui polluent le flux jusqu'à intervention manuelle.
Impact : DoS de l'audit trail possible.
Gravité : Mineur
Type : Risque sécu/conformité
Référence : specification.md §5.4 `reuse_score` injection historique
Description : Le `reuse_score` influence le tri des learnings injectés en step 0. Un attaquant capable d'écrire dans `learnings-injections.jsonl` peut gonfler artificiellement le score d'un learning malveillant (prompt injection en tête de contexte step 0 → réponses biaisées de tous les futurs LLMs). Aucun contrôle d'intégrité n'est exigé sur ce fichier.
Impact : Vecteur de prompt-injection persistant sur l'ensemble du workflow gouvernance.
Gravité : Bloquant
Type : Risque sécu/conformité
Référence : specification.md §5.13 guards inter-modules
Description : Les guards ne sont installés que sur 4 commandes listées. Toute autre commande future (ou existante non listée) qui lirait directement `learnings.jsonl` contournerait silencieusement le rate-limit, le lock, l'audit. Pas de mécanisme fail-closed au niveau fichier.
Impact : Bypass trivial par ajout d'un nouveau script ou par appel Python direct.
Gravité : Majeur

Synthèse

Nombre d'écarts : 31 (6 Ambiguïtés, 5 Contradictions, 5 Non testables, 5 Incohérences Spec↔Tests, 6 Hypothèses dangereuses, 4 Cohérence diagrammes, 6 Risques sécu/conformité — certains se recoupent).

Gravité Nombre
Bloquant 7
Majeur 19
Mineur 7

Points de blocage principaux : schéma d'événement audit non contractualisé (canonicalisation non reproductible), canonicalizer JCS non testé, cardinalité B5 sous corpus insuffisant non définie, flock non distribué, fuite verbatim via subprocess logs, INV-295-02 non observable en boîte noire, vecteur prompt-injection via learnings-injections.jsonl.