Aller au contenu

PD-295 — Spécification (cycle 3 v3)

1. Objectif

La User Story PD-295 DOIT transformer la bibliothèque documentaire en mémoire vivante injectée automatiquement au step 0, via 5 briques contractuelles (B1..B5), avec sécurité fail-closed, audit HMAC canonique JCS, conformité RGPD, et isolation runtime des subprocess manipulant du verbatim PO.

2. Périmètre / Hors périmètre

Inclus

  • B1 : indexation sémantique des fiches de veille.
  • B2 : capture clarifications PO en résumé structuré non-PII, validation binaire PO.
  • B3 : scoring reuse_score et tri secondaire.
  • B4 : promotion scope + archivage stale.
  • B5 : injection unifiée learnings + veille + clarifications.
  • Isolation runtime subprocess verbatim (§3.10.1 besoin v2.2).
  • Signature HMAC des fichiers d’état + filtrage lecture fail-closed (§3.10.2 besoin v2.2).
  • Purge RGPD multi-artefacts + purge backups ciblés.
  • Gate 8 méta : existence/exécutabilité scripts CS-1..CS-4.
  • Mesures CS-1..CS-4 post-merge (T+30/T+60/T+90).

Exclu

  • Changement de stack (FAISS/Ollama/Markdown/YAML/JSONL imposés).
  • Reranker neural, BM25 hybride, knowledge graph, classifier d’intention, router automatique.
  • Refonte specs-index/**/*.yaml.
  • Migration DB/vector engine.
  • Persistance verbatim PO.
  • Mode dégradé autorisant injection non tracée.
  • Non-répudiation forte (EdDSA/HSM), report candidat G33.
  • Exécution multi-hôte active/active, report candidat G34.

3. Définitions

Terme Définition
B1..B5 Briques PD-295 (veille, clarifications, scoring, promotion/éviction, injection unifiée).
Clarification PO Résumé structuré non-PII validé, jamais verbatim persistant.
reuse_score_brut nb_injections*0.4 + nb_stories_gate8_go*0.4 + nb_domains*0.2, avec nb_domains = len(set(injection.domain for injection in learnings-injections.jsonl if injection.learning_id == L.id)) (sans plancher).
reuse_score tanh(reuse_score_brut/10), 4 décimales, plafond effectif 0.9999.
Payload canonique JSON canonicalisé JCS RFC 8785, UTF-8, sans champ de signature.
Trace d’injection Événement signé écrit avant retour B5.
sig_hmac_sha256 Signature de ligne pour fichiers d’état (learnings-injections, learnings-scores, sessions).
Fail-closed Pas d’effet métier si précondition sécurité/audit échoue.
count_configured Cardinalité contractuelle cible (5/3/3).
count_effective min(count_configured, available_after_filter).
under_corpus Booléen vrai si count_effective < count_configured.
CA-295-meta Contrôle Gate 8 : scripts measure-cs1..4.sh présents et exécutables uniquement.

4. Invariants

4.1 Invariants non négociables (runtime/produit)

