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 |