This commit is contained in:
Team3
2026-07-22 16:12:23 +02:00
commit f07ef9653d
851 changed files with 501480 additions and 0 deletions

26
phase0/baue-prompt.py Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Setzt ein Prompt-Template zusammen: <DATEI>, <QUELLE> und optional weitere
Platzhalter ersetzen.
Aufruf: baue-prompt.py <template> <projekt-wurzel> <datei-relpfad> <ausgabe> [k=v-datei ...]
Zusätzliche Platzhalter: k=pfad ersetzt <k> durch den Inhalt der Datei pfad.
"""
import sys
from pathlib import Path
def main():
template, wurzel, datei, ausgabe = sys.argv[1:5]
text = Path(template).read_text(encoding="utf-8")
quelle = (Path(wurzel) / datei).read_text(encoding="utf-8")
text = text.replace("<DATEI>", datei)
text = text.replace("<QUELLE>", "```python\n" + quelle + "```\n")
for extra in sys.argv[5:]:
k, pfad = extra.split("=", 1)
text = text.replace(f"<{k}>", Path(pfad).read_text(encoding="utf-8"))
Path(ausgabe).write_text(text, encoding="utf-8")
print(f"{ausgabe}: {len(text)} Zeichen")
if __name__ == "__main__":
main()

129
phase0/baue-vorschau.py Normal file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Baut die Phase-0-Vorschau: Feature-Sicht + Einheiten → aufklappbares HTML.
Aufruf: baue-vorschau.py <features.md> <projekt-wurzel> <ausgabe.html> <einheiten.md ...>
Der konkrete Code je Einheit wird live aus der Quelle extrahiert (Anker: Pfad::Symbol) —
Artefakte enthalten nie Code-Kopien.
"""
import html
import json
import re
import sys
from pathlib import Path
def einheiten_parsen(pfade) -> dict:
out = {}
aktuelle = None
modus = None
for pfad in pfade:
for zeile in Path(pfad).read_text(encoding="utf-8").splitlines():
if zeile.startswith("## ") and "::" in zeile:
aktuelle = {"id": zeile[3:].strip(), "beschreibung": "", "input": "",
"output": "", "entscheidungen": [], "kanten": [], "anker": []}
out[aktuelle["id"]] = aktuelle
modus = None
elif aktuelle is None:
continue
elif zeile.startswith("beschreibung:"):
aktuelle["beschreibung"] = zeile.split(":", 1)[1].strip(); modus = "beschreibung"
elif zeile.startswith("input:"):
aktuelle["input"] = zeile.split(":", 1)[1].strip(); modus = "input"
elif zeile.startswith("output:"):
aktuelle["output"] = zeile.split(":", 1)[1].strip(); modus = "output"
elif zeile.startswith("anker:"):
aktuelle["anker"].append(zeile.split(":", 1)[1].strip())
elif zeile.strip() in ("entscheidungen:", "fakten:"):
modus = "punkte"
elif zeile.strip() == "kanten:":
modus = "kanten"
elif modus == "punkte" and (m := re.match(r'\s+beleg:\s*"(.*)"', zeile)):
pass # Belege prüft pruefe.py; die Vorschau zeigt echten Code statt Zitaten
elif modus == "punkte" and zeile.startswith("- "):
aktuelle["entscheidungen"].append(zeile[2:].strip())
elif modus == "punkte" and zeile.startswith(" ") and aktuelle["entscheidungen"]:
aktuelle["entscheidungen"][-1] += " " + zeile.strip()
elif modus == "kanten" and (m := re.match(r"- ([\wäöü-]+):\s*(\S+)", zeile)):
aktuelle["kanten"].append({"typ": m.group(1), "ziel": m.group(2)})
elif modus in ("beschreibung", "input", "output") and zeile.startswith(" "):
aktuelle[modus] += " " + zeile.strip()
return out
def features_parsen(pfad) -> list:
bereiche = []
bereich = feature = None
for zeile in Path(pfad).read_text(encoding="utf-8").splitlines():
if zeile.startswith("# Bereich:"):
bereich = {"name": zeile.split(":", 1)[1].strip(), "beschreibung": "", "features": []}
bereiche.append(bereich); feature = None
elif zeile.startswith("## Feature:"):
titel = zeile[11:].strip()
flag = "kern" if "[kern]" in titel else "rand"
feature = {"titel": re.sub(r"\s*\[(kern|rand)\]", "", titel).strip(),
"flag": flag, "beschreibung": "", "teilfeatures": []}
bereich["features"].append(feature)
elif zeile.startswith("### ") and feature is not None:
feature["teilfeatures"].append({"titel": zeile[4:].strip(), "einheiten": []})
elif zeile.startswith("einheiten:") and feature and feature["teilfeatures"]:
feature["teilfeatures"][-1]["einheiten"] = \
[t.strip() for t in zeile.split(":", 1)[1].split(",") if t.strip()]
elif zeile.startswith("beschreibung:"):
ziel = feature if feature is not None else bereich
if ziel is not None:
ziel["beschreibung"] = zeile.split(":", 1)[1].strip()
return bereiche
def code_extrahieren(wurzel: Path, eid: str) -> str:
"""Quellcode eines Ankers (pfad::symbol): def/class-Block per Einrückung."""
if "::" not in eid:
return ""
datei, symbol = eid.split("::", 1)
p = wurzel / datei
if not p.is_file():
return ""
zeilen = p.read_text(encoding="utf-8").splitlines()
start = None
for i, z in enumerate(zeilen):
if re.match(rf"(\s*)(?:async\s+)?(?:def|class)\s+{re.escape(symbol)}\b", z):
start = i
break
if re.match(rf"{re.escape(symbol)}\s*[:=]", z): # Modul-Konstante
start = i
ende = i + 1
while ende < len(zeilen) and (zeilen[ende].startswith((" ", "\t", ")", "]", "}"))
or not zeilen[ende].strip()):
ende += 1
return "\n".join(zeilen[start:ende]).rstrip()
if start is None:
return ""
einzug = len(zeilen[start]) - len(zeilen[start].lstrip())
ende = start + 1
while ende < len(zeilen):
z = zeilen[ende]
if z.strip() and (len(z) - len(z.lstrip())) <= einzug:
break
ende += 1
return "\n".join(zeilen[start:ende]).rstrip()
def main():
features_pfad, wurzel, ausgabe = sys.argv[1], Path(sys.argv[2]), sys.argv[3]
einheiten = einheiten_parsen(sys.argv[4:])
bereiche = features_parsen(features_pfad)
code = {}
for e in einheiten.values():
for eid in [e["id"], *e["anker"]]:
code[e["id"]] = code.get(e["id"]) or code_extrahieren(wurzel, eid)
daten = {"bereiche": bereiche, "einheiten": einheiten, "code": code}
tpl = Path(__file__).with_name("vorschau-template.html").read_text(encoding="utf-8")
Path(ausgabe).write_text(tpl.replace("/*DATEN*/", json.dumps(daten, ensure_ascii=False)),
encoding="utf-8")
n_feat = sum(len(b["features"]) for b in bereiche)
print(f"{ausgabe}: {len(bereiche)} Bereiche, {n_feat} Features, {len(einheiten)} Einheiten")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,97 @@
# Einheiten: backend/config.py
stand: UNBEKANNT
## backend/config.py::_load_env
beschreibung: Lädt KEY=VALUE-Zeilen aus einer .env-Datei in os.environ, wobei die Datei bereits gesetzte Umgebungsvariablen überschreibt.
fakten:
- Liest die .env-Datei UTF-8-codiert ein.
beleg: "text = path.read_text(encoding=\"utf-8\")"
- Bricht den Ladevorgang still ab, wenn die Datei nicht lesbar ist.
beleg: "except OSError:"
- Überspringt Leerzeilen, Kommentarzeilen und Zeilen ohne Gleichheitszeichen.
beleg: "if not line or line.startswith(\"#\") or \"=\" not in line:"
- Entfernt umgebende Anführungszeichen aus den Werten.
beleg: "value = value.strip().strip('\"').strip(\"'\")"
- Schreibt jeden gültigen Schlüssel in os.environ (Datei gewinnt gegen vererbte Env).
beleg: "os.environ[key] = value"
kanten:
## backend/config.py::DEFAULT_PROVIDER
beschreibung: Stellt sicher, dass DEFAULT_PROVIDER aus der Umgebung gesetzt ist, und bricht den Start sonst mit einer RuntimeError ab.
fakten:
- Liest DEFAULT_PROVIDER aus der Umgebung mit leerem Default.
beleg: "DEFAULT_PROVIDER = os.getenv(\"DEFAULT_PROVIDER\", \"\")"
- Bricht den Start ab, wenn DEFAULT_PROVIDER nicht gesetzt ist.
beleg: "if not DEFAULT_PROVIDER:"
- Die Fehlermeldung nennt `minimax` als konkreten Beispielwert.
beleg: "DEFAULT_PROVIDER=minimax)\""
kanten:
## backend/config.py::PROVIDERS
beschreibung: Stellt die unabhängigen Provider-Stacks (claude, minimax, lokal) mit Rollen-Modellen, CLI-Aufrufen und Authentifizierungs-Umgebungsvariablen bereit.
fakten:
- Mehrere voneinander unabhängige Provider-Stacks sind in einem Dict zusammengefasst.
beleg: "PROVIDERS = {"
- Der `lokal`-Stack prüft Ollama-Erreichbarkeit über eine HTTP-URL.
beleg: "\"check_url\": \"http://localhost:11434/api/tags\","
- Der `minimax`-Stack nutzt den ENV-Key `MINIMAX_API_KEY` zur Authentifizierung.
beleg: "\"env_key\": \"MINIMAX_API_KEY\","
- Der `claude`-Stack nutzt OAuth-Authentifizierung statt eines API-Keys.
beleg: "\"env_key\": None, # auth via CLAUDE_CODE_OAUTH_TOKEN"
kanten:
- wird-genutzt-von: backend/config.py::resolve_role
## backend/config.py::ROLE_ROUTING
beschreibung: Bildet die vier Agent-Rollen quick/judge/guide/fast auf optionale prozessweite Override-Provider aus Umgebungsvariablen ab.
fakten:
- Vier Agent-Rollen können prozessweit auf einen anderen Provider-Stack umgeleitet werden.
beleg: "\"quick\": os.getenv(\"ROLE_QUICK\", \"\"),"
- Fehlende Env-Variablen ergeben einen leeren Override-String.
beleg: "os.getenv(\"ROLE_QUICK\", \"\"),"
kanten:
- wird-genutzt-von: backend/config.py::resolve_role
## backend/config.py::resolve_role
beschreibung: Liefert für eine Rolle das konkrete (provider, model)-Paar, wobei ein per Env gesetzter Rollen-Override Vorrang hat und sonst der Lauf-Provider genutzt wird.
fakten:
- Ein gesetzter Rollen-Override aus ROLE_ROUTING hat Vorrang vor dem Lauf-Provider.
beleg: "target = ROLE_ROUTING.get(role, \"\") or run_provider"
- Die Syntax "provider:model" erlaubt es, ein bestimmtes Modell zu pinnen.
beleg: "provider, _, model = target.partition(\":\")"
- Unbekannte Provider fallen auf den Lauf-Provider zurück.
beleg: "if provider not in PROVIDERS:"
- Fehlt eine explizite Modellangabe, wird das Rollen-Default des Stacks genutzt.
beleg: "model = PROVIDERS.get(provider, {}).get(role, \"\")"
kanten:
- nutzt: backend/config.py::ROLE_ROUTING
- nutzt: backend/config.py::PROVIDERS
## backend/config.py::TIMEOUTS
beschreibung: Definiert pro Agent-Schritt ein (Basis, pro-Block)-Timeout-Paar, das unabhängig vom Provider gilt.
fakten:
- Jeder Agent-Schritt hat individuelle Timeout-Werte als (Basis, pro-Block)-Tupel.
beleg: "\"research_mapping\": (600, 3), # n = pre-merged entries"
- Die Judge-Caps wurden am 2026-07-04 reduziert.
beleg: "# Judge caps tightened 2026-07-04: judge p50 is 672 s;"
- Der `writer` skaliert mit 60 s pro Sektion.
beleg: "\"writer\": (450, 60), # per section"
- QA/Repair-Wellen (`qa_judge`) skalieren nicht mit n.
beleg: "\"qa_judge\": (600, 0),"
kanten:
- wird-genutzt-von: backend/config.py::_apply_param_overrides
## backend/config.py::_apply_param_overrides
beschreibung: Liest ein JSON-Override-Dict aus der Umgebungsvariable CREATOR_PARAMS und überschreibt damit numerische Tuning-Konstanten dieses Moduls zur Prozess-Startzeit.
fakten:
- Bricht die Override-Anwendung still ab, wenn CREATOR_PARAMS nicht gesetzt ist.
beleg: "if not raw:"
- Weist ungültiges JSON in CREATOR_PARAMS mit SystemExit zurück.
beleg: "except ValueError:"
- Timeout-Overrides werden über das Präfix `TIMEOUT_` in TIMEOUTS eingespielt.
beleg: "if key.startswith(\"TIMEOUT_\"):"
- Unbekannte Schlüssel oder boolesche Werte werden mit SystemExit abgewiesen.
beleg: "isinstance(g[key], bool):"
- Numerische Overrides behalten den ursprünglichen Typ des Ziels.
beleg: "g[key] = type(g[key])(val)"
kanten:
- nutzt: backend/config.py::TIMEOUTS

