Aller au contenu

PD-295 — Specification Review (cycle 3 v3)

Auditeur technique indépendant — revue contractuelle, testabilité, conformité. Documents audités :

  • PD-295-specification.md (cycle 3 v3)
  • PD-295-tests.md (cycle 3 v3) — limitation d'audit : ambiguïtés / contradictions / règles non testables a priori / hypothèses dangereuses / risques sécurité-conformité. Pas de conclusion de couverture.

Écart R-01

Type : Contradiction (Spec ↔ Spec)
Référence : PD-295-specification.md §5bis « Diagramme de séquence B5 » vs INV-295-09 (§4.1)
Description :
  INV-295-09 impose que la clé HMAC soit chargée une seule fois au démarrage de /gov
  et devienne immuable pour toute la session : « aucune relecture Vault en cours
  de session ». Le diagramme de séquence B5 §5bis décrit pourtant explicitement un bloc
  « alt key absente en mémoire (démarrage session) » à l'intérieur de `gov-learnings-inject`
  avec `B5->>V: read key (once at startup)` et `sys.exit(1)` si Vault KO, comme si B5
  pouvait déclencher le chargement lazy lui-même.
  Deux modèles de cycle de vie coexistent : eager au démarrage /gov vs lazy dans B5.
Impact :
  - Ambiguïté sur QUI charge la clé, QUAND et avec QUEL point d'émission d'erreur
    (ERR-295-AUDIT_KEY_UNAVAILABLE émis par /gov ou par B5 ?).
  - CA-295-06 / TC-ERR-10 / TC-ERR-11 n'ont pas de point d'entrée unique à instrumenter.
  - Si B5 relit Vault en cours de séance, INV-295-09 (« immuable pour toute la session »)
    est violée.
Gravité : Majeur

Écart R-02

Type : Incohérence Spec ↔ Tests
Référence : PD-295-tests.md TC-NEG-08 vs PD-295-specification.md INV-295-01, §5.3, D-295-17, CA-295-02, §6 ERR-295-PII_DETECTED
Description :
  TC-NEG-08 attend explicitement un mécanisme de rédaction :
  « chaque marqueur est remplacé par `<REDACTED_FAMILY_N>` avant écriture disque ;
    sinon ERR-295-PII_DETECTED ».
  Aucune section de la spec ne contractualise un comportement de
  rédaction / substitution :
  - INV-295-01 impose « résumé non-PII validé, jamais verbatim »
  - §5.3 étape 6 parle d'un « résumé structuré non-PII produit »
  - D-295-17 / CA-295-02 énumèrent 6 familles PII mais ne définissent
    aucune action (ni format de token de rédaction, ni fallback, ni brique responsable).
  - §6 décrit ERR-295-PII_DETECTED comme rejet pur (« rejet résumé »).
  Le test introduit un comportement fonctionnel absent de la spec.
Impact :
  - Un reviewer tiers reçoit deux sémantiques PII opposées : rejet dur vs masquage partiel.
  - Risque de divergence d'implémentation entre équipes.
  - CA-295-02 / TC-ERR-14 (rejet pur) et TC-NEG-08 (masquage) ne peuvent pas être satisfaits
    simultanément par la même implémentation.
Gravité : Majeur

Écart R-03

Type : Contradiction (Spec ↔ Spec)
Référence : INV-295-RUNTIME-04 + CA-295-02 vs §10 Q-295-05
Description :
  INV-295-RUNTIME-04 et CA-295-02 exigent que `pii_ruleset_v1.yaml` soit
  « validé DPO par commit signé avant activation » et que hash/version soient tracés en audit.
  Le même document, §10 Points à clarifier, déclare Q-295-05 :
  « Validation DPO des patterns PII `pii_ruleset_v1` — approbation conformité — manquante ».
  La spec contractualise donc comme NON NÉGOCIABLE un prérequis qu'elle qualifie
  simultanément d'ouvert.
Impact :
  - Gate 3 ne peut pas statuer : un invariant dépend d'un livrable conformité non tranché.
  - Toute exécution B2 avant validation DPO viole INV-295-RUNTIME-04.
Gravité : Majeur

Écart R-04

