118 lines
3.8 KiB
Python
118 lines
3.8 KiB
Python
"""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)
|