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>
156 lines
6.1 KiB
Python
156 lines
6.1 KiB
Python
#!/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())
|