Type : Hypothèse dangereuse / Risque de disponibilité
Référence : §5.13 G-295-09 + §6 ERR-295-NTP_UNREACHABLE / ERR-295-CLOCK_DRIFT_EXCEEDED
Description :
  Chaque signature est conditionnée à une vérification de dérive d'horloge via
  `sntp -t 5 pool.ntp.org` (dépendance réseau externe, TTL 5s) avec fallback
  unique sur `/var/db/timed/Library/Preferences/com.apple.timed.plist`.
  Indisponibilité simultanée des deux sources => fail-closed strict, abort /gov + alerte,
  AVANT chaque événement signé (pas seulement au démarrage).
  H-295-04 n'évoque qu'une dérive <=500ms sans mentionner la disponibilité NTP comme
  hypothèse tenue. H-295-06 autorise un hôte « backend unique » (probablement Linux)
  où `/var/db/timed/...` n'existe pas.
Impact :
  - Toute intermittence réseau (wifi laptop, firewall sortant) casse /gov dès la
    première signature.
  - Le fallback `timed` est macOS-spécifique ; sur hôte Linux, le fail-closed devient
    systématique dès que `sntp` échoue.
  - Aucun budget de résilience (cache récent, dérive tolérée plus large en mode dégradé tracé).
Gravité : Majeur

Écart R-05

Type : Hypothèse dangereuse + Contradiction
Référence : INV-295-BACKUP-01 vs H-295-06
Description :
  INV-295-BACKUP-01 contractualise l'exclusion via `.nobackup` ou `tmutil addexclusion`.
  `tmutil` est un binaire exclusivement macOS (Time Machine).
  H-295-06 autorise un déploiement « laptop dev OU serveur backend unique » :
  le cas serveur backend Linux n'a aucune procédure d'exclusion backup contractuelle.
  `scripts/purge-backups.sh` est mentionné sans préciser son périmètre (Time Machine ?
  backup Vault ? quel backend ?).
Impact :
  - Si hôte Linux : INV-295-BACKUP-01 inapplicable => CA-295-BACKUP-01 non satisfiable.
  - Risque RGPD : traces clarifications re-sauvegardées hors périmètre purge.
Gravité : Majeur

Écart R-06

Type : Contradiction (modèle fail-closed rompu)
Référence : INV-295-STATE-04 + §6 ERR-295-PURGE_FAILED vs doctrine générale fail-closed (§5.6, INV-295-10)
Description :
  INV-295-STATE-04 décrit un hook `on_story_close(...)` qui, en cas d'échec de
  `purge-clarifications.py`, effectue : log événement signé, retry unique, puis
  « si 2e échec : alerte opérateur ERR-295-PURGE_FAILED et transition Jira poursuivie
  (fail-safe) ».
  §6 confirme « ERR-295-PURGE_FAILED : alerte opérateur, transition Jira continue ».
  C'est un modèle fail-SAFE (warn + continue) qui contredit la doctrine fail-closed
  affichée partout ailleurs dans la spec (INV-295-10 sur la trace B5, guards §5.13,
  ERR-295-LOCK_TIMEOUT, etc.).
  De plus, la purge RGPD (INV-295-04, CA-295-04) est revendiquée non négociable
  alors que son hook de déclenchement est explicitement dégradable.
Impact :
  - La garantie RGPD « 6 artefacts purgés » devient probabiliste : dépend d'un
    cron daily dont la fréquence, le retry, l'owner et la preuve ne sont pas contractualisés.
  - Gate 8 ne peut pas observer la violation : la story clôt en fail-safe silencieux.
  - Incohérence sémantique avec le reste de la spec : un reviewer ne sait pas si la
    doctrine par défaut est fail-closed ou fail-safe.
Gravité : Majeur

Écart R-07

Type : Ambiguïté / Règle non testable
Référence : INV-295-LS-01 + §3 définition `nb_domains` + §5.7 formule `reuse_score_brut`
Description :
  `nb_domains` est défini comme
  `len(set(injection.domain for injection in learnings-injections.jsonl if injection.learning_id == L.id))`.
  INV-295-LS-01 autorise la promotion STORY_ACTIVE → DOMAIN_ACTIVE si
  `reuse_score >= 0.3 ET nb_domains >= 1`.
  Un learning STORY_ACTIVE vit dans un unique domaine source. Toute première injection
  incrémente `nb_domains` à 1 (son propre domaine), rendant la condition
  `nb_domains >= 1` triviale et indissociable de la simple existence d'une injection.
  INV-295-LS-04 (`nb_domains >= 2` pour global) pose par ailleurs une question logique :
  un learning `scope:story` peut-il être injecté dans un domaine différent du sien avant
  promotion ? Aucune règle ne le précise, et si oui, le couplage `scope` / moteur de
  requête B5 (§5.6) n'est pas contractualisé.