View File

@@ -0,0 +1,150 @@
# Einheiten: backend/kanban.py
stand: UNBEKANNT
## backend/kanban.py::Flow
beschreibung: Verwaltet den Laufzeit-Kontext eines Topics: aktive Zähler, Producer-Status und Koordinations-Events.
fakten:
- Zählt aktive Producer und meldet done, wenn alle fertig sind
beleg: "return self.producers <= 0"
- Notifybar, wenn sich Producer-Zähler oder aktive Karten ändern
beleg: "self.wake.set()"
- Führt Topic-spezifischen Zustand (z.B. infra_paused)
beleg: "self.state: dict = {}"
- Registriert laufende Flows global für Cancel/Attach
beleg: "active_flows: dict[str, \"Flow\"] = {}"
- Verfolgt Karten-IDs, die gerade in einem Processor laufen
beleg: "self.active_cards: set[str] = set()"
kanten:
- nutzt: backend/database.py::kanban_get_card
- wird-genutzt-von: backend/kanban.py::_worker
- wird-genutzt-von: backend/kanban.py::run_flow
## backend/kanban.py::Stage
beschreibung: Kapselt eine Spalte des Kanban-Boards: Board/Stage-Name, Processor und Barrier/Serial/Gate/Drain-Parameter.
fakten:
- Prozessor wird pro Stage nur einmal gespeichert, von allen Rows gemeinsam genutzt
beleg: "self.process = process"
- Serial erzwingt sequentielle Abarbeitung (auch bei drain)
beleg: "self.serial = serial or drain"
- Barrier-Wert wird unverändert übernommen
beleg: "self.barrier = barrier"
- Upstream wird von chain_stages befüllt, nicht im Konstruktor
beleg: "self.upstream: list[str] = []"
kanten:
- nutzt: backend/kanban.py::chain_stages
- wird-genutzt-von: backend/kanban.py::_worker
- wird-genutzt-von: backend/kanban.py::run_flow
## backend/kanban.py::chain_stages
beschreibung: Baut die upstream-Kette aller Stages — jede Stage sieht alle vorherigen Stages als upstream.
fakten:
- Befüllt upstream jeder Stage mit allen davor liegenden Stage-Namen
beleg: "s.upstream = list(seen)"
- Gibt die Eingabeliste zurück (Method-Chaining-fähig)
beleg: "return stages"
- producers gelten implizit als upstream von allem
beleg: "Producers are upstream of everything implicitly"
kanten:
- wird-genutzt-von: backend/kanban.py::Stage
## backend/kanban.py::quiescent
beschreibung: Prüft, ob alle gegebenen Stages ruhen (keine aktiven Worker UND keine Cards in der DB).
fakten:
- Leere Stage-Liste gilt als quiescent
beleg: "if not stages: return True"
- Zählt nur Cards in der DB, nicht in-flight im Speicher
beleg: "return await db.kanban_count(flow.topic, list(stages)) == 0"
- Berücksichtigt auch Producer-Status via flow.active_in
beleg: "if flow.active_in(stages): return False"
kanten:
- nutzt: backend/database.py::kanban_count
- nutzt: backend/kanban.py::Flow
- wird-genutzt-von: backend/kanban.py::_worker
## backend/kanban.py::_sleep_wake
beschreibung: Blockiert bis Wake-Event oder Poll-Timeout, dann cleart das Event.
fakten:
- Wartet maximal _POLL Sekunden
beleg: "await asyncio.wait_for(flow.wake.wait(), timeout=_POLL)"
- Timeout bedeutet kein Wake innerhalb von _POLL
beleg: "except asyncio.TimeoutError: pass"
- Event wird nach Return gecleart
beleg: "flow.wake.clear()"
kanten:
- nutzt: backend/kanban.py::Flow
## backend/kanban.py::_fail_package
beschreibung: Setzt nicht-advanced Cards auf Backoff oder Dead-Letter nach Processor-Fehler.
fakten:
- Überspringt Cards, die bereits weitergingen (ihre Stage hat sich geändert)
beleg: "if cur is None or cur[\"stage\"] != spec.stage: continue"
- Nutzt db.kanban_fail_card für Backoff und potentielles Dead-Lettering
beleg: "dead = await db.kanban_fail_card(flow.topic, spec.board, c[\"card_id\""
- Loggt wann eine Card nach dead geht
beleg: "log.warning(\"kanban %s/%s: card %s → dead (%s)\""
kanten:
- nutzt: backend/database.py::kanban_get_card
- nutzt: backend/database.py::kanban_fail_card
- wird-genutzt-von: backend/kanban.py::_worker
## backend/kanban.py::_worker
beschreibung: Dauerläufer pro Stage: zieht Cards, dispatcht bis INFLIGHT concurrent, handhabt Errors und Exit.
fakten:
- Zieht max KANBAN_BATCH Cards pro Pull (bzw. 100_000 bei drain)
beleg: "batch = 100_000 if spec.drain else KANBAN_BATCH"
- Hält Claims für alle in-flight Cards um Doppel-Pull zu verhindern
beleg: "claimed: set[str] = set()"
- AgentInfraError (429/Timeout) pausiert den Flow komplett
beleg: "flow.state[\"infra_paused\"] = True"
- Any Exception wird nicht-propagiert — nur _fail_package aufgerufen
beleg: "except Exception as e: log.info(\"kanban %s/%s: %s: %s\""
- Prüft Gate (falls gesetzt) VOR dem Pull
beleg: "if spec.gate is not None and not spec.gate(): return False"
- Barrier-Worker wartet auf upstream-Quieszenz vor dem Pull
beleg: "return await quiescent(flow, spec.upstream)"
- Exit erst wenn research_done UND alle Stages quiescent
beleg: "return (flow.research_done and not flow.active_in(all_stages)"
kanten:
- ruft-auf: backend/kanban.py::quiescent
- ruft-auf: backend/kanban.py::_fail_package
- ruft-auf: backend/kanban.py::_sleep_wake
- nutzt: backend/database.py::kanban_pull
- nutzt: backend/kanban.py::Flow
- nutzt: backend/kanban.py::Stage
- wird-genutzt-von: backend/kanban.py::run_flow
## backend/kanban.py::run_flow
beschreibung: Startet Producer + einen Worker pro Stage, läuft bis globaler Quieszenz oder stop-Flag.
fakten:
- Registriert Flow global für Cancel/Attach
beleg: "active_flows[flow.topic] = flow"
- Ein Worker pro Stage (serial=1, sonst WORKER_INFLIGHT)
beleg: "1 if s.serial else WORKER_INFLIGHT"
- Restart-Schleife fängt Producer an, die genau beim Exit dazukamen
beleg: "workers = _spawn_workers()"
- Stoppt Flows bei infra_paused oder bei globaler Quieszenz
beleg: "if flow.stop or (flow.research_done and await quiescent(flow, names))"
- Räumt active_flows im finally-Block auf
beleg: "active_flows.pop(flow.topic, None)"
kanten:
- ruft-auf: backend/kanban.py::_worker
- ruft-auf: backend/kanban.py::_progress
- ruft-auf: backend/kanban.py::quiescent
- nutzt: backend/kanban.py::Flow
- nutzt: backend/kanban.py::Stage
- nutzt: backend/database.py::kanban_stage_counts
- wird-genutzt-von: backend/kanban.py::Flow (spawn_research callback)
## backend/kanban.py::_progress
beschreibung: Reportet periodisch die Gesamtzahl aller Karten im Flow an den Callback.
fakten:
- Zählt Cards pro Stage via db.kanban_stage_counts
beleg: "counts = await db.kanban_stage_counts(flow.topic)"
- Summiert über alle Stages und Boards
beleg: "total = sum(n for stages in counts.values() for n in stages.values())"
- Ruft set_p alle 1.0 Sekunden auf
beleg: "await asyncio.sleep(1.0)"
kanten:
- nutzt: backend/database.py::kanban_stage_counts
- nutzt: backend/kanban.py::Flow
- wird-ausgelöst-von: backend/kanban.py::run_flow

View File

