docs: Architekturplan, ADRs, Spezifikationen und Umsetzungsplan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 14:00:36 +02:00
co-authored by Claude Opus 5.5
commit fef930d176
15 changed files with 854 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
# SPEC-lifecycle: Kandidaten, Graduierung, Training
Pakete: `knaster.lifecycle`, `knaster.graduation`.
## 1. Kandidat
```
id, kind: base_model | adapter | quantization | embedding_model | engine
slot: str # betroffener Slot (bei adapter: Slot, dessen Basis passt)
target: {...} # z. B. {base_model: "qwen3-14b-awq"} oder {cluster: "rechnungen"}
baseline: {pass_rate?} # Messwert der aktuellen Version
state, metrics{}, history[{ts, from, to, note}]
```
## 2. Zustände
```
proposed ──(Freigabe)──▶ building ──report(build_ok)──▶ evaluating
evaluating ──report(eval: pass_rate ≥ baseline − tolerance)──▶ shadow (sonst ▶ rejected)
shadow ──report(shadow: agreement ≥ min_agreement, n ≥ min_n)──▶ canary (sonst ▶ rejected)
canary ──report(canary: pass_rate ≥ baseline − tolerance, n ≥ min_n)──▶ awaiting_promotion (sonst ▶ rejected)
awaiting_promotion ──(Freigabe)──▶ promoted
promoted ──rollback──▶ rolled_back
jeder Zustand ──report(error)──▶ failed
```
Grenzwerte stehen in `config/lifecycle.yaml`: `tolerance` 0,02, `min_agreement` 0,85, `min_n` 50, `canary_share` 0,1.
Die Freigaben laufen über die Policy (`create_candidate`, `promote_candidate`). **Promote** schreibt in den Registry-Laufzeitzustand bzw. in die Adapterliste:
- `base_model`: Slot `base_model` wird umgestellt, alle Adapter der alten Basis werden `retired`, und für jeden Cluster mit Adapter entsteht ein Kandidat `adapter` (Neutraining).
- `adapter`: Der neue Adapter wird `active`, der bisher aktive für denselben Cluster und dieselbe Basis wird `retired` (bleibt als Rollback-Ziel erhalten).
- **Rollback** stellt den Zustand vor dem Promote wieder her (gespeichert in `history`).
Die Jobs selbst (Download, Quantisierung, Training, Eval) laufen **extern**, als Job-Runner auf `gpu-flex`. Sie melden ihr Ergebnis per `POST /v1/lifecycle/candidates/{id}/report`. Der `JobRunner` im Service stößt den Job an: Im Dry-Run wird nur protokolliert, sonst geht ein Webhook an n8n bzw. den Runner.
## 3. Graduierung (`knaster.graduation`)
`graduate(skill, variants[], run_fn, verify_fn, threshold=0.9, min_variants=10) -> GraduationReport`
- Jede Variante wird mit `stage = 1` auf dem Skill-Slot ausgeführt (`run_fn`) und geprüft (`verify_fn`).
- `passed = pass_rate ≥ threshold` und `n ≥ min_variants`.
- Bei Erfolg entsteht ein Vorschlag `promote_skill{skill, stage: 1}` (Policy: approve), bei Misserfolg ein Bericht mit den fehlgeschlagenen Varianten. Diese gehen an das starke Modell, das den Skill überarbeitet.
- Die Varianten erzeugt das starke Modell (Hermes) und schickt sie an `POST /v1/skills/{name}/graduate`.
## 4. Adapter-Training (extern, Vertrag)
Eingang für den Trainingsjob: Export `cases` (split = train, passed = true, skill.cluster = X) plus optional `preference_pairs`. Ausgang: Adapter-Verzeichnis und `report(build_ok, adapter_name)`. Die Eval nutzt die Fälle mit split = eval desselben Clusters.