ID Règle
INV-295-01 Clarifications persistées = résumé structuré non-PII validé, jamais verbatim.
INV-295-03 Clarification non validée PO => rejet sans écriture disque.
INV-295-04 Purge supprime 6 artefacts: data/clarifications.jsonl, data/clarifications-index.faiss, data/clarifications-embeddings.npy, data/clarifications-cache.json, data/sessions/*.jsonl (traces story_id), docs/epics/**/PD-XX-*/PD-XX-clarifications.md.
INV-295-05 retention_until = date_creation + 540 days (jours calendaires stricts) et purge uniquement si date > retention_until.
INV-295-06 Événements audit B1..B5 signés HMAC-SHA256 avec clé Vault kv/pd-295/memory-audit-hmac.
INV-295-07 Clé HMAC Vault obligatoire hex64 -> 32 bytes, sinon rejet.
INV-295-08 Signature sur payload JCS RFC 8785, champ signature exclu.
INV-295-09 Clé Vault chargée une seule fois au démarrage de /gov; indisponible au démarrage => arrêt /gov (ERR-295-AUDIT_KEY_UNAVAILABLE); une fois chargée, clé immuable pour toute la session, aucune relecture Vault en cours de session. Rotation scripts/rotate-audit-hmac.sh uniquement hors session avec contrôle lock global /var/run/gov-session.lock (ou ~/.local/state/gov-session.lock): lock mtime < 2h => ERR-295-ROTATION_BLOCKED; lock mtime > 2h => stale supprimé puis rotation autorisée.
INV-295-10 B5 écrit trace signée avant retour; si échec => bloc vide + ERR-295-TRACE_WRITE_FAILED.
INV-295-11 B5 fixe count_configured à 5/3/3 et calcule count_effective=min(configured, available); si insuffisant, under_corpus=true obligatoire.
INV-295-12 search-clarifications exige --domain et --project.
INV-295-13 reuse_score suit formule figée + 4 décimales + plafond 0.9999.
INV-295-14 rwlock flock advisory obligatoire (LOCK_SH lecture, LOCK_EX écriture) avec lockfile dédié adjacent par cible: data/learnings.jsonl.lock, data/learnings-scores.jsonl.lock, data/learnings-injections.jsonl.lock, data/sessions/current.lock; granularité 1 lock par fichier cible, timeout 30s, stale strict >60s.
INV-295-15 Toute transition non explicitement autorisée est interdite (ERR-295-STATE_TRANSITION_FORBIDDEN).
INV-295-16 Scripts measure-cs1.sh..measure-cs4.sh existent et sont exécutables (CA-295-meta).
INV-295-17 Fail-closed récursif borné: fail_closed_depth valide dans [0,3]; tentative d’incrément à 4 => arrêt process + alerte (ERR-295-FAIL_CLOSED_RECURSION).
INV-295-RUNTIME-01 Architecture B2 obligatoire à 2 niveaux: process parent .claude/commands/gov-step-0.md -> wrapper scripts/b2-sanitizer.py -> sous-subprocess claude -p; verbatim via stdin pipe OS uniquement (jamais argv), stdout/stderr capturés en mémoire, env forcé CLAUDE_DISABLE_SESSION_LOG=1 et TERM=dumb; le wrapper DOIT retirer CLAUDECODE (unset CLAUDECODE && /Users/loic/.local/bin/claude -p ... ou env={k:v for k,v in os.environ.items() if k != "CLAUDECODE"}) car claude -p échoue sinon en session Claude Code active (learning universel .claude/rules/learnings-universal.md, 2026-02-20).
INV-295-RUNTIME-02 Aucun fichier disque n’est autorisé pour transporter du verbatim PO; transit exclusivement via buffers Python (io.BytesIO/io.StringIO) ou os.pipe(); aucun fallback tmpfs/macOS.
INV-295-RUNTIME-03 Chaque invocation subprocess verbatim émet événement signé verbatim_subprocess_invoked{story_id,subprocess_name,duration_ms,success} sans verbatim.
INV-295-RUNTIME-04 pii_ruleset_v1.yaml utilisé par B2 est obligatoire, validé DPO par commit signé avant activation; hash/version du ruleset sont tracés en audit sans verbatim.
INV-295-STATE-01 Chaque ligne JSON écrite dans data/learnings-injections.jsonl, data/learnings-scores.jsonl, data/sessions/*.jsonl contient sig_hmac_sha256 (JCS sans sig).
INV-295-STATE-02 Lecture: lignes non signées/invalides ignorées silencieusement; compteur ERR-295-UNSIGNED_ENTRY incrémenté + événement signé.
INV-295-STATE-03 Écriture de ces fichiers via scripts/lib/write-signed-state.py obligatoire; écriture directe non signée => filtrée à la lecture.
INV-295-STATE-04 Migration one-shot scripts/sign-existing-state.py re-signe les entrées pré-B3 (commit 076dc3e) avant activation B3/B4/B5; hook on_story_close(story_id, final_state) appelé par .claude/commands/gov.md sur REJECTED, DONE_WITH_ANOMALY, DONE n’applique jamais --force et appelle scripts/purge-clarifications.py --story {story_id} --mode eligible-only (idempotent) pour purger uniquement les entrées EXPIRED (date > retention_until); entrées SUMMARY_INDEXED non expirées inchangées et purgées plus tard par cron daily; --force réservé à l’invocation manuelle RGPD art. 17. Si purge-clarifications.py échoue (exit != 0): 1) log événement signé purge_clarifications_failed, 2) retry unique avec backoff 5s, 3) si 2e échec alerte opérateur ERR-295-PURGE_FAILED et transition Jira poursuivie (fail-safe).
INV-295-BACKUP-01 Artefacts purgeables PD-295 exclus des backups automatiques (Time Machine/Vault backup) via .nobackup ou tmutil addexclusion; historique existant purgeable via scripts/purge-backups.sh.
INV-295-LS-01 STORY_ACTIVE -> DOMAIN_ACTIVE autorisée si reuse_score>=0.3 et nb_domains>=1, avec nb_domains = len(set(injection.domain for injection in learnings-injections.jsonl if injection.learning_id == L.id)) (sans plancher). Un learning jamais injecté (nb_domains=0) reste scope:story.
INV-295-LS-02 STORY_ACTIVE -> GLOBAL_ACTIVE interdite.
INV-295-LS-03 STORY_ACTIVE -> ARCHIVED autorisée si nb_injections==0 et âge >56 jours.
INV-295-LS-04 DOMAIN_ACTIVE -> GLOBAL_ACTIVE autorisée si reuse_score>=0.6 et nb_domains>=2.
INV-295-LS-05 DOMAIN_ACTIVE -> STORY_ACTIVE interdite.
INV-295-LS-06 DOMAIN_ACTIVE -> ARCHIVED autorisée si stale.
INV-295-LS-07 GLOBAL_ACTIVE -> DOMAIN_ACTIVE interdite.
INV-295-LS-08 GLOBAL_ACTIVE -> STORY_ACTIVE interdite.
INV-295-LS-09 GLOBAL_ACTIVE -> ARCHIVED autorisée si stale.
INV-295-LS-10 ARCHIVED -> STORY_ACTIVE autorisée uniquement via restauration manuelle auditée.
INV-295-LS-11 ARCHIVED -> DOMAIN_ACTIVE interdite.
INV-295-LS-12 ARCHIVED -> GLOBAL_ACTIVE interdite.
INV-295-CL-01 VERBATIM_IN_MEMORY -> SUMMARY_PENDING_VALIDATION autorisée.
INV-295-CL-02 SUMMARY_PENDING_VALIDATION -> SUMMARY_VALIDATED si PO=oui.
INV-295-CL-03 SUMMARY_PENDING_VALIDATION -> REJECTED si PO=non.
INV-295-CL-04 SUMMARY_VALIDATED -> SUMMARY_INDEXED après écriture résumé.
INV-295-CL-05 SUMMARY_INDEXED -> EXPIRED si date > retention_until.
INV-295-CL-06 EXPIRED -> PURGED après purge vérifiée.
INV-295-CL-07 REJECTED terminal (-> * interdit).
INV-295-CL-08 PURGED terminal (-> * interdit).

4.2 Invariant de code (non-runnable en boîte noire)

ID Type Règle Vérification
INV-295-02 Invariant de code Le verbatim PO reste en mémoire volatile et est détruit en sortie de phase 1 bis. Revue de code + scripts/check-no-verbatim-persisted.sh.

5. Flux nominaux

5.1 Modèle de données canonique

ID Donnée Format / Règle Invalide
D-295-01 story_id ^PD-[0-9]{1,4}$ ERR-295-INVALID_STORY_ID
D-295-02 domain ^[a-z0-9]+(?:-[a-z0-9]+)*$ ERR-295-INVALID_DOMAIN
D-295-03 project enum {ia-governance,backend,app,site,infra,doc,formal,pixel-governance} ERR-295-INVALID_PROJECT
D-295-04 scope enum {story,domain,global,archived} rejet transition
D-295-05 tag_hash ^[a-f0-9]{12}$ ligne ignorée
D-295-06 learning_id ^PD-[0-9]{1,4}-[0-9]{1,2}-[a-f0-9]{12}$ ligne ignorée
D-295-07 query longueur 1..500 chars UTF-8 ERR-295-INVALID_QUERY_LENGTH
D-295-08 query_hash ^sha256:[a-f0-9]{64}$ ERR-295-INVALID_QUERY_HASH
D-295-09 key_hex ^[A-Fa-f0-9]{64}$ ERR-295-AUDIT_KEY_FORMAT_INVALID
D-295-10 key_version ^[1-9][0-9]*$ rejet trace
D-295-11 signature_hmac_sha256 ^[a-f0-9]{64}$ (événements audit) ERR-295-HMAC_VERIFICATION_FAILED
D-295-12 reuse_score format 0.0000..0.9999; règle écrêtage §5.7 rejet calcul
D-295-13 retention_until YYYY-MM-DD avec retention_until = date_creation + 540 days rejet persistance
D-295-14 timestamp_iso8601 YYYY-MM-DDTHH:MM:SS[.mmm]Z rejet trace
D-295-15 verdict enum {signal,bruit,veille} ligne ignorée
D-295-16 impact_pv enum {fort,modere,faible} ligne ignorée
D-295-17 Résumé clarification 4 sections; pii_ruleset_v1 couvre 6 familles minimales (noms propres, emails RFC5322, téléphones FR/E.164, IBAN/BIC/comptes, adresses postales, identifiants externes SIRET/NIR/passeport) ERR-295-PII_DETECTED
D-295-18 source enum {learning,veille,clarification} rejet trace
D-295-19 count entier 0..999 rejet trace
D-295-20 result_ids[] tableau de learning_id rejet trace
D-295-21 schema_version entier fixé 3 rejet trace
D-295-22 event_type enum incluant learning_injected,veille_injected,clarification_injected,verbatim_subprocess_invoked,audit_key_rotated,clarification_purged,purge_clarifications_failed,unsigned_entry_detected,injection_failed_fail_closed rejet trace
D-295-23 brick enum {B1,B2,B3,B4,B5} rejet trace
D-295-24 sig_hmac_sha256 ^[a-f0-9]{64}$ (fichiers d’état) ligne ignorée + ERR-295-UNSIGNED_ENTRY
D-295-25 payload_canonique objet JSON JCS rejet trace
D-295-26 count_configured entier fixe selon source: 5/3/3 (ou 6 pour purge RGPD) rejet trace
D-295-27 count_effective entier 0..count_configured rejet trace
D-295-28 under_corpus booléen rejet trace
D-295-29 subprocess_name string 1..64 rejet runtime trace
D-295-30 duration_ms entier 0..600000 rejet runtime trace
D-295-31 success booléen rejet runtime trace
D-295-32 fail_closed_depth int thread-local (threading.local()), initialisé à 0 début step 0, incrémenté à chaque fail-closed imbriqué, plage valide [0,3] (3 imbriqués acceptés), tentative d’incrément à 4 => abort ERR-295-FAIL_CLOSED_RECURSION, reset à 0 fin step 0 (succès/échec), non persistant et non partagé inter-invocations ERR-295-FAIL_CLOSED_RECURSION

5.2 Flux nominal B1 — Index veille

  1. Collecte ProbatioVault-doc/docs/veille/**/*.md.
  2. Production data/veille.jsonl conforme D-295-15/16.
  3. Index FAISS dimension 768.
  4. Recherche avec filtres --impact et --verdict.
  5. Erreurs de format journalisées sans bloquer valides.
  6. Toute ligne d’état écrite (sessions) est signée sig_hmac_sha256.