@@ -0,0 +1,147 @@
# Einheiten: backend/kanban.py
stand: UNBEKANNT
## backend/kanban.py::Flow
beschreibung: Hält den Laufzeit-Zustand eines Topic-Runs (aktive Worker pro Stage, Producer-Zähler, Wake-Event), damit andere Einheiten Quieszenz und Producer-Lebenszyklus prüfen können.
fakten:
- Hält pro Stage einen Zähler aktiver Worker.
beleg: "self.active: dict[str, int] = {}"
- Zählt zusätzlich die aktuell laufenden Research-Producer.
beleg: "self.producers = 0"
- research_done ist nur True, wenn kein einziger Producer mehr läuft.
beleg: "return self.producers <= 0"
- leave klemmt den Active-Zähler beim Dekrementieren bei Null ab.
beleg: "self.active[stage] = max(0, self.active.get(stage, 0) - 1)"
- active_in meldet True, sobald irgendeine der angegebenen Stages aktive Worker hat.
beleg: "return any(self.active.get(s, 0) > 0 for s in stages)"
kanten:
- wird-genutzt-von: backend/kanban.py::quiescent
- wird-genutzt-von: backend/kanban.py::_sleep_wake
- wird-genutzt-von: backend/kanban.py::_fail_package
- wird-genutzt-von: backend/kanban.py::_worker
- wird-genutzt-von: backend/kanban.py::run_flow
- wird-genutzt-von: backend/kanban.py::_progress
## backend/kanban.py::Stage
beschreibung: Beschreibt eine Spalte des Kanban-Boards mit Board-Name, Stage-Name, Processor und Verhaltens-Flags (barrier, serial, gate, drain), die der Worker beim Pull-Verhalten auswertet.
fakten:
- Repräsentiert genau eine Spalte, identifiziert durch Board- und Stage-Name, mit einem Processor.
beleg: "self.process = process"
- Setzt serial automatisch mit, sobald drain True ist.
beleg: "self.serial = serial or drain"
- Hält eine leere upstream-Liste, die chain_stages mit Vorgänger-Stages füllt.
beleg: "self.upstream: list[str] = []"
kanten:
- wird-genutzt-von: backend/kanban.py::chain_stages
- wird-genutzt-von: backend/kanban.py::_worker
- wird-genutzt-von: backend/kanban.py::_fail_package
- wird-genutzt-von: backend/kanban.py::run_flow
## backend/kanban.py::chain_stages
beschreibung: Füllt für jede Stage deren upstream-Liste mit allen Stages, die in der übergebenen Liste davor stehen, damit Barrier-Worker ihre Vorgänger kennen.
fakten:
- Setzt das upstream jeder Stage auf die zuvor gesehenen Stages.
beleg: "s.upstream = list(seen)"
- Gibt die unveränderte Eingabeliste wieder zurück.
beleg: "return stages"
kanten:
- nutzt: backend/kanban.py::Stage
## backend/kanban.py::quiescent
beschreibung: Liefert True genau dann, wenn kein Worker in den angegebenen Stages aktiv ist UND keine Karte dort in der Queue liegt — die Barrier-/Exit-Bedingung.
fakten:
- Eine leere Stages-Liste gilt sofort als quiescent.
beleg: "return True"
- Sobald irgendeine der Stages aktive Worker hat, wird sofort False zurückgegeben.
beleg: "return False"
- True nur, wenn die Datenbank für keine der Stages eine queued Card meldet.
beleg: "return await db.kanban_count(flow.topic, list(stages)) == 0"
kanten:
- ruft-auf: backend/kanban.py::Flow
- ruft-auf: backend/database.py::kanban_count
- wird-genutzt-von: backend/kanban.py::_worker
- wird-genutzt-von: backend/kanban.py::run_flow
## backend/kanban.py::_sleep_wake
beschreibung: Wartet mit Polling-Timeout auf das Wake-Event des Flows und räumt es danach auf, damit der Worker-Loop reaktiv aber nicht busy-waitend bleibt.
fakten:
- Wartet auf das Wake-Event des Flows mit einem Polling-Timeout.
beleg: "await asyncio.wait_for(flow.wake.wait(), timeout=_POLL)"
- Fängt TimeoutError ab, damit die Polling-Schleife weiterläuft.
beleg: "except asyncio.TimeoutError:"
- Löscht das Wake-Event vor der Rückkehr.
beleg: "flow.wake.clear()"
kanten:
- nutzt: backend/kanban.py::Flow
- wird-genutzt-von: backend/kanban.py::_worker
## backend/kanban.py::_fail_package
beschreibung: Behandelt Package-Fehler mit exponentiellem Backoff bzw. Dead-Letter, aber nur für Karten, die der Processor noch nicht weitergerückt hat.
fakten:
- Überspringt Karten, die nicht mehr auf der Stage des Processors sitzen (bereits advanced).
beleg: "if cur is None or cur["stage"] != spec.stage:"
- Ruft db.kanban_fail_card mit MAX_CARD_RETRIES und RETRY_BACKOFF auf.
beleg: "await db.kanban_fail_card(flow.topic, spec.board, c["card_id"], error,"
- Loggt eine Warnung, wenn eine Karte auf die Dead-Stage verschoben wird.
beleg: "log.warning("kanban %s/%s: card %s → dead (%s)","
kanten:
- nutzt: backend/kanban.py::Flow
- nutzt: backend/kanban.py::Stage
- ruft-auf: backend/database.py::kanban_get_card
- ruft-auf: backend/database.py::kanban_fail_card
- wird-genutzt-von: backend/kanban.py::_worker
## backend/kanban.py::_worker
anker: backend/kanban.py::_run
anker: backend/kanban.py::_idle_exit
anker: backend/kanban.py::_may_pull
beschreibung: Pullt Karten aus der Queue der eigenen Stage, hält bis zu `inflight` Packages gleichzeitig am Laufen und behandelt sowohl Backoff/Dead-Letter als auch Infra-Pausen, bis der gesamte Flow quiescent ist.
fakten:
- Bei drain wird die gesamte Queue in einem Package gezogen, sonst KANBAN_BATCH auf einmal.
beleg: "batch = 100_000 if spec.drain else KANBAN_BATCH"
- Hält ein claimed-Set mit Card-IDs, damit parallele Pulls dieselbe Karte nicht doppelt holen.
beleg: "claimed: set[str] = set()"
- Bei AgentInfraError wird der ganze Flow pausiert (stop + infra_paused), statt zu dead-lettern.
beleg: "flow.stop = True"
- Barrier-Worker pullen nur, wenn das Gate offen ist und alle Upstream-Stages quiescent sind.
beleg: "return await quiescent(flow, spec.upstream)"
- Bricht beim Exit alle noch laufenden Package-Tasks ab und sammelt sie mit Exceptions ein.
beleg: "await asyncio.gather(*tasks, return_exceptions=True)"
kanten:
- nutzt: backend/kanban.py::Flow
- nutzt: backend/kanban.py::Stage
- ruft-auf: backend/kanban.py::quiescent
- ruft-auf: backend/kanban.py::_sleep_wake
- ruft-auf: backend/kanban.py::_fail_package
- ruft-auf: backend/database.py::kanban_pull
- ruft-auf: backend/database.py::kanban_count
- nutzt: backend/pipeline.py::AgentInfraError
- wird-genutzt-von: backend/kanban.py::run_flow
## backend/kanban.py::run_flow
beschreibung: Startet die übergebenen Producer und je einen Worker pro Stage, lässt sie bis zur globalen Quieszenz laufen, respawnt sie bei Bedarf und meldet den Flow live in active_flows an bzw. ab.
fakten:
- Trägt den Flow für die Laufzeit in das Modul-Dict active_flows ein.
beleg: "active_flows[flow.topic] = flow"
- Startet pro Stage einen Worker mit inflight=1 für serial-Stages, sonst WORKER_INFLIGHT.
beleg: "1 if s.serial else WORKER_INFLIGHT,"
- Startet den Progress-Reporter nur, wenn ein set_p-Callback übergeben wurde.
beleg: "progress = asyncio.create_task(_progress(flow, set_p
## backend/kanban.py::_progress
beschreibung: Pollt sekündlich die Stage-Counts aller Boards aus der Datenbank und meldet die Gesamtzahl laufender Karten über das set_p-Callback zurück, bis das Flow-Stop-Flag gesetzt ist.
fakten:
- Läuft in einer Schleife, bis das Stop-Flag des Flows gesetzt ist.
beleg: "while not flow.stop:"
- Holt pro Board die aktuellen Stage-Counts aus der Datenbank.
beleg: "counts = await db.kanban_stage_counts(flow.topic)"
- Summiert alle Karten über alle Boards und Stages zu einer Gesamtzahl.
beleg: "total = sum(n for stages in counts.values() for n in stages.values())"
- Reicht die Gesamtzahl als formatierten deutschen String an set_p weiter.
beleg: "set_p(f\"Kanban: {total} Karten im Fluss\")"
- Schläft eine Sekunde zwischen den Iterationen.
beleg: "await asyncio.sleep(1.0)"
kanten:
- nutzt: backend/kanban.py::Flow
- ruft-auf: backend/database.py::kanban_stage_counts
- wird-genutzt-von: backend/kanban.py::run_flow

View File

