PD-295 — Spécification (cycle 2 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 (RFC 8785), conformité RGPD, isolation runtime des subprocess manipulant du verbatim PO, et signature des fichiers d’état.
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.
- Signature HMAC des fichiers d’état + filtrage lecture fail-closed.
- 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. |
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. |
fail_closed_depth | Compteur thread-local par invocation step 0, initialisé à 0, incrémenté sur fail-closed imbriqué. |
| 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 clarifications.jsonl, index FAISS, embeddings NPY, cache JSON, traces query sessions liées au story_id. |
| INV-295-05 | Purge rétention strictement à 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 indisponible/invalide au chargement de session => arrêt /gov par SystemExit(1). Tentative d’alerte signée avec dernière clé valide en mémoire; sinon log local UNSIGNED_DEGRADED. |
| 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 | Lock flock advisory obligatoire sur learnings.jsonl et learnings-scores.jsonl, 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 | Récursion fail-closed bornée: fail_closed_depth autorisé jusqu’à 3 inclus; tentative de 4e fail-closed (depth=4) => arrêt process + alerte signée ERR-295-FAIL_CLOSED_RECURSION. |
| INV-295-18 | Transition MEMORY_DEGRADED -> MEMORY_HEALTHY autorisée uniquement après 2 cycles consécutifs conformes post-dégradation. |
| INV-295-RUNTIME-01 | Subprocess B2 verbatim: entrée via stdin uniquement, jamais argv; stdout/stderr redirigés vers buffer mémoire; CLAUDE_DISABLE_SESSION_LOG=1 forcé. |
| INV-295-RUNTIME-02 | Aucun fichier disque n’est autorisé pour transporter du verbatim PO; transit uniquement via buffers mémoire Python et pipes OS (os.pipe). Aucun fallback tmpfs sur 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 | config/pii_ruleset_v1.yaml DOIT être validé DPO via commit signé portant le tag dpo-approved; hash commit référencé dans le dépôt de conformité. |
| 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. |
| INV-295-STATE-05 | Écriture append des fichiers d’état signés sous flock write-lock; lecture sans lock. Si ligne >4096 bytes (PIPE_BUF), warning + append sous write-lock strict avec fsync. |
| INV-295-BACKUP-01 | Artefacts purgeables PD-295 exclus des backups automatiques via .nobackup ou tmutil addexclusion; historique existant purgeable via scripts/purge-backups.sh. |
| INV-295-HOOK-01 | Hook on_story_close(story_id, final_state) appelé sur REJECTED, DONE_WITH_ANOMALY, DONE, et déclenche scripts/purge-clarifications.py --story {story_id} de façon idempotente. |
| INV-295-LS-01 | STORY_ACTIVE -> DOMAIN_ACTIVE autorisée si reuse_score>=0.3 et nb_domains>=1. |
| 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 | 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; règles PII (email, phone, IBAN, adresse explicite) via pii_ruleset_v1 | 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 d’identifiants | 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,unsigned_entry_detected,injection_failed_fail_closed,audit_key_unavailable_fatal,fail_closed_recursion | 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 | 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 | entier 0..4; 4 atteignable uniquement à l’instant de l’abort récursif | ERR-295-FAIL_CLOSED_RECURSION |
5.2 Flux nominal B1 — Index veille
- Collecte
ProbatioVault-doc/docs/veille/**/*.md. - Production
data/veille.jsonl conforme D-295-15/16. - Index FAISS dimension 768.
- Recherche avec filtres
--impact et --verdict. - Erreurs de format journalisées sans bloquer les lignes valides.
- Toute ligne d’état écrite (sessions) est signée
sig_hmac_sha256.
5.2.4 Chargement de la clé Vault (session)
- La clé HMAC est chargée une seule fois au démarrage de la session
/gov. - Si Vault est indisponible au chargement ou renvoie un format invalide,
/gov s’arrête immédiatement (SystemExit(1)), avec code opérateur ERR-295-AUDIT_KEY_UNAVAILABLE ou ERR-295-AUDIT_KEY_FORMAT_INVALID. - Si Vault redevient disponible pendant la session, aucune relecture n’est faite; la clé de session reste immuable jusqu’au prochain démarrage.
- Cette règle garantit qu’une session donnée n’utilise qu’un seul contexte clé/version.
5.3 Flux nominal B2 — Clarifications structurées non-PII + isolation runtime
- Step 0 collecte 4 réponses PO en mémoire volatile.
- Appel
scripts/b2-sanitizer.py avec verbatim sur stdin uniquement. stdout/stderr subprocess redirigés vers buffer mémoire (pipe + buffer Python), jamais fichier. - Environnement subprocess impose
CLAUDE_DISABLE_SESSION_LOG=1 et TERM=dumb. - Aucun fichier temporaire disque.
- Résumé structuré non-PII produit et validé PO (binaire).
- Si validé: écriture
PD-XX-clarifications.md + indexation; sinon REJECTED sans persistance. - Événement signé
verbatim_subprocess_invoked émis sans verbatim. - Effacement des buffers verbatim en sortie.
5.3.3 Architecture subprocess B2 sanitizer
gov-step-0 (process parent)
1) Collecte verbatim en mémoire (io.StringIO)
2) Prépare pipe stdin avec verbatim
3) Lance subprocess "b2-sanitizer.py"
b2-sanitizer.py (process enfant)
1) Lit verbatim depuis stdin
2) Lance sous-subprocess "claude -p" avec:
- stdin = verbatim
- stdout = pipe vers b2-sanitizer
- env = CLAUDE_DISABLE_SESSION_LOG=1, TERM=dumb
- argv = ["-p","--append-system-prompt","SANITIZE: extraire intention, supprimer PII"]
3) Lit le résumé via pipe
4) Écrit le résumé sur stdout (pipe retour)
gov-step-0 (retour)
1) Lit le résumé via pipe
2) Écrase le buffer verbatim en mémoire
3) Écrit uniquement le résumé structuré sur disque
5.4 Flux nominal B3 — Scoring de réutilisation + état signé
- Lecture
learnings.jsonl, learnings-injections.jsonl, metrics.jsonl. - Vérification signatures sur lignes
learnings-injections; lignes non signées/invalides ignorées. - Compteur
ERR-295-UNSIGNED_ENTRY incrémenté pour chaque ligne ignorée. - Calcul
reuse_score_brut, puis reuse_score. - Écriture
data/learnings-scores.jsonl via scripts/lib/write-signed-state.py. - Tri secondaire
search-learnings sur reuse_score. /morning affiche 3 entrées top score format score=X.XXXX. - Migration initiale:
scripts/sign-existing-state.py re-signe les entrées pré-B3.
- Migration initiale
scope:story si absent. - Promotion
story->domain et domain->global selon seuils. - Éviction stale (
nb_injections==0, âge >56j) vers archive. - Lecture des fichiers d’état signés; lignes non signées filtrées.
- Restauration uniquement via
scripts/restore-learning.py (audit signé). - Exclusion des
archived de l’index actif.
5.6 Flux nominal B5 — Injection unifiée + fail-closed borné
- Acquisition locks lecture (INV-295-14).
- Recherches:
- learnings (
top_k=5) - veille (
top_k=3) - clarifications (
top_k=3, --domain et --project obligatoires) - Pour chaque source:
count_configured fixé (5/3/3), count_effective=min(configured, available_after_filter), under_corpus=true si insuffisant. - Construction bloc injection 3 sections.
- Construction événement audit conforme schéma générique §5.14.
- Signature HMAC JCS et écriture trace signée en session via helper signé.
- Si écriture réussie: retour bloc injection.
- Si échec écriture trace:
ERR-295-TRACE_WRITE_FAILED, fail_closed_depth +=1, retour EMPTY_BLOCK tant que fail_closed_depth <=3. - Si tentative de 4e fail-closed (
fail_closed_depth==4): événement fail_closed_recursion signé (ou UNSIGNED_DEGRADED si clé absente), puis arrêt process SystemExit(1) avec ERR-295-FAIL_CLOSED_RECURSION. - Si clé audit indisponible/invalide au moment critique: tentative d’événement
audit_key_unavailable_fatal signé avec dernière clé valide en mémoire; sinon log local UNSIGNED_DEGRADED; arrêt process immédiat.
5.6.10 Scope du compteur fail_closed_depth
- Scope: par invocation de step 0, thread-local.
- Initialisation:
0 au début de l’invocation. - Incrément: à chaque fail-closed imbriqué dans le thread courant.
- Réinitialisation:
0 en fin d’invocation (succès ou échec). - Pas de persistance disque, pas de partage inter-invocations.
- Implémentation de référence:
threading.local().
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) |
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_accepted | 3 |
fail_closed_depth_abort | 4 |
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 Hooks de cycle de vie des stories
- Hook:
on_story_close(story_id, final_state). - Déclenchement: transitions vers
REJECTED, DONE_WITH_ANOMALY, DONE. - Implémentation de référence:
scripts/lib/story-hooks.sh, appelé depuis .claude/commands/gov.md lors de la transition Jira finale. - Action contractuelle: appel
scripts/purge-clarifications.py --story {story_id}. - Idempotence: plusieurs appels successifs doivent produire le même état final (sans double-effet indésirable).
Le bloc injecté DOIT respecter la forme suivante (lignes et sections obligatoires):
## Mémoire pertinente pour cette story
### Learnings (count_configured=5, count_effective={N}, under_corpus={bool})
- [{story}] [{tags}] {learning_text} (scope: {scope}, score: {reuse_score:.4f})
### Veille (count_configured=3, count_effective={N}, under_corpus={bool})
- [{date}] [{impact_pv}] {title}
### Clarifications passées (count_configured=3, count_effective={N}, under_corpus={bool}, même domaine)
- [{story}, {date}] Priorités : {summary_priorities_one_line}
5.10 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.11 Machines à états
5.11.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.11.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.12 Migration DDL
Aucune migration DDL SQL.
5.13 Atomicité DB/queue
Non applicable (pas de flux transaction DB + queue dans PD-295).
5.14 Schéma générique d’événement audit + vecteurs HMAC
Schéma contractuel signé (JCS strict, champ signature exclu avant HMAC):
{
"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|unsigned_entry_detected|injection_failed_fail_closed|audit_key_unavailable_fatal|fail_closed_recursion",
"brick": "B1|B2|B3|B4|B5",
"key_version": 1,
"payload_canonique": {},
"sig_hmac_sha256": "..."
}
Vecteurs contractuels pré-calculés (utilisés par TC-NOM-06):
V1 (nominal B5 5/3/3 plein)
key_hex=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
payload_canonique_jcs={"brick":"B5","event_type":"learning_injected","key_version":1,"payload_canonique":{"sections":{"clarification":{"count_configured":3,"count_effective":3,"result_ids":["PD-090","PD-091","PD-092"],"under_corpus":false},"learning":{"count_configured":5,"count_effective":5,"result_ids":["PD-101","PD-102","PD-103","PD-104","PD-105"],"under_corpus":false},"veille":{"count_configured":3,"count_effective":3,"result_ids":["VE-201","VE-202","VE-203"],"under_corpus":false}}},"schema_version":3,"story_id":"PD-295","timestamp_iso8601":"2026-04-11T15:42:33.123Z"}
expected_sig=f92f494419827c18d40d69ca1d606e015ba72618056a9c8863b96f60e1a5240b
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-042"],"under_corpus":true},"schema_version":3,"story_id":"PD-295","timestamp_iso8601":"2026-04-11T15:42:34.123Z"}
expected_sig=84aa1fc05652688b4b426cb7b34f3563372d7225a002ba0ab673b4ed609e3a5a
V6 (rotation de clé 1 -> 2)
key_hex=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
payload_canonique_jcs={"brick":"B3","event_type":"audit_key_rotated","key_version":2,"payload_canonique":{"archive_path":"kv/pd-295/memory-audit-hmac/archive/1","new_key_version":2,"previous_key_version":1},"schema_version":3,"story_id":"PD-295","timestamp_iso8601":"2026-04-11T15:42:35.123Z"}
expected_sig=377b66b089c4e337405c2d48ab93b0950eded52e758ee7a2b5acf25c6d2f60b8
V8 (purge clarification --force, droit à l’oubli)
key_hex=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
payload_canonique_jcs={"brick":"B2","event_type":"clarification_purged","key_version":1,"payload_canonique":{"count_configured":3,"count_effective":0,"force":true,"result_ids":[],"story_id":"PD-295","under_corpus":true},"schema_version":3,"story_id":"PD-295","timestamp_iso8601":"2026-04-11T15:42:36.123Z"}
expected_sig=29262974fd91d2e0812bc52b0f83012ace1a19ca53b7bd69620a3a05699de17a
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 + re-signature existant + archive clé précédente. - Écriture manuelle autorisée via
scripts/write-signed-state.py <file> <payload_json> uniquement.
5.16 Protection distribuée et déploiement
| Sujet | Contrat |
| Lock | flock advisory local sur fichiers ciblés. |
| Fichiers d’état signés | write-lock append obligatoire, lecture lock-free. |
| Déploiement | Mono-hôte obligatoire. Multi-hôte hors scope PD-295 (G34). |
| Idempotence | clé {story_id,step,operation,timestamp_bucket_5min}. |
| Rate-limit | 3 req / 5 min / story_id. |
| Réconciliation lock | stale strict >60s, suppression + retry unique. |
| Clearing | MEMORY_DEGRADED puis MEMORY_HEALTHY après 2 cycles conformes. |
| Non-répudiation | HMAC symétrique: intégrité/authentification sur machine non compromise; non-répudiation forte hors scope (G33). |
5.17 Guards obligatoires (préconditions fail-closed)
| 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 acquis | ERR-295-LOCK_TIMEOUT |
| G-295-09 | clé Vault disponible et valide | ERR-295-AUDIT_KEY_UNAVAILABLE/FORMAT_INVALID |
| G-295-10 | trace signée écrite avec helper signé | ERR-295-TRACE_WRITE_FAILED ou ERR-295-FAIL_CLOSED_RECURSION |
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 (arrêt /gov si clé indisponible)
sequenceDiagram
participant S0 as /gov-step-0
participant B5 as gov-learnings-inject
participant V as Vault
participant L as data/sessions/*.jsonl
participant O as local ops log
S0->>B5: inject(story_id,domain,project,query)
B5->>B5: guard checks + locks + build sections
B5->>V: read session key
alt Vault/clé indisponible ou corrompue
alt last_valid_key disponible en mémoire
B5->>L: append signed audit_key_unavailable_fatal
else aucune clé valide
B5->>O: append marker UNSIGNED_DEGRADED
end
B5->>B5: SystemExit(1)
Note over S0: aucun retour; processus /gov arrêté
else clé disponible
B5->>L: append signed event
alt append OK
B5-->>S0: injection block
else append FAIL
B5->>B5: fail_closed_depth++
alt fail_closed_depth <= 3
B5-->>S0: EMPTY_BLOCK + ERR-295-TRACE_WRITE_FAILED
else fail_closed_depth == 4
B5->>L: append signed fail_closed_recursion (ou UNSIGNED_DEGRADED)
B5->>B5: SystemExit(1)
Note over S0: aucun retour; processus /gov arrêté
end
end
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-AUDIT_KEY_UNAVAILABLE | Vault indisponible au chargement session | arrêt /gov |
| ERR-295-AUDIT_KEY_FORMAT_INVALID | clé invalide | arrêt /gov |
| 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-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 4e fail-closed imbriqué (depth=4) | arrêt process + alerte sécurité |
| ERR-295-FAIL_CLOSED_DEPTH_EXCEEDED | alias legacy non émis en v3 | compat lecture historique |
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é | absence verbatim persistant |
| CA-295-03 | Rejet PO n’écrit rien | artefacts inchangés |
| CA-295-04 | Purge RGPD 5 artefacts + traces query | vérification 100% |
| CA-295-05 | HMAC JCS valide (V1/V4/V6/V8) | signatures exactes |
| CA-295-06 | Vault KO ou clé invalide bloque /gov | arrêt process explicite |
| 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.11 | 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 |
| CA-295-21 | Hook on_story_close déclenché sur REJECTED/DONE_WITH_ANOMALY/DONE et idempotent | purge appelée 3 états, sans double-effet |
| CA-295-22 | Clé Vault immuable pendant la session | même key_version sur session |
| CA-295-23 | pii_ruleset_v1 validé DPO | fichier + commit signé taggé dpo-approved |
| CA-295-24 | Bloc {{LEARNINGS}} respecte le template canonique §5.9 | regex ligne par ligne |
| CA-295-25 | MEMORY_DEGRADED -> MEMORY_HEALTHY après 2 cycles conformes | scénario E2E validé |
8. Scénarios de test contractuels (résumé)
- ST-295-01..26 conservés.
- ST-295-27: arrêt process sur clé Vault invalide avec absence de retour au caller.
- ST-295-28: hook
on_story_close 3 états + idempotence. - ST-295-29: stabilité clé Vault sur session complète.
- ST-295-30: attestation DPO
pii_ruleset_v1. - ST-295-31: validation regex template
{{LEARNINGS}}. - ST-295-32: transition E2E
MEMORY_DEGRADED -> MEMORY_HEALTHY.
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 clarifiés (clos)
| ID | Décision |
| Q-295-01 | Epic interne figée à tooling. |
| Q-295-02 | Chemin de livraison figé: docs/epics/tooling/PD-295-memoire-vivante-5-briques/. |
| Q-295-05 | pii_ruleset_v1 matérialisé en config/pii_ruleset_v1.yaml + validation DPO commit signé tag dpo-approved. |
| Q-295-06 | Bloc {{LEARNINGS}} canonique figé en §5.9. |
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 cycle2-v2 → cycle2-v3
- B-01 : borne fail_closed_depth alignée (0..4 ouvert à 4 pour abort)
- B-02 : vecteurs HMAC V1/V4/V6/V8 recalculés avec schéma v3 {count_configured/effective/under_corpus}
- B-03 : diagramme B5 aligné sur arrêt /gov (sys.exit)
- M-01 : tmpfs interdit macOS, buffer mémoire Python exclusif
- M-02 : architecture subprocess b2-sanitizer contractualisée §5.3.3
- M-03 : hook on_story_close ancré §5.8
- M-04 : scope fail_closed_depth thread-local par invocation §5.6.10
- M-05 : clé Vault chargée une fois par session §5.2.4
- M-06 : locks fcntl.flock étendus aux fichiers d'état signés
- Q-295-05 : pii_ruleset_v1 attestation DPO via commit signé
- Q-295-06 : template YAML bloc d'injection §5.9