Files
Projektwissen/scripts/pruefe_prozesse.py
T
DanielandClaude Opus 5.5 55c024dbc9 Prozessmodelle versioniert unter prozesse/<name>/ ablegen
Jedes Modell bekommt einen eigenen Ordner mit Steckbrief (README.md,
Status Entwurf|abgenommen), der maßgeblichen Arbeitsschritt-.md und der
daraus erzeugten .bpmn (ADR 0003).

- scripts/pruefe_prozesse.py prüft Ordnername, Dateien, Status und ob
  jede .bpmn byte-gleich mit der Umwandlung ihrer .md ist; --erzeugen
  erzeugt alle .bpmn neu. md_to_bpmn.py arbeitet deterministisch.
- tests/test_pruefe_prozesse.py: Bestandsprüfung plus 11 Fälle für die
  Prüflogik; pytest.ini sammelt .agents/skills und tests.
- Die in Anleitung.md genannten, nie committeten Beispiele liegen jetzt
  unter .agents/skills/sn-bpmn/beispiele/ und werden mitgeprüft.
- Skill sn-bpmn: Entwürfe nach entwuerfe/, neuer Schritt 5 „Ablegen“.
- Vorlage docs/vorlagen/prozess_readme.md, Doku und Fallstricke ergänzt.
- Tests: python -m pytest -q -> 44 bestanden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 13:02:48 +02:00