@@ -0,0 +1,230 @@
# Einheiten: backend/pipeline.py
stand: UNBEKANNT
## backend/pipeline.py::cancel_guide
anker: backend/pipeline.py::is_guide_cancelled
anker: backend/pipeline.py::clear_guide_cancelled
beschreibung: Verwaltet den Cancel-Zustand eines Guides (Markieren, Abfragen, Zurücksetzen) und markiert den Guide-Datensatz als fehlgeschlagen unter Erhalt des Fortschritts.
fakten:
- Beim Markieren wird der Guide zur internen Cancel-Menge hinzugefügt, der Agent-Scope gelöscht und laufende Subprozesse beendet.
beleg: "_cancelled.add(guide_id)"
- Nach dem Abbruch wird der Guide-Datensatz auf status="error" gesetzt, der Fortschritt bleibt erhalten und es wird eine UTC-Zeitmarke geschrieben.
beleg: "await update_guide(guide_id, status=\"error\", progress=None, error_msg=\"Cancelled — progress is preserved\""
- `is_guide_cancelled` meldet ausschließlich über die Modul-interne Menge, nicht aus dem Datensatz.
beleg: "return guide_id in _cancelled"
- `clear_guide_cancelled` entfernt den Guide aus der Cancel-Menge und räumt den zugehörigen Agent-Scope auf, sodass ein Neustart nicht blockiert wird.
beleg: "clear_scope(f\"{guide_id}-\") # clear scope → restart not blocked"
kanten:
- ruft-auf: backend/pipeline.py::is_guide_cancelled
- ruft-auf: backend/pipeline.py::clear_guide_cancelled
- nutzt: backend/database.py::update_guide
- nutzt: backend/agents.py::cancel_scope
- nutzt: backend/agents.py::kill_process
- nutzt: backend/agents.py::clear_scope
## backend/pipeline.py::_set_progress
anker: backend/pipeline.py::_set_step
anker: backend/pipeline.py::_fail
beschreibung: Schreibt Fortschritts- und Schrittinformationen sowie Fehlermeldungen eines Guides persistiert in den Datensatz.
fakten:
- `_set_progress` aktualisiert ausschließlich das Fortschrittsfeld zusammen mit einer UTC-Zeitmarke.
beleg: "await update_guide(guide_id, progress=progress, updated_at=now)"
- `_set_step` schreibt zusätzlich den numerischen Schritt zusammen mit dem Fortschritt und einem Zeitstempel.
beleg: "await update_guide(guide_id, step=step, progress=progress, updated_at=now)"
- `_fail` setzt den Guide auf den Fehlerstatus, leert den Fortschritt und hinterlegt die übergebene Fehlermeldung.
beleg: "await update_guide(guide_id, status=\"error\", progress=None, error_msg=msg, updated_at=now)"
kanten:
- nutzt: backend/database.py::update_guide
## backend/pipeline.py::_prompt
anker: backend/pipeline.py::_extra
anker: backend/pipeline.py::_log
anker: backend/pipeline.py::_claude_error
anker: backend/pipeline.py::_is_infra
anker: backend/pipeline.py::_gather_error
anker: backend/pipeline.py::_timeout
anker: backend/pipeline.py::_problems_schema
anker: backend/pipeline.py::_str_list
anker: backend/pipeline.py::_runde_schema
anker: backend/pipeline.py::_enum_map_schema
anker: backend/pipeline.py::_yesno_schema
beschreibung: Stellt Hilfsfunktionen für Prompt-Bau, Logging, Fehlerklassifikation, Timeout-Berechnung und JSON-Schemavalidierung bereit.
fakten:
- `_prompt` lädt ein Prompt-Template aus `TEMPLATES_DIR/Prompt/<name>.md` und formatiert es mit den übergebenen Schlüsselwortargumenten.
beleg: "template = (TEMPLATES_DIR / \"Prompt\" / f\"{name}.md\").read_text(encoding=\"utf-8\")"
- `_extra` hängt zusätzliche Nutzeranweisungen in einem klar markierten Block an, sofern welche vorhanden sind.
beleg: "return f\"\\n\\nADDITIONAL INSTRUCTIONS FROM THE USER:\\n{instructions}\\n\" if instructions else \"\""
- `_is_infra` erkennt Transport- bzw. Infrastrukturfehler anhand einer festen Markerliste, um sie von inhaltlichen Fehlern zu unterscheiden.
beleg: "return any(m in (err or \"\") for m in _INFRA_MARKERS)"
- `_claude_error` formatiert eine Fehlermeldung aus Returncode, stdout und stderr, mit Fallback auf das Ende der stdout-Ausgabe.
beleg: "return f\"{label} (exit {returncode}, no output)\""
- `_timeout` berechnet die Timeout-Dauer eines Schritts als Basis plus skalierten Zuschlag pro Eintrag.
beleg: "return base + per * n"
- `_problems_schema` liefert eine leere Liste bei `{"ok": True}`, eine bereinigte Problemliste bei vorhandenen Problemen, sonst `None`.
beleg: "if data.get(\"ok\") is True:"
- `_runde_schema` liefert im Finalmodus nur dann ein Ergebnis, wenn keine Restfragen offen sind.
beleg: "if include is None or rest is None or (final and rest):"
- `_enum_map_schema` liefert eine Parser-Factory, die bei ungültigen IDs oder Werten strikt `None` zurückgibt.
beleg: "if value not in allowed:"
- `_yesno_schema` ist die konkrete Ausprägung der Enum-Map-Factory für das Triage-Feld "relevant" mit den Werten "ja"/"nein".
beleg: "_yesno_schema = _enum_map_schema(\"relevant\", _YESNO) # triage gate ∈ ja/nein"
kanten:
- nutzt: backend/config.py::TEMPLATES_DIR
- nutzt: backend/config.py::TIMEOUTS
## backend/pipeline.py::_race
anker: backend/pipeline.py::AgentInfraError
beschreibung: Startet parallele Agent-Slots, sammelt eine konfigurierbare Quorum-Anzahl gültiger Ergebnisse und behandelt dabei Timeouts, Infrastrukturfehler, Hedging, Grace-Periode und Stornierung.
fakten:
- Slots, die länger als `hedge_s` ohne Ergebnis laufen, bekommen genau einen parallelen Zwilling mit dem Suffix `-h`.
beleg: "hedged.add(i)"
- Sobald das Quorum steht und die Grace-Frist abgelaufen ist, werden laufende Agents beendet und die bis dahin gesammelten Ergebnisse zurückgegeben.
beleg: "if deadline is not None and len(results) >= quorum and loop.time() >= deadline:"
- Infrastrukturfehler werden mit eigenem Zähler und wachsendem Backoff bis zu `_INFRA_MAX_RETRIES` wiederholt; bei Erschöpfung wird `AgentInfraError` ausgelöst.
beleg: "raise AgentInfraError("
- Eine aktive Stornierung führt sofort zum Abbruch ohne Neustart und Rückgabe von `None`.
beleg: "if cancelled and cancelled():"
- Im `finally`-Block werden alle noch laufenden Tasks storniert und ihre Subprozesse beendet, auch beim vorzeitigen Abbruch.
beleg: "for task, i in tasks.items():"
kanten:
- nutzt: backend/agents.py::run_agent
- nutzt: backend/agents.py::kill_process
- nutzt: backend/pipeline.py::_is_infra
- nutzt: backend/pipeline.py::_claude_error
- nutzt: backend/pipeline.py::_log
- nutzt: backend/pipeline.py::_timeout
- nutzt: backend/config.py::MAX_CONCURRENT_GENERATIONS
## backend/pipeline.py::GenContext
anker: backend/pipeline.py::run_single_slot
beschreibung: Bündelt Pipeline-Parameter (Topic, Provider, Stornierungsprüfung, Guide-ID) zu einem Wert, der durch die Pipeline-Glieder weitergereicht wird.
fakten:
- `GenContext` kapselt die langen Argumentlisten der Pipeline-Aufrufe in einem Wert.
beleg: "\"\"\"Pipeline parameters passed through — saves long argument signatures.\"\"\""
- `is_cancelled` ist eine parameterlose Funktion, die den aktuellen Stornierungszustand abfragt.
beleg: "is_cancelled: Callable[[], bool]"
- `run_single_slot` führt genau einen Agent-Aufruf als Rennen mit Quorum 1 aus und übersetzt das Ergebnis in einen `(status, wert)`-Tripel.
beleg: "res = await _race(ctx.topic, label, slots, 1, timeout, ctx.provider, cancelled=ctx.is_cancelled)"
- Bei gesetztem Quorum und Stornierung gibt `run_single_slot` `(CANCELLED, None)` zurück; bei verfehltem Quorum `(FAILED, None)`.
beleg: "if res is None:"
kanten:
- ruft-auf: backend/pipeline.py::_race
## backend/pipeline.py::_gather_progress
beschreibung: Führt Coroutinen nebenläufig aus, meldet nach jeder Vervollständigung den Live-Fortschritt und liefert die Ergebnisse in Eingabereihenfolge samt Ausnahmen.
fakten:
- Vor dem Start der Coroutinen wird der Initialwert `done` an den Reporter übergeben.
beleg: "await report(done, total)"
- Jeder abgeschlossene Job ruft den Reporter auch dann auf, wenn die Coroutine eine Ausnahme wirft, dank `try/finally`.
beleg: "finally:"
- `asyncio.gather` wird mit `return_exceptions=True` aufgerufen, damit ein Fehler in einer Coroutine die übrigen nicht abbricht.
beleg: "return await asyncio.gather(*[wrap(c) for c in coros], return_exceptions=True)"
## backend/pipeline.py::is_guide_cancelled
beschreibung: Beantwortet die Frage, ob ein Guide abgesagt wurde, ausschließlich anhand der Modul-internen Cancel-Menge.
fakten:
- Die Prüfung liest ausschließlich die Modul-interne Cancel-Menge und nicht den Datensatz.
beleg: "return guide_id in _cancelled"
kanten:
## backend/pipeline.py::clear_guide_cancelled
beschreibung: Entfernt einen Guide aus der internen Cancel-Menge und räumt den zugehörigen Agent-Scope auf, damit ein Neustart nicht blockiert wird.
fakten:
- Entfernt die Guide-ID aus der Modul-internen Cancel-Menge.
beleg: "_cancelled.discard(guide_id)"
- Löscht den Agent-Scope unter dem Prefix "{guide_id}-" mit Hinweis auf Restart-Freigabe.
beleg: "clear_scope(f\"{guide_id}-\") # clear scope → restart not blocked"
kanten:
- nutzt: backend/agents.py::clear_scope
## backend/pipeline.py::_set_step
beschreibung: Schreibt zusätzlich zum Fortschritt den numerischen Schritt eines Guides in den Datensatz und stempelt die Aktualisierungszeit.
fakten:
- Persistiert step, progress und updated_at in einem update_guide-Aufruf.
beleg: "await update_guide(guide_id, step=step, progress=progress, updated_at=now)"
- Setzt den Zeitstempel als UTC-ISO-String.
beleg: "now = datetime.now(timezone.utc).isoformat()"
kanten:
- nutzt: backend/database.py::update_guide
## backend/pipeline.py::_fail
beschreibung: Setzt den Guide-Datensatz konsistent auf den Fehlerstatus mit übergebener Meldung und löscht den Fortschritt.
fakten:
- Schreibt status="error", leert progress und hinterlegt die übergebene Fehlermeldung.
beleg: "await update_guide(guide_id, status=\"error\", progress=None, error_msg=msg, updated_at=now)"
- Stempelt den Datensatz mit einer UTC-Zeitmarke.
beleg: "now = datetime.now(timezone.utc).isoformat()"
kanten:
- nutzt: backend/database.py::update_guide
## backend/pipeline.py::_extra
beschreibung: Erzeugt einen klar markierten Block mit zusätzlichen Nutzeranweisungen für einen Prompt oder liefert einen leeren String, falls keine Anweisungen vorliegen.
fakten:
- Mit Anweisungen wird ein Block mit fester Überschrift eingefügt.
beleg: "return f\"\\n\\nADDITIONAL INSTRUCTIONS FROM THE USER:\\n{instructions}\\n\" if instructions else \"\""
- Ohne Anweisungen ist das Ergebnis der leere String.
beleg: "if instructions else \"\""
kanten:
## backend/pipeline.py::_log
beschreibung: Schreibt eine einheitlich formatierte Info-Meldung mit Topic-Präfix über den Pipeline-Logger.
fakten:
- Nutzt den Modul-Logger mit dem Namen "creator.pipeline".
beleg: "log = logging.getLogger(\"creator.pipeline\")"
- Setzt Topic in eckige Klammern vor die eigentliche Meldung.
beleg: "log.info(\"[%s] %s\", topic, msg)"
kanten:
- wird-genutzt-von: backend/pipeline.py::_race
## backend/pipeline.py::_claude_error
beschreibung: Baut eine kompakte Fehlermeldung aus Returncode, stdout und stderr eines Agent-Aufrufs mit abgestufter Fallback-Strategie.
fakten:
- Zieht zuerst den gestrippten stderr heran.
beleg: "stderr = (stderr or \"\").strip()"
- Bevorzugt stderr bis 1000 Zeichen als Fehlermeldung.
beleg: "return f\"{label}: {stderr[:1000]}\""
- Bei leerem stderr wird das Ende der stdout als Fallback genutzt.
beleg: "tail = (stdout or \"\").strip()[-500:]"
- Bei komplett fehlender Ausgabe erscheint nur Label und Exitcode mit "no output".
beleg: "return f\"{label} (exit {returncode}, no output)\""
kanten:
- wird-genutzt-von: backend/pipeline.py::_race
- wird-genutzt-von: backend/pipeline.py::_gather_error
## backend/pipeline.py::_is_infra
beschreibung: Klassifiziert eine Fehlermeldung als Transport- bzw. Infrastrukturfehler anhand einer festen Markerliste.
fakten:
- Die Markerliste enthält HTTP 429, HTTP 5xx, rate_limit, Timeout after und diverse Netzfehler.
beleg: "_INFRA_MARKERS = (\"HTTP 429\", \"HTTP 5\", \"rate_limit\", \"Timeout after\","
- Ein Marker-Treffer genügt, um die Meldung als Infra-Fehler zu werten.
beleg: "return any(m in (err or \"\") for m in _INFRA_MARKERS)"
- Der Docstring grenzt Infra-Fehler von inhaltlichem Fehlschlag ab.
beleg: "\"\"\"Transport-/Infra-Fehler (retry + pause) statt inhaltlichem Fehlschlag.\"\"\""
kanten:
- wird-genutzt-von: backend/pipeline.py::_race
## backend/pipeline.py::_gather_error
beschreibung: Wählt aus einer Liste von Slot-Ergebnissen den ersten Fehler aus und formatiert ihn als kompakten Fehlertext.
fakten:
- Iteriert die Ergebnisliste in Reihenfolge und meldet den ersten Treffer.
beleg: "for r in results:"
- Ausnahmen werden als Klassenname plus Meldung ausgegeben.
beleg: "return f\"{label}: {type(r).__name__}: {r}\""
- Nicht-null-Returncodes werden an `_claude_error` zur Formatierung delegiert.
beleg: "return _claude_error(label, returncode, stdout, stderr)"
- Ohne verwertbares Ergebnis wird ein pauschaler Fehlertext geliefert.
beleg: "return f\"{label}: no usable result\""
kanten:
- nutzt: backend/pipeline.py::_claude_error
## backend/pipeline.py::_timeout
beschreibung: Berechnet die Timeout-Dauer eines Pipeline-Schritts als Basis plus skalierten Zuschlag pro Eintrag aus der zentralen TIMEOUTS-Tabelle.
fakten:
- Liest Basis und Zuschlag pro Eintrag aus TIMEOUTS für den angegebenen Schritt.
beleg: "base, per = TIMEOUTS[step]"
- Liefert die Summe aus Basis und Zuschlag multipliziert mit n.
beleg: "return base + per * n"
kanten:
- nutzt: backend/config.py::TIMEOUTS
## backend/pipeline.py::_problems_schema
beschreibung: Validiert Agent-Antworten für das Probleme-Feld und liefert je nach Form eine leere Liste, die bereinigte

