204 lines
8.9 KiB
Python
204 lines
8.9 KiB
Python
"""Stufe 1 der Prüfung: alles, was sich zählen lässt, zählt der Code.
|
|
|
|
Das Modell erfand hier Regeln, die im Katalog nicht stehen — „Im" als Füllwort,
|
|
Satzzeichen, wo keins ist. Diese Klasse ist jetzt abschließend hier erledigt, und
|
|
der Prompt verbietet dem Modell, sie noch einmal zu melden.
|
|
|
|
Ein Befund mit Vorschlag geht ohne Anfrage durch. Ein Befund ohne Vorschlag heißt:
|
|
der Code weiß, dass die Stelle falsch ist, aber nicht, was richtig wäre — den
|
|
Auftrag bekommt das Modell.
|
|
"""
|
|
|
|
import re
|
|
|
|
# Höchstzahl Wörter je Zeile, nach (bereich, feld)
|
|
WORTGRENZEN = {
|
|
("feature", "titel"): 3,
|
|
("funktion", "titel"): 5,
|
|
("feature", "regeln"): 10,
|
|
("funktion", "regeln"): 10,
|
|
("funktion", "testen"): 8,
|
|
}
|
|
ZIEL_SAETZE = 2
|
|
ZIEL_WOERTER = 12
|
|
|
|
# Abschließend. Genau diese Wörter, keine sinnverwandten.
|
|
FUELLWOERTER = ("auch", "noch", "einfach", "jeweils", "ggf")
|
|
BADGE = ("kern", "optional")
|
|
# Felder, die als Liste geführt werden — je Zeile ein Eintrag.
|
|
# „funktionen" gehört der Inhaltsprüfung, hat aber dieselbe Gestalt.
|
|
ZEILENFELDER = ("regeln", "testen", "funktionen")
|
|
|
|
# Ein Funktionstitel nennt eine Handlung. Ohne Verb am Ende steht dort ein Bereich
|
|
# („Galerie im Modal") oder ein Zustand („Preis aktualisiert") — beides beschreibt
|
|
# nichts, was man für sich testen könnte. Deutsche Infinitive enden auf -en, -ln,
|
|
# -rn; wie Füllwörter und Wortgrenzen ist auch das an die Sprache gebunden.
|
|
_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.
|
|
# „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)
|
|
_STRICH = re.compile(r"^[-*]\s*")
|
|
|
|
|
|
def _zeilen(feld: str, text: str) -> list[str]:
|
|
"""regeln und testen sind Listen, alles andere ist ein Block."""
|
|
if feld in ZEILENFELDER:
|
|
return [_STRICH.sub("", z.strip()) for z in text.splitlines() if z.strip()]
|
|
return [text.strip()] if text.strip() else []
|
|
|
|
|
|
def _problem(feld: str, original: str, problem: str, vorschlag: str = "") -> dict:
|
|
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:
|
|
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]:
|
|
"""Verstöße, die sich wegstreichen lassen, samt der bereinigten Zeile."""
|
|
gruende, neu = [], zeile
|
|
if neu[-1] in SATZENDE:
|
|
gruende.append("Satzzeichen am Zeilenende")
|
|
neu = neu[:-1].rstrip()
|
|
for wort in FUELLWOERTER:
|
|
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
|
|
|
|
|
|
def _umschreiben(bereich: str, feld: str, zeile: str) -> list[str]:
|
|
"""Verstöße, die der Code sieht, aber nicht beheben kann."""
|
|
gruende = []
|
|
gemessen = _messbar(zeile)
|
|
if _KLAMMER.search(gemessen):
|
|
gruende.append("Klammer oder Schrägstrich")
|
|
if _LUECKE.search(gemessen):
|
|
gruende.append("Platzhalter statt Inhalt")
|
|
grenze = WORTGRENZEN.get((bereich, feld))
|
|
if grenze and len(gemessen.split()) > grenze:
|
|
gruende.append(f"Mehr als {grenze} Wörter")
|
|
if bereich == "funktion" and feld == "titel":
|
|
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"[.!?]+", 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):
|
|
gruende.append(f"Satz mit mehr als {ZIEL_WOERTER} Wörtern")
|
|
return gruende
|
|
|
|
|
|
def _handlung(zeile: str) -> bool:
|
|
woerter = _messbar(zeile).split()
|
|
if not woerter:
|
|
return True
|
|
return bool(_TASTE.match(zeile) or _INFINITIV.search(woerter[-1])
|
|
or _INFINITIV.search(woerter[0]))
|
|
|
|
|
|
def _zweiter_ablauf(zeilen: list[str]) -> str:
|
|
"""Die Zeile, ab der ein zweiter Durchlauf beginnt — sonst leer.
|
|
|
|
Ein Ablauf ist: handeln, handeln, Ergebnis. Steht nach dem Ergebnis wieder eine
|
|
Handlung, sind es zwei Abläufe — und damit zwei Funktionen. Genau so entstand
|
|
„Bildergalerie bedienen" mit blättern, Miniatur wählen und Großansicht in einem.
|
|
"""
|
|
ergebnis = False
|
|
for i, zeile in enumerate(zeilen):
|
|
if ergebnis and _handlung(zeile):
|
|
return 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 ""
|
|
|
|
|
|
def _zeile(bereich: str, feld: str, zeile: str) -> list[dict]:
|
|
"""Höchstens ein Befund je Zeile. Zwei Vorschläge für dieselbe Zeile wären nicht
|
|
unabhängig — der zweite fände seine Stelle nach dem ersten nicht mehr."""
|
|
gruende, neu = _reparierbar(zeile)
|
|
offen = _umschreiben(bereich, feld, zeile)
|
|
if offen:
|
|
# Muss ohnehin neu geschrieben werden — dann alles in einem Zug, vom Modell
|
|
return [_problem(feld, zeile, ", ".join(gruende + offen))]
|
|
return [_problem(feld, zeile, ", ".join(gruende), neu)] if gruende else []
|
|
|
|
|
|
def befunde(bereich: str, felder: dict) -> list[dict]:
|
|
"""Alle messbaren Verstöße einer Prüfstelle, in Reihenfolge der Felder."""
|
|
raus = []
|
|
# 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 (felder.get("testen") or "").strip():
|
|
raus.append(_problem("testen", "", "Funktion ohne Testschritte"))
|
|
return raus
|
|
for feld, text in felder.items():
|
|
if feld == "badge":
|
|
if text and text not in BADGE:
|
|
raus.append(_problem(feld, text, "Weder kern noch optional", "kern"))
|
|
continue
|
|
zeilen = _zeilen(feld, text or "")
|
|
for zeile in zeilen:
|
|
raus += _zeile(bereich, feld, zeile)
|
|
if feld == "testen" and not raus:
|
|
# Erst prüfen, wenn die Zeilen für sich in Ordnung sind: sonst hängen
|
|
# zwei Vorschläge am selben Feld und der zweite findet seine Stelle nicht.
|
|
if bruch := _zweiter_ablauf(zeilen):
|
|
raus.append(_problem(feld, bruch, "Zweiter Ablauf in einer Funktion"))
|
|
return raus
|
|
|
|
|
|
def auftrag(befund: dict) -> str:
|
|
"""Zeile für das Modell: der Code hat gemessen, den Ersatz schreibt das Modell."""
|
|
return f'- {befund["feld"]} „{befund["original"]}“: {befund["problem"]}'
|