Files
creator3/DESIGN.md
2026-07-23 16:07:36 +02:00

211 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: creator3 — Themen-Guide-Generator (Neubau unter /home/arbeit/projects/creator3)
STATUS: FINAL. Basis: 10 Recherche-Agenten (Befunde R1-R11 im Anhang) + 1 Design-Agent.
## Kontext
creator3 ersetzt creator2 für den Themen-Modus: Guides zu Themen (Symfony, Kommunikation, Psychologie) aus Web-Recherche. Kernziel: Abdeckung nahe 100 % (Lücken minimieren), dann Länge minimieren (Dopplungen minimieren). Nur MiniMax-API als LLM. Neu ggü. creator2: Pipeline als konfigurierbarer Graph (pipeline.yaml), Agenten bekommen Skill-Dateien (Rolle/Aufgabe/Stil), Graphensicht statt Kanban, Gates ÜBERALL hart, Budget default AN. uni-Modus, Artefakte (Flashcards/Üben) und Level-Durchgänge entfallen. creator2 nur Lesereferenz. NIEMALS git commit.
## Stack
Python 3.12 + FastAPI + SQLite (WAL, EINE Writer-Connection) + Vue 3/Vite. LLM: MiniMax-M3 via `https://api.minimax.io/anthropic/v1/messages` (thinking IMMER explizit: disabled für Extraktion/Judge, adaptive für Writer). Suche: Wikipedia de+en immer + Kette Tavily→Brave→ddgs (Circuit Breaker). Extraktion: trafilatura ≥2.1 markdown + ftfy NFC (einmalig vor Snapshot). Frontend: markdown-it + Shiki (statt marked/hljs), Graph als Eigenbau (HTML-Knoten + SVG-Kanten + @panzoom/panzoom).
## Pipeline-Graph (24 Knoten, 5 Stufen; Stufe = Spalte in Graphensicht)
Stufe 1 korpus: recherche_plan (llm) → suche (system) → laden (system) → soll_extraktion (llm, 40k-Chunks) → soll_konsens (llm, Barriere; Judge gruppiert, CODE zählt ≥2 Quellen) → gate_korpus.
Stufe 2 inventar: atom_extraktion (llm, 6k-Abschnitte × 2 Reader) → anker_fix (llm; erst det. 5-Stufen-Re-Match) → dedup (llm, Barriere; Auto-Merge Anker-Overlap ≥0.5 + Kandidaten-Kanäle + 2er-Panel einstimmig) → soll_zuordnung (llm) → luecke (llm; art nachextraktion|freispruch) → gate_inventar (Coverage: jeder bestätigte Soll-Punkt ≥1 Atom oder Freispruch — deterministisch).
Stufe 3 struktur: lernziele → bausteine (Band 4-8) → ordnung (Barriere; themen_schnitt + permutation, exakt validiert, Prior-Fallback) → gate_struktur (Coverage: jeder Soll-Punkt ≥1 Baustein).
Stufe 4 guide: writer (DELIMITED ===TEXT===, „bekannt"-Kontext, Verweis statt Wiederholung) + kapitel_intro → det_pruefung (system) → llm_pruefung (2er-Panel, qa_hash fail-closed) → fix (Cap 3) → redundanz (Barriere; Satzpaar-Jaccard ≥0.6 über Kapitelgrenzen → 2er-Panel) → gate_guide (3er-Klärung für Eskalationen).
Stufe 5: montage (system; Marker-Einmaligkeit, Anker-Links, topic=fertig).
Rücklauf-Kanten (nur von Gates, Task-Rezepte mit Runde+1 im Item-Schlüssel): gate_korpus→recherche_plan (neue Lenses, Cap 4); gate_inventar→suche (Web-Nachrecherche für Lücken, Queries deterministisch aus Punkt+Zitat gegroundet, Cap 1). Reparatur-Kanten: gate_korpus→soll_konsens; gate_inventar→anker_fix|luecke; gate_struktur→bausteine; gate_guide→fix|det_pruefung|redundanz.
Gate-Schutz dreifach: max_versuche je Task, GATE_RUNDEN_MAX je Gate, Stillstand-Fingerprint (SHA-256 sortierte Befundmenge, 2× identisch → runs.status='paused', fail-closed).
## Backend-Dateiplan (~4700 Zeilen, deutsche Namen)
- config.py (160): Konstanten + .env-Loader (Datei gewinnt), Topic-Namens-Regex.
- db.py (260): WAL, eine Writer-Connection, busy_timeout 5000, on_change-Hook, Schema.
- engine.py (420): Scheduler auf asyncio.Event-Wecker + 2-s-Poll; Claim UPDATE…WHERE status='offen' (rowcount==1); Zombie-Reset beim Start; Barriere-Logik; Gate-Trigger bei leerer Stufe; Ergebnis+fertig+Folgetasks in EINER Transaktion; Infra-Retries getrennt von versuch; Budget-/Pause-Prüfung.
- graph.py (140): pipeline.yaml laden/validieren (graphlib-DAG, Worker-Registry, Skills existieren, 1 Gate/Stufe) + Layout-Export.
- pipeline.yaml (120): Knoten (id, typ, worker, skills[], stufe, max_parallel, max_versuche, barriere) + Kanten (von, nach, art normal|reparatur|ruecklauf).
- llm.py (280): call()-Zustandsmaschine ok|parse|cap|infra|error|pause (cap = KEIN Retry); Backoff 8/16/32 + Jitter → LaufPause; globale Drossel (429 + Statuscodes 1002/1041/2045, Breite halbieren min 4, 20 Erfolge = +1); Hedge (max(90, timeout/2), Verlierer verbuchen); Budget VOR+NACH; Fake-Hook vor allem außer Pause-Check.
- skills.py (120): Frontmatter, Komposition 1 Rolle + 1 Aufgabe + 0..n Stil (Format nur+zuletzt in Aufgabe), {{platzhalter}} per str.replace mit Vorab-Validierung, zweistufiges Hashing → skill_hash ins Ledger.
- ledger.py (100): events im finally JEDES Versuchs (inkl. hedge/hedge_cancel), budget_pruefen, Kennzahlen (Coverage, Dopplungsquote, Kosten je Knoten).
- panels.py (80): einstimmig (None = keine Stimme), panel mit Ersatz-Richter, klaerung (3er-Mehrheit).
- textkit.py (380): norm, finde_zitat (5 Stufen mit Negations-/Eindeutigkeits-Guards), titel_kern, abschnitte, ueberlappung, negations_menge; NEU satz_split, jaccard_gestemmt (PyStemmer), akronym_kanal.
- suche.py (220): Wikipedia REST+Action-API (UA mit KONTAKT_MAIL, max 3 parallel) + Tavily/Brave/ddgs-Kette, Timeout 10 s/1 Retry, Circuit Breaker, URL-Dedupe; degraded-Befund statt Crash.
- laden.py (200): httpx (Bytes! → trafilatura markdown → ftfy NFC → Snapshot via Temp+os.replace), Roh-HTML archivieren, robots.txt-Cache, 1 req/s/Domain, JS-/PDF-Detektor → Befund.
- korpus.py (280), inventar.py (420), dedup.py (220), struktur.py (280), guide.py (380), redundanz.py (110), montage.py (90), belege.py (60): Stufen-Worker wie im Graph.
- fakes.py (320): Handler-Registry über `<!-- aufgabe:name -->`-Marker, harter Fehler ohne Handler; Fakes LESEN Prompt (Zitat-Gate mitgetestet); Fake-Suche/-Laden mit präparierten Quellen (Dublette inkl. Akronym, Lücke die erst Nachrecherche deckt, Kapitel-übergreifende Dopplung).
- ws.py (60): Hub eine Queue + ein Sender-Task; dirty-Signal {typ, topic, bereich}.
- main.py (260): Lifespan (kein on_event), Routen, ensure_future + _laeufe-Dict.
## DB-Schema
topics(name PK, titel, status neu→…→fertig, auto, erstellt); runs(id, topic, status running|done|paused|stopped|budget|failed, stufe, budget_tokens, grund, git_hash); tasks(id, run_id, topic, knoten, item, status offen|laufend|fertig|fehler, art, versuch, infra_versuch, runde, payload, ergebnis, fehler, erzeugt_von, gestartet, beendet, UNIQUE(topic,knoten,item)); gate_laeufe(run_id, knoten, runde, status gruen|rot|pause, fingerprint, befunde, UNIQUE(run_id,knoten,runde)); events(run_id, ts, stufe, knoten, item, skills, skill_hash, model, role, status, dur_ms, wait_ms, tok_in/out/cache_read/cache_write, meta); quellen(id, topic, titel, url, url_norm UNIQUE(topic,url_norm), backend, snapshot, roh, hash, runde, zweck korpus|luecke:<id>, status, grund, atome_stand); soll(id, topic, punkt, status, belege JSON, kapitel_id, geprueft, freispruch); atome(id, topic, titel, typ, definition, status, soll_id, ziel_id, baustein_id, ord, merged_into); anker(atom_id, quelle_id, start, ende, zitat; start=-1 unverankert); kanten(von_atom, zu_atom, art='verwandt', UNIQUE); lernziele; bausteine(ziel_id, titel, ord, kapitel_id); kapitel(titel, intro, ord); sections(baustein_id PK, stage, text, qa_hash, fix_versuche) — EIN Text, kein kompakt/lang-Dual; befunde(run_id, stufe, knoten, art, item, detail, status offen|behoben|freispruch|veraltet, quelle, runde, geklaert).
## Skills (Frontmatter: name, art, beschreibung, [platzhalter])
- rolle/: extraktor, richter, autor, planer (je mit konkreten Verhaltensregeln, kein Rollen-Theater).
- stil/: deutsch-praezise, guide-lesbarkeit (R10-Regeln inkl. „Ein-Satz-Refresher + Anker-Link statt Wiederholung").
- aufgabe/ (20): recherche-plan, soll-extraktion, soll-konsens, soll-beleg, atom-extraktion, anker-fix, dedup-urteil, soll-zuordnung, luecke-nachextraktion, luecke-freispruch, lernziele, bausteine, themen-schnitt, ordnung, writer, kapitel-intro, guide-pruefung, guide-fix, redundanz-urteil, klaerung. Alle DELIMITED-Ausgabe (===BLOCK===), zusammengesetzt ≤ ~1000 Tokens, ≤ 8-10 Regeln; Rolle+Stil als stabiler Cache-Präfix.
## API + Frontend
Routen: GET/POST/DELETE /api/topics; /start {budget?}, /pause, /weiter, /stop; GET /graph (Struktur + Zähler-Snapshot + Gate-Status — KEIN Monolith); GET /knoten/{id}/tasks (lazy); GET /guide (Kapitel-Struktur); GET /kennzahlen (Coverage %, Dopplungsquote, Kosten je Knoten, Gate-Runden); GET /befunde; WS /ws (dirty-Signale, 250 ms koalesziert).
Frontend: App.vue (Tabs Graph/Guide/Kennzahlen, Drawer mobil), Graph.vue (Eigenbau nach R7), KnotenPanel.vue (Task-Liste + Befunde), Guide.vue (nach R10: 68ch, content-visibility, TOC + Scroll-Spy, print-CSS), Kennzahlen.vue, store.js, markdown.js (markdown-it html:false + anchor + DOMPurify + Shiki dual-theme, Sprachen php/twig/yaml/bash/sql/json/xml, KEIN KaTeX), api.js, style.css (Custom-Properties, dark default, Breakpoint 768px).
Deps: vue ^3.5, vite ^8, markdown-it ^14.3, markdown-it-anchor, shiki ^4.3 + @shikijs/markdown-it, dompurify ^3.4, @panzoom/panzoom, @fontsource-variable/inter.
## Konfiguration
.env: MINIMAX_API_KEY (aus creator2/.env kopieren), TAVILY_API_KEY?, BRAVE_API_KEY?, SEARXNG_URL?, KONTAKT_MAIL, RUN_BUDGET_TOKENS, MAX_PARALLEL_LLM, CREATOR_FAKE, CREATOR_DB.
config.py-Kernwerte: ROLLEN alle MiniMax-M3 (extraktion/judge thinking=disabled temp 0.2/0.1; writer adaptive 0.7; max_tokens 32000); MAX_PARALLEL_LLM 12; INFRA 3×, Backoff 8/16/32+Jitter; DROSSEL_MIN_BREITE 4; HEDGE_NACH_S 90; RUN_BUDGET_TOKENS 10_000_000 (AN, Worst Case ~$24); RECHERCHE_LENSES generisch (ueberblick/tutorial/referenz/praxis); RECHERCHE_RUNDEN_MAX 4; SOLL_MIN_BELEGE 2; ABSCHNITT_CHARS 6000; READER 2; ANKER_OVERLAP 0.5; FUZZY 40/0.08; MERGE_PANEL 2; DEDUP_JACCARD 0.3; DEDUP_TITEL_FUZZY 85; PAARE/CALL 10; LUECKEN_RUNDEN 2; WEB_NACHRECHERCHE 1; BAUSTEIN 4-8; SECTION_WOERTER (40,220); PRUEF_PANEL 2; KLAERUNG 3; FIX_CAP 3; REDUNDANZ_JACCARD 0.6; STILLSTAND_N 2; GATE_RUNDEN_MAX je Gate.
Python-Deps: fastapi, uvicorn, httpx, pyyaml, trafilatura, ftfy, ddgs, fuzzysearch, rapidfuzz, PyStemmer, pytest, pytest-asyncio.
## Baureihenfolge (Meilensteine mit „fertig wenn")
- M1 Gerüst: config, db, skills, graph, engine + Mini-Testgraph. Fertig: test_engine/test_graph/test_skills grün + Mini-Fake-E2E inkl. Resume=0-Events und Stillstand→Pause.
- M2 LLM-Schicht: llm, ledger, panels, Fake-Hook. Fertig: test_llm grün (cap-kein-Retry, Drossel, Hedge-Verbuchung, Budget-Vorab-Stopp).
- M3 Suche+Laden: suche, laden, textkit. Fertig: Tests grün + EIN manueller Echt-Fetch (Wikipedia + Doku-Seite) mit intakten Code-Fences.
- M4 Korpus: bis gate_korpus im Fake grün (Zitat-Fehler blockiert Gate, Rücklauf mit neuen Lenses).
- M5 Inventar: bis gate_inventar grün (Dublette inkl. Akronym gemerged, Lücke via Web-Rücklauf geschlossen, Coverage 100 %).
- M6 Struktur+Guide+Montage: test_e2e komplett grün (Dopplung gefunden+gefixt, Marker einmalig, Resume 0 Events, Pause+Neustart ohne Duplikate).
- M7 API+Frontend: Fake-Lauf im Browser live verfolgbar; Guide mobil+Desktop lesbar.
- M8 Echtlauf-Härtung: kleines Thema, Budget aktiv, ohne Eingriff fertig; Mini-Deutsch-Eval (5 Sections); Konstanten nachjustieren.
## Tests (tests/)
test_graph, test_engine (Claim atomar, Zombie-Reset, Barriere, atomare Folgetasks, Runde-im-Item), test_llm, test_skills, test_suche (Fallback, Circuit Breaker, degraded), test_laden (Normalisierung einmalig, robots, Roh-Archiv), test_dedup (Akronym-Kanal!, Subset-100-Falle, verwandt-Kante = 0 neue Calls), test_redundanz (gepflanzte Dopplung → Befund; selbes Kapitel → kein Befund), test_gates (Gate blockiert hart, Stillstand→Pause, Coverage-Formel), test_e2e.
## Risiken
1. MiniMax-Deutsch unbelegt → M8-Eval, Stil-Skills, Prüfer-Panel.
2. trafilatura-Code-Fences → Roh-HTML-Archiv, M3-Checkpunkt.
3. Harte Gates → mehr Pausen: gewollt; Ventile = Freispruch-Panels, 3er-Klärung, Stillstand-Fingerprint.
4. Web-Nachrecherche bläht Kosten → Cap 1 Runde, gegroundete Queries, Budget-Vorab-Check.
5. Eigenbau-Engine-Races → eine Writer-Connection, atomare Transaktionen, M1 testet zuerst; Notbremse: mehr Barrieren.
---
# ANHANG: Recherche-Befunde (R1-R11)
## Auftrag (Nutzer)
- Neubau creator3 unter /home/arbeit/projects/creator3, creator2 NUR Lesereferenz (kein Code übernehmen).
- Nur Themen-Modus (Websuche als Quelle); uni-Variante entfällt. Keine Artefakte (Flashcards/Fragen/Üben).
- LLM: NUR MiniMax per API (Anthropic-kompatibler Endpoint), Claude komplett raus.
- Kernziel: Guideumfang maximieren (Abdeckung nahe 100 %, Lücken minimieren), dann Guidelänge minimieren (Dopplungen minimieren). Guide gut lesbar, responsiv. Deutsch.
- Nutzer-Vision: Pipeline-System als Graph; Knoten haben genau EINE Aufgabe (erstellen, filtern, zusammenführen, prüfen …); Agenten bekommen Skill-Dateien (Rolle/Aufgabe/Stil) beim Start; Frontend = Graphensicht (Knoten mit Task-Zählern, Kanten) statt Kanban.
- Annahmen bestätigt: Suche „etwas Zuverlässiges" = Multi-Backend mit Fallback; Guides Deutsch; Token-Budget diesmal default AN; kein Eingreifen (Pause nur bei echten Fehlern); MiniMax-Key aus creator2/.env übernehmen; Stack Python+SQLite+Vue.
- Lektionen aus creator2-Analyse: Gates diesmal ÜBERALL hart (creator2 gated nur Korpus), Budget default an.
## Recherche-Befunde (kondensiert)
### R1: creator2 Frontend/API (fertig)
- Lauf-Start: `asyncio.ensure_future` + `_laeufe`-Dict gegen Doppelstart (pipeline.py:43-52); kein Task-Framework. Übernehmen.
- WS-Muster übernehmen: Hub mit EINER Queue + EIN Sender-Task (Ordering!), `call_soon_threadsafe` aus DB-Schicht (ws.py:20-44). Frontend: Delta = nur Ping → debounced (400 ms) Full-State-Reload + Reconnect mit Cleanup-Flag + `{typ:'verbunden'}`-Resync + 5-s-Poll nur bei laufendem Run (App.vue:59-62, api.js:54-74).
- Schwächen vermeiden: State-Monolith (alles bei jedem Reload) → creator3: kompakter Zähler-Snapshot, Task-Liste je Knoten lazy per Klick; WS-Payload tote Fracht → leeres dirty-Signal mit Topic-Filter; Rechtsklick-Menü mobil unbrauchbar → Klick-Panel; `on_event("startup")` deprecated → Lifespan.
- markdown.js-Muster übernehmen: marked+marked-highlight+hljs, DOMPurify über End-HTML, Render-Cache (600 ms/Scroll-Falle), Kapitel-Virtualisierung (nur aktiv ±1 im DOM). KaTeX-Ausheben nur falls Formeln (creator3: vermutlich weglassen, Themen sind nicht mathelastig — Entscheidung im Plan).
- Lesetypografie übernehmen: max-width 680px, 19px/1.55, Inter Variable, hyphens auto, Tabellen overflow-x, CSS-Custom-Properties-Themes (dark default), EIN Breakpoint 768px mit Drawer.
- Deps: vue ^3.5, vite ^8, marked ^18, marked-highlight, highlight.js ^11, dompurify ^3, @fontsource-variable/inter. Keine Graph-Lib im Bestand.
- API-Formen als Vorlage: POST /api/topics {name}, /start {budget}, /pause, /state-Snapshot, /guide (Kapitel-Struktur statt Roh-MD — 0,43 MB gespart), /kennzahlen. encodeURIComponent überall + Server-Namensvalidierung.
### R2: Websuche-Backends (fertig)
- Empfehlung: **Wikipedia de+en immer parallel** (Grundstock, offiziell, quasi nie down) + **Fallback-Kette für Websuche**: Keyed-API (falls Key) → ddgs → weiter-ohne-Crash. KEIN Voll-Fan-out (verbrennt Quota ohne Mehrwert).
- Wikipedia: Suche `https://de.wikipedia.org/w/rest.php/v1/search/page?q=…&limit=10`; Extrakte via Action-API `prop=extracts&explaintext=1` (stabilste Variante, RESTBase-summary unsicher). Pflicht: aussagekräftiger User-Agent mit Kontakt; mit UA 200 req/min, max 3 parallel, Retry-After respektieren.
- ddgs ≥9.14 (Paket umbenannt von duckduckgo-search!): heute Metasearch-Aggregator (`backend="auto"`, Bing/Brave/DDG/Google/Mojeek…), `DDGS().text(q, region="de-de", max_results=10)`. Schwächen: Exceptions untypisiert (#478), auto-Modus kann Ergebnisse verlieren (#427). Grauzone (DDG-ToS) → Best-Effort-Schicht, 1-2 s Pause zwischen Queries.
- Brave API: Free-Tier abgeschafft (Feb 2026) → $5 Gratis-Guthaben/Monat ≈ 1000 Queries, Kreditkarte Pflicht, Überschreitung wird berechnet. Optional via BRAVE_API_KEY.
- Tavily: 1000 Credits/Monat OHNE Karte — bevorzugte Optional-Key-Alternative (TAVILY_API_KEY).
- Muster: Timeout ~10 s/Query, 1 Retry, dann nächstes Backend; Circuit Breaker (Backend n× tot → Rest des Laufs überspringen); URL-Dedupe normalisiert (Tracking-Parameter, Trailing Slash); Lauf scheitert nur, wenn ALLE Backends ausfallen, sonst degraded mit Befund.
- SearxNG: nur als optionales Self-Host-Backend (SEARXNG_URL), öffentliche Instanzen unzuverlässig.
### R3: creator2 LLM-Disziplin (fertig)
- `call()`-Zustandsmaschine übernehmen: Infra (3 Retries, Backoff 8/16/32 s → LaufPause fail-closed) vs. Inhalt (2 Restarts → None) getrennt; Status ok/parse/cap/infra/error/pause. `cap` (stop=max_tokens) = KEIN Retry (deterministisch, Aufrufer halbiert Chunk). 1-Element-Dict zählt als Liste (Modelle lassen Klammern weg).
- Globale 429-Drossel: ein 429 drosselt ALLE (Cooldown global, Breite halbieren min 4, 20 Erfolge = +1); retry-after respektiert (min 60 s). Start-Breite 28.
- Ledger: events-Schema übernehmen (run_id, ebene, stage, item, template, template_hash, model, role, status, dur_ms, wait_ms, tok_in/out/cache, meta); Log im finally JEDES Versuchs; Hedge-Verlierer als eigene Zeile (hedge/hedge_cancel). Budget: Prüfung VOR und nach jedem Call; creator3: Default AN.
- Panel-Primitive: `einstimmig` (volles Panel, None = keine Stimme = kein Entscheid), `panel` mit Ersatz-Richter bei genau einem Ausfall; Größen: Merge 2 einstimmig, Verify 2, Klärung 3 (Mehrheit).
- MiniMax `_text_api`: Endpoint api.minimax.io/anthropic/v1/messages, x-api-key + anthropic-version; max_tokens 32k; thinking DISABLED für Judges/Extraktion (sonst 8-32k Denk-Tokens, 44-min-Stalls), thinking AN nur für Writer; temperature 0.2-0.3; leere Antwort → Fehler MIT Tokens (stop_reason durchreichen für cap-Erkennung); httpx Timeout + wait_for; Rollen: judge/guide=MiniMax-M3, quick/fast=M2.7-highspeed (kalt-Route stallte 20 % der Judges → native Route für Judges).
- Hedge BEHALTEN (API-Stalls belegt): Zwilling nach max(90 s, timeout/2), erster valider gewinnt, Verlierer verbuchen, HEDGE_NACH_S=0 schaltbar.
- Entfällt ohne CLI: RAM-Gate, Token-Bucket, killpg/_spawn, _opencode/_claude_cli, _batch_sem, caps files/full, Provider-Stacks claude/lokal, „database is locked"-Marker.
- Bleibt: AgentErgebnis, Pause/Abbruch-Flags, _active, Fake-Hook VOR allem außer Pause-Check, Semaphor + getrennte wait_ms-Messung.
- LEKTIONEN-Triage: übernehmen 1-6, 10, 12-37, 40, 42-49, 52-83 + 4 Meta-Regeln; entfällt 7-9, 11, 38-39, 41, 51; Lektion 50 = Gründungsentscheidung.
- Konsequenz API-only: Recherche (caps=full) MUSS als eigener Nicht-LLM-Baustein gebaut werden (Suche+Download im Code, LLM destilliert nur).
### R4: creator2 Pipeline/QA (fertig)
- Orchestrierung übernehmen: EBENEN-Kette seriell, Status-Marker nie zurückdrehen, `_fertig_ab`-Skip, Resume = 2. Lauf auf fertigem Topic → 0 neue Calls (bewiesen in test_e2e). Runs-Status running/done/stopped/paused/budget/failed.
- QA: Note-Formel (10.0 nur bei 0 Befunden; Gewichte kritisch 3.0/mittel 1.5/stil 0.5, Basis = geprüfte Items); Befund-Persistenz mit Schlüssel (art,item,detail[:500]); auto_repair_loop (fertig/stillstand/limit, max 10 Iter).
- KRITISCH für creator3: Gate-Aufrufmuster existiert generisch (pipeline ruft `modul.gate(ctx)`, RuntimeError blockiert Marker), aber NUR korpus definiert eins. creator3: gate() je Ebene VERPFLICHTEND; bei stillstand/limit mit offenen kritischen Befunden → PAUSE statt Weiterlauf.
- Korpus-Themen-Modus komplett übernehmen: Runden bis Sättigung (Runde ohne neuen bestätigten Punkt), Zitat-Gate (finde_zitat gegen Quelle), Konsens: Judge gruppiert nur, CODE zählt ≥2 unabhängige Quellen; URL- und Content-Hash-Dedup; Beleg-Nachsuche gebündelt (1 Call je Quelle statt Punkt×Quelle); geprueft-Negativ-Cache; Snapshots: ftfy NACH Steuerzeichen-Ersatz, sha256[:16]-Dateiname.
- Inventar (ausgereiftester Teil) komplett: 6k-Abschnitte (12k riss 32k-Cap), 2 Reader, Cap → Chunk halbieren (Tiefe 2); Anker 5-Stufen-Matching (exakt→casefold→alnum→fuzzy 8 %→Wort-Alignment) mit Negations-/Eindeutigkeits-Guards; unverankert = start=-1 → erst det. Re-Match, dann LLM-Batch-Fix, verwerfen nur ohne Call-Ausfall; Dedup-Kette: Anker-Overlap ≥0.5 auto → Kandidaten (Embedding cos 0.65 / Jaccard 0.3 Titel-Kern) mit Negations-Gleichheit → 2er-Panel einstimmig; verwandt-Kanten = nie zweimal Tokens; Lücke: ±6k-Fenster-Nachextraktion, nach 2 Runden Freispruch nur per einstimmigem Panel.
- Struktur: Themen-Schnitt mit exakter Partitions-Validierung + √n-Fallback; Lernziele 2 Runden + Sammelziel; Band 4-8 mit Merge nur im Thema/Split entlang Quellreihenfolge; Ordnung: Median-Anker-Rang als Prior, Judge-Permutation exakt validiert, Fallback = Prior mit Befund.
- Guide: Stages writer→pruefer→fix→done in sections.stage; DELIMITED (===LANG===/===KOMPAKT===, JSON scheiterte an LaTeX-Backslashes 34 %); Fix-Cap 3 mit Freeze nur für Stil; Auftrags-System (2er-Panel, Klärung 3er einmalig, Eskalation); LLM-QA fail-closed mit qa_hash; det-Checks (Marker fehlend/fremd, Länge ×1.25-Band, Vorwärtsverweise, Stil).
- WICHTIG Dopplungs-Ziel: creator2 garantiert nur Marker-Einmaligkeit, KEINEN inhaltlichen Redundanz-Check. Für creator3-Kernziel „Länge minimieren" NEU bauen: Rückwärts-Redundanz-Check (analog Vorwärtsverweis-Detektor) + „Verweis statt Wiederholung"-Regel im Writer-Skill.
- Fake-System komplett übernehmen: Template-Marker `<!-- template:Name -->`, Handler-Registry, harter Fehler ohne Handler, Fakes LESEN Prompt (Zitat-Gate mitgetestet), In-Memory-DB, Resume-Test 0 neue Events.
- Schema übernehmen minus: artefakte, leitner, lernstand, topics.art, quellen.art='datei', kanten art='braucht'. textkit.py-Funktionsumfang 1:1 nachbauen (norm, finde_zitat 5 Stufen, titel_kern, abschnitte, ueberlappung, negations_menge). prompts.py-Muster (Datei + sha256-Hash) und belege.py (Fenster 300/+1200) übernehmen.
### R5: Skill-Datei-Format (fertig)
- Format: `skills/{rolle,aufgabe,stil}/name.md`, YAML-Frontmatter minimal: name, art (rolle|aufgabe|stil), beschreibung, optional platzhalter (Validierung vor Call). KEIN version-Feld (Hash ist die Version), kein model/tools.
- Komposition statisch erzwungen: genau 1 Rolle + 1 Aufgabe + 0..n Stil; NUR die Aufgabe definiert das Ausgabeformat (eliminiert Konfliktklasse per Konstruktion — Modelle arbitrieren Konflikte nur ~64 % korrekt); Platzhalter `{{name}}` nur im Aufgabe-Body, simples str.replace statt format_map (creator2 muss deshalb JSON-Klammern doppeln).
- Reihenfolge: Rolle → Stil-Regeln → Aufgabe (Input mittig, Ausgabeformat als LETZTER Block — creator2-bewährt).
- „Curse of Instructions": Erfolg fällt ≈ p^n mit Regelzahl → Budget: zusammengesetzter Prompt ohne Input ≤ ~1000 Tokens, ≤ ~8-10 verifizierbare Regeln. Wichtig für MiniMax-Klasse.
- Rollen-Theater bringt nichts (belegt) — Rolle nur, wenn sie konkrete Verhaltensregeln trägt („nichts erfinden, nur Belegbares").
- Hashing zweistufig: hash_i = sha256(Skill-Rohbytes)[:12] je Datei; skill_hash = sha256(name:hash_i-Liste in Reihenfolge)[:12] → ins Ledger (kompatibel zum template_hash-Muster); Einzelliste zusätzlich ins Log.
- DELIMITED-Ausgabeformat (===ATOM===-Blöcke) statt JSON — robuster für Mittelklasse-Modelle, kein Escaping.
### R6: HTML-Extraktion (fertig)
- Stack: **trafilatura ≥2.1.0** mit `output_format="markdown", include_tables=True, include_links=False, include_images=False` — einziger gewarteter Boilerplate-Entferner mit Markdown-Code-Fences + Tabellen (wichtig für Symfony-Doku); Benchmark-Spitzenklasse unter Python-Tools. Roh-HTML-Bytes IMMER mit archivieren (Re-Extraktion ohne Re-Fetch).
- Fetch: httpx mit `follow_redirects=True` (NICHT allow_redirects), explizite Timeouts (connect 10/read 30), ehrlicher UA `creator3/0.1 (+mailto:…)`, Transport-Retries(2) + Backoff für 429/5xx mit Retry-After; **Bytes an trafilatura geben, nie response.text** (Encoding-Fehlerklasse eliminiert); robots.txt via urllib.robotparser, pro Domain gecacht, Disallow → skip + Befund; ~1 req/s/Domain.
- Anker-Stabilität (Architekturregel): Normalisierung GENAU EINMAL bei Ingestion VOR Snapshot-Write: Bytes → trafilatura → ftfy 6.3.1 (NFC, NIE NFKC — verfälscht Zitate) → Snapshot unveränderlich; Zitate nur aus Snapshot; tolerante Normalisierung nur als Lesetransformation beim Vergleich.
- JS-Rendering: YAGNI v1. Detektor: leerer/kurzer Extrakt oder „enable JavaScript" → Befund „braucht Rendering, übersprungen".
- PDF: v1 überspringen mit Befund. Falls später: pypdf (BSD); pymupdf ist AGPL (bewusste Entscheidung nötig).
- Unsicher: Code-Block-Qualität von trafilatura 2.1.0 auf echten Symfony-Seiten → beim ersten Echtlauf prüfen.
### R7: Graph-Frontend (fertig)
- Empfehlung: **hand-gerolltes Hybrid-Rendering** — absolut positionierte HTML-Divs für Knoten (Badges = normales Vue-Templating) + eine SVG-Ebene nur für Kanten (kubische Bezier, `<marker>`-Pfeilspitzen), beide im selben transformierten Container. KEINE Graph-Bibliothek (kein Editor, festes Layout, ~20 Knoten).
- Layout vorberechnet aus YAML-Stufen: Spalte = Stufe, Zeile = Index (~40 Zeilen computed). Mobil: x/y-Tausch → vertikales Layout per Breakpoint (Orientierung wechseln statt schrumpfen).
- Rückwärts-Kanten (Reparatur/Lücken-Loops): feste Offsets pro Loop-Kante, außen herumgeführt — kontrollierter als Auto-Layout.
- Zoom/Pan: Micro-Lib anvaka/panzoom ODER @panzoom/panzoom (~20 Zeilen Anbindung); initial fitView, Pan als Primärgeste, `touch-action: none` nur auf Container (Page-Scroll nicht kapern).
- Live-Zähler: EIN WS-Handler schreibt in zentralen reactive Store `counts[knotenId] = {offen, laufend, fertig, fehler}`; Badges lesen daraus (nur Textknoten-Patches); Graphstruktur als shallowRef/statisch; Events auf ~250 ms koalesziert; Initial-Snapshot + Resync nach Reconnect.
- Klick/Tap → Seitenpanel mit Task-Liste (lazy geladen) — richtiges Touch-Muster, keine Hover-Tooltips.
- Fallback falls Eigenbau hakt: @vue-flow/core 1.48.2 (51 kB gzip), Positionen trotzdem selbst, draggable/connectable=false. Nicht: mermaid (Full-Re-Render), cytoscape/G6/elkjs (überdimensioniert), dagre-d3 (tot).
### R8: Task-Engine (fertig)
- Keine fertige Lib passt (Celery/Dramatiq/Huey/APScheduler/Prefect/LangGraph = Broker/Overkill/falsches Modell). Selbst bauen, ~300-500 Zeilen Engine-Kern. Vorbilder: litequeue-Schema (Task=Zeile, Claim=UPDATE), DBOS/Morling-Checkpoint-Muster, Dagu-YAML, graphlib (stdlib) NUR zur statischen DAG-Validierung beim Config-Laden.
- Scheduling: Hybrid — DB ist die einzige Wahrheit/Queue; Scheduler-Loop blockiert auf asyncio.Event („Wecker", gesetzt bei jedem Task-Abschluss/-Erzeugung) + 2-s-Fallback-Poll. Resume = derselbe Codepfad (Loop starten, DB lesen). Beim Start: Zombie-Reset `laufend → offen` (ein Prozess ⇒ kein Lease/Heartbeat nötig).
- SQLite: WAL, synchronous=NORMAL, busy_timeout 5000, EINE Writer-Connection (eliminiert locked-Probleme architektonisch); Claim = `UPDATE … WHERE id=? AND status='offen'`, rowcount==1; LLM-Calls NIE in Transaktion.
- Idempotenz: tasks UNIQUE(topic, knoten, item) + INSERT OR IGNORE; Reparatur-RUNDE gehört in den Item-Schlüssel (`r2:atom:14`), sonst blockiert UNIQUE die 2. Runde. Atomar in EINER Transaktion: Ergebnis-Zeilen + status='fertig' + Folge-Task-Erzeugung (kein falsch interpretierbarer Zwischenzustand). Datei-Schreiben: Temp + os.replace.
- Zyklen: Normal-Kanten = DAG (graphlib-Check); Rück-Kanten (reparatur|ruecklauf) nur von Gates, stufen-abwärts, sind Task-Erzeugungs-Rezepte (kein Laufzeit-Zyklus). Dreifacher Schutz: max_versuche je Task, max_runden je Gate, Stillstand-Fingerprint (SHA-256 über sortierte Befundmenge; 2× identisch → PAUSE).
- v1: Stufen-seriell mit frei fließendem Graph INNERHALB der Stufe (Gates brauchen Quiesce-Punkt; creator2 beweist, dass das reicht). Barriere-Knoten (`barriere: true`) für globale Schritte (Dedup, Konsens, Ordnung).
- Gate = Knoten-Typ mit deterministischer Registry-Funktion; Trigger: Stufe leer gelaufen → INSERT OR IGNORE Gate-Task `runde:N` (einmal pro Runde); rot → in EINER Transaktion Befunde + Fingerprint + Reparatur-Tasks (Runde+1, gecappt); Stillstand/Cap → runs.status='paused' mit Grund (fail-closed), UI bleibt erreichbar.
- Task-Schema: tasks(id, run_id, topic, knoten, item, status offen|laufend|fertig|fehler, art, versuch, runde, payload, ergebnis, fehler, erzeugt_von, gestartet, beendet, UNIQUE(topic,knoten,item)) + gate_laeufe(run_id, knoten, runde, fingerprint, befunde, UNIQUE(run_id,knoten,runde)).
- YAML-Schema: global{max_parallel, poll_sekunden}; knoten[{id, typ system|llm|gate, worker, skills[], stufe, max_parallel, max_versuche, barriere}]; kanten[{von, nach, art normal|reparatur|ruecklauf}]. Validierung: Worker in Registry, Skill-Dateien existieren, ein Gate pro Stufe (v1).
- Infra vs. Inhalt getrennt (Lektionen 1/2/68): Infra zählt nicht gegen max_versuche, eigenes Retry-Budget → PAUSE.
### R9: MiniMax-API (fertig)
- Endpoint: Anthropic-kompatibel `https://api.minimax.io/anthropic/v1/messages` (offiziell empfohlen; x-api-key wie creator2). Plus `/anthropic/v1/messages/count_tokens`. ACHTUNG: `stop_sequences`/`top_k` werden IGNORIERT → Delimiter im Prompt erzwingen, nicht per stop.
- Modelle Mitte 2026: **MiniMax-M3** (1M Kontext, thinking `adaptive|disabled` — „enabled" gibt 400; einziges Modell mit wirklich abschaltbarem Thinking), M2.7/M2.7-highspeed (Thinking NICHT abschaltbar — erklärt creator2s Denk-Token-Problem!).
- Rollen-Empfehlung: Extraktion + Judge = M3 mit thinking disabled, temp 0.1-0.3; Writer = M3 mit thinking adaptive, temp ~0.7, max_tokens 32000. thinking IMMER explizit setzen (Default widersprüchlich dokumentiert).
- Rate Limits: M3 200 RPM / 10M TPM; M2.x 500 RPM / 20M TPM. Rate-Limit-Fehler auch als Statuscode 1002/1041/2045 (nicht nur HTTP 429). Retry-After NICHT verlässlich → eigenes exponentielles Backoff mit Jitter. Nebenläufigkeit: 8-16 Worker sicher.
- Kein response_format/JSON-Mode bei M2.x/M3 (wird still ignoriert) → DELIMITED + eigener Parser bestätigt alternativlos.
- Prompt-Caching: passiv automatisch ab 512 Input-Tokens (Prefix-Match, keine Write-Gebühr) → stabile Prompt-Präfixe (Rolle+Stil vorn) lohnen sich; usage-Felder cache_read/cache_creation wie creator2 parsen.
- Preis M3: konservativ $0.60/$2.40 pro 1M in/out ansetzen (Quellen widersprüchlich, real messen).
- Embeddings: MiniMax embo-01 ist CN-/Legacy, auf api.minimax.io UNSICHER → für Dedup NICHT auf MiniMax-Embeddings bauen; lexikalisch (Jaccard) + optional lokales Modell.
- Deutsch-Qualität: keine belastbare Quellenlage → kleinen eigenen Deutsch-Eval beim ersten Echtlauf einplanen.
### R10: Guide-Rendering/Lesbarkeit (fertig)
- Stack-Wechsel ggü. creator2: **markdown-it@14.3 (html:false) statt marked** (sicher by default, CommonMark, Plugins) + markdown-it-anchor (Heading-IDs) + dompurify@3.4 vor jedem v-html + **Shiki 4.3 statt highlight.js** (fine-grained: shiki/core + JS-RegExp-Engine ohne WASM, nur benötigte Sprachen php/twig/yaml/bash/sql/json/xml; Dual Themes light/dark per CSS-Variablen; einmal async initialisieren, dann synchron via @shikijs/markdown-it/core).
- Lange Guides: KEINE Virtualisierung nötig (<2 MB); Kapitel-`<section>`s mit `content-visibility: auto` + `contain-intrinsic-size: auto 1200px` (~80 % Render-Ersparnis); HTML einmal rendern und cachen.
- CSS-Kern: Prose max-width 68ch zentriert; font-size clamp(1rem…1.125rem), line-height 1.65; Überschriften 1.25, mehr Luft oben; `scroll-margin-top: 5rem` auf h2/h3 (Sticky-Header!); Tabellen in `.table-wrap{overflow-x:auto}` + td max-width 40ch; pre overflow-x, 0.875em; Dark Mode über Custom Properties + data-theme; @media print (nav aus, break-inside avoid auf pre/table).
- TOC: markdown-it-anchor-Slugs → Headings (H2-H3) aus Tokens extrahieren → Sidebar-TOC (Desktop sticky, mobil Drawer) + IntersectionObserver-Scroll-Spy (~30 Zeilen). Kein Einklappen (bricht Anker + Suche), kein Lesefortschritts-Balken (Deko).
- Writer-Skill-Regeln (aus Instructional Design): 1 Konzept pro H3; **Verweis statt Wiederholung = Ein-Satz-Refresher + Anker-Link** („Wie in [Kapitel 3: X](#anchor) gezeigt, … — hier für Y"); eindeutige Überschriften (Slug-Kollisionen); H2-Einstieg 2-3 Sätze (was/warum/worauf baut es); Beispiel direkt nach Konzept (max ~5 Sätze Erklärung); H2-Kapitel ~1000-2000 Wörter; Tabellen max 3-4 Spalten; Kapitel-Abschluss 3-5 Punkte ohne fremde Inhalte zu wiederholen.
### R11: Dedup + Abdeckung (fertig)
- Grundsatz: deterministisch vor LLM; Ähnlichkeit liefert NUR Kandidaten (Floors niedrig, recall-orientiert), Panel = Präzisions-Gate; Merge nur einstimmig; geprüfte Paare als „verwandt"-Kante persistieren.
- KEIN sentence-transformers in v1. Kandidaten-Kanäle (Union): Anker-Overlap ≥0.5 (einziger Auto-Merge); Titel-Kern exakt + gestemmt; Token-Jaccard (Snowball-German-gestemmt via PyStemmer) auf Definitionen ≥0.3 (gemessener Paraphrasen-Floor); rapidfuzz token_set_ratio auf Titeln ≥85 NUR bei ≥2 Inhaltstoken (Subset-100-Falle; Schwelle kalibrieren); **Akronym-Kanal** ALL-CAPS ↔ Anfangsbuchstaben (Lektion 28 — in creator2 FEHLEND); **Gleicher-Soll-Punkt-Kanal** (alle Atompaare am selben Soll-Punkt — deckt Synonym-Lücke billig ab).
- Guards vor Panel: Negationsmengen-Gleichheit, Kandidaten-Cap, ~10 Paare/Call. Backstop falls Dubletten überleben: model2vec potion-multilingual-128M (MIT, 256-dim, kein torch, cos 0.65). MiniMax embo-01 unsicher — nicht drauf bauen.
- Abdeckung = Nugget-Recall-Prinzip (wissenschaftlich bestätigt, TREC RAG): Coverage % = bestätigte Soll-Punkte mit ≥1 Atom UND ≥1 Baustein / alle bestätigten — deterministisch gezählt, NIE LLM-geschätzt. Checklisten-Recall trennschärfer als Likert-Judges (Deckeneffekte).
- Lückensuche: Suchanfragen aus Punkt-Text + Beleg-Zitat GROUNDEN (freie LLM-Query-Expansion halluziniert bei unbekannten Themen), 2-3 Varianten; Runden-Cap 2, dann Panel-Freispruch persistiert.
- Sättigung schärfer als creator2: Nullrunde zählt nur mit NEUEN Query-Winkeln/Lenses; Cap 3-4 Runden.
- Dopplungs-QA im Text (klein, <100 Zeilen): Satz-Split → norm() → Jaccard aller Satzpaare AUS VERSCHIEDENEN KAPITELN ≥~0.6 → Befund → 2er-Panel „redundant vs. didaktisch gewollt" (Recap ist keine Dopplung). Prävention bleibt Hauptinstrument: exakte Soll-Partition + „bekannt"-Kontext im Writer + inhaltsfreie Intros.
## ALLE 10 RECHERCHEN ABGESCHLOSSEN — Design-Phase folgt.