137
phase0/beispiel-features.md Normal file
View File

@@ -0,0 +1,137 @@
# Bereich: Kanban-Engine (kartenbasierte Themenverarbeitung)
beschreibung: Verteilt einen Themen-Run als Kanban-Board über mehrere Stages und zieht Karten worker-gesteuert durch die Verarbeitung.
## Feature: Themen-Run starten und überwachen [kern]
beschreibung: Initialisiert pro Topic den Laufzeit-Zustand, fährt Worker pro Stage hoch und meldet den Live-Fortschritt der Karten im Fluss.
### Kann für ein Topic einen Lauf starten, Worker hochfahren und live registrieren
einheiten: backend/kanban.py::run_flow
### Kann den Laufzeit-Zustand pro Topic (aktive Worker, Producer, Wachsignal) führen
einheiten: backend/kanban.py::Flow
### Kann sekündlich die Gesamtzahl der Karten im Fluss als deutschen Statustext melden
einheiten: backend/kanban.py::_progress
## Feature: Board und Stages modellieren [kern]
beschreibung: Bildet die Spalten eines Kanban-Boards mit Verarbeitung und Verhaltens-Flags ab und verknüpft Vorgänger-Stages automatisch.
### Kann eine Kanban-Spalte mit zugeordneter Verarbeitung und Verhaltens-Flags (Serial, Drain, Barrier, Gate) beschreiben
einheiten: backend/kanban.py::Stage
### Kann Vorgänger-Spalten automatisch verketten, sodass Barrier-Worker ihre Quellen kennen
einheiten: backend/kanban.py::chain_stages
## Feature: Karten worker-gesteuert abarbeiten [kern]
beschreibung: Pullt Karten aus der Queue der eigenen Stage, hält mehrere Pakete gleichzeitig und wartet reaktiv auf neue Arbeit.
### Kann Karten pullen, mehrere Pakete gleichzeitig halten und beim Stopp alle laufenden Tasks sauber abbrechen
einheiten: backend/kanban.py::_worker
### Kann reaktiv auf neue Arbeit warten, statt dauerhaft zu pollen
einheiten: backend/kanban.py::_sleep_wake
### Kann Ruhe über aktive Worker und Queue-Bestand zuverlässig erkennen und zum Beenden auflösen
einheiten: backend/kanban.py::quiescent
## Feature: Fehler im Karten-Lauf behandeln [kern]
beschreibung: Wiederholt fehlgeschlagene Karten mit exponentiellem Backoff und verschiebt erschöpfte Karten in die Dead-Letter-Stage.
### Kann fehlgeschlagene Karten mit Backoff wiederholen oder als Dead-Letter ablegen, ohne bereits weitergerückte Karten zu bestrafen
einheiten: backend/kanban.py::_fail_package
# Bereich: Pipeline-Steuerung (Guide-Laufzeitverwaltung)
beschreibung: Orchestriert parallele Agent-Aufrufe für einen Guide, verwaltet Stornierung und Fortschritt und stellt Helfer für Prompt-Bau sowie Fehlerformatierung bereit.
## Feature: Guide-Abbruch handhaben [kern]
beschreibung: Markiert einen Guide als abgebrochen, beendet laufende Agent-Slots und gibt die Sperre für einen Neustart wieder frei.
### Kann einen laufenden Guide abbrechen, zugehörige Agent-Slots stoppen und den Datensatz als fehlgeschlagen markieren
einheiten: backend/pipeline.py::cancel_guide
### Kann den Stornierungsstatus eines Guides abfragen
einheiten: backend/pipeline.py::is_guide_cancelled
### Kann nach einem Abbruch die Sperre lösen, damit der Guide neu gestartet werden kann
einheiten: backend/pipeline.py::clear_guide_cancelled
## Feature: Live-Fortschritt und Statusmeldungen [kern]
beschreibung: Schreibt Fortschritt, Schrittnummer und Fehler persistiert und meldet nebenläufige Jobs mit Live-Zähler an einen Reporter.
### Kann Fortschritt, Schrittnummer und Fehlermeldung im Guide-Datensatz persistieren
einheiten: backend/pipeline.py::_set_progress, backend/pipeline.py::_set_step, backend/pipeline.py::_fail
### Kann nebenläufige Jobs mit Live-Zähler an einen Reporter melden und auch bei Ausnahmen weiterzählen
einheiten: backend/pipeline.py::_gather_progress
### Kann Info-Meldungen mit Topic-Präfix einheitlich loggen
einheiten: backend/pipeline.py::_log
## Feature: Parallele Agent-Slots orchestrieren [kern]
beschreibung: Startet mehrere Agent-Slots parallel, sammelt bis zum Quorum gültige Ergebnisse und reagiert auf Stornierung und Infrastrukturfehler.
### Kann mehrere Agent-Slots parallel starten und auf eine konfigurierbare Quorum-Anzahl gültiger Ergebnisse warten, mit Hedging, Grace-Periode und Infrastruktur-Retries
einheiten: backend/pipeline.py::_race
### Kann Pipeline-Parameter einmal bündeln und durch alle Pipeline-Glieder reichen
einheiten: backend/pipeline.py::GenContext
## Feature: Fehler klassifizieren und formatieren [rand]
beschreibung: Erkennt Infrastruktur- von inhaltlichen Fehlern und baut kompakte Fehlertexte für Anzeige und Vergleich.
### Kann Infrastruktur- von inhaltlichen Fehlern anhand einer Markerliste unterscheiden
einheiten: backend/pipeline.py::_is_infra
### Kann Agent-Fehler aus Returncode und Ausgaben zu einer kompakten Meldung formatieren
einheiten: backend/pipeline.py::_claude_error
### Kann aus einer Liste von Slot-Ergebnissen den ersten verwertbaren Fehler auswählen und melden
einheiten: backend/pipeline.py::_gather_error
## Feature: Prompt-Bau und Schemavalidierung [rand]
beschreibung: Stellt Helfer bereit, um Prompts aus Vorlagen aufzubauen, Timeouts zu berechnen und Agent-Antworten zu validieren.
### Kann Prompt-Vorlagen aus dem Templates-Verzeichnis laden und mit Argumenten ausfüllen
einheiten: backend/pipeline.py::_prompt
### Kann zusätzliche Nutzeranweisungen als klar markierten Block an einen Prompt anhängen
einheiten: backend/pipeline.py::_extra
### Kann die Timeout-Dauer eines Schritts als Basis plus skalierten Zuschlag pro Eintrag berechnen
einheiten: backend/pipeline.py::_timeout
### Kann die Probleme-Liste einer Agent-Antwort bereinigen oder leer zurückgeben
einheiten: backend/pipeline.py::_problems_schema
# Bereich: Provider-Konfiguration
beschreibung: Lädt Umgebung und Provider-Defaults, wählt pro Rolle den effektiven Provider samt Modell und überschreibt Tuning-Werte zur Startzeit.
## Feature: Umgebung und Defaults laden [kern]
beschreibung: Lädt eine .env-Datei und stellt den Default-Provider als Pflicht-Voraussetzung für den Start sicher.
### Kann eine .env-Datei laden und damit Umgebungsvariablen setzen, auch über bereits gesetzte Werte hinweg
einheiten: backend/config.py::_load_env
### Kann das System ohne konfigurierten Default-Provider gar nicht starten
einheiten: backend/config.py::DEFAULT_PROVIDER
## Feature: Provider-Stacks und Rollen-Routing [kern]
beschreibung: Stellt mehrere unabhängige Provider-Stacks bereit und routet Agent-Rollen optional auf alternative Provider um.
### Kann mehrere unabhängige Provider-Stacks (Cloud-Auth, API-Key, lokal) mit Modellen und Authentifizierung bereitstellen
einheiten: backend/config.py::PROVIDERS
### Kann die vier Agent-Rollen prozessweit auf alternative Provider umleiten
einheiten: backend/config.py::ROLE_ROUTING
### Kann für eine Rolle das effektive (Provider, Modell)-Paar unter Berücksichtigung von Overrides und "provider:model"-Syntax liefern
einheiten: backend/config.py::resolve_role
## Feature: Schritt-Timeouts und Tuning-Overrides [rand]
beschreibung: Definiert pro Agent-Schritt eine Timeout-Paarung und erlaubt numerische Tuning-Overrides per JSON zur Startzeit.
### Kann pro Agent-Schritt ein Timeout-Paar (Basis, Pro-Eintrag) zentral definieren
einheiten: backend/config.py::TIMEOUTS
### Kann numerische Tuning-Konstanten aus einer JSON-Umgebungsvariablen zur Startzeit überschreiben
einheiten: backend/config.py::_apply_param_overrides