5.3 Flux nominal B2 — Clarifications structurées non-PII + isolation runtime

  1. Step 0 collecte 4 réponses PO en mémoire volatile (io.StringIO).
  2. Appel subprocess wrapper scripts/b2-sanitizer.py avec verbatim sur stdin (pipe OS).
  3. Le wrapper appelle claude -p en sous-subprocess (stdin=verbatim, stdout=pipe, env=CLAUDE_DISABLE_SESSION_LOG=1,TERM=dumb, sans CLAUDECODE via unset/filtrage env).
  4. stdout/stderr de la chaîne subprocess restent en mémoire (pipes), jamais fichier.
  5. Aucun fichier disque pour verbatim.
  6. Résumé structuré non-PII produit et validé PO (binaire).
  7. Si validé: écriture PD-XX-clarifications.md + indexation; sinon REJECTED sans persistance.
  8. Événement signé verbatim_subprocess_invoked émis sans verbatim.
  9. Effacement des buffers verbatim en sortie.

5.4 Flux nominal B3 — Scoring de réutilisation + état signé

  1. Lecture learnings.jsonl, learnings-injections.jsonl, metrics.jsonl.
  2. Vérification signatures sur lignes learnings-injections; lignes non signées/invalides ignorées.
  3. Compteur ERR-295-UNSIGNED_ENTRY incrémenté pour chaque ligne ignorée.
  4. Calcul nb_domains = len(set(injection.domain for injection in learnings-injections.jsonl if injection.learning_id == L.id)), puis reuse_score_brut et reuse_score.
  5. Écriture data/learnings-scores.jsonl via scripts/lib/write-signed-state.py.
  6. Tri secondaire search-learnings sur reuse_score.
  7. /morning affiche 3 entrées top score format score=X.XXXX.
  8. Migration initiale: scripts/sign-existing-state.py re-signe les entrées pré-B3.

