docs: add POC specs, handoffs, and research notes
This commit is contained in:
210
docs/POC/SPECS_ENDPOINT_PERSIST_2026-06-01.md
Normal file
210
docs/POC/SPECS_ENDPOINT_PERSIST_2026-06-01.md
Normal 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.*
|
||||
Reference in New Issue
Block a user