Aller au contenu

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

Auditeur indépendant. Aucune correction proposée. Aucune reformulation. Aucune implémentation.

Documents audités :

  • PD-295-specification.md (v2)
  • PD-295-tests.md (v2)

Périmètre tests : ambiguïtés, contradictions, règles non testables a priori, hypothèses dangereuses, risques sécurité/conformité (sans conclure sur la couverture).


Écarts identifiés

#1 — Condition nb_domains>=1 vacuous dans la promotion story->domain

Type : Contradiction (logique vacuous)
Référence : spec §3 (def `nb_domains`), INV-295-09, §5.4, tests TC-NOM-11
Description :
  §3 définit `nb_domains = max(1, len(set(...)))` — la valeur est par construction toujours
  >= 1. INV-295-09 et §5.4 conditionnent la promotion `story -> domain` sur
  `reuse_score >= 0.30 ET nb_domains >= 1`. Le second prédicat est trivialement vrai pour
  toutes les entrées. La règle de promotion `story -> domain` se réduit donc, en pratique,
  à un simple seuil sur `reuse_score`. Le test TC-NOM-11 ne distingue pas un cas
  `nb_domains == 0` (impossible) d'un cas `nb_domains == 1`.
Impact :
  Règle contractuelle dont la moitié de la condition n'a pas de sens opérationnel.
  Risque que l'implémenteur interprète différemment de l'intention initiale (par exemple
  en codant `nb_domains > 1` strict, ce qui changerait silencieusement la sémantique).
  La revue Gate 5 ne pourra pas trancher entre les deux lectures sans clarification.
Gravité : Majeur

#2 — Cardinalité de la section "clarifications" dans B5 non spécifiée

Type : Incohérence Spec↔Tests
Référence : spec INV-295-12, §5.2 (`top_k_*`), §5.9, §10.2 Q-295-02 ; tests TC-NOM-13, CA-295-13
Description :
  INV-295-12 impose 3 sections (learnings, veille, clarifications). §5.2 fixe
  `top_k_learnings_step0=5` et `top_k_veille_step0=3` mais NE fixe AUCUN top_k pour
  les clarifications. Q-295-02 reconnaît explicitement la cardinalité comme question
  ouverte. CA-295-13 vérifie « 5 learnings, 3 veilles » mais reste muet sur les
  clarifications. TC-NOM-13 vérifie l'existence de la section sans cardinalité.
Impact :
  La sortie B5 n'est pas observablement contrainte sur la section clarifications.
  Deux implémentations conformes peuvent injecter 1, 5 ou N clarifications. Aucun
  critère ne permet de rejeter une dérive. La spec dépend d'une question ouverte
  pour son contrat de sortie.
Gravité : Majeur

#3 — Mécanisme HMAC fondé sur file_mtime_ns du script émetteur

Type : Risque sécurité / conformité
Référence : spec §5.1.6 (`secret_key = str(file_mtime_ns du script émetteur)`),
            INV-295-20, §5.12, ERR-295-13, H-295-07 ; tests TC-NOM-14, TC-NOM-26, TC-ERR-14
Description :
  La « signature » HMAC utilisée pour les événements B1..B5 dérive sa clé du
  `mtime` en nanosecondes du script Python qui l'émet. Conséquences contractuelles
  (sans proposition de correctif) :
  - La valeur n'est pas un secret : tout processus disposant d'un accès lecture
    sur le filesystem peut la lire (`stat`) et forger une signature valide.
  - Tout `git checkout`, `touch`, `cp`, `rsync` ou redéploiement modifie le mtime
    et invalide rétroactivement TOUTES les signatures émises avant. Les vérifications
    HMAC ultérieures (TC-NOM-26) deviennent non reproductibles dans le temps.
  - Deux scripts différents (B1 vs B4 vs `restore-learning.py`) produiront des clés
    différentes pour le même `learning_id`, sans champ permettant à un vérificateur
    a posteriori de retrouver quelle clé attendre.
  La spec qualifie ce mécanisme d'« auditabilité probatoire » (INV-295-20), terme
  qui implique opposabilité et reproductibilité — propriétés que ce schéma ne tient pas.
Impact :
  L'invariant probatoire INV-295-20 et le critère CA-295-22 sont implémentables au
  sens littéral mais ne garantissent pas la propriété attendue. Risque RGPD/audit
  (intégrité non démontrable). TC-NOM-26 et TC-ERR-14 testent l'algorithme HMAC mais
  pas la résistance au scénario « script touché entre émission et vérification ».