5.5 Flux nominal B4 — Promotion et éviction

  1. Migration initiale scope:story si absent.
  2. Promotion story->domain et domain->global selon seuils.
  3. Éviction stale (nb_injections==0, âge >56j) vers archive.
  4. Lecture des fichiers d’état signés; lignes non signées filtrées (INV-295-STATE-02).
  5. Restauration uniquement via scripts/restore-learning.py (audit signé).
  6. Exclusion des archived de l’index actif.

5.6 Flux nominal B5 — Injection unifiée + fail-closed borné

  1. Acquisition locks lecture rwlock (INV-295-14).
  2. Recherches exécutées: learnings (top_k=5), veille (top_k=3), clarifications (top_k=3, --domain et --project obligatoires).
  3. Pour chaque source: count_configured fixé, count_effective=min(configured, available_after_filter), under_corpus=true si insuffisant.
  4. Construction bloc injection 3 sections avec count_configured, count_effective, under_corpus.
  5. Construction événement audit conforme schéma générique §5.14.
  6. Signature HMAC JCS et écriture trace signée en session via helper signé.
  7. Si écriture réussie: retour bloc injection.
  8. Si écriture trace échoue et fail_closed_depth < 3: ERR-295-TRACE_WRITE_FAILED, fail_closed_depth += 1, retour EMPTY_BLOCK.
  9. Si nouvelle erreur impose une tentative d’incrément de fail_closed_depth de 3 vers 4: arrêt process + alerte (ERR-295-FAIL_CLOSED_RECURSION).

5.7 Bornes numériques

Paramètre Valeur
embedding_dimension 768 fixe
summary_temperature 0.2 fixe
top_k_learnings_step0 5 fixe
top_k_veille_step0 3 fixe
top_k_clarifications_step0 3 fixe
query.length_max 500
retention_clarifications 540 jours
eviction_learnings_stale 56 jours
rate_limit_window_sliding 5 min
rate_limit_max_requests 3
idempotency_bucket 5 min
lock_timeout 30 s
lock_stale_threshold 60 s (strict >60 pour stale)
clock_drift_max 500 ms
promotion_threshold_domain reuse_score >=0.3 et nb_domains>=1
promotion_threshold_global reuse_score >=0.6 et nb_domains>=2
score_round_decimals 4
reuse_score_effective_max 0.9999
reuse_score_clip_threshold_pre_round >=0.99995 => écrêtage à 0.9999
fail_closed_depth_max 3