112
phase0/erkenntnisse.md Normal file
View File

@@ -0,0 +1,112 @@
# Phase-0-Erkenntnisse (Scan-Validierung am Creator)
Testfall: `backend/kanban.py` (269 Zeilen) → Einheiten-Datei nach `docs/format.md`.
Prompt: `prompt-einheiten.md`, Prüfung: `pruefe.py`, Ergebnisse: `beispiel-einheiten-*.md`.
## Läufe (2026-07-09)
| Lauf | Modell | Mechanisches Ergebnis |
|---|---|---|
| v1 | M2.7-highspeed (kalt) | 9 Einheiten, volle Abdeckung, 0 falsche Anker/Kanten — aber 6 Zitat-Fehler (mehrzeilige Statements zusammengezogen) + 1 Kanten-Formatfehler |
| v2 | M2.7-highspeed (kalt), geschärfter Prompt | schlechter: beleg-Format gedriftet (inline), 9 Zitat-Fehler, davon 1 echte Fabrikation (f-String zu %-Formatierung umgeschrieben), 1 Einheit vergessen; davor 1 Stall (7-min-Timeout, bekanntes kalt-Routen-Problem) |
| v3 | M3 (nativ) | 0 Fehler: alle 26 Zitate wörtlich, Format exakt, Kanten-Richtungen korrekt, Innenfunktionen sauber als Zusatz-Anker; 1 Einheit vergessen (`_progress` — von der Abdeckungsprüfung gefangen) |
## Erkenntnisse
1. **Das Zitat-Treue-Gate funktioniert und ist nötig** — es hat in v2 eine echte
Code-Fabrikation gefangen (paraphrasiertes Zitat). Kernannahme von Phase 0 bestätigt:
mechanische Gates machen billige Modelle sicher benutzbar.
2. **Modellwahl fürs Schneiden/Fakten: M3, nicht M2.7-highspeed.** M2.7-HS ist bei
wörtlichen Zitaten unzuverlässig (und Prompt-Schärfung hat es nicht behoben — v2 war
schlechter als v1). M3 war fehlerfrei. Später per Telemetrie neu bewerten.
3. **Kein Lauf war vollständig UND fehlerfrei zugleich** → die Pipeline braucht den
Nachfass-Schritt: Abdeckungsprüfung meldet fehlende Symbole, ein Folge-Call ergänzt
nur diese (Karten-Retry der Kanban-Engine passt dafür).
4. **Formattreue schwankt zwischen Läufen** (v2: beleg inline statt eigene Zeile) →
Grammatik-Parse-Fail = Retry (Engine kann das); Prüfskript parst tolerant, wo es
den Prüfzweck nicht schwächt.
5. **kalt-Routen-Stalls bestätigt** (1 von 3 kalt-Calls hing) — Timeouts + Retry sind
Pflicht, wie im Creator.
6. **Format-Anpassungen aus der Praxis**: Zusatz-Anker dürfen verschachtelte Symbole
sein (Innenfunktionen); beleg muss Ausschnitt GENAU EINER Quellzeile sein (steht
jetzt im Prompt und in `docs/format.md` nachzuziehen).
## Zweiter Block (gleicher Tag): Nachfass, weitere Dateien, Feature-Sicht
7. **Nachfass-Kreislauf funktioniert end-to-end.** kanban.py nach Nachfass: 9 Einheiten,
31 Belege, 0 Fehler, 0 Warnungen. pipeline.py nach geschärftem Nachfass: 17 Einheiten,
52 Belege, volle Abdeckung.
8. **Nachfass braucht eine harte Regel gegen Wegsortieren:** M3 nutzte `ergaenze-anker`
zunächst als Fluchtweg (alle 16 fehlenden Symbole in bestehende Einheiten gefaltet,
fachlich teils falsch). Fix im Prompt: ergaenze-anker NUR für unselbstständige
Hilfssatelliten, im Zweifel eigene Einheit — danach korrekt.
9. **Neuer M3-Fehlermodus: Thinking-Overrun.** Bei pipeline.py dachte natives M3 bis ans
32k-Token-Limit und lieferte LEEREN Text (2× hintereinander, je >21k Tokens unsichtbar
verbrannt). Erkennbar an: leere Ausgabe + Output-Tokens am Limit. Pipeline-Regel:
leere Ausgabe = Fehler + Retry; Token-Zähler aus der OpenCode-Session-DB loggen.
M3-kalt (ohne Thinking) als Ausweichroute lief, stallte aber 1× und deckte schwächer
ab (6/22 Symbolen) → Erst-Scan nativ-M3, bei Overrun Retry, Lücken macht der Nachfass zu.
10. **Zitat-Gate fängt auch Minimal-Umformungen:** config.py-Zitat war fast richtig
(Tupel-Teil weggelassen) — existiert so nicht im Code. Genau die gefährliche Klasse.
11. **Escaping-Rauschen normalisieren:** Modelle verdoppeln Backslashes in Zitaten
(`\\n` statt `\n`) → Prüfskript entschärft `\"` und `\\` vor dem Vergleich.
12. **Feature-Sicht aus Einheiten allein funktioniert.** M3 aggregierte aus 42 Einheiten
(kanban+pipeline+config, ohne Quellcode!) eine lesbare Sicht: 3 Bereiche, 13 Features,
Fähigkeits-Sprache, plausible kern/rand-Flags, 0 kaputte Referenzen, unreferenziert
nur echte interne Helfer. Kernannahme „Sichten = billige Aggregation über Einheiten"
bestätigt. → `beispiel-features.md`
13a. **Parallele OpenCode-Kaltstarts hängen** (2× reproduziert: drei gleichzeitige
`opencode run` → alle Exit 124, keine Session erzeugt; sequenziell läuft es).
Der Creator serialisiert Starts deshalb mit einer Stagger-Sperre
(`_opencode_start_lock`, `_OPENCODE_START_DELAY`) — die MUSS mit übernommen werden.
13. **Infra-Fehlerklassen gesehen und unterscheidbar:** (a) harter Stall (inventory:
>1 h tot, kein Output, Session offen), (b) Thinking-Overrun (Output-Tokens am Limit,
leerer Text), (c) Netzausfall (Exit 124/Timeout, KEINE Session in der DB, leeres
Stderr). Alle drei enden in leerer Ausgabe — die Pipeline muss sie an den Signalen
unterscheiden: (c) pausiert den Lauf (Provider nicht erreichbar, Leitprinzip 2),
(a)+(b) sind Retry-Fälle. Erreichbarkeits-Check vor Lauf-Start ist Pflicht.
## Phase-1-Abnahme (Creator-Scan, 2026-07-09 nachmittags): Befunde der ~3h-Untersuchung
14. Sechs Läufe, 614 MiniMax-Calls, 2,16M Input-/1,29M Output-Tokens, ~35 min echte
Rechenzeit — der Rest der Zeit ging an: (a) meine Prozess-Orchestrierung (pkill traf
falsche PIDs → Lauf 2+3 parallel auf EINEM Board → Kartenchaos; tail-Pipe puffert →
kein Live-Output), (b) einen ungeklärten Hänger, (c) Iterationszyklen.
15. Gefixt & belegt: blinde Retries (→ Befund-Feedback), halluzinierte Kanten/Anker
(→ mechanische Streichung), fast-richtige Zitate wie `:=``=` (→ Fenster-Snap),
Barriere-Loch (sammeln drainte vor Producer-Ende — Beweis: Sichten-Calls mit 868 vs.
36.688 Input-Tokens; → gate=research_done), Working-Tree-Check stolperte über eigene
.planer-Dateien (→ Ausnahme), Kaskadentod beim Schreiben (→ degradierend).
16. OFFEN — Hänger: Prozess lebt nach Board-Ende weiter (47 min solo-idle belegt, 15s
CPU, futex). Parallel-Läufe als Alleinursache durch Timeline widerlegt; alle
LLM-/DB-Pfade haben Timeouts. Verdachtsklasse: ein nie endender Processor-Task hält
den Flow über den Aktiv-Zähler wach. SIGUSR1-Stackdump ist jetzt eingebaut — der
nächste Hänger liefert den Stack.
17. OFFEN — Zitate mit eingebetteten Anführungszeichen/Regex (z. B. re.sub-Muster)
brechen Parse (greedy Quote-Regex) und Snap → 3 hartnäckig tote Chunks.
18. OFFEN — Skript-Dateien ohne def/class (creator/projects/ragsystem): Modell erfindet
Pseudo-Symbole ('modul'); Datei-Einheiten (`## pfad` ohne ::) sind im Format
vorgesehen, aber in Parser/Gates nicht implementiert.
19. OFFEN — Sichten referenzieren Einheiten mit FALSCHEM Datei-Prefix (Symbol existiert,
Datei falsch: repair.py-Einheit als board_inventory.py referenziert). Mechanische
Referenz-Streichung ist eingebaut; besser wäre ID-Korrektur (Symbol eindeutig →
Prefix ersetzen).
20. OFFEN — creator/projects/ + uni/ sind Inhaltsdaten, keine Projektquellen → braucht
projekt-eigene Ausschluss-Config. Und: Kern/Features-Qualität bei voller Übersicht
noch unbewiesen (einziger kompletter Sichten-Lauf sah die Teil-Übersicht).
21. **Abschlusslauf erfolgreich** (nach Fixes 1719): 96/98 Chunks, alle 3 Sichten, Kern
beschreibt den ganzen Creator korrekt (Inventar→Artefakte→Guide, 14 Feature-Bereiche).
Rest-Tote: 2 Chunks (database.py, textkit.py — Zitat-Klasse, Fehlertext durch
Paket-Attribution verwischt). Der „Hänger" dieses Laufs war KEIN Hänger — nur die
lange Sichten-Endphase; mein USR1 killte den Prozess vor dem Bericht (faulthandler-
Registrierung wirkte nicht — prüfen). Artefakte liegen final in creator/.planer/.
## Offen in Phase 0
- [ ] board_inventory.py (2344 Zeilen) — Hintergrund-Lauf ausstehend; erwartete
Erkenntnis: große Dateien brauchen Chunking (Symbol-Gruppen je Call)
- [ ] config.py-Zitatfehler per Nachfass/Repair beheben (Muster ist validiert, niedrige Prio)
- [ ] Bewertung mit dem Nutzer: taugt das Ergebnis zum Wiedereinlesen?

View File