Gravité : Bloquant

#4 — CS-2 / CS-3 / CS-4 mentionnés sans définition

Type : Ambiguïté
Référence : spec INV-295-17 ("métriques nécessaires à CS-1/2/3/4")
Description :
  INV-295-17 contractualise l'exposition de métriques pour CS-1, CS-2, CS-3 et CS-4.
  Seul CS-1 est défini ailleurs dans le document (formule, baseline, fenêtre 10/10).
  CS-2, CS-3 et CS-4 n'apparaissent nulle part — ni définition, ni formule, ni
  observable, ni test. Aucun CA ni TC associé.
Impact :
  Trois métriques contractuelles non observables. Toute implémentation est de facto
  conforme (puisque rien n'est exigible). Risque d'écart muet en Gate 8.
Gravité : Majeur

#5 — compute-learning-scores.py (B3) hors de la politique de lock

Type : Hypothèse dangereuse
Référence : spec INV-295-18, §5.7 (flux N3), §5.12 ; tests TC-NOM-17, TC-ERR-09
Description :
  INV-295-18 et §5.12 décrivent un rwlock unique sur `data/learnings.jsonl.lock`,
  avec read-lock pour B5 et write-lock pour B4. Le flux N3 (B3, §5.7) écrit
  `data/learnings-scores.jsonl` (étapes 1–3) sans qu'aucune étape n'exige
  d'acquisition de lock — read ou write. Or B3 lit `learnings.jsonl` ET écrit
  un fichier que B5 lit en jointure (INV-295-19). En cas d'exécution concurrente
  B3 ↔ B4 ou B3 ↔ B5, la spec ne dit ni quel verrou prendre, ni l'ordre.
  TC-NOM-17 ne couvre que la paire B4 (write) / B5 (read).
Impact :
  Race possible entre B3 et B4 (deux écritures sur des fichiers liés sans
  ordonnancement) ou entre B3 et B5 (lecture jointe sur `learnings-scores.jsonl`
  pendant son écriture par B3). L'invariant « pas d'artefact partiel » (TC-NOM-17)
  n'est pas garanti pour ce fichier.
Gravité : Majeur

#6 — Filtres --domain --project côté B5 lors de la recherche clarifications

Type : Ambiguïté
Référence : spec INV-295-14, §5.6 (N2 step 5), §5.9 (N5 step 3-4) ; tests TC-NOM-06, TC-ERR-04
Description :
  INV-295-14 impose `search-clarifications --domain --project` comme commande exigible.
  §5.9 décrit le flux B5 : « Lancer en parallèle recherches learnings/veille/
  clarifications » puis « Appliquer filtres contractuels source par source ». Il n'est
  pas explicite que B5 — qui dispose des champs `{story_id, domain, project}` —
  DOIT effectivement passer ces filtres à `search-clarifications`. La spec implique
  que B5 emprunte cette commande, mais ne l'impose pas littéralement, et ne précise pas
  ce qu'il advient si la story n'a pas de `domain` ou `project` connu.
Impact :
  Une implémentation pourrait, sans violer la lettre, exécuter la recherche clarifications
  sans filtres en interne (court-circuitant l'invariant CA-295-06 qui ne couvre que
  l'usage CLI direct). Aucun TC ne vérifie que B5 propage les filtres vers
  `search-clarifications`.
Gravité : Majeur

#7 — Migration scope répétée vs one-shot dans le flux B4

Type : Ambiguïté
Référence : spec INV-295-08, INV-295-16, §5.8 (N4 steps 3–4)
Description :
  §5.8 décrit le flux N4 (promotion + éviction) avec à l'étape 3 « Migrer scope:story
  là où absent ». La spec ne précise pas si ce flux est exécuté une seule fois
  (one-shot, à la livraison) ou à chaque déclenchement périodique de B4. INV-295-16
  exige un commit git préalable « pour toute migration in-place » : si N4 est récurrent,
  la précondition de commit est-elle exigée à chaque exécution même quand toutes les
  lignes ont déjà un `scope` ?
Impact :
  Risque opérationnel : un déclenchement automatique périodique de B4 pourrait être
  bloqué par ERR-295-08 alors qu'aucune migration réelle n'est nécessaire. Inversement,
  si l'implémenteur choisit le mode one-shot, il faut un état durable « migration faite »
  qui n'est documenté nulle part.
Gravité : Mineur

#8 — Purge clarifications dépendante du déroulement step 10

Type : Hypothèse dangereuse
Référence : spec INV-295-21, §5.6 (N2 step 7) ; ERR-295-14
Description :
  INV-295-21 impose une purge automatique des clarifications > 18 mois via
  `purge-clarifications.py` « à la fin du step 10 ». Toute story qui n'atteint pas
  step 10 (rejetée, archivée, escaladée définitivement) ne déclenchera jamais la
  purge. Sur 18 mois, l'accumulation de clarifications de stories non terminées
  contourne silencieusement la rétention contractuelle. Aucun mécanisme de
  rattrapage périodique n'est documenté.
Impact :
  Engagement de rétention 18 mois non garanti dans tous les cycles de vie de story.
  Risque RGPD si les clarifications contiennent des données personnelles (la spec
  exclut explicitement l'anonymisation). TC-NOM-27 valide la fonction de purge mais
  pas son déclenchement systématique.
Gravité : Majeur

#9 — Collisions involontaires de la clé d'idempotence (bucket 5 min)

Type : Hypothèse dangereuse
Référence : spec §3, §5.1.6, §5.12, ERR-295-10 ; tests TC-NOM-18, TC-ERR-10, TC-NEG-13
Description :
  La clé d'idempotence est `{story_id, step, operation, timestamp_bucket}` avec
  `timestamp_bucket` arrondi à 5 minutes UTC. Deux requêtes step 0 légitimement
  différentes (par exemple deux relances avec `query` distinct) émises dans la même
  fenêtre de 5 minutes pour la même story produiront un conflit ERR-295-10. La spec
  traite ce cas comme une erreur (« conflit »), alors qu'il s'agit d'une collision
  involontaire de bucket, pas d'une violation d'idempotence au sens métier.
Impact :
  Faux positifs ERR-295-10 dans des scénarios opérationnels normaux (PO qui corrige
  sa question, retry avec query enrichi, etc.). Le rate-limit `3 requêtes / 5 min`
  (TC-NOM-19) tolère 3 requêtes — mais la 2e et la 3e seront rejetées en idempotence
  si leur `query_hash` diffère. Contradiction implicite entre rate-limit et idempotence.
Gravité : Majeur

#10 — Schéma de données du corpus indexé clarifications absent

Type : Ambiguïté
Référence : spec §5.1.3 (format MD), §5.6 (N2 step 3 « collecter en JSONL », step 4
            « indexer le corpus »)
Description :
  §5.1.3 normalise le frontmatter du fichier `PD-XX-clarifications.md`. §5.6 décrit
  une étape de « collecte en JSONL » et d'indexation, mais aucun §5.1.x ne donne le
  schéma du JSONL clarifications (champs obligatoires, validation, comportement si
  invalide). Comparativement, B1 dispose de §5.1.2 pour `veille.jsonl` et B3 de
  §5.1.4 pour `learnings-scores.jsonl`.
Impact :
  Format pivot non contractuel. La validation TC-ERR-02 cible le frontmatter MD
  mais aucun test ne porte sur le JSONL clarifications dérivé. Risque d'écart Gate 5
  sur la structure réelle du fichier.
Gravité : Majeur

#11 — /morning rendu obligatoire mais sans contrat d'observabilité d'échec

Type : Non testable (a priori)
Référence : spec INV-295-22, §5.7 (N3 step 6), CA-295-21 ; test TC-NOM-08
Description :
  INV-295-22 impose la publication des 3 learnings au plus haut `reuse_score` dans
  `/morning`. TC-NOM-08 stipule « L'absence de ce bloc provoque un échec de
  conformité ». La spec ne définit pas :
  - où l'échec est observable (code retour de `/morning` ? log ? fichier ?),
  - le comportement en cas d'absence totale de scores (`learnings-scores.jsonl` vide),
  - le comportement si moins de 3 learnings existent (publication tronquée ? rejet ?).
Impact :
  L'observable d'échec (« provoque un échec de conformité ») n'a pas de canal défini.
  La règle est exigible mais le test n'a pas d'oracle déterministe pour les cas
  dégradés.
Gravité : Mineur

#12 — restore-learning.py : événement audit signé sans politique de clé

Type : Risque sécurité / conformité
Référence : spec §5.4 (« exige audit event signé »), §5.1.6 (politique HMAC),
            ERR-295-15 ; test TC-NOM-28
Description :
  La restauration manuelle archive→story exige un audit event signé (§5.4). §5.1.6
  ne traite que la signature des événements B1..B5 (`secret_key` dérivée du
  `file_mtime_ns` du script émetteur). Pour `restore-learning.py`, la spec ne dit
  pas si la même politique s'applique, ni quel script est l'émetteur considéré
  (si l'opérateur copie le script localement, le mtime sera arbitraire). TC-NOM-28
  vérifie « event audit signé » sans exiger un schéma de clé.
Impact :
  Le mécanisme déjà fragile du #3 est étendu à une opération sensible (restoration
  d'un learning archivé) sans contrat additionnel. Audit non opposable.
Gravité : Majeur

#13 — Cohérence diagrammes (axe 5bis)

Type : Incohérence Spec↔Diagramme
Référence : spec §5bis (diagramme d'état), §5.4 (machine à états), INV-295-16
Description :
  Le diagramme d'état Mermaid §5bis :
  - Ne montre pas la précondition contractuelle « commit git préalable » (INV-295-16)
    pour les transitions provoquées par B4. C'est une garde, pas une transition,
    mais aucun commentaire n'apparaît sur les arcs concernés.
  - N'inclut aucune transition `STORY_ACTIVE -> GLOBAL_ACTIVE` (correctement absente
    car interdite par §5.4) — mais le diagramme ne signale pas explicitement les
    transitions interdites, alors que §5.4 les énumère comme contractuellement
    rejetées (TC-ERR-13, TC-NEG-08).
  - L'arc `ARCHIVED -> STORY_ACTIVE` est étiqueté `restore-learning.py (manuel)` —
    cohérent avec §5.4 et CA-295-24.
  Le diagramme de séquence :
  - Montre `LOCK_SH` mais ne mentionne ni le timeout 30s ni le fail-closed
    `ERR-295-09` qui font partie du contrat (§5.12).
  - Ne précise pas les formats d'entrée/sortie des appels `search-*` (axe 5bis :
    « transformations de données explicites »).
Impact :
  Les diagrammes restent compatibles avec le texte mais n'expriment pas certaines
  contraintes contractuelles testables. Risque mineur de dérive d'interprétation.
Gravité : Mineur

#14 — H-295-05 (« aucun consommateur externe ») incompatible avec INV-295-02

Type : Hypothèse dangereuse
Référence : spec INV-295-02, INV-295-16, H-295-05
Description :
  INV-295-02 garantit la non-régression des « 4 skills de recherche existants et
  gov-compounder ». H-295-05 fait l'hypothèse qu'aucun consommateur externe au repo
  ne s'appuie sur le schéma actuel de `learnings.jsonl`. Or l'ajout de `scope`
  (INV-295-08) modifie ce schéma. Si l'hypothèse est fausse pour un consommateur
  hors repo (pipeline d'analytics, dashboards externes, mail-search-mcp, etc.), la
  garantie de non-régression INV-295-02 ne s'applique qu'au périmètre interne et
  ne couvre pas les conséquences externes.
Impact :
  Falsification possible de l'invariant de non-régression hors périmètre observable
  par les tests TC-NR-01/02. Aucun mécanisme de découverte des consommateurs externes
  n'est documenté.
Gravité : Mineur

#15 — score_weight_* figés rendent TC-NEG-12 inatteignable

Type : Incohérence Spec↔Tests
Référence : spec §5.2 (`score_weight_injections`, `_gate8_go`, `_domains` Min=Max=Défaut) ;
            tests TC-NEG-12 (« somme ≠ 1.00 → rejet config »)
Description :
  §5.2 fixe chaque poids comme constante (Min = Max = Défaut). Toute valeur
  différente est rejetée individuellement (« Rejet config »). Le test TC-NEG-12
  vise une configuration où la somme des poids ≠ 1.00, mais cette configuration
  est impossible à construire sans déjà violer la contrainte unitaire d'un poids,
  ce qui déclenche un rejet en amont. Le test ne peut donc jamais valider
  spécifiquement l'invariant « somme = 1.00 ».
Impact :
  Test à oracle non déterministe (le rejet observé ne provient pas de la règle
  testée). Faux signal de couverture.
Gravité : Mineur

#16 — H-295-06 (horloge UTC cohérente) sans contrôle ni dégradation

Type : Hypothèse dangereuse
Référence : spec H-295-06, §5.1.6 (`timestamp_bucket`), §5.12 (idempotence,
            réconciliation)
Description :
  Toute la logique d'idempotence (clé bucketisée 5 min) et de réconciliation
  (orphan_threshold 15 min) repose sur une horloge UTC cohérente entre exécuteurs.
  La spec ne décrit aucun contrôle ni mode dégradé en cas de dérive (NTP coupé,
  exécution offline, horloge mal réglée). Aucun ERR-295-xx ne couvre ce cas.
Impact :
  Une dérive d'horloge non détectée produit silencieusement des collisions ou des
  faux orphelins, sans observable. Risque de corruption de l'audit.
Gravité : Mineur

#17 — Absence d'éviction domain / global non motivée

Type : Ambiguïté
Référence : spec §5.4 (`DOMAIN_ACTIVE -> ARCHIVED : INTERDITE dans PD-295`),
            INV-295-10
Description :
  INV-295-10 ne couvre que l'éviction des learnings `scope=story`. §5.4 confirme
  que `DOMAIN_ACTIVE -> ARCHIVED` et `GLOBAL_ACTIVE -> ARCHIVED` sont INTERDITES
  « dans PD-295 ». La spec ne dit pas si c'est un choix de périmètre (renvoyé à
  une story future) ou une décision définitive. Conséquence concrète : un learning
  promu global qui n'a plus aucune réutilisation pendant des années reste actif
  indéfiniment.
Impact :
  Risque opérationnel à long terme (indexation infinie d'artefacts obsolètes).
  Pas d'implication directe sur Gate 8 mais ambiguïté contractuelle sur l'intention.
Gravité : Mineur

#18 — min_score_default borne supérieure floue

Type : Ambiguïté
Référence : spec §5.2 ligne `min_score_default` (Min=0.00, Max=`<1.00`)
Description :
  La borne max est notée `<1.00` (strictement inférieur) sans valeur explicite.
  Une implémentation devra choisir un epsilon arbitraire (0.999, 0.9999, 0.99999...).
  La représentation flottante de `tanh(reuse_score_brut/10)` (INV-295-07) est elle-même
  bornée par `tanh(+inf) = 1.0` non atteint, donc la valeur exacte de l'epsilon n'est
  pas neutre.
Impact :
  Comportement de filtrage `--min-score` non reproductible entre implémentations.
  Mineur car peu de scénarios pratiques s'approchent de 1.0.
Gravité : Mineur

#19 — H-295-04 (stabilité learning_id) en tension avec la mécanique B4

Type : Hypothèse dangereuse
Référence : spec §3 (def `learning_id`), §5.1.5 (« stable tant que story/gate/tags
            inchangés »), H-295-04, INV-295-19
Description :
  `learning_id` est dérivé de `{story, gate, tag_hash}`. Si un opérateur édite les
  tags d'un learning historique (renommage de tag, normalisation), le `learning_id`
  change, la jointure avec `learnings-scores.jsonl` casse, et le fallback contractuel
  (INV-295-19) attribue silencieusement `reuse_score=0.0`. Le learning conserve
  son historique d'injections mais perd toute son ancienneté de scoring sans
  événement audit. La spec ne documente aucun mécanisme protégeant contre
  l'édition silencieuse des tags d'un learning existant.
Impact :
  Régression silencieuse de scoring possible. Pas d'observable. H-295-04 reconnaît
  l'hypothèse mais aucun garde-fou n'est contractuel.
Gravité : Mineur

Synthèse par gravité

Gravité Nombre IDs
Bloquant 1 #3
Majeur 9 #1, #2, #4, #5, #6, #8, #9, #10, #12
Mineur 9 #7, #11, #13, #14, #15, #16, #17, #18, #19

Synthèse par type

Type IDs
Ambiguïté #4, #6, #7, #10, #17, #18
Contradiction #1
Non testable #11
Incohérence Spec↔Tests #2, #5 (partiel), #15
Hypothèse dangereuse #5, #8, #9, #14, #16, #19
Risque sécu/conformité #3, #12
Incohérence Spec↔Diagramme (5bis) #13