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>
This commit is contained in:
2026-09-23 13:02:48 +02:00
co-authored by Claude Opus 5.5
parent 012509b6d1
commit 55c024dbc9
25 changed files with 835 additions and 35 deletions
+155
View File
@@ -0,0 +1,155 @@
#!/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())