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:
@@ -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())
|
||||
Reference in New Issue
Block a user