init
This commit is contained in:
26
phase0/baue-prompt.py
Normal file
26
phase0/baue-prompt.py
Normal 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
129
phase0/baue-vorschau.py
Normal 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()
|
||||
97
phase0/beispiel-einheiten-config-m3.md
Normal file
97
phase0/beispiel-einheiten-config-m3.md
Normal 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 6–72 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
|
||||
150
phase0/beispiel-einheiten-kanban-m27hs.md
Normal file
150
phase0/beispiel-einheiten-kanban-m27hs.md
Normal 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
|
||||
147
phase0/beispiel-einheiten-kanban-m3.md
Normal file
147
phase0/beispiel-einheiten-kanban-m3.md
Normal 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
|
||||
230
phase0/beispiel-einheiten-pipeline-m3.md
Normal file
230
phase0/beispiel-einheiten-pipeline-m3.md
Normal 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
137
phase0/beispiel-features.md
Normal 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
112
phase0/erkenntnisse.md
Normal 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 17–19): 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?
|
||||
65
phase0/prompt-einheiten.md
Normal file
65
phase0/prompt-einheiten.md
Normal 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.
|
||||
- 2–5 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
44
phase0/prompt-features.md
Normal 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:
|
||||
- 1–3 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
37
phase0/prompt-nachfass.md
Normal 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.
|
||||
- 2–5 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
156
phase0/pruefe.py
Normal 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]))
|
||||
124
phase0/vorschau-template.html
Normal file
124
phase0/vorschau-template.html
Normal 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 => ({"&":"&","<":"<",">":">",'"':"""}[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 & 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
1426
phase0/vorschau.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user