# 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
