Projektwissen der AG Prozesse aus AG_QGIS/Projektwissen abgeleitet
Übernommen ohne Historie aus AG_QGIS/Projektwissen (main @ decd2a0), Skill sn-bpmn vom Branch BPMN-Skill @ 6efcac4 (ADR 0001). - Skripte hallo.py, gitea.py, setup_workspace.py auf die Organisation AG_Prozesse umgestellt; das Workspace-Repo zählt über [workspace] repo als Ticket-Repo, auch ohne weitere [[repos]]. - pruefe_umlaute.py prüft jetzt die .py-Dateien des Workspace selbst. - Branchmodell main plus Arbeitsbranches (ADR 0002). - AGENTS.md, README, Leitfäden, Skills, Vorlagen, Fallstricke und Glossar auf die Prozessmodellierung zugeschnitten; QGIS- und Plugin-Spezifisches entfernt. - Tests: python -m pytest -q .agents/skills -> 32 bestanden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Findet deutschen Text in .py-Dateien, der offenbar ohne Umlaute/ß geschrieben wurde.
|
||||
|
||||
Hintergrund: AG_QGIS, Plugin_SN_Basis #42. KI-generierter Text weicht gelegentlich auf eine
|
||||
ASCII-Ersatzschreibweise aus ("gueltig" statt "gültig", "ueber" statt "über") statt
|
||||
echte Umlaute zu verwenden. Das betrifft vor allem Meldungstexte, Beschriftungen
|
||||
(etwa BPMN-Elementnamen) und Docstrings/Kommentare.
|
||||
|
||||
Technische Bezeichner (Variablen-, Funktions- und Schlüsselnamen) sind davon
|
||||
ausdrücklich nicht betroffen: Sie sind bewusst ASCII und enthalten kein Leerzeichen, während geschriebener deutscher Text so gut
|
||||
wie immer eines enthält. Deshalb prüft dieses Skript nur String-Inhalte mit
|
||||
mindestens einem Leerzeichen sowie Kommentartext - keine Bezeichner.
|
||||
|
||||
Kein Ersatz fürs Korrekturlesen: Die Liste verdächtiger Wortstämme ist bewusst kurz
|
||||
gehalten (nur eindeutige Fälle), um keine Fehlalarme zu erzeugen. Neue, hier nicht
|
||||
erfasste Wortstämme ergänzen.
|
||||
|
||||
Aufrufe (aus dem Workspace-Root):
|
||||
python scripts/pruefe_umlaute.py # alle .py-Dateien im Workspace
|
||||
python scripts/pruefe_umlaute.py .agents/skills # nur unterhalb dieser Ordner
|
||||
|
||||
Exit-Code: 0 = keine Funde, 1 = mindestens ein Fund.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
# Ordner, die nie geprüft werden (Git-Interna, gespiegelte Skills, lokale Entwürfe).
|
||||
AUSGESCHLOSSEN = {".git", ".claude", "__pycache__", "entwuerfe"}
|
||||
|
||||
# Dateien, die bewusst ASCII-Ersatzschreibweisen enthalten: hallo.py schreibt seine
|
||||
# Tabellenwerte absichtlich ohne Umlaute, dieses Skript nennt die Muster selbst.
|
||||
DATEIEN_OHNE_PRUEFUNG = {"scripts/hallo.py", "scripts/pruefe_umlaute.py"}
|
||||
|
||||
# Nur eindeutige Fälle: Wortstämme, die als ASCII-Ersatzschreibweise so gut wie nie
|
||||
# etwas anderes als ein fehlender Umlaut/ß sein können. Absichtlich kein "ss" für
|
||||
# "ß" (zu viele legitime Doppel-S-Wörter) und kein "ae/oe/ue" pauschal (zu viele
|
||||
# Fehlalarme in technischen Bezeichnern und Fremdwörtern).
|
||||
VERDAECHTIGE_STAEMME = [
|
||||
r"gueltig\w*", r"ungueltig\w*", r"verfuegbar\w*", r"ausgewaehlt\w*",
|
||||
r"gewaehlt\w*", r"waehlen\b", r"auswaehlen\b", r"loeschen\b", r"loescht\w*",
|
||||
r"groesse\w*", r"zuruecksetzen\b", r"erlaeuter\w*", r"bestaetig\w*",
|
||||
r"natuerlich\w*", r"moeglich\w*", r"zusaetzlich\w*", r"abhaengig\w*",
|
||||
r"unzulaessig\w*", r"ueberpruef\w*", r"vollstaendig\w*", r"geaendert\w*",
|
||||
r"aenderung\w*", r"einfuegen\b", r"ausfuehr\w*", r"benoetig\w*",
|
||||
r"gewuenscht\w*", r"ueber\w*", r"fuer\b", r"hoehe\w*", r"waere\b",
|
||||
]
|
||||
_STAMM_PATTERN = re.compile(
|
||||
r"\b(?:" + "|".join(VERDAECHTIGE_STAEMME) + r")\b", re.IGNORECASE
|
||||
)
|
||||
|
||||
# Erlaubt genau diese exakten Fundstellen (Datei, Zeile) - für Ausnahmefälle, die
|
||||
# geprüft und bewusst akzeptiert wurden. Aktuell leer: siehe Docstring, warum
|
||||
# technische Bezeichner die Heuristik ohnehin nicht erreichen.
|
||||
ALLOWLIST: set[tuple[str, int]] = set()
|
||||
|
||||
_STRING_PATTERN = re.compile(
|
||||
r'"""(?P<triple_d>(?:.|\n)*?)"""'
|
||||
r"|'''(?P<triple_s>(?:.|\n)*?)'''"
|
||||
r'|"(?P<double>(?:[^"\\\n]|\\.)*)"'
|
||||
r"|'(?P<single>(?:[^'\\\n]|\\.)*)'"
|
||||
)
|
||||
_KOMMENTAR_PATTERN = re.compile(r"#(?P<text>.*)$", re.MULTILINE)
|
||||
|
||||
|
||||
def _kandidaten(quelltext: str):
|
||||
"""Liefert (start_offset, text) für String-Inhalte und Kommentare mit Leerzeichen."""
|
||||
for m in _STRING_PATTERN.finditer(quelltext):
|
||||
text = next(g for g in m.groups() if g is not None)
|
||||
if " " in text:
|
||||
yield m.start(), text
|
||||
for m in _KOMMENTAR_PATTERN.finditer(quelltext):
|
||||
text = m.group("text")
|
||||
if " " in text:
|
||||
yield m.start("text"), text
|
||||
|
||||
|
||||
def pruefe_datei(pfad: Path) -> list[tuple[int, str, str]]:
|
||||
"""Liefert Funde als (Zeilennummer, Wortstamm, Kontext) für eine Datei.
|
||||
|
||||
Ein Kandidat (String-Inhalt oder Kommentar) kann mehrzeilig sein (Docstrings);
|
||||
deshalb wird für jeden Treffer einzeln die Zeile anhand seiner eigenen Position
|
||||
berechnet, nicht anhand des Kandidaten-Anfangs.
|
||||
"""
|
||||
quelltext = pfad.read_text(encoding="utf-8", errors="replace")
|
||||
funde = []
|
||||
for offset, text in _kandidaten(quelltext):
|
||||
for treffer in _STAMM_PATTERN.finditer(text):
|
||||
zeile = quelltext.count("\n", 0, offset + treffer.start()) + 1
|
||||
zeilen_text = text.splitlines()[treffer.string.count("\n", 0, treffer.start())] \
|
||||
if "\n" in text else text
|
||||
kontext = zeilen_text.strip()
|
||||
if len(kontext) > 90:
|
||||
kontext = kontext[:87] + "..."
|
||||
funde.append((zeile, treffer.group(0), kontext))
|
||||
return funde
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("ordner", nargs="*", help="Ordner relativ zum Workspace-Root (Standard: alles)")
|
||||
args = ap.parse_args()
|
||||
|
||||
wurzeln = [ROOT / o for o in args.ordner] if args.ordner else [ROOT]
|
||||
fehlend = [w for w in wurzeln if not w.is_dir()]
|
||||
if fehlend:
|
||||
print("Ordner nicht gefunden: " + ", ".join(str(w) for w in fehlend))
|
||||
return 2
|
||||
|
||||
gesamt_funde = 0
|
||||
dateien = 0
|
||||
for wurzel in wurzeln:
|
||||
for py_datei in sorted(wurzel.rglob("*.py")):
|
||||
if any(teil in AUSGESCHLOSSEN for teil in py_datei.relative_to(ROOT).parts):
|
||||
continue
|
||||
relativ = py_datei.relative_to(ROOT).as_posix()
|
||||
if relativ in DATEIEN_OHNE_PRUEFUNG:
|
||||
continue
|
||||
dateien += 1
|
||||
for zeile, stamm, kontext in pruefe_datei(py_datei):
|
||||
if (relativ, zeile) in ALLOWLIST:
|
||||
continue
|
||||
print(f"{relativ}:{zeile}: '{stamm}' in: {kontext}")
|
||||
gesamt_funde += 1
|
||||
|
||||
status = "GRÜN" if gesamt_funde == 0 else "ROT"
|
||||
print(f"\n{status}: {gesamt_funde} Fund/e in {dateien} Datei(en).")
|
||||
return 1 if gesamt_funde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user