Règle stricte reuse_score: - Calcul pré-arrondi: raw=tanh(reuse_score_brut/10). - Si raw >= 0.99995, valeur sérialisée forcée à 0.9999. - Sinon arrondi décimal 4 chiffres. - 1.0000 est interdit.

5.8 SLA temporels

Paramètre Valeur Expiration
retention_until clarifications 540 jours SUMMARY_INDEXED -> EXPIRED -> PURGED
stale learning 56 jours *_ACTIVE -> ARCHIVED
rate-limit 5 min glissantes décompte fenêtre
idempotence bucket 5 min recalcul autorisé hors bucket
lock timeout 30s ERR-295-LOCK_TIMEOUT
stale lock mtime > 60s suppression stale + retry unique
purge cron 24h purge sécurité

5.9 Machine à états

5.9.1 learning.scope

État Transitions autorisées Interdites
STORY_ACTIVE ->DOMAIN_ACTIVE, ->ARCHIVED ->GLOBAL_ACTIVE
DOMAIN_ACTIVE ->GLOBAL_ACTIVE, ->ARCHIVED ->STORY_ACTIVE
GLOBAL_ACTIVE ->ARCHIVED ->DOMAIN_ACTIVE, ->STORY_ACTIVE
ARCHIVED ->STORY_ACTIVE (manuel audité) ->DOMAIN_ACTIVE, ->GLOBAL_ACTIVE

5.9.2 clarification.lifecycle_state

État Transitions autorisées Interdites
VERBATIM_IN_MEMORY ->SUMMARY_PENDING_VALIDATION autres
SUMMARY_PENDING_VALIDATION ->SUMMARY_VALIDATED, ->REJECTED autres
SUMMARY_VALIDATED ->SUMMARY_INDEXED autres
SUMMARY_INDEXED ->EXPIRED autres
EXPIRED ->PURGED autres
REJECTED aucune ->*
PURGED aucune ->*

5.10 Migration DDL

Aucune migration DDL SQL.

5.11 Atomicité DB/queue

Non applicable (pas de flux transaction DB + queue dans PD-295).

5.12 Protection distribuée et déploiement

