"""Lernschulden-Regeln: Progression und Deckel für offene Guides — die EINZIGE Quelle. Regeln (nur Neu-Erstellungen; Themen, Bausteine, OnePager unbegrenzt): - JE Format (MiniGuide/Guide/FullGuide) höchstens 3 erstellte, nicht absolvierte Guides - Progression pro Thema: Guide erst nach absolviertem MiniGuide, FullGuide erst nach absolviertem Guide Alle Funktionen arbeiten auf einmal geladenen Daten (lade_lernstand) — keine Query-Schleifen mehr pro Guide. """ import json from database import list_guides, list_progress_all from guide import guide_slot_dateien from paths import bausteine_path, guide_content_path MAX_OFFENE_GUIDES = 3 VORSTUFE = {"Guide": "MiniGuide", "FullGuide": "Guide"} FORMATE = ("MiniGuide", "Guide", "FullGuide") async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]]]: """Guides + kompletter Kapitel-Fortschritt in zwei Queries.""" return await list_guides(), await list_progress_all() def _kapitel_titel(topic: str, fmt: str) -> set[str] | None: path = guide_content_path(topic, fmt) if not path.exists(): return None try: chapters = json.loads(path.read_text(encoding="utf-8")).get("chapters", []) except ValueError: return None return {c.get("title") for c in chapters} def _neueste_done(guides: list[dict], fmt: str) -> dict[str, dict]: """Pro Thema der neueste fertige Guide dieses Formats.""" neueste: dict[str, dict] = {} for g in guides: if g["format"] == fmt and g["status"] == "done": if g["topic"] not in neueste or g["created_at"] > neueste[g["topic"]]["created_at"]: neueste[g["topic"]] = g return neueste def _guide_absolviert(g: dict, progress: dict[str, set[str]]) -> bool: titles = _kapitel_titel(g["topic"], g["format"]) return bool(titles) and titles <= progress.get(g["id"], set()) def ist_absolviert(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]]) -> bool: """Alle Kapitel des neuesten fertigen Guides (Thema+Format) abgehakt?""" g = _neueste_done(guides, fmt).get(topic) return g is not None and _guide_absolviert(g, progress) def formate_stats(guides: list[dict], progress: dict[str, set[str]]) -> dict: """Pro Format erstellt/absolviert — pro Thema zählt nur der neueste fertige Guide.""" formate = {} for fmt in FORMATE: neueste = _neueste_done(guides, fmt) absolviert = sum(1 for g in neueste.values() if _guide_absolviert(g, progress)) formate[fmt] = {"erstellt": len(neueste), "absolviert": absolviert} return formate def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]]) -> str | None: """Grund, warum ein Neu-Start für Thema+Format gesperrt ist — None = erlaubt. Exakt die Regeln aus POST /guides: Bausteine nötig, kein Duplikat-Start, Lernschulden nur für echte Neu-Erstellungen (Resume/Regenerieren frei). """ if fmt != "OnePager" and not bausteine_path(topic).exists(): return "Erst Bausteine erstellen" for g in guides: if g["topic"] == topic and g["format"] == fmt and g["status"] in ("queued", "generating"): return "Generierung läuft bereits" content = guide_content_path(topic, fmt) if fmt != "OnePager" and not content.exists() and not guide_slot_dateien(content): vorstufe = VORSTUFE.get(fmt) if vorstufe and not ist_absolviert(topic, vorstufe, guides, progress): return f"Erst den {vorstufe} dieses Themas absolvieren" stat = formate_stats(guides, progress).get(fmt, {"erstellt": 0, "absolviert": 0}) offen = stat["erstellt"] - stat["absolviert"] if offen >= MAX_OFFENE_GUIDES: return f"Erst {fmt}s absolvieren — maximal {MAX_OFFENE_GUIDES} offene erlaubt ({offen} offen)" return None