docs: add POC specs, handoffs, and research notes

This commit is contained in:
Dom
2026-06-02 16:28:34 +02:00
parent 18ed6cb751
commit f2e9aac6b7
86 changed files with 27615 additions and 25 deletions

View File

@@ -0,0 +1,210 @@
# SPECS — POST /api/v1/lea/competences/candidate/persist
> **DRAFT specs pour implémentation — pas encore appliqué — relecture Codex/Dom attendue**
>
> Date : 2026-06-01
> Auteur : Claude (agent Plan)
> Statut : DRAFT — aucun code écrit, aucun fichier de prod modifié
> Cible : `agent_v0/server_v1/api_stream.py`
## Contexte
L'audit Claude du 2026-06-01 17h00 confirme l'absence de ce endpoint. Le cycle Shadow apprentissage actuel s'arrête à `/api/v1/shadow/build` (renvoie le `workflow_ir` en mémoire, ne le persiste pas).
Ce endpoint clôt le cycle Léa-first (6 phases) :
`start → stop → understanding → feedback (N×) → build → **persist**`
Sources :
- `docs/coordination/inbox_codex/2026-06-01_1620_claude-to-codex_ARCHI-apprentissage-Lea-first-validee-Dom.md`
- `docs/coordination/inbox_codex/2026-06-01_1745_claude-to-codex_ADDENDUM-archi-Lea-lecture-semantique-agent-externe.md`
- `docs/coordination/inbox_codex/2026-06-01_1800_claude-to-codex_PRECISIONS-desapprentissage-regle-or-HDS.md`
Schéma YAML de référence : `data/competences/candidate/key_win_r_wait_explorer_exe.yaml`
## Sommaire
1. Contrat de l'endpoint
2. Validation pré-écriture
3. Génération YAML
4. Journalisation
5. Cas particuliers
6. Sécurité
7. Tests à prévoir
8. Estimation effort implémentation
---
## 1. Contrat de l'endpoint
**Méthode / URL** : `POST /api/v1/lea/competences/candidate/persist`
**Headers requis** :
- `Authorization: Bearer <token-poste>` (cohérent avec patch P0 révocation token Codex)
- `Content-Type: application/json`
**Payload entrée (JSON)** :
```json
{
"name": "Saisir texte Word",
"machine_id": "DESKTOP-58D5CAC_windows",
"session_id": "sess_20260601T180000_abc123",
"workflow_ir": { "steps": [...], "preconditions": [...], "success_marker": {...} },
"parameters": [ { "name": "texte", "type": "string", "required": true } ],
"external_decision": {
"agent_id": "t2a_v2_codeur",
"decision_contract": "..."
},
"annotations_semantiques": {
"intent_fr": "saisir le texte transmis dans Word"
},
"learning_metadata": {
"persist_id": "uuid-v4-client",
"partial": false,
"dropout_reason": null,
"source_phase": "shadow_build"
}
}
```
**Codes HTTP** :
- `201 Created` — YAML écrit, audit journalisé
- `400 Bad Request` — payload invalide (schema, slug, agent externe inconnu)
- `401 Unauthorized` — token absent / invalide / révoqué
- `403 Forbidden``machine_id` du payload ≠ machine du token
- `409 Conflict` — slug existe déjà dans `data/competences/candidate/`
- `429 Too Many Requests` — rate limit dépassé
- `500 Internal Server Error` — échec atomic write ou roundtrip
**Payload sortie (201)** :
```json
{
"competence_id": "saisir_texte_word",
"yaml_path": "data/competences/candidate/saisir_texte_word.yaml",
"learning_state": "candidate",
"persist_id": "uuid-v4-serveur",
"audit_entry_id": 4271
}
```
## 2. Validation pré-écriture
**Slugification** (`name``competence_id`) :
- Conversion kebab-case ASCII (translittération accents)
- Lowercase, espaces → `_`, chars non `[a-z0-9_]` retirés
- Longueur ≤ 80 caractères, ≥ 3 caractères
- Pattern regex final : `^[a-z][a-z0-9_]{2,79}$`
- Si collision avec un fichier existant en `candidate/` : **409 sans suffixage automatique**
**Schema YAML cohérent** — champs obligatoires : `schema_version, id, name, version, learning_state, intent, parameters, preconditions, methods, success_marker, failure_message_template, promotion, generalisation, failure_log, created_at, last_updated_at, methods_execution`
**WorkflowIR** :
- `workflow_ir.steps` non vide **sauf si** `learning_metadata.partial == true`
- Chaque step possède `kind` ∈ primitives connues (réutiliser registry primitives existante)
**Idempotence** :
- `persist_id` (UUID v4 client) déduit la clé d'idempotence
- Si `persist_id` déjà présent dans `persist_audit.jsonl` → renvoyer `200 OK` avec le payload de l'entrée précédente (pas 409)
## 3. Génération YAML
**Chemin cible** : `data/competences/candidate/<slug>.yaml`
**Atomic write** :
1. Écrire dans `data/competences/candidate/.<slug>.yaml.tmp.<persist_id>`
2. `os.rename()` vers `<slug>.yaml` (atomique POSIX)
3. Si collision détectée entre validation et rename → 409, supprimer le `.tmp`
**Post-write roundtrip** :
- Recharger via `core.competences.catalog.load_competence(slug)`
- Vérifier que `competence.to_dict()` est cohérent
- Si KO → 500, supprimer le fichier, log critique
## 4. Journalisation
**Audit principal** — append-only `data/competences/persist_audit.jsonl` :
```json
{
"persist_id": "uuid-v4",
"timestamp": "2026-06-01T18:05:32+02:00",
"machine_id": "...",
"session_id": "...",
"competence_name": "Saisir texte Word",
"competence_id": "saisir_texte_word",
"yaml_path": "data/competences/candidate/saisir_texte_word.yaml",
"learning_state": "candidate",
"partial": false,
"dropout_reason": null,
"external_agent_id": null,
"audit_entry_id": 4271
}
```
**Apprentissages incomplets** — si `partial: true`, entrée *en plus* dans `data/competences/incomplete_learnings.jsonl` avec `dropout_reason` obligatoire.
**Append-only strict** : ouvrir en mode `a`, jamais réécrire. Verrou `fcntl.flock` par fichier.
## 5. Cas particuliers
| Cas | Comportement |
|---|---|
| `learning_metadata.partial == true` | accepté, `learning_state` forcé `incomplete`, entrée double dans `incomplete_learnings.jsonl`, `dropout_reason` obligatoire (sinon 400) |
| Payload demande `learning_state: stable` | **rejet silencieux** — forcer `candidate` (jamais stable par persist direct — règle d'or HDS) |
| Aucun `learning_state` fourni | défaut `candidate` |
| `external_decision.agent_id` inconnu de `ExternalDecisionClient` registry | **400** avec liste agents valides |
| `workflow_ir.steps` vide et `partial == false` | **400** |
| YAML déjà présent en `data/competences/stable/` ou `supervised/` avec même slug | **409** (collision cross-states) |
| Désapprentissage en cours sur le slug | **409** `detail: "désapprentissage actif"` |
## 6. Sécurité
- **Token Bearer obligatoire** — réutiliser middleware d'auth du patch P0 révocation token. Pas de fallback "agent local".
- **Couplage machine_id ↔ token** : token référence `machine_id` ; si `payload.machine_id != token.machine_id`**403**.
- **Rate limit** : 10 persists/min/`machine_id` (in-memory token-bucket). Au-delà → **429** avec `Retry-After`.
- **Pas d'écriture hors `data/competences/candidate/`** : path traversal interdit (slug strict, jamais lu d'un champ user).
- **Logs** : ne jamais journaliser le token. `persist_id` et `machine_id` only.
- **Règle d'or HDS** : aucune donnée patient ne doit transiter — refuser le payload si `workflow_ir` ou `annotations_semantiques` contient pattern PII détecté. Détection MVP via regex paramétrable.
## 7. Tests à prévoir
**Unit (`tests/unit/test_competence_persist.py`)** :
- `test_slug_generation_normal` / `_with_accents` / `_too_short` / `_collision_409`
- `test_yaml_schema_required_fields_present`
- `test_atomic_write_then_rename`
- `test_post_write_roundtrip_ok` / `_corrupt_yaml_500`
- `test_idempotence_same_persist_id_returns_previous`
- `test_partial_true_forces_incomplete_state`
- `test_payload_stable_forced_to_candidate`
- `test_external_agent_unknown_400`
- `test_pii_detected_rejected`
**Integration (`tests/integration/test_shadow_full_cycle.py`)** :
- Cycle complet : `shadow/start → POST événements → shadow/stop → shadow/build → persist`
- Vérifier YAML présent, `persist_audit.jsonl` enrichi
- Cas partial : double entrée `incomplete_learnings.jsonl`
**Sécurité (`tests/security/test_persist_auth.py`)** :
- `test_no_token_401`
- `test_token_revoked_401`
- `test_machine_id_mismatch_403`
- `test_rate_limit_11th_call_429`
- `test_path_traversal_in_name_blocked`
## 8. Estimation effort implémentation
| Item | Volume |
|---|---|
| Handler FastAPI + Pydantic models | ~80-120 lignes (`api_stream.py`) |
| Helpers (slugify, atomic_write, audit_append) | ~40 lignes (nouveau module `core/competences/persist.py`) |
| Tests unit + integration + sécu | ~150 lignes |
**Effort total** : faible-moyen — **0.5 à 1 jour**. Pas de dépendance externe nouvelle, réutilise `core.competences.catalog` et le pattern atomic-write de C-γ.
**Points d'attention pour la review** :
- Confirmer comportement collision (409 strict vs suffixage `_v2`)
- Valider mapping `workflow_ir.steps[*].kind``yaml.methods[*].kind` avec registry primitives à jour
- Décider si rate limit par `machine_id` ou par token
- Statuer sur format `audit_entry_id` (auto-incrément vs hash)
---
*Fin DRAFT — relecture Codex/Dom attendue avant kick-off implémentation.*