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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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:*)"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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`) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user