This commit is contained in:
team3
2026-07-29 15:19:41 +02:00
parent 2e86d682eb
commit 5d42b4cfc5
155 changed files with 14482 additions and 1736 deletions

View File

@@ -18,10 +18,18 @@ from collections import Counter
import agents
import config
import features
import inhalt as inhaltspruefung
import quellcode
import ledger
import struktur
class LaeuftBereits(ValueError):
"""Eigener Typ, weil 409 genau eine Bedeutung haben muss.
Vorher war jeder Fehlstart ein 409 — auch ein fehlendes Quellverzeichnis. Das
Frontend liest 409 als „läuft schon" und hängt sich an einen Lauf, den es nicht
gibt. Ergebnis: „Keine Probleme gefunden" statt der Fehlermeldung.
"""
# projektname -> {"laeuft": bool, "fertig": int, "gesamt": int, "fehler": int,
# "probleme": [...], "meldung": str}
_laeufe = {}
@@ -445,7 +453,17 @@ async def _neue_felder(system: str, aufgabe: dict, offen: list, erlaubt: tuple,
return vorschlaege
async def _pruefe(stil: dict, aufgabe: dict) -> list[dict]:
def _stilschluessel(aufgabe: dict) -> str:
return ledger.schluessel(aufgabe["feature"], aufgabe["funktion"])
def _stilabdruck(aufgabe: dict, system: str) -> str:
"""Woran das Urteil hängt: der geprüfte Ausschnitt, der Katalog, die Frage.
Ändert sich eines davon, ist das gemerkte Urteil von gestern."""
return ledger.abdruck(system, inhalt(aufgabe))
async def _pruefe(stil: dict, aufgabe: dict, stand: dict = None) -> list[dict]:
"""Erst messen, dann umschreiben lassen, dann den Unterschied ausrechnen."""
bereich = BEREICHE[aufgabe["bereich"]]
system = SYSTEM.format(
@@ -455,6 +473,15 @@ async def _pruefe(stil: dict, aufgabe: dict) -> list[dict]:
hinweis=bereich["hinweis"],
)
offen = struktur.befunde(aufgabe["bereich"], aufgabe["felder"])
# Gemessene Verstöße stehen fest — die dürfen nie aus dem Gedächtnis kommen.
# Sonst bliebe eine neu hinzugekommene Messregel an alten Karten wirkungslos.
schluessel, wie_gehabt = _stilschluessel(aufgabe), _stilabdruck(aufgabe, system)
if stand is not None and not offen:
gemerkt = stand["stil"].get(schluessel)
if gemerkt and gemerkt["abdruck"] == wie_gehabt:
return [dict(p) for p in gemerkt["probleme"]]
vorschlaege = await _neue_felder(system, aufgabe, offen, bereich["felder"],
katalogzeilen(stil[aufgabe["bereich"]]))
@@ -466,83 +493,33 @@ async def _pruefe(stil: dict, aufgabe: dict) -> list[dict]:
for p in probleme:
p["feature"] = aufgabe["feature"]
p["funktion"] = aufgabe["funktion"]
if stand is not None:
stand["stil"][schluessel] = {"abdruck": wie_gehabt, "probleme": probleme}
return probleme
async def _stil_aufgaben(projekt: str):
"""Prüfstellen der Stilprüfung samt der Kataloge, die sie brauchen."""
"""Prüfstellen der Stilprüfung samt der Kataloge, die sie brauchen.
Auch hier ein Gedächtnis: 176 Anfragen je Lauf brachten den Anbieter zum
Drosseln, und die ausgefallenen Stellen meldeten dann nichts. Unveränderter
Ausschnitt bei unverändertem Katalog heißt: das Urteil von vorhin gilt noch.
"""
stil = {b: _stil_lesen(b) for b in BEREICHE}
liste = [a for f in features.pruefbar(projekt) for a in aufgaben(f)]
return liste, lambda a, _zaehl: _pruefe(stil, a)
def _quelle(projekt: str):
alle = features.pruefbar(projekt)
quelle = quellcode.dateien()
if not quelle:
raise ValueError("Kein Quelltext gefunden")
return alle, quelle
async def _feature_aufgaben(projekt: str):
"""Ein Lauf über alle Funktionen: steht hinter jeder auch Code? Und beschreibt
keine zwei Funktionen dieselbe Sache?
Ein Aufruf statt vieler, weil das Ergebnis je Karte gebündelt wird — zwei
Vorschläge für dieselbe Funktionsliste wären nicht unabhängig anwendbar.
Der Zähler läuft trotzdem je Funktion mit.
"""
alle, quelle = _quelle(projekt)
text = quellcode.als_text(quelle)
anzahl = sum(len(f["sektionen"]["funktionen"]) for f in alle)
async def abgleich(_, zaehl):
return await inhaltspruefung.pruefe_feature_im_code(
projekt, alle, quelle, text, zaehl, _melder(projekt))
return _platzhalter(anzahl), abgleich
async def _code_aufgaben(projekt: str):
"""Ein Lauf über alle Bedienstellen — Vollständigkeit ist eine Eigenschaft der
Menge, nicht der einzelnen Karte."""
alle, quelle = _quelle(projekt)
punkte = quellcode.interaktionen(quelle)
async def abgleich(_, zaehl):
return await inhaltspruefung.pruefe_code_im_feature(
projekt, alle, quelle, zaehl, _melder(projekt))
return _platzhalter(len(punkte)), abgleich
def _melder(projekt: str):
"""Sagt dem Lauf, was schiefging, ohne ihn abzubrechen.
Ein Sammellauf ist eine einzige Aufgabe. Flöge er wegen einer Stelle, wären
alle anderen Befunde weg — und der Balken stünde trotzdem auf 100 %. So bleibt
das Ergebnis erhalten und der Fehlstand steht daneben.
"""
def melde(text: str, fehler: int = 0):
if lauf := _laeufe.get(projekt):
lauf["meldung"] = text
lauf["fehler"] += fehler
return melde
def _platzhalter(anzahl: int) -> list:
"""Eine echte Aufgabe, der Rest nur für den Fortschrittsbalken."""
return [True] + [None] * max(0, anzahl - 1)
stand = ledger.lade(projekt)
stand["stil"] = ledger.behalte(stand["stil"], {_stilschluessel(a) for a in liste})
return (liste,
lambda a, _zaehl: _pruefe(stil, a, stand),
lambda: ledger.sichere(projekt, stand))
ARTEN = {
"stil": _stil_aufgaben,
"feature": _feature_aufgaben, # Feature im Code: was steht ohne Code da?
"code": _code_aufgaben, # Code im Feature: was fehlt auf den Karten?
}
async def _lauf(projekt: str, liste: list[dict], arbeite):
async def _lauf(projekt: str, liste: list[dict], arbeite, abschluss=None):
lauf = _laeufe[projekt]
schranke = asyncio.Semaphore(config.PRUEFUNG_PARALLEL)
@@ -570,6 +547,9 @@ async def _lauf(projekt: str, liste: list[dict], arbeite):
try:
await asyncio.gather(*(eine(a) for a in liste))
finally:
# Auch ein abgebrochener Lauf hat schon Urteile gefällt; die sind bezahlt
if abschluss:
abschluss()
lauf["laeuft"] = False
@@ -578,10 +558,10 @@ async def starte(projekt: str, art: str = "stil") -> dict:
if art not in ARTEN:
raise ValueError(f"Unbekannte Prüfart „{art}“")
if status(projekt)["laeuft"]:
raise ValueError("Prüfung läuft bereits")
raise LaeuftBereits("Prüfung läuft bereits")
agents.pruefe_konfiguration() # früher Stopp statt halbem Lauf
# Vorbereiten vor dem Task: ein Fehler darin wäre sonst unsichtbar
liste, arbeite = await ARTEN[art](projekt)
liste, arbeite, *rest = await ARTEN[art](projekt)
_laeufe[projekt] = {
"laeuft": True,
"art": art,
@@ -591,7 +571,7 @@ async def starte(projekt: str, art: str = "stil") -> dict:
"probleme": [],
"meldung": "",
}
aufgabe = asyncio.create_task(_lauf(projekt, liste, arbeite))
aufgabe = asyncio.create_task(_lauf(projekt, liste, arbeite, *rest))
_aufgaben.add(aufgabe)
aufgabe.add_done_callback(_aufgaben.discard)
return _laeufe[projekt]