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:
2026-09-23 12:34:37 +02:00
co-authored by Claude Opus 5.5
commit 012509b6d1
36 changed files with 4414 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
# Markdown → BPMN 2.0 für PICTURE
`md_to_bpmn.py` wandelt eine einfache textuelle Prozessbeschreibung (`.md`)
in eine standardkonforme BPMN-2.0-XML-Datei um, die sich über den
BPMN-Import in PICTURE laden lässt. BPMN 2.0 ist ein offener, seit 2011
stabiler OMG-Standard – das Skript muss dafür kein PICTURE-internes
Schema "lernen" und erzeugt keine PICTURE-spezifischen UUIDs.
## Empfohlener Arbeitsablauf bei unstrukturiertem Ausgangstext
1. Rohtext (Fließtext, Stichpunkte, Sitzungsnotizen o. ä.) bereitstellen.
2. Daraus wird eine strukturierte Arbeitsschritt-`.md` in der unten
beschriebenen Syntax abgeleitet und zur Prüfung vorgelegt.
3. Fachlich prüfen und in der `.md` direkt anpassen (Reihenfolge,
Verzweigungen, Verantwortliche, Formulierungen) – dabei gegen
`Modellierregeln.md` abgleichen.
4. Aus der freigegebenen `.md` die BPMN-2.0-Datei erzeugen und in PICTURE
importieren.
Schritt 2 erfordert Interpretation von Fließtext und ist kein rein
mechanischer Schritt – die Qualität hängt von der Klarheit der Quelle und
der fachlichen Prüfung in Schritt 3 ab. Die fachlichen BPMN-Modellierregeln
stehen in `Modellierregeln.md`.
## Aufruf
```bash
python3 md_to_bpmn.py <prozess.md> <ausgabe.bpmn>
```
Beispiel:
```bash
python3 md_to_bpmn.py beispiel_prozess.md beispiel_prozess.bpmn
```
Die erzeugte `.bpmn`-Datei kann anschließend über den BPMN-Import-Dialog
in PICTURE geladen werden.
### Diagramm vorab prüfen, ohne PICTURE
Um zu prüfen, ob `md_to_bpmn.py` selbst korrekt gerechnet hat (Struktur,
Anschlusspunkte, Layout) – unabhängig davon, was ein Import-Werkzeug daraus
macht – eignet sich ein originalgetreuer BPMN-Viewer:
- **`demo.bpmn.io`** – im Browser, kein Install nötig: `.bpmn`-Datei per
Drag & Drop reinziehen.
- **Camunda Modeler** – kostenlose Desktop-App, gleiche Rendering-Engine
wie bpmn.io, auch offline.
Beide übernehmen importierte Anschlusspunkte originalgetreu, statt sie neu
zu berechnen.
## Markdown-Syntax
### Kopfzeile (optional, vor dem ersten Schritt)
```
Prozess: <Name des Prozesses>
```
### Schritte
Jeder Schritt ist eine Zeile der Form:
```
<ID> <Typ>: <Beschriftung>
```
- **ID**: ein kurzes, eindeutiges Kürzel ohne Leerzeichen (z. B. `S1`, `3.3`, `A`)
- **Typ** (Groß-/Kleinschreibung egal):
| Schreibweise im .md | Erzeugtes BPMN-Element |
|--------------------------------------|-------------------------------------|
| `Start` | Start-Ereignis |
| `Ende` / `End` | End-Ereignis |
| `Zwischenereignis` / `Intermediate` | Zwischen-Ereignis (Catch) |
| `Task` / `Aufgabe` | Aufgabe (Task) |
| `Manuelle Aufgabe` | Manuelle Aufgabe |
| `User Aufgabe` | User-Aufgabe |
| `Service Aufgabe` | Service-Aufgabe |
| `Script Aufgabe` | Script-Aufgabe |
| `Geschäftsregel Aufgabe` | Geschäftsregel-Aufgabe |
| `Sendeaufgabe` | Aufgabe (sendend) |
| `Empfangsaufgabe` | Aufgabe (empfangend) |
| `Teilprozess` / `Subprocess` | Geschlossener Teilprozess |
| `Aufruf` | Aufruf-Element (Call Activity) |
| `Gateway XOR` | Exklusives Gateway (XOR) |
| `Gateway OR` / `Gateway ODER` | Inklusives Gateway (OR) |
| `Gateway AND` / `Gateway UND` | Paralleles Gateway (AND) |
| `Gateway Ereignisbasiert` | Ereignisbasiertes Gateway |
| `Hinweis` / `Note` | Textuelle Anmerkung (unverbunden) |
| `Datenobjekt` | Datenobjekt (unverbunden) |
| `Datenspeicher` | Datenspeicher (unverbunden) |
| `Gruppe` | Gruppe (unverbunden) |
Vollständige Liste inkl. Bedeutung: `Elementkatalog.md`.
### Typisierte Ereignisse
Start-, Zwischen- und Endereignisse können ein zweites Wort für die Art des
Ereignisses bekommen: `<Start|Zwischenereignis|Ende> <Kind>: <Beschriftung>`.
Mögliche `<Kind>`-Wörter: `Nachricht`, `Zeit`, `Eskalation`, `Bedingung`,
`Link`, `Fehler`, `Signal`, `Terminierung`. Details und Einschränkungen (z. B.
`Terminierung` nur als Endereignis, `Link` nur als Zwischenereignis) stehen in
`Elementkatalog.md` und `Modellierregeln.md`.
```
S1 Start Nachricht: Antrag geht ein
S9 Zwischenereignis Zeit: 14 Tage Frist abwarten
S17 Ende Terminierung: Verfahren beendet
```
Ein Zwischenereignis ist standardmäßig empfangend (Catch); mit einem
zusätzlichen dritten Wort `sendend` wird daraus ein auslösendes (Throw-)
Ereignis, z. B. `S2 Zwischenereignis Nachricht sendend: Status melden`.
### Geschlossene Pools und Nachrichtenflüsse
Externe Beteiligte (Bürger, andere Behörden, …), mit denen nur Nachrichten
ausgetauscht werden, vor dem ersten Schritt deklarieren:
```
Extern: Bürger
Extern: Landratsamt
```
Unter einem Schritt erzeugt `~> <Name>` einen ausgehenden, `<~ <Name>` einen
eingehenden Nachrichtenfluss zu einem so deklarierten Beteiligten:
```
S8 Sendeaufgabe: Bescheid versenden
~> Bürger
-> S9
```
Nachrichtenflüsse sind nur zwischen Pools erlaubt (siehe `Modellierregeln.md`)
– das Ziel muss immer ein per `Extern:` deklarierter Name sein, nie eine
Schritt-ID im selben Pool.
### Verzweigungen / Sprünge
Unter einem Schritt können beliebig viele Pfeil-Zeilen stehen:
```
S3 Gateway XOR: Antrag vollständig?
Ja -> S4
Nein -> S3a
```
- `<Bezeichnung> -> <Ziel-ID>` – beschriftete Kante (z. B. für Ja/Nein-Zweige)
- `-> <Ziel-ID>` – unbeschriftete Kante (z. B. für parallele AND-Zweige oder
einfache Weiterleitung)
**Wichtig:** Wenn ein Schritt **keine** Pfeil-Zeile hat, wird automatisch
eine Kante zum **nächsten Schritt in der Datei** erzeugt. Ihr müsst also nur
dort explizite Pfeile setzen, wo der Ablauf von der reinen Lesereihenfolge
abweicht (Verzweigung, Sprung zurück, Zusammenführung).
Rücksprünge (Schleifen, z. B. „Nachforderung" zurück zur Prüfung) werden
automatisch erkannt und am Rand des Diagramms herumgeführt.
### Verantwortliche / Swimlanes
Schreibt ihr am Anfang der Beschriftung eines Schritts einen kurzen Namen
gefolgt von einem Doppelpunkt, wird dieser als **Verantwortlicher** erkannt
und daraus automatisch eine Swimlane gemacht:
```
V1 Task: Flurneuordnung: Gewannenkonzept festlegen
O3 Task: ÖbVI: Waldgrenzen verhandeln
O9 Task: LRA: Aufmessung im Ortstermin
```
Aus den drei unterschiedlichen Verantwortlichen entstehen drei Lanes in
einem Pool; Schritte ohne Verantwortlichen-Angabe landen gesammelt in einer
Lane „Nicht zugeordnet". Ohne jede Verantwortlichen-Angabe entsteht wie
bisher ein Diagramm ohne Lanes.
Das ist eine Heuristik: Ein Doppelpunkt weit vorn in einer kurzen
Beschriftung (max. 30 Zeichen, max. 4 Wörter, kein „?") wird als
Verantwortlicher gedeutet. Enthält eine Beschriftung an dieser Stelle
zufällig einen Doppelpunkt, der keine Zuständigkeit meint, entsprechend
umformulieren.
### Vollständige Beispiele
- Ohne Lanes: `beispiel_prozess.md` / `beispiel_prozess.bpmn`
- Mit Lanes: `gewannenvermessung_lanes.md` / `gewannenvermessung_lanes.bpmn`
## Einschränkungen der aktuellen Version
- Es wird pro `.md`-Datei **ein** Diagramm (ein Prozess) erzeugt, keine
mehrstufigen Prozessnetze mit verknüpften Teilprozess-Diagrammen.
- `Teilprozess` (geschlossener Teilprozess) und `Aufruf` (Aufruf-Element)
werden als Bausteine angelegt, aber **ohne** Inhalt bzw. **ohne**
Verknüpfung zu einem Ziel-Prozess – Details bzw. Verknüpfung lassen sich
in PICTURE nach dem Import nachtragen. Der **aufgeklappte** Teilprozess
(Inhalt inline im selben Diagramm) wird noch nicht unterstützt.
- Wiederholungs-Marker (Schleife, Multi-Instanz sequenziell/parallel) werden
noch nicht unterstützt; bei Bedarf als `Hinweis` an der betroffenen
Aktivität dokumentieren.
- Assoziationen (gepunktete Linien) von Datenobjekten/-speichern zu
Aktivitäten werden nicht automatisch gezeichnet; Datenobjekt/-speicher
landen unverbunden im Diagramm und werden nach dem Import in PICTURE bei
Bedarf manuell mit den betroffenen Aktivitäten verbunden.
- Das automatische Layout ist ein einfacher Schichten-Algorithmus
(ähnlich den meisten BPMN-Auto-Layouts) – bei komplexen Prozessen lohnt
sich nach dem Import ein manuelles Nachjustieren einzelner Boxen und
Beschriftungen. Das gilt besonders für geschlossene Pools (externe
Beteiligte): Sie werden schlicht unterhalb des Hauptpools gestapelt.
- Jede Kante bekommt in der erzeugten `.bpmn`-Datei einen eigenen
Anschlusspunkt (Modellierregel 4, in `Modellierregeln.md`) statt eines
gemeinsamen Mittelpunkts. Bei Task/Ereignis/Datenobjekt usw. werden dazu
mehrere Punkte über die tatsächliche Kontur verteilt (Rechteckkante bzw.
Kreisbogen). **Ein Gateway bekommt dagegen nur einen der vier festen
PICTURE-Eckpunkte (0°/90°/180°/270°) je Kante**, nie einen Punkt
dazwischen auf der Rautenkante – mehr als vier Kanten an einem Gateway
(eingehend plus ausgehend) können sich damit nicht mehr eindeutig
darstellen lassen, siehe Modellierregel 4. Wirkt das Ergebnis in einem
Anzeige-Werkzeug trotzdem so, als würden Verzweigungen an einem Punkt
zusammenlaufen, zuerst in einem originalgetreuen BPMN-Viewer (z. B.
`demo.bpmn.io`, siehe unten) prüfen: Ein Viewer, der importierte
Anschlusspunkte respektiert statt sie neu zu berechnen, zeigt die
verteilten Punkte korrekt an.
- Der BPMN-Import in PICTURE übernimmt die reine Ablaufstruktur
zuverlässig; PICTURE-/FIM-spezifische Zusatzfelder (Fallzahl,
Bearbeitungszeit, RACI usw.) sind kein Teil des BPMN-Standards und
müssten nach dem Import ergänzt werden.
+99
View File
@@ -0,0 +1,99 @@
# PICTURE-Elementkatalog (BPMN 2.0)
Quelle: Informationsportal der PICTURE-Prozessplattform, Abschnitt „BPMN 2.0"
(Stand der Übernahme: 2026-09-21). PICTURE deckt die OMG-Konformitätsklassen
„Deskriptives BPMN" und „Analytisches BPMN" ab – Symbole/Icons sind hier
bewusst weggelassen, da für den Skill nur Name, Bedeutung und DSL-Zuordnung
relevant sind. Bei Bedarf original im PICTURE-Portal nachschlagen.
Spalte „`md_to_bpmn.py`" zeigt das DSL-Schlüsselwort aus `Anleitung.md`.
**„–“ bedeutet: von `md_to_bpmn.py` aktuell nicht erzeugbar** – im Entwurf als
Freitext/Hinweis dokumentieren und nach dem Import manuell in PICTURE ergänzen.
## Aktivitäten
| Name | Beschreibung | `md_to_bpmn.py` |
|---|---|---|
| Aufgabe | Arbeitsschritt, der nicht weiter verfeinert wird. | `Task` / `Aufgabe` |
| Manuelle Aufgabe | Ausführung durch einen Menschen ohne IT-Unterstützung. | `Manuelle Aufgabe` |
| User-Aufgabe | Ausführung durch einen Menschen mit Software-Unterstützung. | `User Aufgabe` |
| Service-Aufgabe | Automatische Ausführung durch einen Dienst/Service. | `Service Aufgabe` |
| Script-Aufgabe | Automatische Ausführung durch eine Process Engine per Script. | `Script Aufgabe` |
| Geschäftsregel-Aufgabe | Automatische Ausführung nach Entscheidungs-/Berechnungsregeln. | `Geschäftsregel Aufgabe` |
| Aufgabe (sendend) | Versand einer Nachricht an einen externen Beteiligten. | `Sendeaufgabe` |
| Aufgabe (empfangend) | Empfang einer Nachricht von einem externen Beteiligten. | `Empfangsaufgabe` |
| Geschlossener Teilprozess | Gekapselte Aktivitätsmenge, Details in separatem Diagramm ausgelagert. | `Teilprozess` |
| Aufgeklappter Teilprozess | Gekapselte, aber inline dargestellte Aktivitätsmenge (keine Auslagerung). | – (noch nicht unterstützt, siehe Einschränkungen in `Anleitung.md`) |
| Aufruf-Element (Call Activity) | Verknüpfung mit einem separaten, eigenständigen Prozess. | `Aufruf` |
## Wiederholungen
| Name | Beschreibung | `md_to_bpmn.py` |
|---|---|---|
| Schleife | Aktivität wiederholt sich, solange eine Bedingung zutrifft. | – (noch nicht unterstützt) |
| Multi-Instanz (sequenziell) | Aktivität läuft für eine bekannte Anzahl Objekte nacheinander. | – (noch nicht unterstützt) |
| Multi-Instanz (parallel) | Wie sequenziell, aber alle Ausführungen parallel. | – (noch nicht unterstützt) |
## Ereignisse
Start-, Zwischen- und Endereignisse können optional typisiert werden. In der
DSL: `<Start|Zwischenereignis|Ende> <Kind> [sendend|empfangend]: <Label>`.
| Ereignis-Kind | Bedeutung | `md_to_bpmn.py`-Wort |
|---|---|---|
| (untypisiert) | Allgemeines Ereignis, meist Start/Ende. | – (kein Zusatzwort) |
| Nachricht | Empfang/Versand einer Nachricht (Dokument, Information). | `Nachricht` |
| Zeit | Periodisches, zeitpunkt- oder zeitspannenbezogenes Ereignis. | `Zeit` |
| Eskalation | Meldung an den nächsthöheren Verantwortlichen. | `Eskalation` |
| Bedingung | Reaktion auf veränderte (Geschäfts-)Bedingungen. | `Bedingung` |
| Link | Fortsetzung eines aufgetrennten Sequenzflusses; nur als Zwischenereignis, paarweise sendend/empfangend. Von Analysefunktionen nicht unterstützt. | `Link` (nur Zwischenereignis) |
| Fehler | Auslösen/Behandeln definierter Fehler. | `Fehler` |
| Signal | Signal an (potenziell) mehrere Prozesse gleichzeitig. | `Signal` |
| Terminierung | Sofortige Beendigung der Prozessinstanz. | `Terminierung` (nur Endereignis) |
Start-Ereignisse sind immer eintretend/empfangend, Endereignisse immer
auslösend/sendend (Skript weist eine widersprüchliche Angabe ab).
Zwischenereignisse sind standardmäßig empfangend (Catch); mit `sendend`
wird daraus ein Throw-Ereignis.
## Kontrollfluss-Operatoren (Gateways)
| Name | Bedeutung | `md_to_bpmn.py` |
|---|---|---|
| Parallele Verzweigung (AND-Split) | Alle nachfolgenden Pfade laufen gleichzeitig. | `Gateway AND` |
| Exklusive datenbasierte Verzweigung (XOR) | Genau ein Pfad wird abhängig von den Daten gewählt. | `Gateway XOR` |
| Inklusive datenbasierte Verzweigung (OR) | Ein oder mehrere Pfade werden gewählt. | `Gateway OR` |
| Exklusive ereignisbasierte Verzweigung | Es folgt der Pfad, dessen Ereignis/empfangende Aufgabe zuerst eintritt; alle anderen Pfade verfallen für diese Instanz. Darf **nicht** spiegelbildlich als Zusammenführung genutzt werden. | `Gateway Ereignisbasiert` |
| Entweder-Oder-Zusammenführung (XOR-Merge) | Ankommende Pfade laufen unabhängig voneinander einfach weiter; Nachfolger wird ggf. mehrfach durchlaufen. | `Gateway XOR` als Zusammenführung |
| Verschmelzung (AND-Merge) | Wartet auf **alle** eingehenden Pfade, bevor es weitergeht. | `Gateway AND` als Zusammenführung |
| Oder-Verschmelzung (OR-Merge) | Wartet auf alle **aktiven** eingehenden Pfade. | `Gateway OR` als Zusammenführung |
`md_to_bpmn.py` erzeugt für Split und Merge denselben Gateway-Typ (XOR/OR/AND
sind in BPMN dieselben Elemente, nur mit unterschiedlicher Kanten-Anzahl) –
siehe `Modellierregeln.md` für die Regel, welcher Merge-Typ zu welchem
Split-Typ passt, und für PICTUREs Grenze von vier Anschlusspunkten je
Gateway (Modellierregel 4).
## Sequenz- und Nachrichtenflüsse
| Name | Beschreibung | `md_to_bpmn.py` |
|---|---|---|
| Sequenzfluss | Reihenfolge innerhalb eines Pools. | `->` |
| Nachrichtenfluss | Nur zwischen Pools, nie innerhalb desselben Pools. | `~>` (ausgehend) / `<~` (eingehend) zu einem per `Extern:` deklarierten Beteiligten |
## Artefakte
| Name | Beschreibung | `md_to_bpmn.py` |
|---|---|---|
| Datenobjekt / Dokument | Information, die im Prozess erstellt/genutzt/weitergegeben wird. Sparsam einsetzen, nur bei besonders wichtigem Sachverhalt. | `Datenobjekt` |
| Datenspeicher | Dauerhafte, von der Prozessinstanz unabhängige Ablage (Datenbank, Aktenschrank). | `Datenspeicher` |
| Textuelle Anmerkung | Kommentar zu einem beliebigen Modellelement. | `Hinweis` |
| Gruppe | Rein visuelle Klammer ohne Einfluss auf Ablauf/Verantwortlichkeiten. | `Gruppe` |
## Pools & Schwimmbahnen
| Name | Beschreibung | `md_to_bpmn.py` |
|---|---|---|
| Pool | Grenze eines Prozesses/Teilprozesses. | entsteht automatisch, sobald Lanes oder `Extern:`-Beteiligte vorkommen |
| Schwimmbahn | Unterteilung eines Pools nach Organisationseinheit/Rolle/System. | automatisch aus `Verantwortlicher: Aufgabe`-Präfix |
| Geschlossener Pool | Externe/r Beteiligte/r bzw. externer Prozess ohne dargestellten Inhalt, tauscht nur Nachrichten aus. | `Extern: <Name>` + `~>`/`<~` |
+73
View File
@@ -0,0 +1,73 @@
# BPMN-Modellierregeln
Fachliche Regeln, an denen sich ein Entwurf (`entwuerfe/<name>.md`) und die
daraus erzeugte BPMN-Datei messen lassen müssen – zusätzlich zur reinen
Markdown-Syntax aus `Anleitung.md`. Beim Erzeugen und beim Prüfen eines
Entwurfs (Schritt 2 in `SKILL.md`) gegen diese Liste abgleichen und
Verstöße im Annahmen-/Hinweis-Abschnitt benennen. Welche Elemente es
überhaupt gibt und wofür sie stehen, steht in `Elementkatalog.md`.
1. **Verzweigungen laufen passend zusammen.** Verzweigte Pfade müssen in
einem Verzweigungselement (Gateway) zusammenlaufen, das zur Art der
Verzweigung passt, bevor der Pfad in den nächsten Arbeitsschritt nach
der Verzweigung mündet. Pfade nicht einfach ohne zusammenführendes
Gateway in denselben Folgeschritt münden lassen. Welcher Gateway-Typ
zusammenführt, hängt vom Split ab – und die drei Typen unterscheiden
sich fachlich, nicht nur formal:
- **XOR-Split** (genau ein Pfad wird genommen) → **XOR-Merge**
(Entweder-Oder-Zusammenführung): die ankommenden Pfade laufen
unabhängig voneinander einfach weiter, es wird auf nichts gewartet.
- **AND-Split** (alle Pfade parallel) → **AND-Merge** (Verschmelzung):
wartet, bis **alle** eingehenden Pfade angekommen sind, bevor es
weitergeht. Ein AND-Split ohne passenden AND-Merge am Ende der
parallelen Pfade führt zu mehrfach durchlaufenen Folgeschritten.
- **OR-Split** (ein oder mehrere Pfade) → **OR-Merge**
(Oder-Verschmelzung): wartet auf alle **aktiven** eingehenden Pfade,
nicht zwingend auf alle theoretisch möglichen.
- **Ereignisbasierte Verzweigung** ist ein Sonderfall: Sie darf nicht
spiegelbildlich als Zusammenführung genutzt werden, also nie mehr
als eine eingehende Kante haben (`md_to_bpmn.py` lehnt das ab).
2. **Nachrichtenfluss nur zwischen Pools.** Ein Nachrichtenfluss verbindet
ausschließlich Elemente unterschiedlicher Pools (z. B. den eigenen
Prozess mit einem externen Beteiligten). Innerhalb desselben Pools
werden Informationsübergaben über den normalen Sequenzfluss
modelliert, nie über Nachrichtenfluss.
3. **Datenobjekte, Datenspeicher und Gruppen sind keine Ablaufelemente.**
Sie hängen nicht am Sequenzfluss (kein `->` zu/von ihnen) und werden
sparsam eingesetzt – nur wenn der Sachverhalt (z. B. ein zentrales
Dokument oder eine geteilte Ablage) für das Verständnis des Prozesses
wirklich wichtig ist, nicht für jede beiläufig erwähnte Unterlage.
4. **Jede Kante bekommt einen eigenen Anschlusspunkt.** Jeder Eingang und
jeder Ausgang an einem Element muss einen eigenen Verbindungspunkt am
Element nutzen, nicht denselben Punkt wie eine andere Kante. Laufen
mehrere Kanten sichtbar in einem einzigen Punkt zusammen, sieht das
Diagramm so aus, als wären mehrere fachlich getrennte Verzweigungen
eine einzige Linie – das verschleiert genau die Verzweigung, die
dargestellt werden soll. `md_to_bpmn.py` setzt das automatisch um, aber
je nach Elementtyp unterschiedlich:
- **Bei Task/Ereignis/Datenobjekt usw.** werden mehrere Punkte entlang
der tatsächlichen Kontur verteilt (Rechteckkante bzw. Kreisbogen).
- **Bei einem Gateway hat PICTURE nur vier feste Anschlusspunkte** – je
einen bei 0°/90°/180°/270° (Ost/Süd/West/Nord), die Kante verläuft
dort immer gerade. `md_to_bpmn.py` weist deshalb jeder Kante an einem
Gateway einen dieser vier Eckpunkte zu, nicht einen Punkt auf der
Rautenkante dazwischen. **Daraus folgt eine harte Obergrenze: ein
Gateway kann höchstens vier Kanten insgesamt (eingehend plus
ausgehend zusammengerechnet) eindeutig darstellen.** Ein XOR-Split
mit einer eingehenden Kante hat also nur für maximal drei ausgehende
Zweige Platz; bei vier oder mehr Zweigen muss mindestens eine Kante
sich einen Punkt mit einer anderen teilen – das ist eine Grenze von
PICTURE selbst, kein Darstellungsfehler von `md_to_bpmn.py`. Bei
einer Verzweigung mit vielen Zweigen und einer eingehenden Kante
lohnt sich daher, die Entscheidung in mehrere, weniger stark
verzweigte Gateways aufzuteilen, wenn jeder Zweig einzeln sichtbar
bleiben soll.
Wirkt ein Diagramm trotzdem so, als würden Verzweigungen an einem
Punkt zusammenlaufen, zuerst in einem originalgetreuen BPMN-Viewer
(z. B. `demo.bpmn.io`) prüfen, ob es an der erzeugten Datei liegt oder
tatsächlich an der Vier-Punkte-Grenze eines Gateways mit mehr als vier
Kanten.
+47
View File
@@ -0,0 +1,47 @@
---
name: sn-bpmn
description: Wandelt eine Prozessbeschreibung (Rohtext, Stichpunkte oder bereits strukturierte Arbeitsschritt-.md) in eine BPMN-2.0-Datei für den Import in PICTURE um. Aufrufen, wenn ein Verwaltungsprozess als Ablaufdiagramm für PICTURE modelliert werden soll.
---
# BPMN-Prozess-Konverter
Begleitet den Weg von einer (oft unstrukturierten) Prozessbeschreibung zu
einer importierbaren BPMN-2.0-Datei für PICTURE, mit `md_to_bpmn.py` in
diesem Verzeichnis.
Lies `Anleitung.md` in diesem Verzeichnis für die vollständige
Markdown-Syntax (Schritt-Typen, Verzweigungen, Rücksprung-Erkennung,
Verantwortliche/Swimlanes), `Elementkatalog.md` für die vollständige
Liste der PICTURE-BPMN-Elemente und `Modellierregeln.md` für die
fachlichen BPMN-Modellierregeln, bevor du eine Arbeitsschritt-.md
schreibst oder bearbeitest.
## Ablauf
1. **Quelle ermitteln.** Frage nach der Rohtext-, Word- oder PDF-Quelle,
falls keine angegeben oder angehängt wurde.
2. **Entwurf erzeugen.** Lies die Quelle und leite daraus eine
strukturierte Arbeitsschritt-`.md` nach der Syntax aus `Anleitung.md`
ab. Triff nachvollziehbare fachliche Annahmen (Schrittgrenzen,
Verzweigungen, Verantwortliche wo erkennbar) und schreibe das Ergebnis
nach `entwuerfe/<name>.md` (Verzeichnis bei Bedarf anlegen). Gleiche den
Entwurf gegen `Modellierregeln.md` ab und behebe Verstöße direkt im
Entwurf. Fasse kurz zusammen, welche Annahmen getroffen wurden, und
weise auf Stellen hin, die fachlich geprüft werden sollten.
3. **Auf Freigabe warten.** Nicht eigenständig weiterbearbeiten. Der
Nutzer passt `entwuerfe/<name>.md` selbst an (Reihenfolge,
Verzweigungen, Verantwortliche, Formulierungen) und meldet sich, wenn
sie passt.
4. **BPMN erzeugen.** Erst nach ausdrücklicher Freigabe (z. B. "ist
angepasst", "passt so", "wandle um"):
```
python .agents/skills/sn-bpmn/md_to_bpmn.py entwuerfe/<name>.md <name>.bpmn
```
Ergebnis kurz melden (Anzahl Schritte/Kanten/Lanes, Pfad der Datei).
Wenn der Nutzer direkt eine bereits fertige Arbeitsschritt-`.md` angibt
und ausdrücklich nur die Umwandlung möchte, Schritt 2-3 überspringen
und direkt umwandeln.
File diff suppressed because it is too large Load Diff
+379
View File
@@ -0,0 +1,379 @@
# -*- coding: utf-8 -*-
"""Tests für md_to_bpmn.py - reines Python/pytest, ohne Netzwerk."""
import xml.etree.ElementTree as ET
import pytest
import md_to_bpmn as m
def test_bestehende_syntax_bleibt_kompatibel():
"""Prüft, dass Start/Task/Gateway-XOR/Ende mit Verzweigung wie bisher geparst werden."""
text = """
S1 Start: Beginn
-> S2
S2 Task: Antrag pruefen
S3 Gateway XOR: Vollständig?
Ja -> S4
Nein -> S2
S4 Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
assert externals == []
assert steps['S1'].type == 'StartEvent'
assert steps['S3'].type == 'XorGateway'
assert ('Ja', 'S4') in steps['S3'].edges
defs = m.build_bpmn(meta, steps, order, externals)
assert defs.find(f".//{{{m.NS['bpmn']}}}exclusiveGateway") is not None
def test_lane_erkennung_bleibt_unveraendert():
"""Prüft, dass 'Verantwortlicher: Rest' weiterhin als Swimlane erkannt wird."""
text = """
S1 Start: Beginn
-> V1
V1 Task: Flurneuordnung: Gewannenkonzept festlegen
O3 Task: ÖbVI: Waldgrenzen verhandeln
S_end Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
lanes = {s.lane for s in steps.values() if s.lane}
assert lanes == {'Flurneuordnung', 'ÖbVI'}
@pytest.mark.parametrize('typewort, erwarteter_typ', [
('Manuelle Aufgabe', 'ManualTask'),
('User Aufgabe', 'UserTask'),
('Service Aufgabe', 'ServiceTask'),
('Script Aufgabe', 'ScriptTask'),
('Geschäftsregel Aufgabe', 'BusinessRuleTask'),
('Sendeaufgabe', 'SendTask'),
('Empfangsaufgabe', 'ReceiveTask'),
('Teilprozess', 'SubProcess'),
('Aufruf', 'CallActivity'),
('Gateway Ereignisbasiert', 'EventBasedGateway'),
('Datenobjekt', 'DataObject'),
('Datenspeicher', 'DataStore'),
('Gruppe', 'Group'),
])
def test_neue_elementtypen_werden_aufgeloest(typewort, erwarteter_typ):
"""Prüft, dass alle neuen Aufgaben-/Gateway-/Artefakttypen korrekt zugeordnet werden."""
typ, kind, direction = m.resolve_type(typewort, 'Testzeile')
assert typ == erwarteter_typ
def test_typisiertes_startereignis_erzeugt_eventdefinition():
"""Prüft, dass ein typisiertes Startereignis das passende BPMN-eventDefinition-Kind erhaelt."""
text = """
S1 Start Nachricht: Antrag eingegangen
-> S2
S2 Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
assert defs.find(f".//{{{m.NS['bpmn']}}}messageEventDefinition") is not None
def test_zwischenereignis_sendend_wird_throw_event():
"""Prüft, dass 'sendend' bei einem Zwischenereignis ein intermediateThrowEvent erzeugt."""
text = """
S1 Start: Beginn
-> S2
S2 Zwischenereignis Nachricht sendend: Status melden
-> S3
S3 Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
assert m.bpmn_tag_for(steps['S2']) == 'intermediateThrowEvent'
def test_terminierung_nur_als_endereignis_erlaubt():
"""Prüft, dass 'Terminierung' bei einem Startereignis einen Fehler ausloest."""
text = """
S1 Start Terminierung: X
-> S2
S2 Ende: Y
"""
with pytest.raises(ValueError, match='Terminierung'):
m.parse_markdown(text)
def test_link_nur_als_zwischenereignis_erlaubt():
"""Prüft, dass 'Link' bei einem Endereignis einen Fehler ausloest."""
text = """
S1 Start: X
-> S2
S2 Ende Link: Y
"""
with pytest.raises(ValueError, match='Link'):
m.parse_markdown(text)
def test_ereignisbasiertes_gateway_verbietet_zusammenfuehrung():
"""Prüft, dass ein ereignisbasiertes Gateway mit mehreren eingehenden Kanten abgelehnt wird."""
text = """
S1 Start: X
-> S2
-> S3
S2 Task: A
-> S4
S3 Task: B
-> S4
S4 Gateway Ereignisbasiert: C
-> S5
S5 Ende: Y
"""
with pytest.raises(ValueError, match='Zusammenführung'):
m.parse_markdown(text)
def test_datenobjekt_bekommt_keinen_sequenzfluss():
"""Prüft, dass ein Sequenzfluss zu einem Datenobjekt abgelehnt wird."""
text = """
S1 Start: X
-> D1
D1 Datenobjekt: Akte
S2 Ende: Y
"""
with pytest.raises(ValueError, match='keine Ablaufelemente'):
m.parse_markdown(text)
def test_datenobjekt_wird_nicht_automatisch_verkettet():
"""Prüft, dass die automatische Verkettung über unverbundene Artefakte hinwegspringt."""
text = """
S1 Start: X
D1 Datenobjekt: Akte
S2 Ende: Y
"""
meta, steps, order, externals = m.parse_markdown(text)
assert steps['S1'].edges == [(None, 'S2')]
assert steps['D1'].edges == []
def test_datenobjekt_und_gruppe_erzeugen_erwartete_elemente():
"""Prüft, dass Datenobjekt und Gruppe als eigenstaendige, unverbundene Artefakte gerendert werden."""
text = """
S1 Start: X
-> S2
S2 Ende: Y
D1 Datenobjekt: Akte
G1 Gruppe: Fachliche Pruefung
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
assert defs.find(f".//{{{m.NS['bpmn']}}}dataObjectReference") is not None
assert defs.find(f".//{{{m.NS['bpmn']}}}group") is not None
assert defs.find(f".//{{{m.NS['bpmn']}}}categoryValue") is not None
def test_externer_beteiligter_erzeugt_messageflow():
"""Prüft, dass ein deklarierter externer Beteiligter mit Nachrichtenfluss korrekt verknuepft wird."""
text = """
Extern: Bürger
S1 Start: X
-> S2
S2 SendTask: Bescheid senden
~> Bürger
-> S3
S3 Ende: Y
"""
meta, steps, order, externals = m.parse_markdown(text)
assert externals == ['Bürger']
defs = m.build_bpmn(meta, steps, order, externals)
msg_flow = defs.find(f".//{{{m.NS['bpmn']}}}messageFlow")
assert msg_flow is not None
participants = defs.findall(f".//{{{m.NS['bpmn']}}}participant")
assert any(p.get('name') == 'Bürger' and p.get('processRef') is None for p in participants)
def test_unbekannter_externer_beteiligter_wird_abgelehnt():
"""Prüft, dass ein Nachrichtenfluss zu einem nicht deklarierten externen Beteiligten scheitert."""
text = """
S1 Start: X
~> Unbekannt
S2 Ende: Y
"""
with pytest.raises(ValueError, match='unbekannter externer Beteiligter'):
m.parse_markdown(text)
def test_verteilte_anschlusspunkte_liegen_auf_der_rautenkontur():
"""Prüft, dass verteilte Anschlusspunkte an einem Gateway auf der Diamant-Kontur liegen, nicht auf der Bounding-Box."""
x, y, w, h = 590, 2290, 50, 50
cx, cy = x + w / 2, y + h / 2
for idx in range(4):
px, py = m.spread_point(x, y, w, h, 'S', idx, 4, 'diamond')
# Manhattan-Abstand zum Mittelpunkt, skaliert auf die Diamant-Halbachsen, muss 1 ergeben.
assert abs(abs(px - cx) / (w / 2) + abs(py - cy) / (h / 2) - 1) < 1e-9
def test_verteilte_anschlusspunkte_liegen_auf_dem_ereigniskreis():
"""Prüft, dass verteilte Anschlusspunkte an einem Ereignis auf dem Kreisradius liegen, nicht auf der Bounding-Box."""
x, y, w, h = 100, 100, 36, 36
cx, cy = x + w / 2, y + h / 2
r = w / 2
for idx in range(3):
px, py = m.spread_point(x, y, w, h, 'S', idx, 3, 'circle')
dist = ((px - cx) ** 2 + (py - cy) ** 2) ** 0.5
assert abs(dist - r) < 1e-9
def test_gateway_kanten_bekommen_eigene_kardinalpunkte():
"""Prüft, dass bis zu vier Kanten an einem Gateway je einen eigenen Eckpunkt (N/O/S/W) bekommen, wie PICTURE es rendert."""
text = """
S1 Start: X
-> S2
S2 Gateway XOR: Auswahl
A -> S3
B -> S4
C -> S5
S3 Ende: A
S4 Ende: B
S5 Ende: C
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
ns = m.NS
gw = next(el for el in defs.iter(f"{{{ns['bpmn']}}}exclusiveGateway") if el.get('name') == 'Auswahl')
shape = defs.find(f".//{{{ns['bpmndi']}}}BPMNShape[@bpmnElement='{gw.get('id')}']")
x, y, w, h = (float(shape[0].get(a)) for a in ('x', 'y', 'width', 'height'))
vertices = {(x + w / 2, y), (x + w / 2, y + h), (x, y + h / 2), (x + w, y + h / 2)}
out_ids = {o.text for o in gw.findall(f"{{{ns['bpmn']}}}outgoing")}
in_ids = {i.text for i in gw.findall(f"{{{ns['bpmn']}}}incoming")}
used = set()
for fid in out_ids | in_ids:
edge = defs.find(f".//{{{ns['bpmndi']}}}BPMNEdge[@bpmnElement='{fid}']")
wps = edge.findall(f"{{{ns['di']}}}waypoint")
wp = wps[0] if fid in out_ids else wps[-1]
point = (float(wp.get('x')), float(wp.get('y')))
assert point in vertices
used.add(point)
assert len(used) == 4
def test_gateway_mit_mehr_als_vier_kanten_bleibt_gueltiges_xml():
"""Prüft, dass ein Gateway mit mehr als vier anliegenden Kanten (über PICTUREs Grenze hinaus) trotzdem gültiges XML ohne Absturz erzeugt."""
text = """
S1 Start: X
-> S2
S2 Gateway XOR: Fuenf-Wege
A -> S3
B -> S4
C -> S5
D -> S6
E -> S7
S3 Ende: A
S4 Ende: B
S5 Ende: C
S6 Ende: D
S7 Ende: E
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
ET.tostring(defs) # loest bei kaputter Struktur eine Exception aus
def test_mehrere_kanten_an_derselben_seite_bekommen_eigenen_anschlusspunkt():
"""Prüft, dass mehrere ausgehende Kanten an einem Gateway nicht im selben Punkt starten."""
text = """
S1 Start: X
-> S2
S2 Gateway XOR: Auswahl
A -> S3
B -> S4
C -> S5
S3 Ende: A
S4 Ende: B
S5 Ende: C
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
starts = []
for edge in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNEdge"):
wp = edge.find(f"{{{m.NS['di']}}}waypoint")
starts.append((wp.get('x'), wp.get('y')))
# 3 der 4 Kanten gehen vom selben Gateway aus - deren Startpunkte muessen sich unterscheiden.
assert len(set(starts)) == len(starts)
def _liegt_in_rautenflaeche(px, py, x, y, w, h, sicherheitsabstand=0.5):
"""Manhattan-Abstand zum Mittelpunkt, skaliert auf die Diamant-Halbachsen: < 1 heisst innerhalb."""
cx, cy = x + w / 2, y + h / 2
wert = abs(px - cx) / (w / 2) + abs(py - cy) / (h / 2)
return wert < 1 - (sicherheitsabstand / max(w, h))
def _kante_kreuzt_gateway(defs, ns):
"""Liefert (Kanten-Id, Gateway-Id) für die erste Kante, deren Streckenzug durchs Innere
eines Gateways (statt nur an dessen Kontur entlang) laeuft - oder None, wenn keine kreuzt."""
shapes = {}
for shape in defs.iter(f"{{{ns['bpmndi']}}}BPMNShape"):
b = shape.find(f"{{{ns['dc']}}}Bounds")
if b is not None:
shapes[shape.get('bpmnElement')] = tuple(float(b.get(a)) for a in ('x', 'y', 'width', 'height'))
gateway_ids = set()
for tag in ('exclusiveGateway', 'inclusiveGateway', 'parallelGateway', 'eventBasedGateway'):
gateway_ids.update(el.get('id') for el in defs.iter(f"{{{ns['bpmn']}}}{tag}"))
for edge in defs.iter(f"{{{ns['bpmndi']}}}BPMNEdge"):
pts = [(float(wp.get('x')), float(wp.get('y'))) for wp in edge.findall(f"{{{ns['di']}}}waypoint")]
for gid in gateway_ids:
if gid not in shapes:
continue
x, y, w, h = shapes[gid]
for (x1, y1), (x2, y2) in zip(pts, pts[1:]):
for s in range(41):
t = s / 40
if _liegt_in_rautenflaeche(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t, x, y, w, h):
return edge.get('bpmnElement'), gid
return None
def test_kante_fuehrt_nicht_durchs_innere_eines_gateways():
"""Prüft, dass eine Kante von/zu einem Gateway, dessen Ziel/Quelle nicht auf der Achse des
zugewiesenen Anschlusspunkts liegt, aussen herum und nicht durchs Innere der Raute fuehrt."""
text = """
S1 Start: X
-> S5
S5 Gateway XOR: Sofort?
Ja -> S6
Nein -> S6m
S6 Task: Leitungsplaene einholen
-> S6m
S6m Gateway XOR: Zusammenfuehrung
-> S7
S7 Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
assert _kante_kreuzt_gateway(defs, m.NS) is None
def test_ausgabe_ist_wohlgeformtes_xml(tmp_path):
"""Prüft, dass die erzeugte .bpmn-Datei sich als valides XML parsen laesst."""
src = tmp_path / 'prozess.md'
src.write_text("""
Extern: Bürger
S1 Start Nachricht: Antrag geht ein
-> S2
S2 Gateway XOR: Vollständig?
Ja -> S3
Nein -> S2
S3 SendTask: Bescheid senden
~> Bürger
-> S4
S4 Ende Terminierung: Fertig
""", encoding='utf-8')
out = tmp_path / 'prozess.bpmn'
meta, steps, order, externals = m.parse_markdown(src.read_text(encoding='utf-8'))
defs = m.build_bpmn(meta, steps, order, externals)
out.write_bytes(m.prettify(defs))
parsed = ET.parse(out)
assert parsed.getroot().tag == f"{{{m.NS['bpmn']}}}definitions"