Sujet Contrat
Lock rwlock flock advisory local (LOCK_SH lecture, LOCK_EX écriture) appliqué à data/learnings.jsonl, data/learnings-scores.jsonl, data/learnings-injections.jsonl, data/sessions/*.jsonl via lockfiles dédiés data/learnings.jsonl.lock, data/learnings-scores.jsonl.lock, data/learnings-injections.jsonl.lock, data/sessions/current.lock; granularité 1 lock par fichier cible.
Déploiement Mono-hôte obligatoire (laptop dev OU serveur backend unique). Multi-hôte hors scope PD-295 (candidat G34).
Idempotence clé {story_id,step,operation,timestamp_bucket_5min}.
Rate-limit 3 req / 5 min / story_id.
Réconciliation lock timeout acquisition 30s puis ERR-295-LOCK_TIMEOUT; stale strict >60s, suppression + retry unique.
Rotation clé scripts/rotate-audit-hmac.sh interdit si session /gov active détectée via lock global (mtime < 2h) -> ERR-295-ROTATION_BLOCKED; lock >2h traité stale puis rotation.
Clé Vault session chargée une fois au démarrage /gov, conservée en mémoire immuable jusqu’à fin de session.
Clearing MEMORY_DEGRADED puis MEMORY_HEALTHY après 2 cycles conformes.
Non-répudiation HMAC symétrique = intégrité/authentification sur machine non compromise; administrateur avec accès Vault peut forger. Non-répudiation forte hors scope (G33).

5.13 Guards obligatoires (préconditions fail-closed)

Chaque guard est évalué avant toute opération; échec => arrêt du flux concerné, jamais warn+continue.

Guard ID Précondition Erreur
G-295-01 story_id conforme D-295-01 ERR-295-INVALID_STORY_ID
G-295-02 domain conforme D-295-02 ERR-295-INVALID_DOMAIN
G-295-03 project conforme D-295-03 ERR-295-INVALID_PROJECT
G-295-04 query et query_hash conformes ERR-295-INVALID_QUERY_LENGTH/QUERY_HASH
G-295-05 search-clarifications reçoit --domain + --project ERR-295-INVALID_DOMAIN/PROJECT
G-295-06 rate-limit admissible ERR-295-RATE_LIMIT_EXCEEDED
G-295-07 idempotence non conflictuelle ERR-295-IDEMPOTENCY_CONFLICT
G-295-08 lock rwlock acquis ERR-295-LOCK_TIMEOUT
G-295-09 timestamp_ntp_reference obtenu via sntp -t 5 pool.ntp.org (timeout 5s) ou fallback /var/db/timed/Library/Preferences/com.apple.timed.plist; puis abs(timestamp_local_monotonic - timestamp_ntp_reference) <= 500ms au démarrage /gov (avant chargement clé Vault) et avant chaque signature ERR-295-NTP_UNREACHABLE ou ERR-295-CLOCK_DRIFT_EXCEEDED
G-295-10 clé Vault disponible/valide chargée en mémoire session + trace signée écrite via helper signé ERR-295-AUDIT_KEY_UNAVAILABLE/FORMAT_INVALID ou ERR-295-TRACE_WRITE_FAILED/ERR-295-FAIL_CLOSED_RECURSION

La dérive d’horloge est revalidée en précondition de chaque événement signé; dépassement >500ms => fail-closed strict, abort /gov + alerte opérateur. Indisponibilité simultanée sntp + cache timed => ERR-295-NTP_UNREACHABLE et abort /gov.

5.14 Schéma générique d’événement audit + vecteurs HMAC

Schéma contractuel (JCS signé) :

{
  "schema_version": 3,
  "timestamp_iso8601": "2026-04-11T15:42:33.123Z",
  "story_id": "PD-295",
  "event_type": "learning_injected|veille_injected|clarification_injected|verbatim_subprocess_invoked|audit_key_rotated|clarification_purged|purge_clarifications_failed|unsigned_entry_detected|injection_failed_fail_closed",
  "brick": "B1|B2|B3|B4|B5",
  "key_version": 1,
  "payload_canonique": {},
  "sig_hmac_sha256": "..."
}

Vecteurs contractuels pré-calculés (JCS strict, champ signature exclu avant HMAC) :

V1 (nominal 5/5)
key_hex=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
payload_canonique_jcs={"brick":"B5","event_type":"learning_injected","key_version":1,"payload_canonique":{"count_configured":5,"count_effective":5,"result_ids":["PD-19-3-a1b2c3d4e5f6","PD-42-8-abcdef123456","PD-77-1-111111111111","PD-88-2-222222222222","PD-99-4-333333333333"],"source":"learning","under_corpus":false},"schema_version":3,"story_id":"PD-295","timestamp_iso8601":"2026-04-11T15:42:33.123Z"}
expected_sig=1e2893b482800032ac24a72966a09957e5baba795aa8908c908ddf7de8080f48

V4 (sous-corpus learnings 1/5)
key_hex=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
payload_canonique_jcs={"brick":"B5","event_type":"learning_injected","key_version":1,"payload_canonique":{"count_configured":5,"count_effective":1,"result_ids":["PD-10-1-aaaaaaaaaaaa"],"source":"learning","under_corpus":true},"schema_version":3,"story_id":"PD-295","timestamp_iso8601":"2026-04-11T15:42:34.123Z"}
expected_sig=737a7cb44264ba6d7dd513a7e12e0466977c0694d1e4ac5dfe34854a605c53ba

V6 (rotation clé 1->2, émis par `scripts/rotate-audit-hmac.sh` hors session `/gov`, signé avec la nouvelle clé v2)
key_hex_v1=1111111111111111111111111111111111111111111111111111111111111111
key_hex_v2=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
hmac_key_used=key_hex_v2
payload_canonique_jcs={"brick":"B3","event_type":"audit_key_rotated","key_version":2,"payload_canonique":{"archive_path":"kv/pd-295/memory-audit-hmac/archive/v1","count_configured":1,"count_effective":1,"key_version_new":2,"key_version_old":1,"under_corpus":false},"schema_version":3,"story_id":"PD-295","timestamp_iso8601":"2026-04-11T15:42:36.123Z"}
expected_sig=a0d289da4aa24d6a803ca98c812ce8df177915c8c77e28407539bb908e9269c2
verify_note=V6 se vérifie avec v2; v1 archivée pour vérifier les événements historiques signés avant rotation.

V8 (droit à l’oubli `clarification_purged --force`)
key_hex=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
payload_canonique_jcs={"brick":"B2","event_type":"clarification_purged","key_version":2,"payload_canonique":{"count_configured":6,"count_effective":6,"force":true,"scope":"story","under_corpus":false},"schema_version":3,"story_id":"PD-295","timestamp_iso8601":"2026-04-11T15:42:37.123Z"}
expected_sig=a2519bc7eca3e3b187671fda529d99858e084c15dc69bdfaf1755ca5fa286314

5.15 Signature des fichiers d’état

  • Writer unique: scripts/lib/write-signed-state.py.
  • Reader filtering: compute-learning-scores.py, promote-learnings.py, gov-learnings-inject.
  • Migration one-shot: scripts/sign-existing-state.py.
  • Rotation clé: scripts/rotate-audit-hmac.sh + contrôle anti-session active + re-signature existant + archive clé précédente.
  • Écriture manuelle autorisée via scripts/write-signed-state.py <file> <payload_json> uniquement.

5bis. Diagrammes

Diagramme d’état learning.scope

stateDiagram-v2
    [*] --> STORY_ACTIVE
    STORY_ACTIVE --> DOMAIN_ACTIVE: seuil domain
    STORY_ACTIVE --> ARCHIVED: stale
    DOMAIN_ACTIVE --> GLOBAL_ACTIVE: seuil global
    DOMAIN_ACTIVE --> ARCHIVED: stale
    GLOBAL_ACTIVE --> ARCHIVED: stale
    ARCHIVED --> STORY_ACTIVE: restore manuel audité

Diagramme d’état clarification.lifecycle_state

stateDiagram-v2
    [*] --> VERBATIM_IN_MEMORY
    VERBATIM_IN_MEMORY --> SUMMARY_PENDING_VALIDATION
    SUMMARY_PENDING_VALIDATION --> SUMMARY_VALIDATED: PO=oui
    SUMMARY_PENDING_VALIDATION --> REJECTED: PO=non
    SUMMARY_VALIDATED --> SUMMARY_INDEXED
    SUMMARY_INDEXED --> EXPIRED: date > retention_until
    EXPIRED --> PURGED

Diagramme de séquence B5 (EMPTY_BLOCK en fail-closed)

sequenceDiagram
    participant S0 as /gov-step-0
    participant B5 as gov-learnings-inject
    participant V as Vault
    participant L as data/sessions/*.jsonl

    S0->>B5: inject(story_id,domain,project,query)
    B5->>B5: guard checks + rwlock + check key session
    alt key absente en mémoire (démarrage session)
        B5->>V: read key (once at startup)
        alt Vault KO
            B5->>B5: sys.exit(1) ERR-295-AUDIT_KEY_UNAVAILABLE
            Note over S0,B5: /gov terminé, aucun retour au caller
        else Vault OK
            B5->>B5: cache key immuable en session
        end
    end
    B5->>B5: build 3 sections with count_configured/count_effective/under_corpus
    B5->>L: append signed event
    alt append OK
        B5-->>S0: injection block
    else append FAIL
        B5->>B5: fail_closed_depth++
        B5-->>S0: EMPTY_BLOCK + ERR-295-TRACE_WRITE_FAILED
    end

6. Cas d’erreur

ID Condition Réponse
ERR-295-INVALID_STORY_ID story_id invalide rejet immédiat
ERR-295-INVALID_DOMAIN domain invalide rejet immédiat
ERR-295-INVALID_PROJECT project invalide rejet immédiat
ERR-295-INVALID_QUERY_LENGTH query vide ou >500 rejet
ERR-295-INVALID_QUERY_HASH hash invalide rejet trace
ERR-295-RATE_LIMIT_EXCEEDED >3 req/5min rejet
ERR-295-IDEMPOTENCY_CONFLICT même clé, payload divergent rejet
ERR-295-LOCK_TIMEOUT lock non acquis 30s fail-closed
ERR-295-LOCK_STALE_RECOVERY_FAILED stale non récupérable fail-closed
ERR-295-NTP_UNREACHABLE sntp indisponible et cache timed indisponible fail-closed strict, abort /gov, alerte opérateur
ERR-295-CLOCK_DRIFT_EXCEEDED abs(timestamp_local_monotonic - timestamp_ntp_reference) > 500ms fail-closed strict, abort /gov, alerte opérateur
ERR-295-AUDIT_KEY_UNAVAILABLE Vault indisponible au démarrage arrêt /gov (SystemExit)
ERR-295-AUDIT_KEY_FORMAT_INVALID clé invalide au démarrage arrêt /gov (SystemExit)
ERR-295-ROTATION_BLOCKED tentative de rotation pendant session /gov active (mtime lock <2h) rotation refusée, attente fin session
ERR-295-HMAC_VERIFICATION_FAILED signature invalide ligne/trace rejetée
ERR-295-TRACE_WRITE_FAILED écriture trace impossible EMPTY_BLOCK
ERR-295-PII_DETECTED PII détectée rejet résumé
ERR-295-STATE_TRANSITION_FORBIDDEN transition interdite état inchangé
ERR-295-PURGE_VERIFICATION_FAILED purge incomplète alerte conformité
ERR-295-PURGE_FAILED échec purge hook on_story_close après retry alerte opérateur, transition Jira continue
ERR-295-MEASURE_SCRIPT_MISSING script CS absent/non-x Gate 8 NON_CONFORME
ERR-295-UNSIGNED_ENTRY ligne état non signée/invalide compteur + événement signé, exécution continue
ERR-295-FAIL_CLOSED_RECURSION tentative d’incrément fail_closed_depth à 4 arrêt process + alerte

7. Critères d’acceptation

ID Critère Observable
CA-295-01 B1 index veille + filtres sorties B1 conformes
CA-295-02 B2 persiste uniquement résumé non-PII validé; pii_ruleset_v1.yaml (validé DPO par commit signé) couvre au minimum 6 familles: 1 noms propres, 2 emails RFC 5322, 3 téléphones FR/E.164, 4 IBAN/BIC/comptes, 5 adresses postales, 6 identifiants externes (SIRET/NIR/passeport) absence verbatim persistant
CA-295-03 Rejet PO n’écrit rien artefacts inchangés
CA-295-04 Purge RGPD 6 artefacts + traces query + PD-XX-clarifications.md vérification 100%
CA-295-05 HMAC JCS valide (V1/V4/V6/V8) signatures exactes
CA-295-06 Vault KO bloque /gov ERR-295-AUDIT_KEY_UNAVAILABLE + SystemExit
CA-295-07 Pas de bloc non vide sans trace signée écrite trace KO => EMPTY_BLOCK
CA-295-08 count_configured fixe 5/3/3, count_effective borné, under_corpus exact comptage par section
CA-295-09 reuse_score formule + 4 décimales + écrêtage 0.9999 recalcul indépendant
CA-295-10 Promotion/éviction suivent §5.9 transitions conformes
CA-295-11 Transition interdite => erreur + état inchangé avant/après identique
CA-295-12 lock/idempotence/rate-limit/réconciliation/clearing opérationnels tests concurrence OK
CA-295-13 /morning affiche 3 top scores 3 lignes score=X.XXXX
CA-295-14 search-clarifications refuse sans --domain --project rejet explicite
CA-295-15 CA-295-meta (Gate 8): scripts measure-cs1..4.sh existent + test -x OK contrôle méta vert
CA-295-16 CS-1 mesuré T+30/T+60/T+90 (post-merge) rapports horodatés
CA-295-17 CS-2 mesuré T+90, cible >=30% (post-merge) rapport
CA-295-18 CS-3 mesuré 30j glissants (post-merge) rapport
CA-295-19 CS-4 mesuré sur 5 premières stories post-B5 (post-merge) rapport
CA-295-20 Aucun artefact DDL SQL inspection dépôt
CA-295-RUNTIME-01 Aucun marqueur verbatim ne fuite sur disque/session logs via subprocess B2 test UUID + grep
CA-295-STATE-01 Ligne injectée non signée ignorée au scoring/promotion/injection score inchangé + compteur unsigned
CA-295-BACKUP-01 Exclusion backup active sur artefacts PD-295 + script purge historique opérationnel tmutil isexcluded/journal script

8. Scénarios de test contractuels (résumé)

  • ST-295-01..20 conservés (nominal, erreurs, transitions, purge, lock, métriques, non-DDL).
  • ST-295-21 : isolation subprocess verbatim.
  • ST-295-22 : filtrage lignes non signées.
  • ST-295-23 : re-signature après rotation clé.
  • ST-295-24 : guards préconditions (10/10).
  • ST-295-25 : fail-closed depth borné.
  • ST-295-26 : exclusion backups PD-295.
  • ST-295-27 : frontière rétention 540/541 jours.
  • ST-295-28 : on_story_close succès + échec purge avec retry/fail-safe.

9. Hypothèses

ID Hypothèse Impact si faux
H-295-01 Vault kv/pd-295/memory-audit-hmac provisionné /gov bloqué
H-295-02 Schéma learnings-injections.jsonl #28 stable B3 invalide
H-295-03 Repos ProbatioVault lisibles corpus incomplet
H-295-04 Dérive horloge <=500ms idempotence/rate-limit fragiles
H-295-05 Purge traces session par story_id autorisée RGPD incomplet
H-295-06 Déploiement mono-hôte effectif lock non suffisant sinon (G34)

10. Points à clarifier

ID Point Donnée manquante
Q-295-01 Référence épique détaillée ID Epic finale interne
Q-295-02 Chemin de livraison final chemin cible finalisé
Q-295-05 Validation DPO des patterns PII pii_ruleset_v1 approbation conformité
Q-295-06 Bloc {{LEARNINGS}} d’orchestration contenu d’injection final

Références

  • Epic : tooling
  • JIRA : PD-295
  • Repos : ProbatioVault-ia-governance, ProbatioVault-doc, ProbatioVault-app, ProbatioVault-backend, ProbatioVault-infra, ProbatioVault-site
  • Besoin : docs/epics/tooling/PD-295-memoire-vivante-5-briques/PD-295-besoin.md
  • Archive cycle 1 : docs/epics/tooling/PD-295-memoire-vivante-5-briques/cycle-1/

Changelog cycle3-v2 → cycle3-v3

  • F-01 : INV-295-RUNTIME-01 explicite unset CLAUDECODE (learning universel 2026-02-20)
  • F-02 : INV-295-STATE-04 hook on_story_close → purge-clarifications --mode eligible-only (pas de purge immédiate non expirée)
  • F-03 : §5.12 source NTP sntp/timed + ERR-295-NTP_UNREACHABLE
  • F-04 : rotate-audit-hmac lock anti-session + ERR-295-ROTATION_BLOCKED
  • F-05 : §5.12 rwlock portée (4 fichiers + granularité + timeout + stale)
  • F-06 : V6 signé avec la nouvelle clé, archive v1 pour vérification historique
  • F-07 : matrice ST↔TC complétée (INV-295-STATE-01..04, RUNTIME-04)