156 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Prüft die versionierten Prozessmodelle unter prozesse/ und die Beispiele des Skills sn-bpmn.
Jeder Prozess liegt in einem eigenen Ordner prozesse/<name>/ mit genau drei Dateien:
README.md Steckbrief mit Status, Stand, Zuständigkeit (Vorlage docs/vorlagen/prozess_readme.md)
<name>.md Arbeitsschritt-Beschreibung – die maßgebliche Quelle
<name>.bpmn daraus erzeugt, nie von Hand bearbeitet
Geprüft wird:
* Ordnername in Kleinbuchstaben, Ziffern und Bindestrichen,
* genau diese drei Dateien, keine weiteren (Originalunterlagen gehören nach referenz/),
* README.md nennt einen gültigen Status (Entwurf | abgenommen),
* <name>.md lässt sich umwandeln, und <name>.bpmn ist byte-gleich mit dem Ergebnis.
Die Beispiele in .agents/skills/sn-bpmn/beispiele/ werden auf die letzte Bedingung geprüft.
Aufrufe (aus dem Workspace-Root):
python scripts/pruefe_prozesse.py # alles prüfen
python scripts/pruefe_prozesse.py --erzeugen # alle .bpmn aus ihren .md neu erzeugen, dann prüfen
Exit-Code: 0 = in Ordnung, 1 = mindestens ein Befund.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SKILL = ROOT / ".agents" / "skills" / "sn-bpmn"
PROZESSE = ROOT / "prozesse"
BEISPIELE = SKILL / "beispiele"
sys.path.insert(0, str(SKILL))
import md_to_bpmn # noqa: E402 (liegt im Skill-Ordner, nicht als Paket installiert)
ORDNERNAME = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
STATUSWERTE = ("Entwurf", "abgenommen")
STATUSZEILE = re.compile(r"^\|\s*Status\s*\|\s*(?P<wert>[^|]*?)\s*\|", re.MULTILINE)
def erzeuge_bpmn(md_pfad: Path) -> bytes:
"""Wandelt eine Arbeitsschritt-.md genau so um wie md_to_bpmn.py auf der Kommandozeile."""
text = md_pfad.read_text(encoding="utf-8")
meta, steps, order, externals = md_to_bpmn.parse_markdown(text)
return md_to_bpmn.prettify(md_to_bpmn.build_bpmn(meta, steps, order, externals))
def _relativ(pfad: Path) -> str:
try:
return pfad.relative_to(ROOT).as_posix()
except ValueError:
return pfad.as_posix()
def pruefe_modell(md_pfad: Path) -> list[str]:
"""Befunde zum Paar <name>.md / <name>.bpmn; leer, wenn die .bpmn aktuell ist."""
bpmn_pfad = md_pfad.with_suffix(".bpmn")
try:
erwartet = erzeuge_bpmn(md_pfad)
except ValueError as fehler:
return [f"{_relativ(md_pfad)}: lässt sich nicht umwandeln: {fehler}"]
if not bpmn_pfad.is_file():
return [f"{_relativ(bpmn_pfad)}: fehlt – erzeugen mit python scripts/pruefe_prozesse.py --erzeugen"]
vorhanden = bpmn_pfad.read_bytes().replace(b"\r\n", b"\n")
if vorhanden != erwartet.replace(b"\r\n", b"\n"):
return [f"{_relativ(bpmn_pfad)}: passt nicht zu {md_pfad.name} (von Hand geändert oder nicht neu "
f"erzeugt) – erzeugen mit python scripts/pruefe_prozesse.py --erzeugen"]
return []
def pruefe_ordner(ordner: Path) -> list[str]:
"""Befunde zu Name, Dateien und Steckbrief eines Prozessordners."""
name = ordner.name
ort = _relativ(ordner)
befunde: list[str] = []
if not ORDNERNAME.match(name):
befunde.append(f"{ort}: Ordnername nur aus Kleinbuchstaben, Ziffern und Bindestrichen (z. B. antrag-pruefen)")
erwartet = {"README.md", f"{name}.md", f"{name}.bpmn"}
vorhanden = {p.name for p in ordner.iterdir()}
for fehlend in sorted(erwartet - vorhanden):
befunde.append(f"{ort}: {fehlend} fehlt")
for fremd in sorted(vorhanden - erwartet):
befunde.append(f"{ort}: {fremd} gehört nicht hierher (nur README.md, {name}.md, {name}.bpmn; "
f"Originalunterlagen nach referenz/)")
readme = ordner / "README.md"
if readme.is_file():
treffer = STATUSZEILE.search(readme.read_text(encoding="utf-8"))
if not treffer:
befunde.append(f"{ort}/README.md: Zeile '| Status | … |' fehlt (Vorlage docs/vorlagen/prozess_readme.md)")
elif treffer.group("wert") not in STATUSWERTE:
befunde.append(f"{ort}/README.md: Status '{treffer.group('wert')}' unbekannt, erlaubt: "
+ " | ".join(STATUSWERTE))
md_pfad = ordner / f"{name}.md"
if md_pfad.is_file():
befunde += pruefe_modell(md_pfad)
return befunde
def prozessordner(wurzel: Path = PROZESSE) -> list[Path]:
return sorted(p for p in wurzel.iterdir() if p.is_dir()) if wurzel.is_dir() else []
def beispiele(wurzel: Path = BEISPIELE) -> list[Path]:
return sorted(wurzel.glob("*.md")) if wurzel.is_dir() else []
def pruefe_alles() -> list[str]:
befunde: list[str] = []
if PROZESSE.is_dir():
for fremd in sorted(p for p in PROZESSE.iterdir() if p.is_file() and p.name != "README.md"):
befunde.append(f"{_relativ(fremd)}: Dateien gehören in einen Prozessordner prozesse/<name>/")
for ordner in prozessordner():
befunde += pruefe_ordner(ordner)
for md_pfad in beispiele():
befunde += pruefe_modell(md_pfad)
return befunde
def erzeuge_alles() -> None:
quellen = [o / f"{o.name}.md" for o in prozessordner()] + beispiele()
for md_pfad in quellen:
if not md_pfad.is_file():
continue
try:
md_pfad.with_suffix(".bpmn").write_bytes(erzeuge_bpmn(md_pfad))
print(f"erzeugt: {_relativ(md_pfad.with_suffix('.bpmn'))}")
except ValueError as fehler:
print(f"übersprungen: {_relativ(md_pfad)}: {fehler}")
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--erzeugen", action="store_true", help="alle .bpmn aus ihren .md neu erzeugen, dann prüfen")
args = ap.parse_args()
if args.erzeugen:
erzeuge_alles()
print()
befunde = pruefe_alles()
for befund in befunde:
print(befund)
anzahl = len(prozessordner())
status = "GRÜN" if not befunde else "ROT"
print(f"{status}: {len(befunde)} Befund/e, {anzahl} Prozess(e), {len(beispiele())} Beispiel(e).")
return 1 if befunde else 0
if __name__ == "__main__":
raise SystemExit(main())