This commit is contained in:
team3
2026-09-07 07:44:00 +02:00
parent 5d42b4cfc5
commit c6bbed4049
250 changed files with 13372 additions and 6203 deletions

View File

@@ -37,10 +37,17 @@ _INFINITIV = re.compile(r"(?:e[lr]?n|ln|rn)$", re.I)
# Eine Handlung steht als Infinitiv am Anfang oder am Ende der Zeile („Bestätigen im
# Modal", „Text ändern") oder beginnt mit einer Taste. Alles andere ist ein Ergebnis.
# Nur Anfang und Ende, nicht irgendwo: deutsche Mehrzahl endet auch auf -en.
_TASTE = re.compile(r"^(?:LMB|RMB|MMB|Klick|Doppelklick|Enter|Escape|Strg|Tab)\b", re.I)
# „Befehl" und „Kommando" stehen dabei wie eine Taste: auf der Kommandozeile ist der
# Aufruf die Handlung, und ohne sie wäre ein Setup nicht bedienbar zu beschreiben.
_TASTE = re.compile(
r"^(?:LMB|RMB|MMB|Klick|Doppelklick|Enter|Escape|Strg|Tab|Befehl|Kommando)\b", re.I)
SATZENDE = ".;"
_KLAMMER = re.compile(r"[()/]")
# Ein Zitat ist eine Einheit: ein Menüeintrag oder ein Kommando. „Befehl "make install"
# ausführen" sind drei Wörter, nicht vier, und der Schrägstrich in „npm run build/dev"
# ist keiner von uns. Darum wird außerhalb der Zitate gemessen.
_ZITAT = re.compile(r'"[^"]*"|„[^“]*“')
# Eine Vorlage ist kein Inhalt. Das Modell schlug `<testschritt>` als Testschritt
# vor — formal richtig, gemeint war „hier fehlt noch etwas".
_LUECKE = re.compile(r"<[^<>]*>|\.\.\.|\bTODO\b|\bXXX\b", re.I)
@@ -58,9 +65,30 @@ def _problem(feld: str, original: str, problem: str, vorschlag: str = "") -> dic
return {"feld": feld, "original": original, "problem": problem, "vorschlag": vorschlag}
def _messbar(zeile: str) -> str:
"""Die Zeile mit jedem Zitat als einem Wort — so wird gezählt und gesucht."""
return _ZITAT.sub("Zitat", zeile)
def _ausserhalb(zeile: str, wandel) -> str:
"""Wendet `wandel` nur auf die Teile außerhalb der Zitate an.
Ein Füllwort im Menüeintrag ist keins — dort steht der Text, den die Anwendung
zeigt. Würde er mitgestrichen, verwiese der Testschritt auf einen Knopf, den es
nicht gibt.
"""
teile, letzt = [], 0
for m in _ZITAT.finditer(zeile):
teile.append(wandel(zeile[letzt:m.start()]))
teile.append(m.group(0))
letzt = m.end()
teile.append(wandel(zeile[letzt:]))
return "".join(teile)
def _ohne_wort(zeile: str, wort: str) -> str:
neu = re.sub(rf"\s*\b{re.escape(wort)}\b\.?", "", zeile, flags=re.I).strip()
return re.sub(r"\s{2,}", " ", neu)
weg = lambda t: re.sub(rf"\s*\b{re.escape(wort)}\b\.?", "", t, flags=re.I)
return re.sub(r"\s{2,}", " ", _ausserhalb(zeile, weg)).strip()
def _reparierbar(zeile: str) -> tuple[list[str], str]:
@@ -70,7 +98,7 @@ def _reparierbar(zeile: str) -> tuple[list[str], str]:
gruende.append("Satzzeichen am Zeilenende")
neu = neu[:-1].rstrip()
for wort in FUELLWOERTER:
if re.search(rf"\b{wort}\b", neu, re.I):
if re.search(rf"\b{wort}\b", _messbar(neu), re.I):
gruende.append(f"Füllwort „{wort}“")
neu = _ohne_wort(neu, wort)
return gruende, neu
@@ -79,19 +107,24 @@ def _reparierbar(zeile: str) -> tuple[list[str], str]:
def _umschreiben(bereich: str, feld: str, zeile: str) -> list[str]:
"""Verstöße, die der Code sieht, aber nicht beheben kann."""
gruende = []
if _KLAMMER.search(zeile):
gemessen = _messbar(zeile)
if _KLAMMER.search(gemessen):
gruende.append("Klammer oder Schrägstrich")
if _LUECKE.search(zeile):
if _LUECKE.search(gemessen):
gruende.append("Platzhalter statt Inhalt")
grenze = WORTGRENZEN.get((bereich, feld))
if grenze and len(zeile.split()) > grenze:
if grenze and len(gemessen.split()) > grenze:
gruende.append(f"Mehr als {grenze} Wörter")
if bereich == "funktion" and feld == "titel":
woerter = zeile.split()
woerter = gemessen.split()
if woerter and not _INFINITIV.search(woerter[-1]):
gruende.append("Kein Verb im Infinitiv am Ende")
# Der Titel nennt die Wirkung, nicht den Griff. „Befehl "make install"
# ausführen" ist ein Testschritt — als Titel sagt er nicht, was danach gilt.
if _TASTE.match(zeile):
gruende.append("Auslöser statt Wirkung im Titel")
if feld == "ziel":
saetze = [s.strip() for s in re.split(r"[.!?]+", zeile) if s.strip()]
saetze = [s.strip() for s in re.split(r"[.!?]+", gemessen) if s.strip()]
if len(saetze) > ZIEL_SAETZE:
gruende.append(f"Mehr als {ZIEL_SAETZE} Sätze")
if any(len(s.split()) > ZIEL_WOERTER for s in saetze):
@@ -100,7 +133,7 @@ def _umschreiben(bereich: str, feld: str, zeile: str) -> list[str]:
def _handlung(zeile: str) -> bool:
woerter = zeile.split()
woerter = _messbar(zeile).split()
if not woerter:
return True
return bool(_TASTE.match(zeile) or _INFINITIV.search(woerter[-1])
@@ -115,10 +148,12 @@ def _zweiter_ablauf(zeilen: list[str]) -> str:
„Bildergalerie bedienen" mit blättern, Miniatur wählen und Großansicht in einem.
"""
ergebnis = False
for zeile in zeilen:
for i, zeile in enumerate(zeilen):
if ergebnis and _handlung(zeile):
return zeile
if not _handlung(zeile):
# Die erste Zeile darf ein Zustand sein: das ist die Vorbedingung, nicht das
# Ergebnis. „Badge ist sichtbar" vor „RMB auf Badge" ist ein Ablauf, nicht zwei.
if i and not _handlung(zeile):
ergebnis = True
return ""
@@ -137,14 +172,15 @@ def _zeile(bereich: str, feld: str, zeile: str) -> list[dict]:
def befunde(bereich: str, felder: dict) -> list[dict]:
"""Alle messbaren Verstöße einer Prüfstelle, in Reihenfolge der Felder."""
raus = []
# Ein bloßer Titel beschreibt nichts — und lässt sich über alles stülpen. Der
# Prüfer hielt damit einen Druckknopf für „Modal mit Tastatur steuern".
# Testschritte sind Pflicht. Eine Funktion ist definiert als etwas, das man für
# sich testen kann — ohne Ablauf ist die Behauptung unbelegt und der Titel lässt
# sich über alles stülpen. Ziel und Regeln bleiben freiwillig.
# Nur wenn die ganze Funktion vorliegt: bei einem einzelnen Feld sagt das Fehlen
# der anderen nichts.
inhaltsfelder = ("ziel", "regeln", "testen")
if bereich == "funktion" and all(f in felder for f in inhaltsfelder) \
and not any((felder.get(f) or "").strip() for f in inhaltsfelder):
raus.append(_problem("testen", "", "Funktion ohne Ziel, Regeln und Testen"))
and not (felder.get("testen") or "").strip():
raus.append(_problem("testen", "", "Funktion ohne Testschritte"))
return raus
for feld, text in felder.items():
if feld == "badge":