Impact :
  - Non testable : TC-NOM-11 ne peut pas distinguer un learning « vraiment réutilisé »
    d'un learning injecté une seule fois dans son domaine origine.
  - Risque de promotion triviale vers DOMAIN_ACTIVE dès la première injection.
  - Contradiction implicite avec le besoin métier (promouvoir la RÉUTILISATION inter-domaines).
Gravité : Majeur

Écart R-08

Type : Ambiguïté / Risque auditabilité
Référence : §5.14 V6 + §5.15 Rotation clé + §5.12 « Rotation clé »
Description :
  §5.15 et §5.12 mentionnent une rotation via `scripts/rotate-audit-hmac.sh` +
  re-signature existant + archive clé précédente, sans contractualiser :
  - où est stockée la clé archivée (chemin Vault précis, droits d'accès) ;
  - qui et comment vérifie a posteriori des événements signés v1 après passage en v2 ;
  - jusqu'à quand les clés archivées sont conservées ;
  - comment le reader (`compute-learning-scores.py`, `gov-learnings-inject`) sélectionne
    la bonne clé par `key_version` (trust-store ? seule clé courante en mémoire session ?).
  V6 affirme « v1 archivée pour vérifier les événements historiques » mais aucun
  mécanisme de sélection de clé côté reader n'est spécifié.
Impact :
  - Non testable : TC-STATE-02 « vérification historique possible avec clé archivée N »
    n'a pas de procédure déterministe à exécuter.
  - Risque de perte d'auditabilité après rotation : des lignes signées v1 deviendraient
    silencieusement « non signées » côté reader (INV-295-STATE-02 les ignorerait).
  - Trou d'auditabilité : une rotation mal gérée équivaut à une purge silencieuse des traces.
Gravité : Majeur

Écart R-09

Type : Hypothèse dangereuse / Risque sécurité
Référence : §5.3 B2 + §5.6 B5 — absence d'invariant anti-prompt-injection
Description :
  La chaîne B2 ingère un verbatim PO libre, le fait résumer par `claude -p` en
  sous-subprocess, puis réinjecte ce résumé via B5 dans le bloc contexte de l'étape 0
  des stories suivantes (INV-295-CL-04, §5.6).
  Aucun invariant ne traite le risque de prompt-injection : un contenu interposé
  dans les réponses PO pourrait, une fois « résumé non-PII », contenir des instructions
  qui deviennent du contexte de confiance pour un prochain /gov.
  Le ruleset `pii_ruleset_v1.yaml` couvre 6 familles de PII — pas les consignes
  prompt-like, ni les fragments de prompt système, ni les tokens de contrôle modèle.
Impact :
  - Vecteur d'injection transitive : le verbatim n'est pas persistant (OK) mais son
    résumé l'est (INV-295-CL-04) et devient contexte injecté B5.
  - Aucun guard / filtre / sandbox sémantique n'est contractualisé.
  - Conformité : chemin d'influence non tracé, difficilement auditable. Impact élevé
    compte tenu de l'objectif « mémoire vivante injectée automatiquement ».
Gravité : Majeur

Écart R-10

Type : Contradiction (Spec ↔ Spec)
Référence : D-295-26 vs INV-295-11 / CA-295-08
Description :
  D-295-26 définit `count_configured` comme « entier fixe selon source: 5/3/3
  (ou 6 pour purge RGPD) ».
  INV-295-11 et CA-295-08 n'énoncent que 5/3/3 (B5) sans mention de la valeur 6.
  Le vecteur V8 (`clarification_purged`) utilise `count_configured=6`, mais aucun
  invariant ne lie ce champ à « 6 artefacts purgés » (INV-295-04 ne parle pas de
  `count_configured`).
Impact :
  - Ambiguïté : `count_configured` est-il un champ B5 uniquement, ou un champ générique
    dont la sémantique change selon `event_type` ?
  - Un reviewer Gate 8 ne sait pas si un événement `clarification_purged` avec
    `count_configured != 6` est conforme.
Gravité : Mineur

Écart R-11

Type : Incohérence Spec ↔ Tests
Référence : PD-295-tests.md TC-NOM-06-bis vs PD-295-specification.md §5.14
Description :
  §5.14 énumère explicitement 4 vecteurs contractuels HMAC pré-calculés : V1, V4, V6, V8.
  TC-NOM-06-bis introduit un 5e vecteur (« objet arbitraire schéma v3 », signature attendue
  `2afd2cd6dcedc730e1abc41203bf40a67764d69704e19cfd032691879d0a3ab0`) sans référence
  dans la spec (payload, clé, contexte absents).
Impact :
  - Soit un vecteur contractuel manque dans la spec (elle est incomplète sur ce périmètre),
  - soit le test fixe une valeur non contractualisée, non reproductible par un tiers
    qui n'aurait que la spec.
Gravité : Mineur

Écart R-12

Type : Ambiguïté / Règle non testable
Référence : §6 ERR-295-LOCK_STALE_RECOVERY_FAILED vs §5.8 / §5.12
Description :
  ERR-295-LOCK_STALE_RECOVERY_FAILED est listée en §6 (« stale non récupérable »)
  mais §5.8 et §5.12 ne définissent que : `lock_stale_threshold` strict `>60s`,
  « suppression + retry unique ». Les conditions exactes de « non récupérable »
  (échec `unlink`, course avec un autre writer, retry >1 ?) ne sont pas précisées.
Impact :
  - TC-ERR-09 n'a pas de scénario déterministe pour déclencher l'erreur.
  - Ambiguïté d'implémentation (panique vs retry silencieux).
Gravité : Mineur

Écart R-13

Type : Incohérence diagramme ↔ spec (axe 5bis)
Référence : Diagramme de séquence B5 §5bis vs §5.6 étapes 1-4
Description :
  Le diagramme de séquence B5 montre :
  guard checks → rwlock → check key session → build sections → append signed event.
  Il ne représente PAS les 3 recherches (étape 2 : learnings / veille / clarifications)
  ni le calcul `count_effective` / `under_corpus` (étape 3 §5.6). Les locks sur les
  4 fichiers d'état listés par INV-295-14 ne sont pas individualisés.
  Le diagramme §5bis du cycle de vie `clarification` (et celui de `learning.scope`)
  n'indique pas non plus les préconditions fail-closed (guards §5.13) qui conditionnent
  chaque transition signée.
Impact :
  - Un auditeur ne peut pas valider visuellement que chaque transformation est sous lock
    ni que chaque transition traverse ses guards.
  - Divergence entre le flux textuel §5.6 (9 étapes) et le diagramme (5 étapes environ).
Gravité : Mineur

Écart R-14

Type : Règle non testable
Référence : CA-295-16..19 + §12 verdict QA des tests
Description :
  CA-295-16..19 exigent des mesures T+30 / T+60 / T+90 avec cibles chiffrées
  (ex: CS-2 >=30%). Gate 8 observe uniquement CA-295-meta (existence + `test -x` des scripts,
  INV-295-16). Aucun observable contractuel n'est défini pour un résultat CS-N en échec
  post-merge : owner, canal d'alerte, réaction, escalade.
Impact :
  - Non testable à la clôture : Gate 8 ne valide que la présence des scripts.
  - Risque de dérive silencieuse (CS-2 <30% reste un fait non actionné).
Gravité : Mineur

Écart R-15

Type : Incohérence Spec ↔ Tests
Référence : PD-295-tests.md TC-NOM-17 vs INV-295-14 / §5.12 / INV-295-STATE-01..03
Description :
  INV-295-14 liste 4 lockfiles dédiés dont `data/learnings.jsonl.lock`. TC-NOM-17
  teste les 4. Or INV-295-STATE-01/03 n'imposent la signature/écriture via
  `write-signed-state.py` QUE pour 3 fichiers d'état (`learnings-injections`,
  `learnings-scores`, `sessions`). `data/learnings.jsonl` (source des learnings)
  n'est pas dans le périmètre signature : la sémantique writer/lecteur du lock
  associé n'est pas contractualisée.
Impact :
  - Indétermination : quel est le rôle attendu de `learnings.jsonl.lock` ?
    Lecture, migration, insertion manuelle ? TC-NOM-17 exerce un élément qu'aucune
    règle de la spec n'encadre.
Gravité : Mineur

Synthèse (hors verdict)

  • Bloquants : 0
  • Majeurs : 9 (R-01, R-02, R-03, R-04, R-05, R-06, R-07, R-08, R-09)
  • Mineurs : 6 (R-10, R-11, R-12, R-13, R-14, R-15)

Zones de risque concentrées :

  1. Cycle de vie et vérification de la clé HMAC (chargement, rotation, archive, reader multi-versions) — R-01, R-08.
  2. Chaîne B2 et sémantique PII / prompt-injection transitive — R-02, R-03, R-09.
  3. Cohérence fail-closed vs fail-safe sur la purge RGPD — R-06.
  4. Dépendances environnementales non portables (NTP externe, tmutil) — R-04, R-05.
  5. Logique de promotion learning.scope non discriminante — R-07.

⚠️ Aucune correction proposée. Aucune reformulation. Aucune implémentation.