@@ -0,0 +1,65 @@
# Auftrag: Quelldatei in Einheiten zerlegen
Du bekommst unten den vollständigen Inhalt der Datei `<DATEI>` aus einem
Python-Projekt. Zerlege sie in **Einheiten** und gib GENAU EINE Markdown-Datei im
unten definierten Format zurück — nichts davor, nichts danach, keine Code-Fences
um das Gesamtergebnis.
## Was ist eine Einheit
Das Kleinste mit **eigener Verantwortung**, das sich in einem Satz beschreiben lässt —
meist eine Funktion oder Klasse; mehrere kleine, zusammengehörige Symbole dürfen eine
Einheit bilden. Jede Funktion/Klasse der Datei gehört zu genau einer Einheit (MECE).
Modul-Konstanten und Imports darfst du unzugeordnet lassen.
## Ausgabeformat (exakt einhalten)
```
# Einheiten: <DATEI>
stand: UNBEKANNT
## <DATEI>::<hauptsymbol>
beschreibung: <genau ein Satz: die Verantwortung der Einheit, ohne „und"-Aufzählung>
input: <was die Einheit entgegennimmt — Parameter/Quellen, eine knappe Zeile>
output: <was sie liefert bzw. bewirkt — Rückgabe/Effekt, eine knappe Zeile>
entscheidungen:
- <kurzer Satz: eine im Code getroffene Design-Entscheidung oder Garantie>
beleg: "<kurzes WÖRTLICHES Zitat aus der Datei, das sie stützt>"
- <weitere Entscheidungen nach demselben Muster>
kanten:
- ruft-auf: <einheiten-id>
- nutzt: <einheiten-id>
- wird-genutzt-von: <einheiten-id>
```
Regeln:
- Einheiten-ID = `<DATEI>::<symbolname>` (bei gebündelten Symbolen: das
Hauptsymbol als ID, zusätzliche Symbole je als Zeile `anker: <DATEI>::<symbol>`
direkt unter der Überschrift).
- `beschreibung:` ist genau EIN Satz und beschreibt EINE Aufgabe — keine
„und"-Aufzählungen.
- `input:` und `output:` sind Pflicht: je EINE knappe Zeile (was geht rein, was kommt
raus bzw. welcher Effekt tritt ein). Bei Klassen: was sie zum Bau braucht / was sie
bereitstellt. Keine Prosa.
- 25 Entscheidungen je Einheit: kurze Sätze über im Code getroffene Design-
Entscheidungen und Garantien (was ist zugesichert, was wurde bewusst so gebaut) —
KEIN Nacherzählen der Implementierung. Jede Entscheidung gehört zu der Einheit, in
deren Code-Körper der Beleg steht (Modul-Ebene nicht einer Klasse zuschlagen).
- Jede Entscheidung hat GENAU EINE `beleg:`-Zeile. Das Zitat muss WÖRTLICH (Zeichen für
Zeichen, inkl. Klammern und Unterstrichen) in der Datei vorkommen und ein
zusammenhängender Ausschnitt aus GENAU EINER Quellzeile sein — NIEMALS mehrere
Zeilen zu einer zusammenziehen (kein `if x: return y`, wenn das `return` in der
nächsten Zeile steht; zitiere dann nur `if x:` oder nur `return y`). Max. ~60 Zeichen.
- Kanten-Typen nur: `ruft-auf`, `nutzt`, `wird-genutzt-von`, `löst-aus`, `wird-ausgelöst-von`.
Hinter dem Ziel steht NICHTS mehr — kein Kommentar, keine Klammern. Richtungen:
`ruft-auf`/`nutzt` = diese Einheit verwendet das Ziel in ihrem Code-Körper;
`wird-genutzt-von` = das Ziel verwendet diese Einheit in SEINEM Code-Körper.
Ziele innerhalb der Datei: `<DATEI>::<symbol>`. Ziele in anderen Modulen
(aus den Imports ableitbar): `backend/<modul>.py::<symbol>`, z. B. nutzt die Datei
`db.kanban_pull``backend/database.py::kanban_pull`.
- Keine Kante erfinden, die nicht im Code steht. Lieber weniger Kanten als falsche.
- Schreibe auf Deutsch.
## Quelldatei `<DATEI>`
<QUELLE>

44
phase0/prompt-features.md Normal file
View File

