Eingangsordner input/ und Skill sn-einlesen (#5)

- input/ ist lokal und nicht versioniert (.gitignore); scripts/einlesen.py
  legt ihn bei Bedarf an.
- scripts/einlesen.py ordnet jede Datei ein: abgelehnt (kein .md, nicht
  UTF-8, leer, Unterordner), neu, geändert, eingelesen. Für neue und
  geänderte Dateien: Prozessname, Ordnernamen-Vorschlag, ob schon eine
  Arbeitsschritt-.md, und ähnliche Prozesse aus prozesse/ und entwuerfe/.
  „vermerk“ schreibt nach Erfolg eine erste Zeile
  <!-- eingelesen: <Zeitstempel> | neu|aktualisiert | <Ziel> | inhalt: <sha> -->;
  die Prüfsumme erkennt spätere Änderungen. Zeilenenden bleiben erhalten.
- Skill sn-einlesen: Übersicht, Bestandsvergleich, Entscheidung des
  Nutzers, neue Prozesse durch sn-bpmn, Vermerk nur bei Erfolg.
- docs/leitfaden/07_prozesse_einlesen.md: Ablauf für neue Nutzer.
- AGENTS.md, README, Leitfaden 03, Skill sn-bpmn, Fallstricke, STATUS.
- Tests: python -m pytest -q -> 79 bestanden (18 neu in
  tests/test_einlesen.py).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 14:09:48 +02:00
co-authored by Claude Opus 5.5
parent 47b59e2d83
commit 27589749ff
13 changed files with 769 additions and 7 deletions
+3 -1
View File
@@ -19,7 +19,9 @@ schreibst oder bearbeitest.
## Ablauf
1. **Quelle ermitteln.** Frage nach der Rohtext-, Word- oder PDF-Quelle,
falls keine angegeben oder angehängt wurde. Gibt es den Prozess schon
falls keine angegeben oder angehängt wurde. Liegen Beschreibungen im
Eingangsordner `input/`, übernimmt der Skill `sn-einlesen` die Steuerung
und ruft diesen Skill je Datei auf. Gibt es den Prozess schon
unter `prozesse/<name>/`, ist dessen `<name>.md` der Ausgangspunkt:
dann auf einem Arbeitsbranch direkt dort ändern statt in `entwuerfe/`,
und nach Schritt 3 mit Schritt 5 weitermachen.
+97
View File
@@ -0,0 +1,97 @@
---
name: sn-einlesen
description: Liest den Eingangsordner input/ ein - prüft die Dateien (vorerst nur .md), sucht zu jeder neuen Prozessbeschreibung bereits vorhandene Prozesse, lässt den Nutzer über das weitere Vorgehen entscheiden, führt neue Prozesse durch den Skill sn-bpmn und vermerkt erfolgreiches Einlesen mit Zeitstempel in der Datei. Verwenden bei "lies input ein", "einlesen", "neue Prozesse im Eingang", "was liegt in input".
---
# Eingangsordner einlesen
`input/` im Workspace-Root ist der Eingang für Prozessbeschreibungen. Er ist **nicht versioniert** (`.gitignore`)
und kann interne Inhalte enthalten. Ablauf für Menschen: `docs/leitfaden/07_prozesse_einlesen.md`.
**Grundregeln**
- Dateien in `input/` werden **nie gelöscht, verschoben oder umbenannt**. Der einzige Eingriff ist der Vermerk
nach erfolgreichem Einlesen – über das Skript, nie von Hand.
- **Eine Datei nach der anderen**, jede bis zu einer Entscheidung des Nutzers. Nicht mehrere Prozesse parallel
entwerfen.
- Inhalte aus `input/` landen nur bereinigt (ohne Namen, Aktenzeichen, Verfahrensdaten) in `entwuerfe/` oder
`prozesse/`.
## 1. Eingang prüfen
```bash
python scripts/einlesen.py --json
```
Das Skript legt `input/` an, falls es fehlt, und ordnet jede Datei ein:
| Status | Bedeutung | Was tun |
|---|---|---|
| `abgelehnt` | kein `.md` (vorerst nur Markdown), keine UTF-8-Datei, leer oder Unterordner | Nur melden, mit Grund. Nicht öffnen, nicht umwandeln. Hinweis, wie es geht (`07_prozesse_einlesen.md` Abschnitt 2). |
| `eingelesen` | Vermerk vorhanden, Inhalt seitdem unverändert | Überspringen, nur in der Zusammenfassung nennen. |
| `neu` | `.md` ohne Vermerk | Abschnitt 2. |
| `geändert` | Vermerk vorhanden, Inhalt danach geändert | Abschnitt 2; dem Nutzer sagen, wann und wohin sie zuletzt eingelesen wurde (`vermerk`). |
Dem Nutzer zuerst eine kurze Übersicht geben (je Status die Dateien), dann mit der ersten neuen oder
geänderten Datei beginnen. Ist nichts zu tun: ein Satz, fertig.
## 2. Je Datei: Gibt es den Prozess schon?
1. Datei lesen. In zwei Sätzen zusammenfassen, welchen Prozess sie beschreibt (Anlass, Ergebnis, Beteiligte
als Funktionen).
2. Bestand prüfen. Das Skript liefert `kandidaten` (Name und Wortüberschneidung, Schwelle 0,25) und die Liste
`bestand` aller Prozesse unter `prozesse/` und lokalen Entwürfe in `entwuerfe/`. **Die Zahl ist nur ein
Hinweis:** die `.md` der Kandidaten lesen und fachlich beurteilen, ob derselbe Ablauf gemeint ist. Auch die
Titel in `bestand` durchgehen – ein anders benannter Prozess kann derselbe sein.
3. Ergebnis mitteilen: „kein vorhandener Prozess“ oder „ähnlich zu …, weil …“ mit den Unterschieden.
## 3. Nutzer entscheiden lassen
**Gibt es einen passenden vorhandenen Prozess**, den Nutzer fragen (in Claude Code mit der Rückfrage-Funktion,
eine Frage je Datei) – und nichts tun, bevor er geantwortet hat:
| Option | Folge |
|---|---|
| **Vorhandenen Prozess aktualisieren** | Abschnitt 4b |
| **Als neuen, eigenen Prozess anlegen** (Namen vorschlagen, `vorschlag_name` als Ausgangspunkt, nicht gleich dem vorhandenen) | Abschnitt 4a |
| **Datei überspringen** | Kein Vermerk; die Datei erscheint beim nächsten Mal wieder als `neu`/`geändert`. |
| **Einlesen beenden** | Zusammenfassung (Abschnitt 6), restliche Dateien unberührt. |
**Gibt es keinen passenden Prozess**, ist die Datei ein neuer Prozess: Namensvorschlag nennen und mit 4a
beginnen. Der Nutzer kann den Namen dabei ändern.
## 4a. Neuer Prozess – wie im Skill `sn-bpmn`
Den Skill `sn-bpmn` ab Schritt 2 durchlaufen, mit der Datei aus `input/` als Quelle:
- Ist die Datei schon eine gültige Arbeitsschritt-`.md` (`arbeitsschritt_md: true`), sie nach
`entwuerfe/<name>.md` kopieren (ohne Vermerkzeilen), gegen `Modellierregeln.md` prüfen und Abweichungen nennen.
Sonst den Entwurf aus dem Rohtext ableiten, wie in `sn-bpmn` Schritt 2.
- **Auf Freigabe warten** (`sn-bpmn` Schritt 3). Vorschau anbieten: `python scripts/vorschau.py entwuerfe/<name>.md`.
- Nach Freigabe umwandeln (`sn-bpmn` Schritt 4). Das ist der **Erfolg** für einen neuen Prozess.
- Ablegen unter `prozesse/` nur auf Wunsch (`sn-bpmn` Schritt 5; braucht ein Issue und einen Pull Request).
## 4b. Vorhandenen Prozess aktualisieren
- **Liegt er unter `prozesse/`:** Änderungen an versionierten Modellen gehen über Issue, Branch und Pull Request
(`AGENTS.md`). Gibt es kein Issue, einen Entwurf vorbereiten (Skill `sn-ticket`) und warten. Dann auf dem
Branch `prozesse/<name>/<name>.md` um die neuen Inhalte ergänzen, Unterschiede dem Nutzer zeigen, auf
Freigabe warten, `python scripts/pruefe_prozesse.py --erzeugen`, `python -m pytest -q`, im Steckbrief unter
„Änderungen“ eintragen. **Erfolg** = Änderung committet.
- **Ist er nur ein lokaler Entwurf in `entwuerfe/`:** den Entwurf ergänzen, Unterschiede zeigen, auf Freigabe
warten, neu umwandeln. **Erfolg** = neue `.bpmn` erzeugt.
## 5. Vermerk – nur bei Erfolg
```bash
python scripts/einlesen.py vermerk input/<datei>.md --ergebnis neu --ziel entwuerfe/<name>.md
python scripts/einlesen.py vermerk input/<datei>.md --ergebnis aktualisiert --ziel prozesse/<name>/
```
Das Skript schreibt als erste Zeile einen HTML-Kommentar mit Zeitstempel, Ergebnis, Ziel und Inhalts-Prüfsumme;
ältere Vermerke bleiben darunter stehen. Kein Vermerk bei Überspringen, Abbruch, fehlender Freigabe oder
fehlgeschlagener Umwandlung.
## 6. Zusammenfassung
Am Ende eine Tabelle: Datei | Status vorher | Entscheidung | Ergebnis (Pfad) | Vermerk ja/nein. Dazu abgelehnte
Dateien mit Grund und, falls etwas unter `prozesse/` geändert wurde, den vorbereiteten Pull-Request-Text
(vollständig nach `docs/vorlagen/pull_request.md`). Push und Pull Request macht der Mensch.
+1
View File
@@ -40,6 +40,7 @@
"Bash(python scripts/pruefe_umlaute.py:*)",
"Bash(python scripts/pruefe_prozesse.py:*)",
"Bash(python scripts/vorschau.py:*)",
"Bash(python scripts/einlesen.py:*)",
"Bash(python -m pytest:*)"
]
}
+4
View File
@@ -9,6 +9,10 @@
# Arbeitsschritt-Entwürfe des Skills sn-bpmn. Sie werden nicht versioniert.
entwuerfe/
# Eingangsordner für den Skill sn-einlesen: hier abgelegte Prozessbeschreibungen (.md) sind Rohmaterial
# und können interne Inhalte enthalten. Wird von scripts/einlesen.py bei Bedarf angelegt.
input/
# Lokales Referenzmaterial (Quelldokumente, Word/PDF-Prozessbeschreibungen).
# Enthält ggf. interne Unterlagen und wird deshalb nie versioniert.
referenz/
+6 -2
View File
@@ -44,7 +44,9 @@ Sie bleibt bewusst kurz: Details liegen in `docs/` und werden nur bei Bedarf gel
- **Keine personenbezogenen Daten, keine Zugangsdaten, keine internen Originalunterlagen** im Repository, in der Doku
oder in Prompts. Quelldokumente (Word/PDF) bleiben lokal unter `referenz/` (nicht versioniert); ins Repository kommen
nur die daraus abgeleiteten, bereinigten Arbeitsschritt-`.md` und BPMN-Dateien.
nur die daraus abgeleiteten, bereinigten Arbeitsschritt-`.md` und BPMN-Dateien. Dasselbe gilt für den
Eingangsordner `input/` (Skill `sn-einlesen`): Dateien dort nie löschen, verschieben oder umbenennen; einziger
Eingriff ist der Einlese-Vermerk über `scripts/einlesen.py`.
- **Prozessmodelle liegen in `prozesse/<name>/`** (ADR 0003, Regeln in `prozesse/README.md`). Die `.md` ist
maßgeblich, die `.bpmn` wird nie von Hand bearbeitet, sondern mit `python scripts/pruefe_prozesse.py --erzeugen`
neu erzeugt. Den Status `abgenommen` setzt der Agent nie von sich aus.
@@ -93,10 +95,12 @@ Sie bleibt bewusst kurz: Details liegen in `docs/` und werden nur bei Bedarf gel
- Umlaute prüfen: `python scripts/pruefe_umlaute.py`
- Prozessmodelle prüfen bzw. alle `.bpmn` neu erzeugen: `python scripts/pruefe_prozesse.py [--erzeugen]`
- Diagramm im Browser ansehen (offline): `python scripts/vorschau.py <datei.bpmn|datei.md>`
- Eingangsordner prüfen bzw. Einlese-Vermerk setzen: `python scripts/einlesen.py [--json]` |
`python scripts/einlesen.py vermerk input/<datei>.md --ergebnis neu|aktualisiert --ziel <pfad>`
- Workspace einrichten oder aktualisieren: `python scripts/setup_workspace.py` (mit `--pull` für fast-forward)
- Entwurf umwandeln: `python .agents/skills/sn-bpmn/md_to_bpmn.py entwuerfe/<name>.md entwuerfe/<name>.bpmn`
## Skills
Liegen in `.agents/skills/` (Agent-Skills-Standard) und werden nach `.claude/skills/` gespiegelt:
`sn-bpmn`, `sn-aufgabe-bearbeiten`, `sn-ticket`, `sn-uebergabe`.
`sn-bpmn`, `sn-einlesen`, `sn-aufgabe-bearbeiten`, `sn-ticket`, `sn-uebergabe`.
+9 -2
View File
@@ -53,6 +53,12 @@ gestartet.
## Einen Prozess modellieren
**Am einfachsten über den Eingangsordner:** Beschreibung als `.md` nach `input/` legen und *„Lies den Ordner input
ein“* sagen. Der Agent prüft, ob es den Prozess schon gibt, fragt dich, wie es weitergeht, und vermerkt das Einlesen
in der Datei. Schritt für Schritt für Einsteiger: [`docs/leitfaden/07_prozesse_einlesen.md`](docs/leitfaden/07_prozesse_einlesen.md).
Oder direkt:
Im Ordner `AG_Prozesse` Claude Code starten und zum Beispiel sagen:
> *„Modelliere den Prozess aus `referenz/antragspruefung.docx` als BPMN."*
@@ -62,7 +68,7 @@ Du prüfst und änderst den Entwurf fachlich — als Diagramm ansehen geht jeder
`python scripts/vorschau.py entwuerfe/<name>.md` (offline im Browser) —, dann: *„passt so, wandle um"*. Heraus kommt `<name>.bpmn` für den
Import in PICTURE. Syntax des Entwurfs: [`.agents/skills/sn-bpmn/Anleitung.md`](.agents/skills/sn-bpmn/Anleitung.md).
`referenz/` und `entwuerfe/` werden nicht versioniert — Quelldokumente bleiben auf deinem Rechner.
`input/`, `referenz/` und `entwuerfe/` werden nicht versioniert — Quelldokumente bleiben auf deinem Rechner.
Soll das Modell für alle verfügbar sein, legt der Agent es auf Wunsch unter [`prozesse/`](prozesse/README.md) ab —
mit Steckbrief und Status `Entwurf`, per Pull Request. Dort liegen auch die fertigen `.bpmn` zum Herunterladen.
@@ -98,7 +104,8 @@ Die vollständige, für Agenten verbindliche Fassung steht in [`AGENTS.md`](AGEN
| [`AGENTS.md`](AGENTS.md) / `CLAUDE.md` | Anweisungen für Agenten (kanonisch ist `AGENTS.md`) |
| [`workspace.toml`](workspace.toml) | Gitea-Organisation und Verzeichnis weiterer Repositories |
| [`prozesse/`](prozesse/README.md) | Versionierte Prozessmodelle, ein Ordner je Prozess (ADR 0003) |
| `scripts/` | `hallo.py`, `setup_workspace.py`, `gitea.py`, `pruefe_umlaute.py`, `pruefe_prozesse.py`, `vorschau.py` |
| `scripts/` | `hallo.py`, `setup_workspace.py`, `gitea.py`, `pruefe_umlaute.py`, `pruefe_prozesse.py`, `vorschau.py`, `einlesen.py` |
| `input/` | Eingangsordner für Prozessbeschreibungen (`.md`), lokal, nicht versioniert – Skill `sn-einlesen` |
| [`werkzeuge/vorschau/`](werkzeuge/vorschau/README.md) | BPMN-Vorschau im Browser (bpmn-js, offline); `vorschau.html` auch ohne Python |
| `.agents/skills/` | Skills, gespiegelt nach `.claude/skills/`; `sn-bpmn` enthält den Konverter |
| `.claude/commands/` | Slash-Befehle für Claude Code (`/hallo`) |
+1
View File
@@ -9,6 +9,7 @@
| `leitfaden/03_arbeitsweise.md` | stabil | Einstieg, Arbeitszyklus, Checklisten |
| `leitfaden/04_werkzeuge_einrichten.md` | stabil | Einrichtung: Claude Code als CLI und in VS Code; von null zur ersten Aufgabe |
| `leitfaden/05_git_arbeitsweise.md` | **verbindlich** | Branches, Pull Requests, was der Agent darf |
| `leitfaden/07_prozesse_einlesen.md` | stabil | Einsteiger-Ablauf: Beschreibung in `input/` legen, einlesen, freigeben |
| `vorlagen/` | Vorlagen | Issue, PR, Übergabe, Repo-`AGENTS.md`, Steckbrief eines Prozesses |
Die Fachdoku des Konverters (Markdown-Syntax, Elementkatalog, Modellierregeln) liegt beim Skill in
+3 -2
View File
@@ -12,8 +12,8 @@ Einrichtung Schritt für Schritt: `04_werkzeuge_einrichten.md`.
| **Einen Prozess modellieren** | Fachleute | Skill `sn-bpmn` | BPMN-Datei für PICTURE |
| **Werkzeug und Wissen verbessern** | alle, die einen PR stellen | Skill `sn-aufgabe-bearbeiten` | Pull Request auf `main` |
Wer nur modelliert, braucht Git kaum: Claude Code im Ordner `AG_Prozesse` starten, Quelle nennen, Entwurf prüfen,
umwandeln. Git und Pull Requests kommen erst ins Spiel, wenn sich am Konverter, an den Regeln oder an der Doku
Wer nur modelliert, braucht Git kaum: Beschreibung als `.md` nach `input/` legen, Claude Code im Ordner
`AG_Prozesse` starten, „Lies den Ordner input ein“, Entwurf prüfen, umwandeln (`07_prozesse_einlesen.md`). Git und Pull Requests kommen erst ins Spiel, wenn sich am Konverter, an den Regeln oder an der Doku
etwas ändern soll — oder wenn ein Modell unter `prozesse/` abgelegt oder geändert wird (Abschnitt 2).
## 2. Einen Prozess modellieren
@@ -99,6 +99,7 @@ Werkzeugs (Claude Auto-Memory) ist ein Notizzettel; teilbares Wissen gehört in
| `.claude/settings.json` | Berechtigungen – sperrt `git push`, `merge`, `tag` |
| `docs/` | auf Abruf, nicht automatisch |
| `prozesse/` | auf Abruf; versionierte Modelle mit Steckbrief |
| `input/` | beim Einlesen (Skill `sn-einlesen`); nicht versioniert |
| `referenz/`, `entwuerfe/` | nur, wenn du darauf verweist; nicht versioniert |
| `CLAUDE.local.md` | ja, lokal, nicht versioniert |
+149
View File
@@ -0,0 +1,149 @@
# Prozesse einlesen: vom Eingangsordner zum BPMN-Diagramm
Für Kolleginnen und Kollegen, die eine Prozessbeschreibung in ein BPMN-Modell für PICTURE überführen wollen.
Git- oder Programmierkenntnisse braucht es dafür nicht. Stand 23.09.2026.
**Kurz:** Beschreibung als `.md`-Datei in den Ordner `input/` legen, Claude Code starten, „Lies den Ordner input
ein“ sagen, die Rückfragen beantworten, den Entwurf prüfen und freigeben. Fertig ist eine `.bpmn`-Datei für PICTURE.
```mermaid
flowchart TD
A["Beschreibung als .md<br/>nach input/ legen"] --> B["Claude Code: „Lies input ein“"]
B --> C{"Dateityp .md<br/>und UTF-8?"}
C -->|nein| X["abgelehnt – Datei bleibt liegen,<br/>Grund wird genannt"]
C -->|ja| D{"schon eingelesen<br/>und unverändert?"}
D -->|ja| Y["übersprungen"]
D -->|nein| E{"gibt es den Prozess<br/>schon?"}
E -->|nein| F["neuer Prozess:<br/>Entwurf in entwuerfe/"]
E -->|ja| G{"deine Entscheidung"}
G -->|aktualisieren| H["vorhandenen Prozess<br/>ergänzen"]
G -->|eigener Prozess| F
G -->|überspringen| Z["kein Vermerk,<br/>kommt beim nächsten Mal wieder"]
F --> I["Entwurf prüfen,<br/>Vorschau ansehen"]
H --> I
I -->|"„passt so“"| J["BPMN erzeugen"]
J --> K["Vermerk mit Datum/Uhrzeit<br/>oben in der .md"]
```
## 1. Einmalig: Einrichtung
Wer schon mit Claude Code im Ordner `AG_Prozesse` arbeitet, kann diesen Abschnitt überspringen. Sonst:
[`04_werkzeuge_einrichten.md`](04_werkzeuge_einrichten.md) Abschnitte 2 und 3 (Git, Python, Projektwissen holen,
Claude Code). Danach gibt es den Ordner `AG_Prozesse`; `input/` darin entsteht beim ersten Einlesen von selbst.
`input/` wird **nicht** mit Git geteilt. Was dort liegt, sieht nur, wer an deinem Rechner arbeitet.
## 2. Die Datei vorbereiten
**Vorerst werden nur `.md`-Dateien angenommen** (Markdown, also reiner Text). Word-, PDF- oder `.txt`-Dateien
werden abgelehnt und bleiben unverändert liegen.
So wird aus einer Word- oder PDF-Beschreibung eine `.md`-Datei:
1. In VS Code (oder dem Windows-Editor) eine neue Datei anlegen.
2. Den Text der Beschreibung hineinkopieren. Überschriften dürfen mit `# ` beginnen, Aufzählungen mit `- `.
Die erste Überschrift oder eine Zeile `Prozess: <Name>` wird als Prozessname verwendet.
3. Speichern unter `AG_Prozesse\input\<kurzer-name>.md`, **Codierung UTF-8** (VS Code speichert so; im Windows-Editor
unter „Speichern unter“ → Codierung „UTF-8“ wählen). Sonst werden Umlaute nicht gelesen und die Datei abgelehnt.
Vorher prüfen und entfernen: **Namen von Personen, Aktenzeichen, Verfahrensdaten.** Zuständigkeiten als Funktion
schreiben („Sachbearbeitung“, „Teamleitung“). Der Inhalt wird zur Verarbeitung an den KI-Anbieter übertragen.
Wer die Syntax schon kennt, kann direkt eine Arbeitsschritt-`.md` schreiben
(`.agents/skills/sn-bpmn/Anleitung.md`). Sie wird dann nur geprüft und nicht neu formuliert.
## 3. Einlesen starten
Im Ordner `AG_Prozesse` Claude Code starten (Terminal: `claude`, oder VS Code mit der Erweiterung) und schreiben:
> *Lies den Ordner input ein.*
(oder den Befehl `/sn-einlesen` wählen). Der Agent zeigt zuerst eine Übersicht, zum Beispiel:
```
[neu] input/antragspruefung.md: „Antragsprüfung im Sachgebiet“ (Rohtext)
ähnlich (0.44): Entwurf (lokal) entwuerfe/antrag-pruefen.md – „Antrag prüfen“
[abgelehnt] input/vorlage.docx: Dateityp .docx – vorerst nur .md
[eingelesen] input/wegebau.md: am 2026-09-20 10:12:03 +0200 (neu -> entwuerfe/wegebau-abnehmen.md)
```
Dieselbe Übersicht bekommst du ohne Agent mit `python scripts/einlesen.py`.
## 4. Entscheiden: Gibt es den Prozess schon?
Für jede neue Datei sucht der Agent im Bestand – versionierte Prozesse unter `prozesse/` und deine lokalen Entwürfe
unter `entwuerfe/` – und sagt, ob und warum ein vorhandener Prozess passt. Gibt es einen, fragt er dich:
| Wenn du wählst … | … passiert |
|---|---|
| **Vorhandenen Prozess aktualisieren** | Der vorhandene Entwurf bzw. Prozess wird um die neuen Inhalte ergänzt. Bei Prozessen unter `prozesse/` geht das über ein Issue und einen Pull Request (Abschnitt 7). |
| **Als neuen Prozess anlegen** | Es entsteht ein eigener Entwurf mit anderem Namen. |
| **Datei überspringen** | Nichts passiert. Die Datei erscheint beim nächsten Einlesen wieder. |
| **Einlesen beenden** | Der Agent hört auf; die übrigen Dateien bleiben unberührt. |
Gibt es keinen passenden Prozess, legt der Agent einen neuen Entwurf an und schlägt einen Namen vor. Du kannst
ihn ändern.
## 5. Entwurf prüfen und freigeben
Der Agent schreibt den Entwurf nach `entwuerfe/<name>.md` und nennt die Stellen, an denen er etwas **annehmen**
musste (Schrittgrenzen, Verzweigungen, Zuständigkeiten). Genau dort lohnt der fachliche Blick.
- Als Diagramm ansehen: `python scripts/vorschau.py entwuerfe/<name>.md` – öffnet den Browser, offline.
- Änderungen direkt in `entwuerfe/<name>.md` machen oder dem Agenten sagen, was anders sein soll.
- Wenn es passt: *„passt so, wandle um“*. **Vorher wandelt der Agent nicht um.**
Danach liegt `entwuerfe/<name>.bpmn` bereit für den Import in PICTURE.
## 6. Der Vermerk
Nach erfolgreichem Einlesen steht in der ersten Zeile deiner Datei in `input/` ein Vermerk, zum Beispiel:
```
<!-- eingelesen: 2026-09-23 14:05:12 +0200 | neu | entwuerfe/antrag-pruefen.md | inhalt: 3f2a9c1e0b7d -->
```
Datum und Uhrzeit, Ergebnis (`neu` oder `aktualisiert`) und wo das Ergebnis liegt. In der Markdown-Vorschau ist die
Zeile unsichtbar, im Editor sichtbar. Bitte nicht von Hand ändern.
- **Unveränderte Dateien mit Vermerk** werden beim nächsten Einlesen übersprungen.
- **Änderst du die Datei danach**, erkennt das der Agent (Prüfsumme) und bietet sie als „geändert“ erneut an.
Der neue Vermerk kommt obenauf, der alte bleibt als Verlauf stehen.
- **Kein Vermerk** bei Überspringen, Abbruch oder wenn der Entwurf nicht freigegeben wurde.
## 7. Für alle sichtbar ablegen (optional)
Entwürfe liegen nur auf deinem Rechner. Soll das Modell für die Gruppe verfügbar sein, legt der Agent es auf
Wunsch unter `prozesse/<name>/` ab – mit Steckbrief und Status `Entwurf` (Regeln: `prozesse/README.md`). Das ist
eine Änderung am gemeinsamen Repository:
1. Der Agent bereitet einen Issue-Text vor; **du** legst das Issue in Gitea an und nennst ihm die Nummer.
2. Der Agent legt den Ordner auf einem eigenen Branch an und bereitet Commit und Pull-Request-Text vor.
3. **Du** pushst und stellst den Pull Request; eine Kollegin oder ein Kollege prüft.
## 8. Häufige Fragen
**Meine Word-Datei wird abgelehnt.** Vorerst nur `.md`, siehe Abschnitt 2.
**„keine UTF-8-Datei“.** Die Datei in VS Code öffnen, unten rechts auf die Codierung klicken → „Mit Codierung
speichern“ → UTF-8.
**Die Datei wird immer wieder als neu angezeigt.** Sie wurde übersprungen oder nicht freigegeben – dann gibt es
keinen Vermerk. Einlesen erneut starten und abschließen, oder die Datei aus `input/` entfernen, wenn sie nicht
mehr gebraucht wird.
**Der Agent hält zwei Prozesse für gleich, die es nicht sind.** Die Ähnlichkeit ist nur ein Hinweis. „Als neuen
Prozess anlegen“ wählen und einen unterscheidbaren Namen vergeben.
**Ich möchte eine Datei noch einmal komplett neu einlesen.** Die Vermerkzeilen oben löschen; dann gilt sie wieder
als neu.
**Kann ich mehrere Dateien auf einmal einlesen?** Ja, sie liegen alle in `input/`. Der Agent bearbeitet sie
nacheinander und fragt bei jeder einzeln.
## Siehe auch
- Skill `sn-einlesen` (`.agents/skills/sn-einlesen/SKILL.md`) – was der Agent genau tut
- Skill `sn-bpmn` und `.agents/skills/sn-bpmn/Anleitung.md` – Entwurf und Syntax
- [`03_arbeitsweise.md`](03_arbeitsweise.md) – Arbeitsweise insgesamt
- `prozesse/README.md` – versionierte Ablage
+4
View File
@@ -30,6 +30,10 @@ Skills sind übernommen, alles QGIS- und Plugin-Spezifische ist entfernt. Kern i
## Erledigt
- 23.09.2026 **Eingangsordner `input/` und Skill `sn-einlesen`** (#5): `.md`-Dateien werden geprüft (andere
Typen vorerst abgelehnt), mit dem Bestand verglichen, nach Entscheidung des Nutzers durch `sn-bpmn` geführt und
bei Erfolg mit Zeitstempel vermerkt. Einsteiger-Ablauf: `docs/leitfaden/07_prozesse_einlesen.md`.
- 23.09.2026 **Regeln für PR- und Issue-Texte an AG_QGIS angeglichen:** Vertragsabschnitt in der PR-Vorlage
(„Änderungen an Syntax und Ablageformat“), Vertragsfrage in der Issue-Vorlage, Testpflicht wieder für jede neue
Funktionalität, vollständige Checklisten, kein Arbeitsbranch ohne Issue. `gitea.py` meldet ein nicht öffentliches
+6
View File
@@ -17,6 +17,12 @@ Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik
Umwandlung ihrer `.md`. Nach einer Konverteränderung `python scripts/pruefe_prozesse.py --erzeugen` ausführen und
die neu erzeugten Dateien mit in den Pull Request nehmen. Das setzt voraus, dass der Konverter deterministisch
bleibt: keine Zufalls-IDs, keine Zeitstempel, keine Reihenfolge aus `set`-Iteration. (ADR 0003, 23.09.2026)
- **Zusätzliche Zeilen in einer Arbeitsschritt-`.md` nur ganz oben oder mit `#`.** `parse_markdown()` bricht
nach dem ersten Schritt bei jeder unbekannten Zeile ab; davor überspringt es Zeilen, die nicht wie
`Schlüssel: Wert` aussehen. Deshalb steht der Einlese-Vermerk (`<!-- eingelesen: … -->`) als erste Zeile.
(23.09.2026, `scripts/einlesen.py`)
- **`Path.read_text()` macht aus CRLF stillschweigend LF.** Wer eine Datei liest und mit Zusatz zurückschreibt,
stellt so Windows-Dateien unbemerkt um. `scripts/einlesen.py` liest deshalb als Bytes. (23.09.2026, Test)
- **Metadaten gehören nicht als Kopfzeile in die Arbeitsschritt-`.md`.** `parse_markdown()` liest eine Zeile wie
`Verantwortliche Stelle: …` als Schritt mit ID `Verantwortliche` und Typ `Stelle` und bricht ab. Status und Stand
stehen deshalb im Steckbrief `README.md` des Prozessordners. (23.09.2026)
+281
View File
@@ -0,0 +1,281 @@
#!/usr/bin/env python3
"""Prüft den Eingangsordner input/ für den Skill sn-einlesen und setzt Einlese-Vermerke.
Der Ordner input/ ist nicht versioniert. Dort legen Fachleute Prozessbeschreibungen als .md ab
(Rohtext, Stichpunkte oder schon eine Arbeitsschritt-.md). Dieses Skript erledigt nur das
Mechanische; die fachliche Einschätzung und die Rückfrage an den Menschen macht der Skill.
pruefen (Standard) Jede Datei einordnen:
abgelehnt kein .md, keine UTF-8-Datei oder Unterordner – wird nicht bearbeitet
neu .md ohne Einlese-Vermerk
geändert .md mit Vermerk, aber der Inhalt hat sich seitdem geändert
eingelesen .md mit Vermerk, Inhalt unverändert – wird übersprungen
Für neue und geänderte Dateien: vorgeschlagener Prozessname, ob die Datei schon eine
gültige Arbeitsschritt-.md ist, und ähnliche vorhandene Prozesse (prozesse/, entwuerfe/).
vermerk Trägt nach erfolgreichem Einlesen oben in die .md einen Vermerk mit Zeitstempel ein.
Aufrufe (aus dem Workspace-Root):
python scripts/einlesen.py # Übersicht
python scripts/einlesen.py --json # dasselbe maschinenlesbar (für den Agenten)
python scripts/einlesen.py vermerk input/antrag.md --ergebnis neu --ziel entwuerfe/antrag-pruefen.md
Der Vermerk ist ein HTML-Kommentar in der ersten Zeile, z. B.
<!-- eingelesen: 2026-09-23 14:05:12 +0200 | neu | entwuerfe/antrag-pruefen.md | inhalt: 3f2a9c1e0b7d -->
Er ist in der Markdown-Ansicht unsichtbar und stört md_to_bpmn.py nicht (steht vor dem ersten Schritt).
„inhalt“ ist der Anfang der SHA-256 des Dateiinhalts ohne Vermerke; so fällt eine spätere Änderung auf.
Exit-Code: 0 = in Ordnung, 1 = Fehler beim Vermerk (Datei fehlt, kein .md, …), 2 = Aufruffehler.
"""
from __future__ import annotations
import argparse
import difflib
import hashlib
import json
import re
import sys
import unicodedata
from datetime import datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
EINGANG = ROOT / "input"
PROZESSE = ROOT / "prozesse"
ENTWUERFE = ROOT / "entwuerfe"
SKILL = ROOT / ".agents" / "skills" / "sn-bpmn"
sys.path.insert(0, str(SKILL))
import md_to_bpmn # noqa: E402 (liegt im Skill-Ordner, nicht als Paket installiert)
ERGEBNISSE = ("neu", "aktualisiert")
VERMERK = re.compile(
r"^<!-- eingelesen: (?P<zeit>[^|]+?) \| (?P<ergebnis>[^|]+?) \| (?P<ziel>[^|]+?) \| inhalt: (?P<hash>[0-9a-f]+) -->$"
)
# Von Windows oder Werkzeugen angelegte Dateien, die niemand bewusst in input/ legt
STILL_IGNORIERT = {"desktop.ini", "thumbs.db"}
FUELLWOERTER = {
"aber", "alle", "auch", "beim", "bzw", "dann", "dass", "diese", "dieser", "durch", "eine", "einem", "einen",
"einer", "eines", "falls", "für", "hier", "immer", "jede", "jeder", "kann", "keine", "mehr", "muss", "nach",
"noch", "oder", "ohne", "sich", "sind", "soll", "sowie", "über", "unter", "wenn", "werden", "wird", "zwei",
# Wörter der Arbeitsschritt-Syntax sagen nichts über den Inhalt
"prozess", "start", "ende", "task", "aufgabe", "gateway", "extern", "hinweis", "sendeaufgabe",
"empfangsaufgabe", "zwischenereignis", "nachricht", "beispiel",
}
SCHWELLE = 0.25 # ab dieser Ähnlichkeit wird ein vorhandener Prozess als Kandidat genannt
MAX_KANDIDATEN = 3
# ------------------------------------------------------------------ Vermerk ---
def ohne_vermerke(text: str) -> str:
return "\n".join(z for z in text.splitlines() if not VERMERK.match(z.strip()))
def inhalt_hash(text: str) -> str:
"""Kurz-Hash des Inhalts ohne Vermerke und ohne Unterschiede bei Zeilenenden/Leerzeilen am Ende."""
kern = ohne_vermerke(text.replace("\r\n", "\n")).strip()
return hashlib.sha256(kern.encode("utf-8")).hexdigest()[:12]
def letzter_vermerk(text: str) -> dict | None:
"""Der oberste (= neueste) Vermerk der Datei oder None."""
for zeile in text.splitlines():
treffer = VERMERK.match(zeile.strip())
if treffer:
return treffer.groupdict()
return None
def setze_vermerk(pfad: Path, ergebnis: str, ziel: str, jetzt: datetime | None = None) -> str:
"""Schreibt den Vermerk als erste Zeile; ältere Vermerke bleiben darunter stehen. Liefert die Zeile."""
if ergebnis not in ERGEBNISSE:
raise ValueError(f"Ergebnis '{ergebnis}' unbekannt, erlaubt: {', '.join(ERGEBNISSE)}")
# Als Bytes lesen: read_text() würde CRLF stillschweigend zu LF machen und die Datei so umschreiben.
text = pfad.read_bytes().decode("utf-8-sig")
zeit =(jetzt or datetime.now().astimezone()).strftime("%Y-%m-%d %H:%M:%S %z")
zeile = f"<!-- eingelesen: {zeit} | {ergebnis} | {ziel} | inhalt: {inhalt_hash(text)} -->"
zeilenende = "\r\n" if "\r\n" in text else "\n"
pfad.write_text(zeile + zeilenende + text, encoding="utf-8", newline="")
return zeile
# ------------------------------------------------------------ Einordnung ---
def slug(text: str) -> str:
"""Ordnername nach ADR 0003: Kleinbuchstaben, Ziffern, Bindestriche; Umlaute ausgeschrieben."""
text = text.lower()
for alt, neu in (("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss")):
text = text.replace(alt, neu)
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode("ascii")
return re.sub(r"[^a-z0-9]+", "-", text).strip("-")
def titel_von(text: str, ersatz: str) -> str:
"""Prozessname aus 'Prozess:'-Zeile, sonst erster Überschrift, sonst dem Dateinamen."""
for zeile in ohne_vermerke(text).splitlines():
treffer = re.match(r"^\s*(?:prozess|prozessname)\s*:\s*(.+)$", zeile, re.IGNORECASE)
if treffer:
return treffer.group(1).strip()
for zeile in ohne_vermerke(text).splitlines():
treffer = re.match(r"^\s*#{1,3}\s+(.+)$", zeile)
if treffer:
return treffer.group(1).strip()
return ersatz
def woerter(text: str) -> set[str]:
return {w for w in re.findall(r"[a-zäöüß]{4,}", text.lower()) if w not in FUELLWOERTER}
def ist_arbeitsschritt_md(text: str) -> bool:
"""True, wenn md_to_bpmn.py die Datei ohne Fehler liest und mindestens einen Schritt findet."""
try:
_, _, order, _ = md_to_bpmn.parse_markdown(text)
except ValueError:
return False
return bool(order)
def bestand() -> list[dict]:
"""Vorhandene Prozesse (prozesse/) und lokale Entwürfe (entwuerfe/) mit Titel und Wörtern."""
eintraege = []
if PROZESSE.is_dir():
for ordner in sorted(p for p in PROZESSE.iterdir() if p.is_dir()):
md = ordner / f"{ordner.name}.md"
text = md.read_text(encoding="utf-8") if md.is_file() else ""
readme = ordner / "README.md"
titel = titel_von(readme.read_text(encoding="utf-8"), ordner.name) if readme.is_file() else ordner.name
eintraege.append({"art": "Prozess", "name": ordner.name, "titel": titel_von(text, titel),
"pfad": ordner.relative_to(ROOT).as_posix() + "/", "woerter": woerter(text)})
if ENTWUERFE.is_dir():
for md in sorted(ENTWUERFE.glob("*.md")):
if re.match(r"^\d{8}-\d{6}-(issue|kommentar)-", md.name):
continue # Gitea-Entwürfe aus scripts/gitea.py
text = md.read_text(encoding="utf-8", errors="replace")
eintraege.append({"art": "Entwurf (lokal)", "name": md.stem, "titel": titel_von(text, md.stem),
"pfad": md.relative_to(ROOT).as_posix(), "woerter": woerter(text)})
return eintraege
def aehnlichkeit(titel: str, text: str, kandidat: dict) -> float:
"""0 … 1: halb Namensähnlichkeit, halb Wortüberschneidung. Gleicher Ordnername ergibt 1."""
eigener = slug(titel)
if eigener and eigener in (kandidat["name"], slug(kandidat["titel"])):
return 1.0
name = max(difflib.SequenceMatcher(None, eigener, slug(t)).ratio()
for t in (kandidat["name"], kandidat["titel"]))
a, b = woerter(text), kandidat["woerter"]
ueberschneidung = len(a & b) / len(a | b) if a and b else 0.0
return round(0.5 * name + 0.5 * ueberschneidung, 2)
def ordne_ein(pfad: Path, vorhanden: list[dict]) -> dict:
eintrag = {"datei": pfad.relative_to(ROOT).as_posix()}
if pfad.is_dir():
return eintrag | {"status": "abgelehnt", "grund": "Unterordner werden nicht gelesen"}
if pfad.suffix.lower() != ".md":
return eintrag | {"status": "abgelehnt",
"grund": f"Dateityp {pfad.suffix or '(ohne Endung)'} – vorerst nur .md"}
try:
text = pfad.read_text(encoding="utf-8-sig")
except UnicodeDecodeError:
return eintrag | {"status": "abgelehnt", "grund": "keine UTF-8-Datei – im Editor als UTF-8 speichern"}
if not ohne_vermerke(text).strip():
return eintrag | {"status": "abgelehnt", "grund": "Datei ist leer"}
vermerk = letzter_vermerk(text)
if vermerk and vermerk["hash"] == inhalt_hash(text):
return eintrag | {"status": "eingelesen", "vermerk": vermerk}
titel = titel_von(text, pfad.stem)
kandidaten = sorted(
({"art": k["art"], "name": k["name"], "titel": k["titel"], "pfad": k["pfad"],
"aehnlichkeit": aehnlichkeit(titel, text, k)} for k in vorhanden),
key=lambda k: -k["aehnlichkeit"],
)
return eintrag | {
"status": "geändert" if vermerk else "neu",
"vermerk": vermerk,
"titel": titel,
"vorschlag_name": slug(titel) or slug(pfad.stem),
"arbeitsschritt_md": ist_arbeitsschritt_md(ohne_vermerke(text)),
"kandidaten": [k for k in kandidaten if k["aehnlichkeit"] >= SCHWELLE][:MAX_KANDIDATEN],
}
def pruefe_eingang(eingang: Path | None = None) -> dict:
"""Ordnet alle Einträge in input/ ein; legt den Ordner an, falls er fehlt."""
eingang = eingang or EINGANG
angelegt = not eingang.is_dir()
eingang.mkdir(exist_ok=True)
vorhanden = bestand()
dateien = [ordne_ein(p, vorhanden) for p in sorted(eingang.iterdir())
if not p.name.startswith(".") and p.name.lower() not in STILL_IGNORIERT]
return {
"eingang": eingang.relative_to(ROOT).as_posix() + "/",
"angelegt": angelegt,
"dateien": dateien,
"bestand": [{k: v for k, v in e.items() if k != "woerter"} for e in vorhanden],
}
# ---------------------------------------------------------------- Ausgabe ---
def als_text(bericht: dict) -> str:
zeilen = []
if bericht["angelegt"]:
zeilen.append(f"Ordner {bericht['eingang']} war nicht vorhanden und wurde angelegt.")
if not bericht["dateien"]:
zeilen.append(f"{bericht['eingang']} ist leer – nichts einzulesen.")
for d in bericht["dateien"]:
status = d["status"]
if status == "abgelehnt":
zeilen.append(f"{'[abgelehnt]':<13}{d['datei']}: {d['grund']}")
elif status == "eingelesen":
v = d["vermerk"]
zeilen.append(f"{'[eingelesen]':<13}{d['datei']}: am {v['zeit']} ({v['ergebnis']} -> {v['ziel']})")
else:
art = "Arbeitsschritt-.md" if d["arbeitsschritt_md"] else "Rohtext"
zeilen.append(f"{'[' + status + ']':<13}{d['datei']}: „{d['titel']}“ ({art}), "
f"Vorschlag: {d['vorschlag_name']}")
for k in d["kandidaten"]:
zeilen.append(f" ähnlich ({k['aehnlichkeit']:.2f}): {k['art']} {k['pfad']} – „{k['titel']}“")
if not d["kandidaten"]:
zeilen.append(" kein ähnlicher Prozess im Bestand")
zaehler = {s: sum(1 for d in bericht["dateien"] if d["status"] == s)
for s in ("neu", "geändert", "eingelesen", "abgelehnt")}
zeilen.append("\n" + ", ".join(f"{n} {s}" for s, n in zaehler.items()) +
f" | Bestand: {len(bericht['bestand'])} Prozess(e)/Entwurf(e)")
return "\n".join(zeilen)
def main() -> int:
for strom in (sys.stdout, sys.stderr):
if hasattr(strom, "reconfigure"):
strom.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--json", action="store_true", help="Übersicht als JSON")
unter = ap.add_subparsers(dest="befehl")
v = unter.add_parser("vermerk", help="Einlese-Vermerk in eine .md schreiben")
v.add_argument("datei", help="Datei in input/")
v.add_argument("--ergebnis", required=True, choices=ERGEBNISSE)
v.add_argument("--ziel", required=True, help="wo das Ergebnis liegt, z. B. entwuerfe/<name>.md oder prozesse/<name>/")
args = ap.parse_args()
if args.befehl == "vermerk":
pfad = Path(args.datei)
if not pfad.is_file() or pfad.suffix.lower() != ".md":
print(f"Keine .md-Datei: {pfad}")
return 1
print(setze_vermerk(pfad, args.ergebnis, args.ziel))
return 0
bericht = pruefe_eingang()
print(json.dumps(bericht, ensure_ascii=False, indent=2) if args.json else als_text(bericht))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+205
View File
@@ -0,0 +1,205 @@
"""Tests für scripts/einlesen.py (Skill sn-einlesen) – reines Python/pytest, in einem temporären Workspace."""
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import einlesen # noqa: E402
ARBEITSSCHRITTE = """Prozess: Antrag prüfen
S1 Start: Antrag geht ein
S2 Task: Antrag erfassen
S3 Ende: Antrag entschieden
"""
ROHTEXT = "# Wegebau abnehmen\n\nNach Fertigstellung wird der Weg begangen und die Abnahme protokolliert.\n"
FESTE_ZEIT = datetime(2026, 9, 23, 14, 5, 12, tzinfo=timezone(timedelta(hours=2)))
@pytest.fixture
def ws(tmp_path, monkeypatch):
"""Leerer Workspace mit input/, prozesse/ und entwuerfe/ an Stelle des echten."""
for name in ("input", "prozesse", "entwuerfe"):
(tmp_path / name).mkdir()
monkeypatch.setattr(einlesen, "ROOT", tmp_path)
monkeypatch.setattr(einlesen, "EINGANG", tmp_path / "input")
monkeypatch.setattr(einlesen, "PROZESSE", tmp_path / "prozesse")
monkeypatch.setattr(einlesen, "ENTWUERFE", tmp_path / "entwuerfe")
return tmp_path
def lege_prozess_an(ws, name, text):
ordner = ws / "prozesse" / name
ordner.mkdir()
(ordner / f"{name}.md").write_text(text, encoding="utf-8")
(ordner / "README.md").write_text(f"# {name}\n\n| | |\n|---|---|\n| Status | Entwurf |\n", encoding="utf-8")
def eintrag(bericht, dateiname):
return next(d for d in bericht["dateien"] if d["datei"].endswith(dateiname))
def test_fehlender_eingangsordner_wird_angelegt(ws):
"""Prüft, dass input/ angelegt wird, wenn es fehlt, und das im Bericht steht."""
(ws / "input").rmdir()
bericht = einlesen.pruefe_eingang()
assert (ws / "input").is_dir()
assert bericht["angelegt"] is True
assert bericht["dateien"] == []
def test_andere_dateitypen_werden_abgelehnt(ws):
"""Prüft, dass .docx, .pdf und .txt abgelehnt und nicht angefasst werden."""
for name in ("a.docx", "b.pdf", "c.txt"):
(ws / "input" / name).write_bytes(b"inhalt")
bericht = einlesen.pruefe_eingang()
assert {d["status"] for d in bericht["dateien"]} == {"abgelehnt"}
assert all("nur .md" in d["grund"] for d in bericht["dateien"])
assert (ws / "input" / "a.docx").read_bytes() == b"inhalt"
def test_unterordner_leere_und_nicht_utf8_dateien_werden_abgelehnt(ws):
"""Prüft, dass Unterordner, leere .md und .md in cp1252 mit Grund abgelehnt werden."""
(ws / "input" / "unter").mkdir()
(ws / "input" / "leer.md").write_text("\n", encoding="utf-8")
(ws / "input" / "alt.md").write_bytes("Straße".encode("cp1252"))
bericht = einlesen.pruefe_eingang()
assert "Unterordner" in eintrag(bericht, "unter")["grund"]
assert "leer" in eintrag(bericht, "leer.md")["grund"]
assert "UTF-8" in eintrag(bericht, "alt.md")["grund"]
def test_systemdateien_werden_still_uebergangen(ws):
"""Prüft, dass desktop.ini und versteckte Dateien gar nicht erst im Bericht auftauchen."""
(ws / "input" / "desktop.ini").write_text("x", encoding="utf-8")
(ws / "input" / ".gitkeep").write_text("", encoding="utf-8")
assert einlesen.pruefe_eingang()["dateien"] == []
def test_neue_md_mit_titel_namensvorschlag_und_art(ws):
"""Prüft, dass eine neue .md Titel, Ordnernamen-Vorschlag und die Art (Rohtext/Arbeitsschritte) bekommt."""
(ws / "input" / "roh.md").write_text(ROHTEXT, encoding="utf-8")
(ws / "input" / "schritte.md").write_text(ARBEITSSCHRITTE, encoding="utf-8")
bericht = einlesen.pruefe_eingang()
roh, schritte = eintrag(bericht, "roh.md"), eintrag(bericht, "schritte.md")
assert roh["status"] == "neu" and roh["titel"] == "Wegebau abnehmen"
assert roh["vorschlag_name"] == "wegebau-abnehmen" and roh["arbeitsschritt_md"] is False
assert schritte["vorschlag_name"] == "antrag-pruefen" and schritte["arbeitsschritt_md"] is True
def test_gleichnamiger_prozess_wird_als_kandidat_mit_1_gemeldet(ws):
"""Prüft, dass ein vorhandener Prozess mit gleichem Ordnernamen als Kandidat mit Ähnlichkeit 1 erscheint."""
lege_prozess_an(ws, "antrag-pruefen", ARBEITSSCHRITTE)
(ws / "input" / "neu.md").write_text(ARBEITSSCHRITTE.replace("erfassen", "prüfen"), encoding="utf-8")
kandidaten = eintrag(einlesen.pruefe_eingang(), "neu.md")["kandidaten"]
assert kandidaten[0]["pfad"] == "prozesse/antrag-pruefen/"
assert kandidaten[0]["aehnlichkeit"] == 1.0
def test_inhaltlich_aehnlicher_entwurf_wird_gefunden_fremder_nicht(ws):
"""Prüft, dass ein ähnlicher lokaler Entwurf als Kandidat erscheint, ein fachfremder Prozess dagegen nicht."""
(ws / "entwuerfe" / "antragsbearbeitung.md").write_text(
"Prozess: Antragsbearbeitung\nS1 Start: Antrag geht ein\nS2 Task: Antrag erfassen und prüfen\nS3 Ende: Fertig\n",
encoding="utf-8")
lege_prozess_an(ws, "wegebau-abnehmen", "Prozess: Wegebau abnehmen\nS1 Start: Weg fertig\nS2 Ende: Protokoll\n")
(ws / "input" / "antrag.md").write_text(ARBEITSSCHRITTE, encoding="utf-8")
pfade = [k["pfad"] for k in eintrag(einlesen.pruefe_eingang(), "antrag.md")["kandidaten"]]
assert pfade == ["entwuerfe/antragsbearbeitung.md"]
def test_gitea_entwuerfe_zaehlen_nicht_zum_bestand(ws):
"""Prüft, dass Issue- und Kommentarentwürfe aus scripts/gitea.py nicht als Prozess gelten."""
(ws / "entwuerfe" / "20260923-134657-issue-Projektwissen.md").write_text("# Entwurf", encoding="utf-8")
assert einlesen.pruefe_eingang()["bestand"] == []
def test_vermerk_steht_oben_mit_zeitstempel_und_markiert_als_eingelesen(ws):
"""Prüft, dass der Vermerk als erste Zeile mit Zeitstempel geschrieben wird und die Datei danach als eingelesen gilt."""
datei = ws / "input" / "antrag.md"
datei.write_text(ARBEITSSCHRITTE, encoding="utf-8")
zeile = einlesen.setze_vermerk(datei, "neu", "entwuerfe/antrag-pruefen.md", jetzt=FESTE_ZEIT)
inhalt = datei.read_text(encoding="utf-8")
assert inhalt.splitlines()[0] == zeile
assert zeile.startswith("<!-- eingelesen: 2026-09-23 14:05:12 +0200 | neu | entwuerfe/antrag-pruefen.md | inhalt: ")
assert inhalt.endswith(ARBEITSSCHRITTE)
befund = eintrag(einlesen.pruefe_eingang(), "antrag.md")
assert befund["status"] == "eingelesen"
assert befund["vermerk"]["ziel"] == "entwuerfe/antrag-pruefen.md"
def test_vermerk_stoert_die_umwandlung_nicht(ws):
"""Prüft, dass eine Arbeitsschritt-.md mit Vermerk weiterhin von md_to_bpmn.py gelesen wird."""
datei = ws / "input" / "antrag.md"
datei.write_text(ARBEITSSCHRITTE, encoding="utf-8")
einlesen.setze_vermerk(datei, "neu", "entwuerfe/antrag-pruefen.md")
_, _, order, _ = einlesen.md_to_bpmn.parse_markdown(datei.read_text(encoding="utf-8"))
assert order == ["S1", "S2", "S3"]
def test_aenderung_nach_dem_vermerk_ergibt_status_geaendert(ws):
"""Prüft, dass eine nach dem Einlesen bearbeitete Datei als geändert statt eingelesen erkannt wird."""
datei = ws / "input" / "antrag.md"
datei.write_text(ARBEITSSCHRITTE, encoding="utf-8")
einlesen.setze_vermerk(datei, "neu", "entwuerfe/antrag-pruefen.md")
datei.write_text(datei.read_text(encoding="utf-8") + "S4 Hinweis: Nachtrag\n", encoding="utf-8")
befund = eintrag(einlesen.pruefe_eingang(), "antrag.md")
assert befund["status"] == "geändert"
assert befund["vermerk"]["ergebnis"] == "neu"
def test_zweiter_vermerk_kommt_obenauf_der_erste_bleibt(ws):
"""Prüft, dass ein erneutes Einlesen einen neuen Vermerk oben ergänzt und den alten stehen lässt."""
datei = ws / "input" / "antrag.md"
datei.write_text(ARBEITSSCHRITTE, encoding="utf-8")
einlesen.setze_vermerk(datei, "neu", "entwuerfe/antrag-pruefen.md", jetzt=FESTE_ZEIT)
einlesen.setze_vermerk(datei, "aktualisiert", "prozesse/antrag-pruefen/", jetzt=FESTE_ZEIT + timedelta(days=1))
zeilen = datei.read_text(encoding="utf-8").splitlines()
assert "2026-09-24" in zeilen[0] and "aktualisiert" in zeilen[0]
assert "2026-09-23" in zeilen[1] and "| neu |" in zeilen[1]
assert eintrag(einlesen.pruefe_eingang(), "antrag.md")["status"] == "eingelesen"
def test_zeilenenden_und_bom_bleiben_erhalten(ws):
"""Prüft, dass der Vermerk CRLF-Dateien nicht auf LF umstellt und ein UTF-8-BOM den Status nicht verfälscht."""
datei = ws / "input" / "win.md"
datei.write_bytes("".encode("utf-8") + ARBEITSSCHRITTE.replace("\n", "\r\n").encode("utf-8"))
einlesen.setze_vermerk(datei, "neu", "entwuerfe/x.md")
roh = datei.read_bytes()
assert b"\r\n" in roh and b"\n" not in roh.replace(b"\r\n", b"")
assert eintrag(einlesen.pruefe_eingang(), "win.md")["status"] == "eingelesen"
def test_unbekanntes_ergebnis_wird_abgewiesen(ws):
"""Prüft, dass nur die Ergebnisse 'neu' und 'aktualisiert' vermerkt werden können."""
datei = ws / "input" / "antrag.md"
datei.write_text(ARBEITSSCHRITTE, encoding="utf-8")
with pytest.raises(ValueError):
einlesen.setze_vermerk(datei, "übersprungen", "x")
assert datei.read_text(encoding="utf-8") == ARBEITSSCHRITTE
def test_vermerk_auf_andere_dateitypen_endet_mit_exit_code_1(tmp_path):
"""Prüft, dass 'vermerk' eine Nicht-.md-Datei ablehnt, ohne sie zu verändern."""
datei = tmp_path / "vorlage.docx"
datei.write_bytes(b"x")
ergebnis = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "einlesen.py"), "vermerk", str(datei),
"--ergebnis", "neu", "--ziel", "x"],
capture_output=True, text=True, encoding="utf-8",
)
assert ergebnis.returncode == 1
assert datei.read_bytes() == b"x"
@pytest.mark.parametrize("titel, erwartet", [
("Antrag prüfen", "antrag-pruefen"),
("Größe & Maßnahme (Entwurf)", "groesse-massnahme-entwurf"),
(" Wegebau – Abnahme ", "wegebau-abnahme"),
])
def test_namensvorschlag_folgt_adr_0003(titel, erwartet):
"""Prüft, dass der vorgeschlagene Ordnername Kleinbuchstaben, Ziffern und Bindestriche hat, Umlaute ausgeschrieben."""
assert einlesen.slug(titel) == erwartet