"""Prüft die Karten eines Projekts gegen den Stil-Katalog. Ein Lauf je Projekt, Zustand nur im Speicher — er überlebt keinen Serverneustart. Je Karte eine Anfrage, höchstens PRUEFUNG_PARALLEL gleichzeitig; Probleme erscheinen, sobald die jeweilige Karte geprüft ist. """ import asyncio import json import re import agents import config import features # projektname -> {"laeuft": bool, "fertig": int, "gesamt": int, # "probleme": [...], "meldung": str} _laeufe = {} SYSTEM = """Du prüfst Karten einer Feature-Verwaltung gegen einen Stil-Katalog. {stil} Du bekommst eine Karte als Markdown. Melde jede Abweichung vom Katalog. Antworte ausschließlich mit JSON: {{"probleme": [{{"feld": "...", "funktion": "...", "original": "...", "problem": "...", "vorschlag": "..."}}]}} - feld: titel, ziel, regeln oder testen - funktion: Titel der Funktion, zu der die Stelle gehört; "" für die Karte selbst - original: der fehlerhafte Text wortwörtlich; bei regeln und testen genau eine Zeile ohne führendes "- " - problem: die verletzte Vorgabe in höchstens 8 Wörtern - vorschlag: der korrigierte Text, sonst nichts Keine Probleme gefunden: {{"probleme": []}} Erfinde nichts und melde nur, was der Katalog verlangt.""" def status(projekt: str) -> dict: lauf = _laeufe.get(projekt) if not lauf: return {"laeuft": False, "fertig": 0, "gesamt": 0, "probleme": [], "meldung": ""} return lauf def _ohne_strich(text: str) -> str: return re.sub(r"^[-*]\s*", "", (text or "").strip()) def _antwort_lesen(text: str) -> list[dict]: """Holt das JSON aus der Antwort, auch wenn es in ```-Zäunen steckt. Listenzeilen kommen mal mit, mal ohne „- " zurück — hier vereinheitlicht, damit die Anzeige die Stelle wiederfindet.""" treffer = re.search(r"\{.*\}", text, re.S) if not treffer: raise ValueError("Antwort enthält kein JSON") daten = json.loads(treffer.group(0)) probleme = [] for p in daten.get("probleme") or []: if not isinstance(p, dict) or not p.get("original"): continue p["original"] = _ohne_strich(p["original"]) p["vorschlag"] = _ohne_strich(p.get("vorschlag")) p["funktion"] = (p.get("funktion") or "").strip() probleme.append(p) return probleme async def _pruefe_karte(stil: str, feature: dict) -> list[dict]: text = await agents.frage(SYSTEM.format(stil=stil), feature["roh"]) probleme = _antwort_lesen(text) for p in probleme: p["feature"] = feature["id"] return probleme async def _lauf(projekt: str, liste: list[dict]): lauf = _laeufe[projekt] stil = config.STIL_DATEI.read_text() schranke = asyncio.Semaphore(config.PRUEFUNG_PARALLEL) async def eine(feature): async with schranke: if _laeufe.get(projekt) is not lauf: return # verworfen (Panel geschlossen) — keine weitere Anfrage try: lauf["probleme"].extend(await _pruefe_karte(stil, feature)) except Exception as e: lauf["meldung"] = str(e) lauf["fertig"] += 1 try: await asyncio.gather(*(eine(f) for f in liste)) finally: lauf["laeuft"] = False async def starte(projekt: str) -> dict: """Startet einen Lauf im Hintergrund; ein laufender wird nicht verdoppelt.""" if status(projekt)["laeuft"]: raise ValueError("Prüfung läuft bereits") agents.pruefe_konfiguration() # früher Stopp statt halbem Lauf liste = features.liste(projekt) _laeufe[projekt] = { "laeuft": True, "fertig": 0, "gesamt": len(liste), "probleme": [], "meldung": "", } asyncio.create_task(_lauf(projekt, liste)) return _laeufe[projekt] def beende(projekt: str): """Ergebnis verwerfen (Panel schließen).""" _laeufe.pop(projekt, None)