@@ -0,0 +1,44 @@
# Auftrag: Feature-Sicht aus Einheiten aggregieren
Unten stehen die Einheiten-Dateien mehrerer Quelldateien eines Projekts (ein
KI-Lernguide-Generator). Jede Einheit hat eine Beschreibung, belegte Fakten und
Kanten. Aggregiere daraus eine **Feature-Sicht**: Was kann dieses Teilsystem aus
Nutzer-/Betreiber-Sicht?
WICHTIG: Du siehst nur einen Ausschnitt des Projekts (Kanban-Engine, Agenten-Slot-
Verwaltung, Konfiguration). Beschreibe NUR, was die vorliegenden Einheiten belegen —
erfinde keine Features aus Weltwissen über Lernguide-Generatoren.
## Ausgabeformat (exakt einhalten)
```
# Bereich: <name>
beschreibung: <ein Satz>
## Feature: <titel> [kern|rand]
beschreibung: <ein Satz>
### <teilfeature-titel>
einheiten: <einheiten-id>, <einheiten-id>
```
Regeln:
- 13 Bereiche; jedes Feature gehört zu genau einem Bereich, jedes Teilfeature zu
genau einem Feature (MECE je Ebene: vollständig, überschneidungsfrei).
- Bereich-Titel: kurz, OHNE Klammern, OHNE „und". Bereich-Beschreibung: EIN kurzer
Satz, der den Kern trifft — keine „und"-Aufzählung dessen, was drin ist.
- Feature-Titel: OHNE „und" — jedes Feature erfüllt genau EINE Aufgabe. Wenn ein
Titel „und" bräuchte, mach zwei Features daraus. Feature-Beschreibung: ebenfalls
EIN kurzer Satz ohne „und"-Aufzählung.
- Teilfeature-Titel sind Fähigkeits-Sätze aus Betreibersicht (z. B. „Kann nach
Abbruch fortsetzen"), keine Techniknamen.
- `einheiten:` listet NUR IDs, die unten wirklich vorkommen (exakte Schreibweise).
Jedes Teilfeature hat mindestens eine. KEINE Fakten kopieren — nur referenzieren.
- Flag: `[kern]` wenn das Feature das Teilsystem trägt, `[rand]` für Komfort/Beiwerk.
- Jede Einheit von unten sollte in mindestens einem Teilfeature auftauchen; reine
interne Helfer dürfen unreferenziert bleiben.
- Schreibe auf Deutsch. Gib NUR die Markdown-Datei aus, ohne Code-Fences drumherum.
## Einheiten-Dateien
<EINHEITEN>

37
phase0/prompt-nachfass.md Normal file
View File

@@ -0,0 +1,37 @@
# Auftrag: Fehlende Einheiten ergänzen
Eine Quelldatei wurde bereits in Einheiten zerlegt (Bestand unten), aber die
Abdeckungsprüfung hat gemeldet, dass folgende Top-Level-Symbole von `<DATEI>`
noch KEINER Einheit zugeordnet sind:
<FEHLEND>
Erzeuge NUR die fehlenden Einheiten-Abschnitte im selben Format wie der Bestand —
nichts davor, nichts danach, keine Wiederholung bestehender Einheiten, keine
Code-Fences um das Gesamtergebnis.
NUR WENN ein fehlendes Symbol ein unselbstständiger Hilfssatellit einer bestehenden
Einheit ist — d. h. es wird AUSSCHLIESSLICH vom Hauptsymbol dieser Einheit verwendet
und hat keine eigene Verantwortung — darfst du statt einer neuen Einheit eine Zeile
`ergaenze-anker: <DATEI>::<symbol> zu <bestehende-einheiten-id>` ausgeben.
Im Zweifel IMMER eine eigene Einheit. Symbole, die von mehreren Stellen verwendet
werden oder eine benennbare eigene Aufgabe haben (z. B. eine Exception-Klasse, ein
Schema-Bauer, ein Fehler-Klassifizierer), sind IMMER eigene Einheiten — auch wenn
sie klein sind. Verwandte kleine Symbole mit GEMEINSAMER Verantwortung darfst du zu
EINER neuen Einheit bündeln (Hauptsymbol als ID, Rest als `anker:`-Zeilen).
Regeln (wie beim Erst-Scan):
- Einheiten-ID = `<DATEI>::<symbolname>`; `beschreibung:` genau EIN Satz.
- 25 Fakten je Einheit; jeder Fakt GENAU EINE `beleg:`-Zeile auf eigener Zeile;
Zitat WÖRTLICH und aus GENAU EINER Quellzeile, max. ~60 Zeichen.
- Kanten-Typen nur: `ruft-auf`, `nutzt`, `wird-genutzt-von`, `löst-aus`,
`wird-ausgelöst-von`; hinter dem Ziel steht nichts mehr.
- Schreibe auf Deutsch.
## Bestand (bereits zerlegte Einheiten)
<VORHANDEN>
## Quelldatei `<DATEI>`
<QUELLE>

156
phase0/pruefe.py Normal file
View File

@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Mechanische Prüfung einer Einheiten-Datei (Phase 0).
Aufruf: pruefe.py <einheiten.md> <projekt-wurzel>
Prüft tokenfrei:
1. Anker-Existenz — jede Einheiten-ID und jedes Kanten-Ziel zeigt auf existierende
Datei + darin definiertes Symbol (def/class/Zuweisung).
2. Zitat-Treue — jeder beleg:-String kommt wörtlich in der Anker-Datei vor.
3. Struktur — Schlüssel-Grammatik (beschreibung/fakten/kanten, Kanten-Typen).
4. Abdeckung — Report: welche Top-Level-Symbole der Quelldatei keiner Einheit
zugeordnet sind (v1 kein Fehler).
"""
import re
import sys
from pathlib import Path
KANTEN_TYPEN = {"ruft-auf", "nutzt", "wird-genutzt-von", "löst-aus", "wird-ausgelöst-von"}
def entschaerfe(s: str) -> str:
"""Escaping-Rauschen der Modelle normalisieren: \\"" und \\\\\\."""
return s.replace('\\"', '"').replace("\\\\", "\\")
def symbole_in(quelle: str) -> set[str]:
"""Top-Level-Symbole: def/class/Konstanten-Zuweisung auf Spalte 0 (+ async def)."""
out = set()
for m in re.finditer(r"^(?:async\s+)?(?:def|class)\s+(\w+)", quelle, re.M):
out.add(m.group(1))
for m in re.finditer(r"^(\w+)\s*[:=]", quelle, re.M):
out.add(m.group(1))
return out
def main(pfad: str, wurzel: str) -> int:
text = Path(pfad).read_text(encoding="utf-8")
wurzel_p = Path(wurzel)
fehler, warnungen = [], []
quell_cache: dict[str, str | None] = {}
def quelle(datei: str) -> str | None:
if datei not in quell_cache:
p = wurzel_p / datei
quell_cache[datei] = p.read_text(encoding="utf-8") if p.is_file() else None
return quell_cache[datei]
def pruefe_id(eid: str, kontext: str):
if "::" not in eid:
if quelle(eid) is None:
fehler.append(f"{kontext}: Datei fehlt: {eid}")
return
datei, symbol = eid.split("::", 1)
q = quelle(datei)
if q is None:
fehler.append(f"{kontext}: Datei fehlt: {datei}")
elif symbol not in symbole_in(q):
fehler.append(f"{kontext}: Symbol '{symbol}' nicht top-level in {datei}")
# --- parsen ---
einheiten: list[dict] = []
zusatz_anker: set[str] = set()
aktuelle = None
modus = None # None | fakten | kanten
for nr, zeile in enumerate(text.splitlines(), 1):
if zeile.startswith("## "):
aktuelle = {"id": zeile[3:].strip(), "zeile": nr, "belege": [], "kanten": [],
"beschreibung": False}
einheiten.append(aktuelle)
modus = None
elif zeile.startswith(("anker:", "ergaenze-anker:")):
# Zusatz-Anker dürfen auch verschachtelte Symbole sein (Innenfunktionen);
# ergaenze-anker kommt auch außerhalb von Einheiten vor (Nachfass-Ausgabe)
aid = zeile.split(":", 1)[1].strip().split(" zu ")[0].strip()
zusatz_anker.add(aid)
if "::" in aid:
datei, symbol = aid.split("::", 1)
q = quelle(datei)
if q is None:
fehler.append(f"Z{nr} anker: Datei fehlt: {datei}")
elif not re.search(rf"(?:def|class)\s+{re.escape(symbol)}\b|^{re.escape(symbol)}\s*[:=]",
q, re.M):
fehler.append(f"Z{nr} anker: Symbol '{symbol}' nicht in {datei}")
else:
pruefe_id(aid, f"Z{nr} anker")
elif aktuelle is None:
continue
elif zeile.startswith("beschreibung:"):
aktuelle["beschreibung"] = True
modus = None
elif zeile.strip() in ("fakten:", "entscheidungen:"):
modus = "fakten"
elif zeile.startswith("input:"):
aktuelle["input"] = True
elif zeile.startswith("output:"):
aktuelle["output"] = True
elif zeile.strip() == "kanten:":
modus = "kanten"
elif m := re.match(r'\s+beleg:\s*"(.*)"\s*$', zeile):
aktuelle["belege"].append((nr, entschaerfe(m.group(1))))
elif modus == "fakten" and (m := re.search(r'\bbeleg:\s*"(.*)"\s*$', zeile)):
# tolerant: beleg inline am Ende der Fakt-Zeile (streng schreiben, tolerant parsen)
aktuelle["belege"].append((nr, entschaerfe(m.group(1))))
elif modus == "kanten" and (m := re.match(r"- ([\wäöü-]+):\s*(\S+)\s*$", zeile)):
aktuelle["kanten"].append((nr, m.group(1), m.group(2)))
elif modus == "kanten" and zeile.startswith("- "):
fehler.append(f"Z{nr}: Kante unlesbar (Zusatztext?): {zeile.strip()!r}")
# --- prüfen ---
for e in einheiten:
kontext = e["id"]
pruefe_id(e["id"], f"Z{e['zeile']} einheit")
if not e["beschreibung"]:
fehler.append(f"{kontext}: beschreibung: fehlt")
datei = e["id"].split("::")[0]
q = quelle(datei) or ""
for nr, beleg in e["belege"]:
if beleg not in q:
fehler.append(f"{kontext} Z{nr}: Zitat nicht wörtlich in {datei}: \"{beleg[:50]}\"")
if not e["belege"]:
warnungen.append(f"{kontext}: keine Belege")
if not e.get("input") or not e.get("output"):
warnungen.append(f"{kontext}: input:/output: fehlt")
for nr, typ, ziel in e["kanten"]:
if typ not in KANTEN_TYPEN:
fehler.append(f"{kontext} Z{nr}: unbekannter Kanten-Typ '{typ}'")
pruefe_id(ziel, f"{kontext} Z{nr} kante")
# --- Abdeckung der gescannten Datei ---
dateien = {e["id"].split("::")[0] for e in einheiten if "::" in e["id"]}
for datei in sorted(dateien):
q = quelle(datei)
if q is None:
continue
zugeordnet = {e["id"].split("::", 1)[1] for e in einheiten
if e["id"].startswith(datei + "::")}
zugeordnet |= {a.split("::", 1)[1] for a in zusatz_anker
if a.startswith(datei + "::")}
oeffentlich = {s for s in symbole_in(q)
if re.search(rf"^(?:async\s+)?(?:def|class)\s+{re.escape(s)}\b", q, re.M)}
fehlt = oeffentlich - zugeordnet
if fehlt:
warnungen.append(f"Abdeckung {datei}: nicht zugeordnet: {', '.join(sorted(fehlt))}")
print(f"Einheiten: {len(einheiten)}, Belege: {sum(len(e['belege']) for e in einheiten)}, "
f"Kanten: {sum(len(e['kanten']) for e in einheiten)}")
for f in fehler:
print(f"FEHLER {f}")
for w in warnungen:
print(f"WARNUNG {w}")
print(f"\n{len(fehler)} Fehler, {len(warnungen)} Warnungen")
return 1 if fehler else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1], sys.argv[2]))

View File

@@ -0,0 +1,124 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Planer-Vorschau: Creator (Teilscan)</title>
<style>
:root {
--bg: #f6f7f9; --karte: #ffffff; --text: #1a202c; --dezent: #64748b;
--rand: #e2e8f0; --akzent: #2563eb; --kern: #166534; --kern-bg: #dcfce7;
--rand-flag: #92400e; --rand-bg: #fef3c7; --code-bg: #f1f5f9;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f1420; --karte: #1a2130; --text: #e2e8f0; --dezent: #94a3b8;
--rand: #2d3748; --akzent: #60a5fa; --kern: #86efac; --kern-bg: #14532d;
--rand-flag: #fcd34d; --rand-bg: #451a03; --code-bg: #111827;
}
}
* { box-sizing: border-box; }
body { margin: 0; padding: 2rem 1rem 4rem; background: var(--bg); color: var(--text);
font: 15px/1.55 system-ui, sans-serif; }
main { max-width: 900px; margin: 0 auto; }
h1 { font-size: 1.4rem; margin: 0 0 .3rem; }
.untertitel { color: var(--dezent); margin: 0 0 2rem; font-size: .92rem; }
.bereich { background: var(--karte); border: 1px solid var(--rand); border-radius: 10px;
padding: 1rem 1.2rem; margin-bottom: 1rem; }
.bereich > h2 { font-size: 1.08rem; margin: 0; }
.bereich > p { color: var(--dezent); margin: .25rem 0 .8rem; font-size: .92rem; }
details { border-top: 1px solid var(--rand); }
summary { cursor: pointer; padding: .55rem .2rem; list-style: none; display: flex;
align-items: baseline; gap: .5rem; }
summary::before { content: "▸"; color: var(--dezent); font-size: .8em; transition: transform .12s; }
details[open] > summary::before { transform: rotate(90deg); }
summary:hover { color: var(--akzent); }
.flag { font-size: .7rem; font-weight: 600; padding: .1rem .45rem; border-radius: 99px;
text-transform: uppercase; letter-spacing: .04em; }
.flag.kern { color: var(--kern); background: var(--kern-bg); }
.flag.rand { color: var(--rand-flag); background: var(--rand-bg); }
.feature-besch { color: var(--dezent); font-size: .9rem; margin: 0 0 .5rem 1.15rem; }
.abschnitt { margin: .2rem 0 .7rem 1.15rem; }
.abschnitt > h4 { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em;
color: var(--dezent); margin: .6rem 0 .25rem; }
.entscheidungen { margin: 0; padding-left: 1.2rem; font-size: .92rem; }
.entscheidungen li { margin: .18rem 0; }
.teil { margin: 0 0 .35rem; border-top: 1px dashed var(--rand); }
.teil > summary { font-size: .93rem; padding: .35rem .2rem; }
.methode { margin: .3rem 0 .55rem 1.1rem; border: 1px solid var(--rand); border-radius: 8px;
background: var(--bg); overflow: hidden; }
.methode > summary { font-family: ui-monospace, monospace; font-size: .82rem;
padding: .45rem .6rem; flex-wrap: wrap; }
.io { font-size: .82rem; color: var(--dezent); padding: 0 .8rem .1rem;
display: grid; grid-template-columns: max-content 1fr; gap: .1rem .6rem; }
.io dt { font-weight: 600; }
.io dd { margin: 0; }
pre { margin: .4rem .8rem .7rem; padding: .55rem .7rem; background: var(--code-bg);
border-radius: 6px; font-size: .76rem; line-height: 1.45; overflow-x: auto; }
.hinweis { color: var(--dezent); font-size: .85rem; margin-top: 2rem;
border-top: 1px solid var(--rand); padding-top: 1rem; }
</style>
</head>
<body>
<main>
<h1>Creator — Projektansicht (Phase-0-Vorschau)</h1>
<p class="untertitel">Teilscan: kanban.py, pipeline.py, config.py · Belege mechanisch
verifiziert · Code wird live aus den Ankern gezogen, nie kopiert</p>
<div id="wurzel"></div>
<p class="hinweis">Wegwerf-Vorschau aus Phase 0 — kein Frontend-Code des Planers.
Bereich → Feature (Entscheidungen + Methoden) → Teilfeature → Methode (Input/Output, Code).</p>
</main>
<script>
const DATEN = /*DATEN*/;
const wurzel = document.getElementById("wurzel");
const esc = s => (s || "").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
function methodenElement(id) {
const e = DATEN.einheiten[id];
const d = document.createElement("details");
d.className = "methode";
if (!e) { d.innerHTML = `<summary>${esc(id)} (nicht im Teilscan)</summary>`; return d; }
const code = DATEN.code[id];
d.innerHTML = `<summary>${esc(id)}</summary>
<dl class="io"><dt>Input</dt><dd>${esc(e.input) || "—"}</dd>
<dt>Output</dt><dd>${esc(e.output) || "—"}</dd></dl>
${code ? `<pre>${esc(code)}</pre>` : ""}`;
return d;
}
for (const b of DATEN.bereiche) {
const bx = document.createElement("section");
bx.className = "bereich";
bx.innerHTML = `<h2>${esc(b.name)}</h2><p>${esc(b.beschreibung)}</p>`;
for (const f of b.features) {
const ids = f.teilfeatures.flatMap(t => t.einheiten);
const entsch = [...new Set(ids.flatMap(id => (DATEN.einheiten[id]?.entscheidungen) || []))];
const fd = document.createElement("details");
fd.innerHTML = `<summary><strong>${esc(f.titel)}</strong>
<span class="flag ${f.flag}">${f.flag}</span></summary>
<p class="feature-besch">${esc(f.beschreibung)}</p>`;
if (entsch.length) {
const ab = document.createElement("div");
ab.className = "abschnitt";
ab.innerHTML = `<h4>Entscheidungen</h4>
<ul class="entscheidungen">${entsch.map(t => `<li>${esc(t)}</li>`).join("")}</ul>`;
fd.appendChild(ab);
}
const mb = document.createElement("div");
mb.className = "abschnitt";
mb.innerHTML = `<h4>Dateien &amp; Methoden</h4>`;
for (const t of f.teilfeatures) {
const td = document.createElement("details");
td.className = "teil";
td.innerHTML = `<summary>${esc(t.titel)}</summary>`;
for (const id of t.einheiten) td.appendChild(methodenElement(id));
mb.appendChild(td);
}
fd.appendChild(mb);
bx.appendChild(fd);
}
wurzel.appendChild(bx);
}
</script>
</body>
</html>

1426
phase0/vorschau.html Normal file

File diff suppressed because it is too large Load Diff