Projektwissen: Git-Vorgaben, Ticketzugriff, Tests und Einstieg

Erster Stand des Repositories AG_QGIS/Projektwissen (bisher Arbeitstitel
"Workspace", ADR 0006). Der lokale Ordner heisst weiter AG_QGIS, weil darin
auch die Plugin-Klone liegen.

Git verbindlich (ADR 0007), aus dem bisherigen Wiki-Konzept entwickelt:
- docs/leitfaden/05_git_arbeitsweise.md mit Kanal-Kette, Branch-Namen und
  Gitea-Branch-Schutz.
- Der Einstiegspunkt eines Fixes richtet sich nach dem Kanal, in dem der
  Fehler sitzt; ein Fix an testing oder stable ist erst mit der
  Rueckfuehrung nach unten abgeschlossen. Das fehlte bisher.
- Der stabile Kanal heisst stable, nicht main: der Release-Workflow in
  Repository:hidden/workflows fuehrt git checkout "$CHANNEL" aus, der
  Branchname muss also dem Kanalnamen entsprechen. Er existiert in keinem
  Plugin-Repo und ist vom Maintainer anzulegen.
- Der Mensch drueckt ab: .claude/settings.json sperrt push, merge, tag.
- scripts/hallo.py und /hallo holen den Stand aller Klone, per
  Fast-Forward wo gefahrlos; nie mergen, stashen oder verwerfen.

Ticketzugriff:
- scripts/gitea.py liest Issues, Kommentare und PRs ueber die oeffentliche
  API, ohne Token, nur GET. Schreibendes nur als Entwurf in entwuerfe/.
- Skill sn-ticket.

Tests (ADR 0009):
- docs/leitfaden/06_tests.md: Testebenen, Testpflicht, und wie eine
  Fachperson beurteilt, ob Tests fachlich sinnvoll sind.
- scripts/testkatalog.py erzeugt docs/tests/testkatalog.md per ast-Parsing,
  ohne Import und ohne QGIS. Befund: nur 100 von 332 Tests haben einen
  Docstring, sn_basis keinen einzigen von 87.
- Vorlage und erste Testkonzept-Seite mit Mermaid. Dabei gefunden und gegen
  origin geprueft: _ermittle_gebietstyp_aus_fbschl hat keinen Test, obwohl
  PR #44/#45 genau diese Funktion tolerant gemacht hat.
- docs/konzept/07_qgis_tests_plan.md: Stufenplan fuer Tests gegen echtes
  QGIS. Dabei aufgefallen: QGIS 3.40 ist seit Januar 2026 End of Life,
  qgis_min steht aber noch darauf.

Werkzeuge (ADR 0008):
- 04_werkzeuge_einrichten.md nur noch Claude Code als CLI und in VS Code.
- .vscode/ entfernt, brauchbare Einstellungen als persoenliche Vorlage.

Ausserdem: README als Einstieg neu aufgebaut, Mermaid-Diagramm in
schnittstellen.md repariert (unmaskierte Klammern, rendert seither nicht).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 11:57:26 +02:00
co-authored by Claude Sonnet 5
commit ab416fb34c
57 changed files with 7202 additions and 0 deletions
@@ -0,0 +1,65 @@
---
name: sn-aufgabe-bearbeiten
description: Standardablauf für eine Entwicklungsaufgabe an den LNO-Plugins (sn_basis, sn_verfahrensgebiet, sn_plan41, sn_widmung) von Gitea-Issue über Branch, Umsetzung und Tests bis zum vorbereiteten Pull Request mit Übergabenotiz. Verwenden bei "bearbeite Issue", "setze um", "Bugfix", "neue Funktion".
---
# Aufgabe bearbeiten
## 0. Stand holen (nicht überspringen)
```bash
python scripts/hallo.py
```
Holt für alle Klone den Serverstand und aktualisiert die Kanal-Branches per Fast-Forward, wo das gefahrlos geht.
Ohne diesen Schritt wird womöglich etwas behoben, das längst behoben ist.
## 1. Auftrag verstehen (nicht überspringen)
- Issue **mit Kommentaren** lesen Skill `sn-ticket`, `python scripts/gitea.py ticket <repo> <nr>`.
Label `Diskussion` = noch kein Konsens: Annahmen im PR kennzeichnen.
- Betroffenes Paket bestimmen (`workspace.toml`). Bei `sn_basis`: alle Fachplugins sind Nutzer.
- Ziel, Abnahmekriterien und Grenzen in drei Sätzen zurückspiegeln, bevor Code angefasst wird.
Fehlt ein Abnahmekriterium, ist das die erste Rückfrage nicht der erste Commit.
## 2. Kontext laden gezielt
- `docs/status/STATUS.md` (wer arbeitet woran), `docs/architektur/modulkarte.md` (Dateien und Größen),
bei Verträgen `docs/architektur/schnittstellen.md`, bei Überraschungen `docs/wissen/fallstricke.md`.
- Dann `grep -n "def \|class " <datei>` für die Struktur, dann nur die betroffenen Abschnitte lesen.
- Verwendungen finden: `grep -rn "<funktion>" Plugin_SN_*/ --include=*.py`.
- Gibt es zu dem Ablauf eine Seite in `docs/tests/`? Dann zeigt sie, was heute abgedeckt ist.
## 3. Branch
Der Einstiegspunkt richtet sich nach dem Kanal, in dem der Fehler sitzt nicht nach der Dringlichkeit.
Neue Funktionen beginnen immer bei `unstable`. Vollständig: `docs/leitfaden/05_git_arbeitsweise.md` Abschnitt 4.
```bash
cd Plugin_SN_<Repo>
git switch unstable && git pull --ff-only # bzw. testing / stable bei einem Fix dort
git switch -c feature/<issue-nr>-<kurzname> # oder bugfix/… ; klein, ASCII, mit Nummer
```
Fix an `testing` oder `stable`: die Rückführung nach unten ist Teil der Aufgabe, nicht „später".
## 4. Umsetzen
- Kleine Schritte, jeder für sich testbar. Nach jedem Schritt `python scripts/run_tests.py <paket> --baseline`
(aus dem Projektwissen-Root, also ohne `../`).
- Regeln aus `AGENTS.md`: nur Wrapper-Importe, `get_variable`/`set_variable`, keine Fachlogik in `sn_basis`,
kein Refactoring nebenbei, Stil der Datei beibehalten, Deutsch.
- **Neue Logik bekommt einen Test** in `tests/test_<modul>.py` `unittest`, Mock-Modus, kein Netzwerk,
ein Test je Verhalten. Bei einem Bugfix zuerst den Test schreiben, der den Fehler zeigt.
- **Jeder neue oder geänderte Test bekommt einen deutschen Docstring**, erste Zeile `"""Prüft, dass …"""`.
Warum: `docs/leitfaden/06_tests.md` Abschnitt 5.
- Bei Bedarf Wrapper erweitern: reales Symbol im `try`-Block, Mock-Klasse im `except`-Block
(Muster `qgiscore_wrapper.py`).
## 5. Nachweis
- `python scripts/run_tests.py <paket> --baseline` → keine Zeile „NEU ROT". Neu grüne Tests im PR nennen.
- `python scripts/testkatalog.py` erzeugt `docs/tests/testkatalog.md` neu, gehört mit in den Commit.
- Betrifft die Änderung einen dokumentierten Fachablauf: die Seite in `docs/tests/` nachziehen.
- UI-, Layer-, WFS- oder Layoutverhalten: manueller QGIS-Test durch den Entwickler nötig → im PR als offenen
Punkt eintragen, wenn du ihn nicht ausführen kannst. **Nicht als erledigt ausgeben.**
## 6. Abschluss
- `changelog.txt` des Plugins: neue Zeile(n) oben vor dem ersten `---` (Block ohne Versionszeile).
- Commit-Nachrichten deutsch, Präsens, erste Zeile ≤ 72 Zeichen, Issue-Nummer am Ende.
- Pull-Request-Text nach `docs/vorlagen/pull_request.md` vorbereiten; Übergabenotiz mit Skill `sn-uebergabe`.
- **Hier ist Schluss.** Push, Pull Request, Merge und Tag macht der Mensch. Ebenso: Baseline ändern,
`metadata.txt` von Hand anfassen, irgendetwas auf Gitea verändern.
@@ -0,0 +1,41 @@
---
name: sn-neues-fachplugin
description: Neues sn_*-Fachplugin oder neuen Tab nach dem Integrationsvertrag von sn_basis anlegen (Registrierung in Menü/Toolbar, DockManager, BaseDockWidget, Tabs, Services, Release-Dateien). Verwenden bei "neues Plugin", "Plugin anlegen", "FLT/Topografie/Widmung ausbauen", "neuen Tab hinzufügen".
---
# Neues Fachplugin anlegen
Vertrag: `docs/architektur/schnittstellen.md` Abschnitte 12. Minimalmuster: `Plugin_SN_Widmung/`. Vollständiges Muster mit Services: `Plugin_SN_Verfahrensgebiet/main.py` und `ui/dockwidget.py`.
## Dateien (Paketname `sn_<name>`, Repo `Plugin_SN_<Name>`)
```
__init__.py classFactory(iface) → Klasse aus main.py
main.py Klasse <Name>: __init__(iface), initGui(), unload(), run()
ui/dockwidget.py class DockWidget(BaseDockWidget): tabs = [ArbeitsTab, PrintTab, SettingsTab]
ui/tabs/<tab>.py class <Tab>(QWidget): tab_title = "…"; optional set_services(...), cleanup()
functions/… Fachfunktionen (keine Qt-Importe außer über Wrapper)
tests/__init__.py, tests/test_main.py Mock-Modus (Muster: Plugin_SN_Verfahrensgebiet/tests/test_main.py)
changelog.txt, README.md, .gitignore
.gitea/workflows/release.yaml Kopie aus einem bestehenden Plugin (unverändert)
.gitea/templates/metadata.template Name, Beschreibung, Homepage/Tracker/Repository-URLs dieses Repos, qgisMinimumVersion=3.40, supportsQt6=true
```
## main.py Pflichtverhalten
- `initGui()`: `basis = plugins.get("sn_basis")`; ohne `basis.ui` **nichts tun** (Basis ruft später nach). Sonst
`self.action = basis.ui.add_action(self.plugin_name, self.run, tooltip=…, priority=<frei, 10/20/30 sind vergeben>)`, dann `basis.ui.finalize_menu_and_toolbar()`.
- `run()`: Dock erzeugen, `setObjectName("sn_dock_<name klein>")`, `dock.action = self.action`, `DockManager.show(dock)`, `basis.ui.set_active_plugin(self.action)`.
- `unload()`: `iface.removeDockWidget`, `deleteLater`, `basis.ui.remove_action(self.action)`.
- Services: `Pruefmanager(ui_modus="qgis")` und `DataGrabber(pruefmanager=…)` in `__init__` anlegen und an das Dock geben, wenn Prüfungen/Datenabruf gebraucht werden.
## Imports
Nur `from qgis.utils import plugins` (main.py) und `sn_basis.functions.*`/`sn_basis.modules.*`/`sn_basis.ui.*`. Kein `qgis.core`, kein `PyQt*`.
## Registrieren im Workspace
- `workspace.toml`: Eintrag `[[repos]]` mit `paket`, `rolle`, `tests`.
- `docs/architektur/modulkarte.md` und `schnittstellen.md` (Priorität) ergänzen.
- Repo-`AGENTS.md`/`CLAUDE.md` aus `docs/vorlagen/AGENTS.repo.md` und `CLAUDE.repo.md`.
- Gitea: Repo-Variable `ZIP_FOLDER=sn_<name>` und Secret/Variablen wie bei den anderen Plugins (Maintainer).
## Abnahme
- `python scripts/run_tests.py sn_<name>` grün (mindestens `test_main.py`).
- In QGIS: Plugin erscheint in Menü/Toolbar „LNO Sachsen“, Dock öffnet, andere Docks schließen, Unload hinterlässt keine Reste.
+33
View File
@@ -0,0 +1,33 @@
---
name: sn-release
description: Release eines LNO-Plugins über den Gitea-Tag-Workflow (Kanäle unstable/testing/stable, Versionsschema JJ.M.N, changelog, metadata.template, Feeds in Repository). Verwenden bei "Release", "Version veröffentlichen", "Tag setzen", "testing/unstable bauen". Nur für Maintainer; Agenten bereiten vor, taggen nicht.
---
# Release erstellen
Ablauf und Fallstricke: `docs/architektur/schnittstellen.md` Abschnitt 9, `docs/wissen/fallstricke.md` (Release).
## Vorbereitung (Agent darf)
1. Kanal-Branch prüfen: `git switch unstable && git pull --ff-only` (bzw. `testing`). **Gebaut wird der Kopf dieses Branches, nicht der getaggte Commit.**
2. `changelog.txt`: Block über dem ersten `---` enthält die Einträge seit dem letzten Release; darüber Zeile `Version <JJ.M.N>-<kanal>:` ergänzen (Muster im Verlauf der Datei). Er wird zum Release-Text.
3. `.gitea/templates/metadata.template` prüfen: Name, Beschreibung, URLs zeigen auf **dieses** Repo (bekannte Fehler: Plan41 → Test_Action, FLT → `Plugin_SN_Neu`). Version wird vom Workflow gesetzt.
4. Tests: `python scripts/run_tests.py <paket> --baseline` ohne NEU ROT. Bei Fachplugins prüfen, ob die verwendete `sn_basis`-Version passende Schnittstellen hat (Änderungen seit letztem Basis-Release im Changelog).
5. Version wählen: `JJ.M.N` = Jahr.Monat.Laufnummer im Monat (z. B. `26.9.1`). Unstable und testing zählen getrennt hoch.
## Auslösen (nur Maintainer)
```bash
git tag v26.9.1-u # -u unstable, -t testing, ohne Suffix stable (derzeit ohne Branch nicht verwenden)
git push origin v26.9.1-u
```
Der Repo-Workflow schickt einen Payload an `Repository` (Branch `hidden/workflows`); dieser baut das ZIP unter dem
Gitea-Variablennamen `ZIP_FOLDER`, erzeugt `metadata.txt` aus dem Template, legt das Gitea-Release an und aktualisiert
`plugins-<kanal>.xml`.
## Prüfen
- Gitea → Repo → Releases: Eintrag `Version 26.9.1-unstable` mit ZIP-Asset.
- `Repository/plugins-unstable.xml` enthält die neue Version.
- QGIS mit Feed `unstable`: Plugin-Aktualisierung sichtbar, Installation startet ohne Fehler.
## Nach dem Release
- `docs/status/STATUS.md`: Versionen je Kanal nachtragen.
- Bekannter offener Punkt: Release-ZIPs enthalten derzeit `AGENTS.md`, `CLAUDE.md`, `docs/`, `tests/` (Excludes im zentralen Workflow ergänzen).
+84
View File
@@ -0,0 +1,84 @@
---
name: sn-tests
description: Tests der LNO-Plugins ausführen, deuten, neu schreiben und dokumentieren - Mock-Modus ohne QGIS über scripts/run_tests.py mit Baseline-Vergleich, Testkatalog erzeugen, Testkonzept-Seiten pflegen, Hinweise für Tests in echter QGIS-Umgebung. Verwenden bei "Tests ausführen", "sind die Tests grün", "Test schlägt fehl", "Baseline", "schreib einen Test", "Testabdeckung".
---
# Tests ausführen, schreiben und dokumentieren
Ausführliche Fassung für Menschen: `docs/leitfaden/06_tests.md`.
## Ausführen (Mock-Modus, ohne QGIS)
```bash
python scripts/run_tests.py # alle Pakete mit Tests
python scripts/run_tests.py sn_verfahrensgebiet --baseline # ein Paket, Vergleich mit Baseline
python scripts/run_tests.py sn_plan41 --verbose # volle unittest-Ausgabe (nur zum Verstehen eines Fehlers)
```
- Das Skript verlinkt die Repos unter ihren Paketnamen (`sn_basis`, …) in ein temporäres Verzeichnis und startet
`python -m unittest discover`. Voraussetzung: Python ≥ 3.11, `openpyxl` installiert, Repos vorhanden
(`scripts/setup_workspace.py`).
- Ausgabe je Paket: `GRÜN`/`ROT`, Zähler, mit `--baseline` zusätzlich `NEU ROT` (verboten) und `neu grün`.
- Exit-Code 0 = in Ordnung. `gesamt = 0` bedeutet Import-Fehler → `--verbose`.
## Einen neuen Test schreiben
Pflicht bei jeder neuen Funktionalität ein Test je neuem beobachtbarem Verhalten, nicht je Funktion.
Bei einem Bugfix zuerst der Test, der den Fehler zeigt.
```python
def test_fbschl_mit_punkt_wird_erkannt(self):
"""Prüft, dass der Shape-Import auch das Feld 'FBSchl.' mit Punkt akzeptiert.
Manche DAVID-Exporte liefern den Feldnamen mit Punkt, andere ohne.
"""
layer = self._layer_mit_feldern({"FBSchl.": "901/10110F"})
self.assertEqual(_ermittle_gebietstyp_aus_fbschl(layer), "Verfahrensgebiet")
```
Drei Anforderungen, alle verbindlich:
1. Deutscher, fachlich sprechender Name nicht `test_case_3`.
2. **Docstring, erste Zeile `Prüft, dass …`.** Sie landet im Testkatalog und ist für die meisten Leser der
einzige Kontakt mit dem Test.
3. Ein Verhalten je Test. Steht im Namen ein „und", sind es zwei Tests.
Weiter: `unittest`, Datei `tests/test_<modul>.py`, kein Netzwerk, keine echten Verfahrensdaten, keine
Abhängigkeit von der Testreihenfolge. Muster: vorhandene Tests des Pakets.
Dialoge: `Pruefmanager(ui_modus="test")` liefert Standardantworten; `dialog_wrapper`-Funktionen mit
`unittest.mock.patch` ersetzen.
## Testkatalog
```bash
python scripts/testkatalog.py # erzeugt docs/tests/testkatalog.md
python scripts/testkatalog.py --pruefen # Exit 1, wenn die Datei veraltet ist
```
Wird aus den Testdateien per `ast` erzeugt (kein Import, läuft ohne QGIS). **Nicht von Hand bearbeiten.**
Nach jeder Änderung an Tests neu erzeugen und mitcommitten. Der Abschnitt „Lücken" listet Tests ohne
Docstring Stand 04.09.2026 haben nur 100 von 332 Tests einen, `sn_basis` keinen einzigen.
## Testkonzept-Seiten
Für fachlich wichtige Abläufe liegt in `docs/tests/<ablauf>.md` eine Seite nach `docs/vorlagen/testkonzept.md`:
Mermaid-Diagramm des fachlichen Ablaufs, Tabelle Schritt → deckender Test → bewusst nicht getestet mit
Begründung, bekannte Lücken. Sie ermöglicht Fachleuten die Beurteilung, ob die Tests das Richtige prüfen.
Wird ein dokumentierter Ablauf geändert, wird die Seite mit nachgezogen.
## Fehler deuten
- `ModuleNotFoundError: sn_basis` → nicht über das Skript gestartet oder Repo fehlt.
- Mock-Klassen ohne Methode (`AttributeError: _MockQgs… has no attribute`) → Mock im Wrapper ergänzen,
nicht den Test umgehen.
- Test erwartet alte Signatur (Einzeldatei statt Liste, ein Layer statt zwei) → veralteter Test nach Umbau auf
Mehrfachauswahl; prüfen, ob das neue Verhalten gewollt ist (Issue/Changelog), dann Test anpassen und im PR begründen.
- Test hängt oder braucht Netz → der Test ist falsch gebaut, Dienstantwort nachstellen.
**Niemals** einen roten Test durch Löschen, `@unittest.skip` oder Anpassen der Erwartung grün machen außer das
Verhalten wurde bewusst geändert und der PR begründet es.
## Baseline
- Datei `docs/status/testbaseline.json`; wird nur von Maintainern mit
`python scripts/run_tests.py --update-baseline` verändert. Ein Agent fasst sie nicht an.
- Regel: keine neuen roten Tests; bekannte rote Tests werden über eigene Issues grün gemacht.
## In echter QGIS-Umgebung (Windows, OSGeo4W)
- Plugins ohne Release in ein QGIS-Profil bringen: `python scripts/link_qgis_profile.py --profil dev`
(verknüpft die Klone unter ihren Paketnamen; danach in QGIS experimentelle Plugins anzeigen, `sn_basis` zuerst aktivieren).
- Heute nur manuell bzw. über die repo-eigenen `tests\test_qgis.bat` (enthalten fest verdrahtete Pfade).
Der Ausbau zu automatisierten QGIS-Tests ist geplant: `docs/konzept/07_qgis_tests_plan.md`.
- Pflicht bei: Layer-Operationen, WFS/REST, Drucklayouts, Signalen/Docks. Ergebnis im PR unter „Wie geprüft".
- Agenten ohne QGIS: Test als offenen Punkt im PR benennen, **nicht** als erledigt ausgeben.
+74
View File
@@ -0,0 +1,74 @@
---
name: sn-ticket
description: Gitea-Tickets lesen, durchsuchen und beantworten - offene Issues auflisten, ein Issue mit allen Kommentaren holen, Issue- und Kommentartexte als Entwurf vorbereiten, den ein Mensch absendet. Verwenden bei "welche Tickets gibt es", "lies Issue", "was steht in dem Ticket", "leg ein Issue an", "kommentiere das Ticket".
---
# Tickets auf Gitea
Alle Repositories der Organisation `AG_QGIS` liegen auf `entwicklung.flurneuordnung-sachsen.de`. Die API ist
**lesend ohne Anmeldung** erreichbar; dafür gibt es `scripts/gitea.py` (nur Standardbibliothek, kein Token).
> **Der Agent verändert auf dem Server nichts.** Kein Anlegen, kein Kommentieren, kein Schließen, kein Zuweisen.
> Der Agent bereitet den Text vor, der Mensch fügt ihn ein und sendet ab. Grund: Jede Aktion trägt den Namen
> einer Person und deren Verantwortung. Siehe `docs/leitfaden/05_git_arbeitsweise.md` Abschnitt 6.
## Lesen
```bash
python scripts/gitea.py tickets # alle offenen Issues der Organisation
python scripts/gitea.py tickets Basis # nur ein Repo (Kurzname genügt)
python scripts/gitea.py tickets --label Bug # nach Label filtern
python scripts/gitea.py ticket Basis 48 # ein Issue mit allen Kommentaren
python scripts/gitea.py suche "FBSchl" # Volltext über alle Repos
python scripts/gitea.py pulls # offene Pull Requests
```
Repo-Kurznamen: `Basis`, `Plan41`, `Verfahrensgebiet`, `Widmung`, … werden auf die vollen Namen aus
`workspace.toml` aufgelöst.
**Immer `ticket` statt `tickets` verwenden, bevor mit der Arbeit begonnen wird.** Die Kommentare enthalten
regelmäßig die eigentliche Anforderung — die Titelzeile führt in die Irre.
## Ein Ticket verstehen, bevor Code angefasst wird
1. Issue **mit Kommentaren** lesen (`ticket`), nicht nur den Titel.
2. Label `Diskussion` bedeutet: es gibt noch keinen Konsens. Dann nicht umsetzen, sondern die offene Frage
benennen. Wird trotzdem umgesetzt, sind die Annahmen im Pull Request zu kennzeichnen.
3. Betroffenes Paket über `workspace.toml` bestimmen. Bei `sn_basis` sind alle Fachplugins Nutzer — Änderungen
an öffentlichen Funktionen sind API-Änderungen.
4. **Ziel, Abnahmekriterien und Grenzen in drei Sätzen zurückspiegeln**, bevor die erste Datei geöffnet wird.
Steht im Ticket kein Abnahmekriterium, ist das die erste Rückfrage — nicht der erste Commit.
5. Fehlt fachlicher Kontext: `docs/wissen/glossar.md`, dann `docs/architektur/schnittstellen.md`.
Danach weiter mit dem Skill `sn-aufgabe-bearbeiten`.
## Entwurf vorbereiten
```bash
python scripts/gitea.py entwurf issue Verfahrensgebiet # neues Issue
python scripts/gitea.py entwurf kommentar Basis 48 # Kommentar zu Issue 48
```
Das legt eine Datei unter `entwuerfe/` an (nicht versioniert) und nennt die Gitea-URL zum Absenden. Vorgehen:
1. Entwurf anlegen lassen.
2. Text ausfüllen — bei Issues nach `docs/vorlagen/issue.md`: Ziel, Betroffen, fachlicher Hintergrund,
Abnahmekriterien, Nicht Teil dieses Issues.
3. Dem Menschen den Pfad der Datei und die Ziel-URL nennen.
4. **Dort aufhören.** Nicht versuchen, den Text per `curl`, API-Token oder auf anderem Weg abzusenden.
Ein guter Issue-Entwurf ist so geschrieben, dass ein anderer Agent ihn ohne Rückfragen umsetzen kann: ein
beobachtbares Ziel, benannte Dateien, klare Abgrenzung.
## Wenn Gitea nicht erreichbar ist
Das Skript meldet das im Klartext. Mögliche Ursachen: kein Netz, Proxy nicht gesetzt (`HTTPS_PROXY`), Server
nicht verfügbar. Dann **nicht raten**, was im Ticket steht — mit der Arbeit warten oder den Menschen um den
Ticketinhalt bitten.
## Grenzen
- Keine Zugangsdaten, keine personenbezogenen Daten und keine echten Verfahrensdaten in Ticket-Texten.
- Keine Ticket-Inhalte in Zusammenfassungen erfinden oder ergänzen. Was nicht im Ticket steht, ist eine offene
Frage, keine Annahme.
- Labels, Zuweisungen, Meilensteine und Status ändert der Mensch im Browser.
+33
View File
@@ -0,0 +1,33 @@
---
name: sn-uebergabe
description: Aufgabe sauber abschließen oder pausieren Übergabenotiz nach Vorlage schreiben, STATUS.md und Wissensbasis (fallstricke, schnittstellen, modulkarte, entscheidungen) nachziehen, damit die nächste Person oder der nächste Agent nicht neu erkunden muss. Verwenden am Ende jeder Aufgabe, vor dem PR, bei Sitzungsende oder "Übergabe".
---
# Übergabe schreiben und Wissen sichern
## 1. Übergabenotiz (immer)
Vorlage `docs/vorlagen/uebergabe.md` ausfüllen; sie gehört in die PR-Beschreibung (bei Pausen: als Kommentar im Issue).
Kurz und konkret: Stand, geänderte Dateien mit Grund, Nachweis (Testzeile, QGIS-Test), Annahmen, nächste Schritte,
und die zwei bis drei Dateien, mit denen es weitergeht.
## 2. Wissensbasis prüfen nur was **nicht aus dem Code ablesbar** ist
| Frage | Wenn ja → |
|---|---|
| Hat etwas überraschend Zeit gekostet (Dienstgrenze, Qt-Unterschied, Release-Falle)? | `docs/wissen/fallstricke.md`, passende Rubrik, oben, mit Datum und Quelle |
| Wurde ein Vertrag geändert oder erst entdeckt (Variable, Layername, Signatur in `sn_basis`, Dock, Release)? | `docs/architektur/schnittstellen.md` |
| Modul neu, verschoben, deutlich gewachsen? | `docs/architektur/modulkarte.md` (Zeile mit Zweck und Zeilenzahl) |
| Fachbegriff musste erklärt werden? | `docs/wissen/glossar.md` |
| Hat das Team etwas entschieden? | neue Datei `docs/entscheidungen/NNNN-<thema>.md` nach Vorlage im dortigen README, Tabelle im README ergänzen |
| Nichts davon? | In der Übergabenotiz „Wissensbasis: nichts Neues“ |
## 3. `docs/status/STATUS.md`
- Zeile in „Wer arbeitet woran“ aktualisieren; bei Merge Eintrag unter „Zuletzt gemergt“; erledigte Baustellen streichen.
- Datum im Kopf aktualisieren.
## 4. Werkzeug-Gedächtnis
Notizen aus der Claude-Auto-Memory, die andere brauchen, jetzt in die Dateien oben
übertragen. Lokale Pfade und persönliche Vorlieben bleiben lokal (`CLAUDE.local.md`, gitignored).
## 5. Vor dem Beenden
- Zwischenstand committet (auch WIP-Commit auf dem Feature-Branch ist besser als ungesicherte Änderungen).
- Offene Fragen im Issue notiert, nicht nur im Chat.
+23
View File
@@ -0,0 +1,23 @@
---
description: Sitzungsbeginn - Stand aller Repositories holen und Kanal-Branches aktualisieren
allowed-tools: Bash(python scripts/hallo.py:*), Bash(python3 scripts/hallo.py:*), Read
---
Führe `python scripts/hallo.py` aus und werte das Ergebnis aus.
Berichte danach kurz und ohne die Rohausgabe zu wiederholen:
1. **Welche Repositories aktualisiert wurden** und um wie viele Commits.
2. **Was Aufmerksamkeit braucht** — ungesicherte Änderungen, eigene nicht gepushte Commits,
Repositories auf einem fremden Branch, fehlende Klone, nicht erreichbare Repositories.
Nenne dabei jeweils den konkreten nächsten Schritt.
3. **Wenn alles sauber ist:** ein Satz, mehr nicht.
Wenn Repositories fehlen, weise auf `python scripts/setup_workspace.py` hin.
Verändere selbst nichts: kein `merge`, kein `stash`, kein `reset`, kein Branchwechsel, kein `push`.
Das Skript hat bereits alles getan, was gefahrlos möglich ist. Alles Übrige entscheidet der Mensch —
siehe `docs/leitfaden/05_git_arbeitsweise.md` Abschnitt 6.
Frage zum Schluss, woran gearbeitet werden soll. Biete an, mit `python scripts/gitea.py tickets`
die offenen Tickets zu zeigen.
+48
View File
@@ -0,0 +1,48 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"deny": [
"Bash(git push:*)",
"Bash(git merge:*)",
"Bash(git rebase:*)",
"Bash(git tag:*)",
"Bash(git reset --hard:*)",
"Bash(git clean:*)",
"Bash(git stash:*)",
"Bash(git remote set-url:*)",
"Bash(git config --global:*)",
"Bash(gh:*)",
"Bash(tea:*)",
"Read(./**/.env)",
"Read(./**/*.token)",
"Read(./**/credentials*)"
],
"ask": [
"Bash(git commit:*)",
"Bash(git switch:*)",
"Bash(git checkout:*)",
"Bash(python scripts/run_tests.py --update-baseline:*)",
"Edit(./docs/status/testbaseline.json)",
"Edit(./**/metadata.txt)"
],
"allow": [
"Bash(git status:*)",
"Bash(git log:*)",
"Bash(git diff:*)",
"Bash(git show:*)",
"Bash(git branch:*)",
"Bash(git fetch:*)",
"Bash(git pull --ff-only:*)",
"Bash(git add:*)",
"Bash(python scripts/hallo.py:*)",
"Bash(python scripts/run_tests.py:*)",
"Bash(python scripts/gitea.py:*)",
"Bash(python scripts/testkatalog.py:*)",
"Bash(python scripts/setup_workspace.py:*)",
"Bash(python scripts/link_qgis_profile.py:*)"
]
}
}
+13
View File
@@ -0,0 +1,13 @@
# Zeilenenden vereinheitlichen. Das Team arbeitet unter Windows; ohne diese Datei
# landen je nach Rechner CRLF im Repository und erzeugen Diffs ohne Inhalt.
* text=auto eol=lf
# Binärdateien nicht anfassen
*.png binary
*.jpg binary
*.ico binary
*.xlsx binary
*.zip binary
*.pdf binary
*.qgz binary
*.gpkg binary
+30
View File
@@ -0,0 +1,30 @@
# Die Plugin- und Daten-Repositories sind eigenständige Git-Klone (siehe workspace.toml)
# und werden NICHT im Workspace-Repository versioniert.
Plugin_*/
Repository/
Linkliste/
Defaults/
# Von scripts/setup_workspace.py aus .agents/skills/ gespiegelt (Claude Code liest nur .claude/skills/).
# Ohne Schrägstrich, damit auch ein Symlink erfasst wird - mit Schrägstrich greift die Regel nur
# bei echten Verzeichnissen, und unter Linux entsteht hier ein Symlink mit absolutem Pfad.
.claude/skills
# Entwuerfe fuer Gitea-Tickets und -Kommentare (scripts/gitea.py entwurf ...).
# Sie werden vom Menschen im Browser abgesendet und nicht versioniert.
entwuerfe/
# Lokale, nicht geteilte Dateien
CLAUDE.local.md
AGENTS.local.md
.claude/settings.local.json
.codex/
*.local.toml
# Python / Tests
__pycache__/
*.py[cod]
.pytest_cache/
.coverage
coverage_html/
.sn_testlauf/
+105
View File
@@ -0,0 +1,105 @@
# Projektwissen AG_QGIS Anweisungen für KI-Agenten und Entwickler
Kanonische, werkzeugneutrale Anweisungsdatei. Claude Code liest sie über `CLAUDE.md`.
Sie bleibt bewusst kurz: Details liegen in `docs/` und werden nur bei Bedarf gelesen.
## Worum es geht
- QGIS-Plugins der sächsischen Flurbereinigungsbehörden („LNO Sachsen"): Plattform-Plugin `sn_basis` plus Fachplugins
`sn_verfahrensgebiet`, `sn_plan41` (Wege- und Gewässerplan nach § 41 FlurbG), `sn_widmung` (Gerüst) und Platzhalter.
- Dieser Ordner ist der Klon des Repositories **`AG_QGIS/Projektwissen`**. Die Plugin- und Daten-Repositories liegen
als **eigenständige Git-Klone** in Unterordnern (`Plugin_SN_*/`, `Repository/`, `Linkliste/`, `Defaults/`), jedes mit
eigenen Branches, Issues und Releases auf Gitea. Zuordnung Repo-Name → Paketname (`Plugin_SN_Basis``sn_basis`)
steht in `workspace.toml`.
- Zielumgebung: QGIS 3.44 LTR (Qt5) und QGIS 4 (Qt6) unter Windows/OSGeo4W. Die Mindestversion steht noch auf 3.40,
das seit Januar 2026 End of Life ist offene Entscheidung, siehe `docs/konzept/07_qgis_tests_plan.md`.
Die KI-Agenten laufen meist ohne QGIS; dafür gibt es den Mock-Modus der Wrapper und `scripts/run_tests.py`.
## Zuerst lesen nur was die Aufgabe braucht
1. `docs/status/STATUS.md` aktueller Stand, offene Baustellen, wer woran arbeitet.
2. `docs/architektur/modulkarte.md` welche Datei was tut und wie groß sie ist (statt Dateien blind zu lesen).
3. `docs/architektur/schnittstellen.md` Verträge zwischen `sn_basis` und Fachplugins, Projektvariablen, Release.
4. `docs/wissen/fallstricke.md` bekannte Stolperfallen (Qt5/Qt6, Mock-Modus, WFS-Grenzen, Release-Kanäle).
5. Fachbegriffe: `docs/wissen/glossar.md`. Übersicht aller Dokumente: `docs/README.md`.
## Harte Regeln
### Git und Server der Mensch drückt ab
- **Nie pushen, mergen, taggen oder etwas auf Gitea verändern.** Agenten bereiten vor, eine Person sendet ab.
`.claude/settings.json` sperrt `git push`, `git merge`, `git rebase`, `git tag` technisch.
- Arbeitsbranch ist `unstable`. Nie direkt auf `unstable`, `testing` oder `stable` committen:
eigener Branch `feature/<nr>-<kurzname>` bzw. `bugfix/<nr>-<kurzname>`, dann Pull Request.
- **Wo ein Fix beginnt, richtet sich nach dem Kanal, in dem der Fehler sitzt** bei `testing` von `testing`
abzweigen, danach Rückführung nach `unstable`. Vollständig: `docs/leitfaden/05_git_arbeitsweise.md`.
- Sitzungsbeginn: `python scripts/hallo.py` (bzw. `/hallo`), bevor irgendetwas geändert wird.
### Tests
- Vor jedem Commit: `python scripts/run_tests.py <paket> --baseline`. Neue rote Tests sind nicht erlaubt; bereits rote
Tests (Baseline in `docs/status/testbaseline.json`) dürfen nicht vermehrt werden. Die Baseline ändert nur ein Maintainer.
- **Neue Funktionalität ohne Test wird nicht gemergt.** Mindestens ein Test je neuem beobachtbarem Verhalten,
im Mock-Modus, ohne Netzwerk. Bei einer Fehlerbehebung zuerst der Test, der den Fehler zeigt.
- **Jeder neue oder geänderte Test bekommt einen deutschen Docstring**, erste Zeile in der Form `"""Prüft, dass …"""`.
Danach `python scripts/testkatalog.py` neu erzeugen und mitcommitten.
- Tests werden nicht gelöscht, nicht mit `skip` stillgelegt und nicht in ihrer Erwartung angepasst, um grün zu werden
außer das Verhalten wurde bewusst geändert und der Pull Request begründet es. Details: `docs/leitfaden/06_tests.md`.
### Code
- QGIS und Qt **nie direkt importieren** (einzige Ausnahme: `qgis.utils` in `main.py`). Ausschließlich über die Wrapper in
`sn_basis.functions` (`qt_wrapper`, `qgiscore_wrapper`, `qgisui_wrapper`, `dialog_wrapper`, `message_wrapper`,
`variable_wrapper`). Fehlt ein Symbol, wird es zuerst im Wrapper mit Mock-Fallback ergänzt.
- Projektvariablen nur über `get_variable`/`set_variable` (Präfix `sn_` wird automatisch gesetzt Schlüssel ohne Präfix übergeben).
- Fachlogik gehört in das Fachplugin, nicht nach `sn_basis`. `sn_basis` importiert keine Fachplugins (dokumentierte
Ausnahme: Lazy-Import in `DataGrabber`, siehe `schnittstellen.md`).
- Änderungen an öffentlichen Funktionen von `sn_basis` sind API-Änderungen: vorher alle Verwendungen in den Fachplugins
per `grep` prüfen und mit anpassen.
- Keine Python-Abhängigkeiten außerhalb der QGIS-Python-Umgebung ohne Issue-Absprache und Eintrag in `fallstricke.md`.
- Kein Refactoring „nebenbei". Strukturänderungen nur über eigenes Issue; das grundlegende Refactoring ist eine eigene Phase.
### Allgemein
- Sprache: Doku, Kommentare, Docstrings, Commit-Nachrichten, Issues und PRs auf Deutsch. Bezeichner nach dem Stil der jeweiligen Datei.
- Keine Zugangsdaten, API-Keys, personenbezogene Daten oder echte Verfahrensdaten in Repositories, Doku oder Prompts.
- `AGENTS.md`, `CLAUDE.md`, `.agents/`, `.claude/` gehören nicht in Release-ZIPs (siehe `fallstricke.md`, Release-Excludes).
## Arbeitsablauf in Kurzform (Details: Skill `sn-aufgabe-bearbeiten`)
1. `python scripts/hallo.py` Stand aller Klone holen.
2. Gitea-Issue lesen (Skill `sn-ticket`, `python scripts/gitea.py ticket <repo> <nr>`) oder als Entwurf vorbereiten.
Ziel, Abnahmekriterien und betroffenes Plugin klären, bevor Code angefasst wird.
3. Branch `feature/<issue-nr>-<kurzname>` bzw. `bugfix/<issue-nr>-<kurzname>` vom passenden Kanal.
4. In kleinen Schritten umsetzen; nur betroffene Dateien und Abschnitte lesen. Neue Logik bekommt einen Test.
5. `python scripts/run_tests.py <paket> --baseline` und `python scripts/testkatalog.py`.
UI- oder QGIS-Verhalten zusätzlich in QGIS prüfen und das Ergebnis im PR nennen.
6. `changelog.txt` des Plugins oben ergänzen (Block ohne Versionszeile = nächstes Release).
7. Pull-Request-Text nach `docs/vorlagen/pull_request.md` vorbereiten, inklusive Übergabenotiz (Skill `sn-uebergabe`).
**Push und Pull Request macht der Mensch.**
8. Neue Erkenntnisse in `docs/wissen/` oder `docs/entscheidungen/` festhalten, nicht nur im lokalen Gedächtnis des Werkzeugs.
## Tokensparend arbeiten
- Erst `modulkarte.md` und `schnittstellen.md`, dann gezielt `grep`, dann nur die nötigen Abschnitte lesen.
Dateien über 600 Zeilen (Liste in der Modulkarte) nie komplett laden.
- Keine Dateien oder Testausgaben in den Chat kopieren; Pfade mit Zeilennummern nennen.
- Tests immer über `scripts/run_tests.py` (Zusammenfassung statt Rohausgabe; `--verbose` nur bei Bedarf).
- Am Ende jeder Aufgabe eine Übergabenotiz, damit die nächste Sitzung nicht neu erkunden muss.
- Breite Suchen an einen Unteragenten delegieren, wenn das Werkzeug das kann; Hauptkontext schlank halten.
## Befehle
- Sitzungsbeginn: `python scripts/hallo.py` (Claude Code: `/hallo`)
- Tickets: `python scripts/gitea.py tickets` | `python scripts/gitea.py ticket Basis 48` | `… suche "<text>"`
- Tests (Mock-Modus, Sekunden): `python scripts/run_tests.py` | `python scripts/run_tests.py sn_basis --baseline`
- Testkatalog: `python scripts/testkatalog.py` | `--pruefen` (meldet, wenn veraltet)
- Workspace einrichten oder aktualisieren: `python scripts/setup_workspace.py` (mit `--pull` für fast-forward)
- Klone in ein QGIS-Profil verknüpfen: `python scripts/link_qgis_profile.py --profil dev`
- Tests in echter QGIS-Umgebung: Skill `sn-tests`; Ausbauplan `docs/konzept/07_qgis_tests_plan.md`
- Release: Tag `v<JJ.M.N>-u` (unstable), `-t` (testing) oder ohne Suffix (stable) auf dem Kanal-Branch **nur Maintainer**, Skill `sn-release`
## Skills
Liegen in `.agents/skills/` (Agent-Skills-Standard) und werden nach `.claude/skills/` gespiegelt:
`sn-aufgabe-bearbeiten`, `sn-ticket`, `sn-tests`, `sn-uebergabe`, `sn-neues-fachplugin`, `sn-release`.
+17
View File
@@ -0,0 +1,17 @@
@AGENTS.md
## Hinweise für Claude Code
- **Sitzungsbeginn:** `/hallo` ausführen, bevor irgendetwas geändert wird. Der Befehl liegt in
`.claude/commands/hallo.md` und ruft `scripts/hallo.py` auf.
- **Berechtigungen:** `.claude/settings.json` sperrt `git push`, `git merge`, `git rebase`, `git tag` und
`gh`/`tea`. Das ist Absicht, keine Fehlkonfiguration: auf dem Server verändert nur der Mensch etwas
(`docs/leitfaden/05_git_arbeitsweise.md` Abschnitt 6). Läuft ein Kommando in eine Ablehnung, ist der richtige
nächste Schritt, den fertigen Befehl zu nennen — nicht, ihn zu umgehen.
- **Skills** liegen kanonisch in `.agents/skills/`; `python scripts/setup_workspace.py --nur-skills` spiegelt sie
nach `.claude/skills/`. Nur `.agents/skills/` bearbeiten.
- **Die Auto-Memory ist lokal** und wird nicht geteilt. Alles, was andere Entwickler wissen müssen, gehört nach
`docs/wissen/`, `docs/entscheidungen/` oder `docs/status/STATUS.md` (Skill `sn-uebergabe`).
- **Beim Arbeiten in einem Plugin-Ordner** gilt zusätzlich dessen `AGENTS.md` (über die dortige `CLAUDE.md`).
- **Für breite Suchen im Code** den Explore-Unteragenten nutzen; Ergebnisse nur als Zusammenfassung in den
Hauptkontext übernehmen.
+164
View File
@@ -0,0 +1,164 @@
# Projektwissen — AG QGIS Sachsen
**Hier fängst du an.** Dieses Repository ist das gemeinsame Gedächtnis der QGIS-Plugin-Entwicklung der
sächsischen Flurbereinigungsbehörden („LNO Sachsen"). Es enthält **keinen Plugin-Code**, sondern alles, was man
wissen muss, um an den Plugins mitzuarbeiten: die Regeln, die Wissensbasis, die Anweisungen für KI-Agenten, die
Skripte und das Verzeichnis aller Repositories.
Der Plugin-Code liegt in eigenen Repositories, die als Unterordner **hierher geklont** werden. Danach ist dieser
Ordner der Ort, an dem du arbeitest — mit einem KI-Agenten, der die Regeln aus diesem Repository automatisch liest.
```mermaid
flowchart TB
subgraph GITEA["Gitea · entwicklung.flurneuordnung-sachsen.de/AG_QGIS"]
PW[("Projektwissen<br/><i>dieses Repo</i>")]
PB[("Plugin_SN_Basis")]
PF[("Plugin_SN_Verfahrensgebiet<br/>Plugin_SN_Plan41<br/>Plugin_SN_Widmung · …")]
RE[("Repository<br/><i>Plugin-Feeds</i>")]
end
subgraph LOKAL["Dein Rechner · Ordner AG_QGIS/"]
W["Regeln, Wissensbasis,<br/>Skills, Skripte"]
K["Plugin-Klone<br/>Plugin_SN_*/"]
end
PW -->|"git clone"| W
PB --> K
PF --> K
W -.->|"setup_workspace.py<br/>holt die Klone"| K
LOKAL -->|"Pull Request"| GITEA
RE -->|"Plugin-Feed"| QGIS(["QGIS auf den<br/>Arbeitsplätzen"])
style PW fill:#e8f6ec,stroke:#2f9e44
style W fill:#e8f6ec,stroke:#2f9e44
```
> **Ordnername ≠ Reponame.** Das Repository heißt `Projektwissen`, der lokale Ordner heißt `AG_QGIS` — weil darin
> nicht nur das Projektwissen liegt, sondern auch alle Plugin-Klone daneben. Der Klon-Befehl unten setzt das
> automatisch richtig.
## In 15 Minuten startklar
**Vorher da sein muss:** Git, Python ≥ 3.11, ein Gitea-Konto mit Schreibrecht in der Organisation `AG_QGIS`.
Details und Windows-Besonderheiten: [`docs/leitfaden/04_werkzeuge_einrichten.md`](docs/leitfaden/04_werkzeuge_einrichten.md).
```bash
# 1. Projektwissen holen — Zielordner AG_QGIS nicht vergessen
git clone https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Projektwissen.git AG_QGIS
cd AG_QGIS
# 2. Alle Plugin-Repositories dazuholen
python scripts/setup_workspace.py
# 3. Prüfen, dass alles läuft (Sekunden, ohne QGIS)
python scripts/run_tests.py
```
Erwartete Ausgabe von Schritt 3: drei Test-Suiten mit Zahlen. Dass dabei nicht alles grün ist, ist bekannt und in
Ordnung — siehe [`docs/status/STATUS.md`](docs/status/STATUS.md).
```bash
# 4. KI-Werkzeug starten — im Ordner AG_QGIS, nicht in einem Plugin-Unterordner
claude
```
Zum Warmwerden diese Frage stellen:
> *„Fasse `docs/status/STATUS.md` zusammen und nenne die drei wichtigsten Regeln aus `AGENTS.md`."*
Kommt eine brauchbare Antwort, ist die Einrichtung fertig. Kommt keine, wurde das Werkzeug im falschen Ordner
gestartet.
## Dein erster Beitrag
```mermaid
flowchart LR
A["/hallo<br/><i>Stand holen</i>"] --> B["Ticket wählen<br/><i>gitea.py tickets</i>"]
B --> C["Branch von unstable"]
C --> D["Umsetzen<br/><i>mit Test</i>"]
D --> E["run_tests.py<br/>--baseline"]
E -->|rot| D
E -->|grün| F["Push + Pull Request"]
F --> G["Review<br/><i>durch Kollegin</i>"]
G --> H["Merge + STATUS.md"]
style E fill:#fff4e6,stroke:#d9822b
style F fill:#e6f2ff,stroke:#2b7dd9
```
Konkret, mit Befehlen:
| # | Schritt | Womit |
|---|---|---|
| 1 | Stand holen — **immer zuerst** | `/hallo` bzw. `python scripts/hallo.py` |
| 2 | Offene Tickets ansehen | `python scripts/gitea.py tickets` |
| 3 | Ein Ticket im Detail lesen | `python scripts/gitea.py ticket Basis 48` |
| 4 | Aufgabe an den Agenten geben | Skill `sn-aufgabe-bearbeiten`, Muster in [`03_arbeitsweise.md`](docs/leitfaden/03_arbeitsweise.md) Abschnitt 3 |
| 5 | Nachweis erbringen | `python scripts/run_tests.py <paket> --baseline` |
| 6 | Pull Request | Vorlage [`docs/vorlagen/pull_request.md`](docs/vorlagen/pull_request.md) |
Für den Anfang eignet sich ein kleines Ticket oder ein einzelner roter Test aus
[`docs/status/testbaseline.json`](docs/status/testbaseline.json).
## Die Regeln, die immer gelten
Die vollständige, für Agenten verbindliche Fassung steht in [`AGENTS.md`](AGENTS.md). Das Wichtigste in Kurzform:
1. **Der Mensch drückt ab.** Agenten bereiten alles vor — Push, Merge, Tag und alles auf dem Gitea-Server macht
eine Person mit ihrem eigenen Zugang. → [`05_git_arbeitsweise.md`](docs/leitfaden/05_git_arbeitsweise.md)
2. **Nie direkt auf `stable`, `testing` oder `unstable` committen.** Immer Arbeitsbranch und Pull Request.
3. **Neue Funktionalität ohne Test wird nicht gemergt.** Vor jedem Commit
`python scripts/run_tests.py <paket> --baseline`, keine neuen roten Tests.
→ [`06_tests.md`](docs/leitfaden/06_tests.md)
4. **QGIS und Qt nie direkt importieren**, nur über die Wrapper in `sn_basis.functions`. Deshalb laufen die Tests
in Sekunden ohne QGIS. → [`schnittstellen.md`](docs/architektur/schnittstellen.md)
5. **Kein Refactoring nebenbei.** Strukturänderungen brauchen ein eigenes Issue.
6. **Deutsch** in Doku, Kommentaren, Commits, Issues und Pull Requests.
7. **Keine Zugangsdaten, keine echten Verfahrensdaten** in Repositories, Doku oder Prompts.
8. **Wissen gehört in `docs/`**, nicht in das Gedächtnis des KI-Werkzeugs — das ist lokal und teilt sich nicht.
## Wegweiser
**Wenn du neu bist**, lies in dieser Reihenfolge:
[`03_arbeitsweise.md`](docs/leitfaden/03_arbeitsweise.md) →
[`05_git_arbeitsweise.md`](docs/leitfaden/05_git_arbeitsweise.md) →
[`04_werkzeuge_einrichten.md`](docs/leitfaden/04_werkzeuge_einrichten.md).
| Pfad | Inhalt |
|---|---|
| [`AGENTS.md`](AGENTS.md) / `CLAUDE.md` | Anweisungen für Agenten (kanonisch ist `AGENTS.md`) |
| [`workspace.toml`](workspace.toml) | Verzeichnis aller Repositories: Branch, Paketname, Rolle |
| `scripts/` | `hallo.py`, `setup_workspace.py`, `run_tests.py`, `gitea.py`, `testkatalog.py`, `link_qgis_profile.py` |
| `.agents/skills/` | Skills für wiederkehrende Abläufe, gespiegelt nach `.claude/skills/` |
| `.claude/commands/` | Slash-Befehle für Claude Code (`/hallo`) |
| `docs/status/` | **Aktueller Stand**, Test-Baseline — immer zuerst lesen |
| `docs/leitfaden/` | Arbeitsweise, Werkzeuge, Git, Tests |
| `docs/architektur/` | Modulkarte und Schnittstellen (lebende Referenz) |
| `docs/wissen/` | Fallstricke, Glossar, externe Dienste — das gemeinsame Gedächtnis |
| `docs/entscheidungen/` | Warum etwas so ist (ADR, append-only) |
| `docs/tests/` | Testkatalog und Testkonzepte je Fachablauf |
| `docs/konzept/` | Zusammenarbeit mit KI-Agenten, QGIS-Testplan, Repo-Einrichtung |
| `docs/analyse/` | Momentaufnahmen 09/2026, werden nicht fortgeschrieben |
| `docs/vorlagen/` | Issue, Pull Request, Übergabe, Testkonzept, Repo-`AGENTS.md`, CI-Workflow |
| `Plugin_SN_*/`, `Repository/`, `Linkliste/`, `Defaults/` | eigenständige Git-Klone, **nicht** Teil dieses Repos |
## Wenn etwas nicht geht
| Symptom | Ursache und Abhilfe |
|---|---|
| `git clone` meldet „Repository not found" | Zugang zur Organisation `AG_QGIS` fehlt — Maintainer fragen |
| `ModuleNotFoundError: sn_basis` | Tests nicht über `scripts/run_tests.py` gestartet, oder Klone fehlen (`setup_workspace.py`) |
| `ModuleNotFoundError: openpyxl` | `pip install openpyxl` im selben Python, mit dem du `run_tests.py` aufrufst |
| Der Agent kennt die Regeln nicht | Er wurde in einem Plugin-Unterordner gestartet statt in `AG_QGIS/` |
| Plugin erscheint nicht in QGIS | Profil verknüpfen: `python scripts/link_qgis_profile.py --profil dev`, dann `sn_basis` zuerst aktivieren |
| Konflikte beim Pullen | `/hallo` vor der Arbeit vergessen — siehe [`05_git_arbeitsweise.md`](docs/leitfaden/05_git_arbeitsweise.md) Abschnitt 11 |
Mehr davon in [`docs/leitfaden/03_arbeitsweise.md`](docs/leitfaden/03_arbeitsweise.md) Abschnitt 9 und in
[`docs/wissen/fallstricke.md`](docs/wissen/fallstricke.md).
## Mitmachen
Auch dieses Repository wird per Pull Request geändert — Regeln, Wissensbasis und Skripte sind genauso
versioniert wie Code. Wer eine Regel für falsch hält, macht ein Issue auf. Wer etwas gelernt hat, das andere
Zeit kostet, trägt es in [`docs/wissen/fallstricke.md`](docs/wissen/fallstricke.md) ein.
+26
View File
@@ -0,0 +1,26 @@
# Dokumentation Wegweiser
| Ordner | Charakter | Wann lesen |
|---|---|---|
| `status/STATUS.md` | lebend, kurz | **immer zuerst** aktueller Stand, Baustellen |
| `architektur/modulkarte.md` | lebend | vor dem Lesen von Code: welche Datei, wie groß |
| `architektur/schnittstellen.md` | lebend | vor Änderungen an `sn_basis`, Projektvariablen, Docks, Release |
| `wissen/fallstricke.md` | lebend | bei Überraschungen; vor Arbeiten an WFS, Release, Qt |
| `wissen/glossar.md` | lebend | Fachbegriffe |
| `wissen/externe_dienste.md` | lebend | URLs, Abhängigkeiten |
| `entscheidungen/` | append-only (ADR) | warum etwas so ist; neue Entscheidung = neue Datei |
| `leitfaden/03_arbeitsweise.md` | stabil | Einstieg für Entwickler, 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, Fix-Einstieg, Rückführung, Tags, was der Agent darf |
| `leitfaden/06_tests.md` | **verbindlich** | Testebenen, Testpflicht, Katalog, wie man Tests fachlich beurteilt |
| `tests/testkatalog.md` | generiert | was die Suite prüft erzeugt von `scripts/testkatalog.py` |
| `tests/*.md` | lebend | Testkonzepte je Fachablauf, mit Mermaid; Vorlage in `vorlagen/` |
| `konzept/07_qgis_tests_plan.md` | Vorschlag | Stufenplan für Tests gegen echtes QGIS |
| `konzept/08_repo_einrichten.md` | Prüfliste | Repo auf Gitea anlegen, `stable` und Branch-Schutz |
| `konzept/02_zusammenarbeit_ki_agenten.md` | stabil | Begründung und Aufbau der Zusammenarbeit |
| `analyse/01_projektanalyse.md` | Momentaufnahme 09/2026 | Zusammenspiel der Komponenten, Qualitätsbefund, Risiken |
| `analyse/00_*` | Momentaufnahme 09/2026 | Gesamtüberblick, Vergleich Alternativansatz (Vorarbeiten) |
| `vorlagen/` | Vorlagen | Issue, PR, Übergabe, Testkonzept, Repo-`AGENTS.md`, CI-Workflow |
Regel: **Lebende** Dokumente werden mit jeder Änderung gepflegt, **Momentaufnahmen** nicht nachträglich geändert
(neue Analyse = neue Datei mit Datum).
@@ -0,0 +1,205 @@
# Gesamtüberblick AG_QGIS
Stand: 2. September 2026
Quelle: [Gitea-Organisation AG_QGIS](https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS)
## Kurzfazit
`AG_QGIS` ist kein einzelnes Git-Repository, sondern eine Organisation mit elf getrennten Repositories. Die Zielarchitektur besteht aus einem gemeinsamen QGIS-Basisplugin (`sn_basis`) und mehreren darauf aufbauenden Fachplugins. Wirklich ausgebaut sind derzeit `sn_basis`, `sn_plan41` und `sn_verfahrensgebiet`. `sn_widmung` ist ein UI-Gerüst; `sn_flt`, Topografie und Template sind noch Platzhalter.
Die Entwicklung erfolgt überwiegend auf dem Branch `unstable`. Es gibt einen eigenen Gitea-basierten Release-Prozess mit den Kanälen stable, testing und unstable. Der stable-Kanal ist noch leer. Die vorhandenen Unit-/Mock-Tests sind umfangreich, auf den aktuellen unstable-Ständen aber nicht vollständig grün.
## Lokaler Stand
Alle elf öffentlichen Repositories wurden in Unterordner dieses Verzeichnisses geklont. Jeder Checkout steht auf seinem vom Server vorgegebenen Default-Branch und war nach dem Klonen unverändert.
| Repository | Branch | Commit | Stand | Umfang / Rolle |
|---|---|---:|---:|---|
| `Plugin_SN_Basis` | `unstable` | `7755e3e` | 31.08.2026 | Zentrale Laufzeit, 58 Python-Dateien, 13 Testdateien |
| `Plugin_SN_Plan41` | `unstable` | `7fe8115` | 22.07.2026 | Plan-§-41-Fachplugin, 17 Python-Dateien, 5 Testdateien |
| `Plugin_SN_Verfahrensgebiet` | `unstable` | `70c1478` | 02.09.2026 | Fachplugin Verfahrensgebiet, 29 Python-Dateien, 12 Testdateien |
| `Plugin_SN_Widmung` | `unstable` | `b72369d` | 02.07.2026 | UI-Gerüst ohne Fachlogik |
| `Plugin_SN_FLT` | `unstable` | `c1b7460` | 19.06.2026 | Platzhalter, noch kein Plugin-Code |
| `Plugin_SN_Topografie` | `unstable` | `443b5bd` | 19.06.2026 | Platzhalter, noch kein Plugin-Code |
| `Plugin_SN_Template` | `unstable` | `92462d3` | 19.06.2026 | Noch unvollständige Vorlage |
| `Plugin_Test_Action` | `unstable` | `3fd13d8` | 25.03.2026 | Minimales Testplugin für den Release-Prozess |
| `Defaults` | `main` | `440e84b` | 17.07.2026 | Beispiel-Verfahrensdaten und ein QML-Stil |
| `Linkliste` | `main` | `08fbb29` | 17.07.2026 | Zentrale Diensteliste und Formatkonvertierung |
| `Repository` | `main` | `a8db6c0` | 02.09.2026 | QGIS-Plugin-Feeds und Release-Orchestrierung |
Die drei großen Plugins umfassen zusammen rund 29.500 Python-Zeilen einschließlich Tests. Der gesamte lokale Klon ist etwa 7 MB groß.
## Architektur
```mermaid
flowchart TD
QGIS[QGIS 3.40+] --> BASIS[sn_basis]
BASIS --> NAV[Menü, Toolbar und DockManager]
BASIS --> SHARED[Prüfen, Laden, Schreiben, Druck und Projekteinstellungen]
BASIS --> VG[sn_verfahrensgebiet]
BASIS --> P41[sn_plan41]
BASIS --> WID[sn_widmung]
VG --> ALKIS[Sächsische ALKIS-WFS]
P41 --> FACH[Fachdienste aus Linkliste]
P41 --> API[VLN-Karten-API]
FACH --> GPKG[Verfahrens-GeoPackage]
REPO[Repository] --> QGIS
LINKS[Linkliste] -. derzeit nicht direkt angebunden .-> P41
DEFAULTS[Defaults] -. derzeit nicht direkt angebunden .-> BASIS
```
### Plugin-Zusammenspiel
- `sn_basis` ist eine zwingende Laufzeitabhängigkeit der Fachplugins. Diese importieren Klassen und Wrapper direkt aus `sn_basis`.
- `sn_basis` erzeugt das gemeinsame Menü und die Toolbar „LNO Sachsen“. Fachplugins registrieren dort ihre Aktionen mit Prioritäten.
- Der `DockManager` zeigt jeweils nur ein `sn_dock_*`-Dock an.
- Gemeinsam genutzte Tabs für Druck und Projekteigenschaften werden in die Fachplugin-Docks eingebaut.
- QGIS-Projektvariablen werden zentral mit dem Präfix `sn_` gespeichert, beispielsweise `sn_verfahrensnummer`, `sn_landkreis_kennzahl` und die ID des Verfahrensgebiet-Layers.
- Die Repository-Namen (`Plugin_SN_Basis` usw.) unterscheiden sich von den installierten QGIS-Paketnamen (`sn_basis`, `sn_plan41`, `sn_verfahrensgebiet`, `sn_widmung`). Die Release-ZIPs stellen die korrekten Paketordner her.
## Zentrale Basis: `Plugin_SN_Basis`
Das Basisplugin ist sowohl UI-Rahmen als auch gemeinsame Bibliothek.
Wesentliche Bausteine:
- QGIS-, Qt-, Datei-, Dialog-, Layer-, Style-, Netzwerk- und Variablen-Wrapper mit einem Mock-Modus für Tests ohne QGIS.
- Einheitliches Fehler- und Entscheidungsmodell über `pruef_ergebnis` und `Pruefmanager`.
- `DataGrabber` zur Klassifikation und Prüfung von Excel-, GeoPackage-/SQLite- und Dienstquellen.
- `Datenabruf` für WFS, ArcGIS REST und lokale OGR-Quellen; räumliche Filterung über Verfahrensgebiet, Puffer oder ohne Filter.
- `LayerLoader` und `Datenschreiber` zum Erzeugen, Filtern, Stylen, Speichern und erneuten Laden von Layern in GeoPackages.
- Zentrale Verfolgung des jeweils gewählten Verfahrensgebiet-Layers.
- Drucklogik für § 38, § 41 und „Anlage zur Baufreigabe“, einschließlich Druckbereich, Maßstab, Papierformat/Endlosrolle, Karten-Themen, Legende, Gitternetz und Atlas.
- Projekt- und Benutzerdaten wie Amt, Behörde, Landkreis, Sachgebiet, Verfahrensname und Verfahrensnummer.
Die UI enthält derzeit noch sichtbaren Entwicklungsballast: Beim Aufbau der gemeinsamen Navigation wird eine Aktion `TEST ACTION` angelegt und mehrere Debug-Ausgaben werden per `print()` geschrieben.
## Fachplugin `Plugin_SN_Plan41`
Das Plugin hat zwei fachliche Datenwege.
### Fachdaten aus Linkliste
Die mitgelieferte `assets/Linkliste.xlsx` enthält 44 Dienstzeilen. Das erwartete Schema lautet:
`ident`, `gruppe`, `kartenebene`, `inhalt`, `link`, `provider`, `stildatei`
Der Ablauf ist:
1. Eingabe-GeoPackage und Linkliste prüfen.
2. Linklistenzeilen einlesen und validieren.
3. Raumfilter bestimmen: Verfahrensgebiet, Pufferlayer oder ohne.
4. Fachdienste abrufen und lokal nach Geometrie/Extent filtern.
5. Ergebnislayer und QML-Stile in das Verfahrens-GeoPackage schreiben.
6. Layer ins Projekt laden und ein HTML-Protokoll erzeugen.
Die aktuelle Liste enthält vor allem Umwelt-, Gewässer-, Naturschutz-, Bauleitplanungs-, Energie- und Altlastendienste von LUIS Sachsen und RAPIS. Alle 44 zentral erfassten Einträge sind derzeit als ArcGIS-REST-Dienste gekennzeichnet; 24 haben einen zugeordneten QML-Stil.
### VLN-Karten-API
Das Plugin bindet `https://api.flurneuordnung-sachsen.de/v2` direkt ein:
- Login per E-Mail und Passwort, danach Authentifizierung über `X-API-Key`.
- Laden der verfügbaren Verfahren und des Plan-41-Datensatzes einer VKZ.
- GeoJSON-Daten werden in QGIS-Memory-Layer nach Punkt-, Linien- und Flächengeometrie getrennt.
- Arbeits-CRS ist EPSG:25833.
- Beim Upload werden alle zusammengehörigen Teillayer wieder zu einer FeatureCollection vereinigt; der PUT ersetzt den kompletten Serverbestand dieser VKZ.
- API-Key und E-Mail werden über `QSettings` lokal gespeichert, die VKZ in der QGIS-Projektdatei.
Der dokumentierte Datenbankentwurf in `doc/Datenbank_ERD.md` beschreibt unter anderem Maßnahmen, Konten, Ausbauarten und Akteure. Er ist derzeit eher fachlicher Zielentwurf; eine entsprechende Schema-Migration oder ORM-Schicht ist im Plugin nicht vorhanden.
## Fachplugin `Plugin_SN_Verfahrensgebiet`
Dies ist aktuell das größte und am aktivsten bearbeitete Fachplugin. Der jüngste Commit vom 02.09.2026 erweitert den Shape-Import um Mehrfachauswahl, temporäres ZIP-Entpacken und `FBSchl`-Prüfung.
Wesentliche Funktionen:
- Verfahrensgebiet über `BauRaumOderBodenordnungsrecht` aus dem sächsischen ALKIS laden, wahlweise per Verfahrensnummer oder Landkreis.
- Mehrfachauswahl und Zusammenführung mehrerer ALKIS-Objekte.
- Verfahrensgebiet aus einem oder mehreren Shapefiles bzw. ZIP-Archiven laden.
- Klassifikation der NoBIS-Shapes über `FBSchl` (`901/10110F` Neuordnungsgebiet, `901/10120F` Neuvermessungsgebiet).
- Gemarkungen und Flurstücke aus den vereinfachten bzw. vollständigen ALKIS-WFS laden.
- Verfahrensgebiet aus ausgewählten Flurstücken bilden sowie vorhandene Gebiete ergänzen, verkleinern oder ersetzen.
- Grenzpunkte/PunktortTA gemarkungsweise und in URL-sicheren Batches laden.
- Flächenvergleich zwischen ALKIS-/Shape-Gebiet und flurstücksbasiertem Gebiet.
- Umrings-, Knickpunkt- und Grenzpunktstatistik mit Ergebnislayern.
- Abgleich eines BI-Server-CSV-Exports mit den im Verfahrensgebiet beteiligten Flurstücken.
Externe Hauptquellen:
- Vereinfachtes ALKIS: `https://geodienste.sachsen.de/aaa/public_alkis/vereinf/wfs`
- ALKIS-NAS/SF: `https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs`
Die Implementierung berücksichtigt konkrete Diensteinschränkungen, etwa blockierte POST-/GML-Filter, URL-Längen und serverseitige Feature-Limits. Viele Operationen sind deshalb batching- und dialogorientiert.
## Weitere Repositories
### `Plugin_SN_Widmung`
Das Grundgerüst registriert sich korrekt im gemeinsamen Menü und verwendet das gemeinsame Dock. Die beiden Fachtabs enthalten aber nur Beispiel-Labels und Eingabefelder; produktive Widmungslogik ist noch nicht implementiert. Es gibt keine Tests.
### `Plugin_SN_FLT`, `Plugin_SN_Topografie`, `Plugin_SN_Template`
Diese Repositories enthalten jeweils nur Release-Workflow, Metadata-Vorlage, leere README und Changelog. Es fehlen insbesondere `__init__.py`, `main.py`, UI und Tests. Die Metadata-Vorlagen verwenden noch den Platzhalternamen `Plugin_SN_Neu`.
### `Defaults`
Enthält vier kleine JSON-Beispiele für Verfahren in den Bereichen `22` und `99` sowie einen Verfahrensgebiet-Stil. Im aktuellen Plugin-Code gibt es keine direkte Referenz auf dieses Repository; es ist derzeit eher ein externer Datenvorrat bzw. Prototyp.
### `Linkliste`
`linkliste.csv` ist die manuell zu pflegende Quelle. Eine Gitea Action erzeugt daraus XML, JSON und YAML und committed die Ergebnisse zurück nach `main`. Die Plan41-Laufzeit verwendet derzeit jedoch nicht dieses Repository, sondern eine separat mitgelieferte Excel-Datei. Damit existieren zwei zu synchronisierende Wahrheiten.
### `Repository`
Stellt drei QGIS-Plugin-Feeds bereit:
- `plugins.xml`: stable, aktuell leer.
- `plugins-testing.xml`: Basis 26.7.2, Plan41 26.7.2 und Verfahrensgebiet 26.7.3.
- `plugins-unstable.xml`: FLT 0, Widmung 4711, Plan41 26.7.2, Basis 26.8.3 und Verfahrensgebiet 26.8.3.
Die eigentliche Release-Orchestrierung liegt auf den separaten Branches `hidden/workflows` und `hidden/templates`. Ein `v*`-Tag in einem Plugin-Repository löst dessen Workflow aus. Aus dem Tagsuffix wird der Kanal bestimmt, das Plugin als korrekt benannter ZIP-Ordner gepackt, als Gitea Release veröffentlicht und der passende XML-Feed aktualisiert.
## Entwicklungs- und Teststand
Die vorhandenen Tests wurden in einer temporären Kopie unter Python 3.14 im eingebauten QGIS-/Qt-Mock-Modus ausgeführt. Das ist ein schneller Logiktest, ersetzt aber keinen Test in QGIS 3.40/OSGeo4W.
| Plugin | Ergebnis | Einordnung |
|---|---:|---|
| Basis | 87 Tests: 86 bestanden, 1 Fehler | Aufruferpräfix-Erkennung erwartet im Test `sn`, erkennt aber `test` |
| Plan41 | 44 Tests: 43 bestanden, 1 Fehler | Ein UI-Test initialisiert die neu hinzugekommene VLN-Statusanzeige nicht vollständig |
| Verfahrensgebiet | 201 Tests: 157 bestanden, 15 fehlgeschlagen, 21 Fehler, 8 übersprungen | Viele Tests erwarten noch ältere Einzeldatei-/Einzellayer- und WFS-Mock-Schnittstellen |
Für Widmung, FLT, Topografie und Template gibt es keine Test-Suiten. Es existiert außerdem kein Push-/Pull-Request-CI-Workflow, der die Tests automatisch ausführt; die vorhandenen Plugin-Workflows dienen ausschließlich Releases.
Offene öffentliche Vorgänge zum Stichtag:
- Basis: 17, Schwerpunkt Drucklayout, Atlas, Legende, Schriftfeld und weitere Fachprüfungen.
- Plan41: 6, unter anderem Fachdatenumfang, Pipeline-Meldungen und Planstand-Versionierung.
- Verfahrensgebiet: 12, aktuell besonders Shape-Laden, Beteiligungsabgleich, doppelte Flurstücke, BROBOR-Manipulation und `openpyxl` unter Linux.
- Test-Action: 1; alle anderen Repositories: 0.
## Auffälligkeiten und Risiken
1. **Entwicklungsreife:** Der produktive stable-Feed ist leer; die wichtigen Plugins sind offiziell experimentell und werden hauptsächlich aus unstable entwickelt.
2. **Rote Tests:** Vor weiterer Funktionsentwicklung sollte geklärt werden, welche Fehler echte Regressionen und welche lediglich veraltete Tests nach den jüngsten Schnittstellenänderungen sind.
3. **Implizite Basisabhängigkeit:** Plan41, Verfahrensgebiet und Widmung starten ohne `sn_basis` nicht. Diese Abhängigkeit und die notwendige Ladereihenfolge sind in den Metadata-Dateien nicht als Plugin-Abhängigkeit deklariert.
4. **Nicht reproduzierbare Python-Abhängigkeiten:** Es gibt weder `requirements.txt` noch `pyproject.toml`. `openpyxl` ist ein harter Import der Basis, `requests` ein optionaler Fallback. Ein offenes Linux-Issue bestätigt bereits Probleme mit fehlendem `openpyxl`.
5. **Doppelte Linklistenpflege:** Das zentrale Linklisten-Repository und die eingebettete Plan41-Exceldatei sind technisch nicht verbunden. Änderungen können auseinanderlaufen.
6. **Release-/Metadata-Inkonsistenzen:** Die Plan41-Metadata verweist als Quellrepository auf `Plugin_Test_Action`; das XML-Template hängt an bereits vollständige Tracker-URLs nochmals `/issues` an; FLT zeigt noch auf `Plugin_SN_Neu`; die Widmungs-Version `4711-unstable` passt nicht zum letzten fachlichen Tag `26.3.1-unstable`.
7. **Branch-/Metadata-Mischung:** Auf den ausgecheckten unstable-Branches tragen Basis, Plan41 und Verfahrensgebiet teilweise noch testing-Versionen bzw. testing-URLs in `metadata.txt`. Der Release-Prozess generiert diese Datei neu, der Quellstand selbst ist dadurch aber missverständlich.
8. **Dokumentationslücken:** Die meisten README-Dateien sind leer oder nur ein Zweizeiler. Im Repository-README heißen die Dateien in der Tabelle irrtümlich `plugins_testing.xml` und `plugins_unstable.xml`, tatsächlich verwenden sie Bindestriche.
9. **Kein organisationsweiter Versions-Lock:** Die elf Repositories werden unabhängig versioniert. Es gibt kein Manifest/Superprojekt, das eine nachweislich kompatible Kombination von Commitständen festhält.
10. **Laufzeit und Integration:** Umfangreiche WFS-Abrufe und räumliche Verarbeitung laufen weitgehend im QGIS-Prozess mit blockierenden Requests und manuell gepumpten Fortschrittsdialogen. Reale QGIS-Integrationstests sind daher besonders wichtig.
11. **Lokale Zugangsdaten:** Der VLN-API-Key wird über `QSettings` persistiert. Je nach Plattform ist das eine normale Konfigurationsdatei bzw. Registry und kein sicherer Schlüsselspeicher.
## Sinnvolle nächste technische Schritte
1. Einen reproduzierbaren Entwicklungsaufbau für QGIS 3.40 mit den Paketnamen `sn_*` dokumentieren.
2. Die drei aktuellen Test-Suiten auf einen grünen unstable-Ausgangsstand bringen und als Gitea CI bei Push/Pull Request ausführen.
3. Basisabhängigkeit, Python-Abhängigkeiten und unterstützte QGIS-/Python-Versionen explizit festhalten.
4. Metadata- und Release-Feed-Inkonsistenzen korrigieren und Releases automatisiert validieren.
5. `Linkliste` zur einzigen Quelle machen und daraus die vom Plugin benötigte Laufzeitdatei erzeugen.
6. Einen kompatiblen organisationsweiten Stand per Manifest oder Meta-Repository festhalten.
7. Danach die nächste Fachfunktion gezielt in Plan41 oder Verfahrensgebiet umsetzen.
@@ -0,0 +1,251 @@
# Positionierung des Alternativansatzes zu AG_QGIS
Stand: 2. September 2026
Verglichen wurden:
- der bisherige Projektstand in `AG_QGIS` gemäß `AG_QGIS/GESAMTUEBERBLICK.md`,
- das sechsseitige Dokument `Alternativansatz/Konzept LNO Verfahren in QGIS.pdf`,
- der zugehörige Prototyp `Alternativansatz/lnofachschale_plugin`.
## Ergebnis in einem Satz
Der Alternativansatz ist kein fachlich gleichwertiger Ersatz für AG_QGIS, sondern ein früher Architekturprototyp für eine **zentrale, versionierte und konfigurationsgetriebene GeoPackage-Fachschale**. Diese Idee kann das bestehende Projekt sinnvoll ergänzen; die bestehende modulare Plugin- und Fachlogik sollte sie aber nicht ersetzen.
## Grundpositionierung
AG_QGIS denkt primär von **Fachaufgaben und Plugins** aus:
- gemeinsame Plattform in `sn_basis`,
- getrennte Fachplugins für Verfahrensgebiet, Plan41, Widmung usw.,
- Daten werden für einen konkreten Arbeitsablauf geladen, geprüft, bearbeitet, gedruckt oder in ein GeoPackage geschrieben,
- komplexe Fachlogik liegt im jeweils zuständigen Fachplugin.
Der Alternativansatz denkt primär von **einem dauerhaften Datencontainer** aus:
- ein einzelnes Plugin,
- ein GeoPackage als zentrale „Fachschale“ eines Verfahrens,
- ein JSON beschreibt die vorgesehenen Layer und ihre Version,
- ein zweites JSON beschreibt auswählbare Webdienste und deren Ziellayer,
- das Plugin erzeugt bzw. erweitert das GeoPackage und lädt die darin enthaltenen Layer strukturiert in QGIS.
Damit beantworten beide Ansätze unterschiedliche Fragen:
| Ansatz | Leitfrage |
|---|---|
| AG_QGIS | Welche fachliche Aufgabe soll der Anwender ausführen? |
| Alternativansatz | In welcher einheitlichen, fortschreibbaren Struktur sollen die Verfahrensdaten liegen? |
Diese Perspektiven sind grundsätzlich kombinierbar.
## Vergleich der wesentlichen Merkmale
| Aspekt | AG_QGIS | Alternativansatz |
|---|---|---|
| Organisation | Elf getrennte Repositories | Ein kopierter Plugin-Prototyp ohne eigenes Git-Repository |
| Architektur | Plattformplugin plus Fachplugins | Ein monolithisches Fachschalen-Plugin |
| Zentraler Baustein | `sn_basis` als Code- und UI-Plattform | Ein GeoPackage als Daten- und Projektplattform |
| Fachlogik | Umfangreiche spezialisierte Workflows | Generisches Erzeugen und Befüllen von Layern |
| Datenmodell | Teilweise dynamisch und workflowbezogen; Plan41-ERD als Entwurf | Layerstruktur deklarativ in `geopackage_schema.json` |
| Schemaänderungen | Keine organisationsweite GeoPackage-Migration | Einfacher Versionsvergleich und additive Schemaergänzung |
| Dienstekatalog | 44 Einträge; zentrale CSV plus separate Plugin-XLSX | 23 Einträge direkt in `services_config.json` |
| Dienstimport | WFS, REST/ArcGIS und OGR, mehrere Raumfilter, Protokollierung | WFS/ArcGIS, BBOX-Vorfilter und lokaler räumlicher Filter |
| Verfahrensgebiet | Eigenes großes Fachplugin mit ALKIS-, Shape-, Flurstücks- und Prüfworkflows | Verfahrensumring wird aus einer vorhandenen Vektordatei importiert |
| Plan § 41 | Fachdatenpipeline, Druck, VLN-API, GeoJSON-Laden und -Upload | Nicht umgesetzt |
| Druck/Atlas | Umfangreich in `sn_basis` | Nicht umgesetzt |
| Projekteigenschaften | QGIS-Projektvariablen und gemeinsamer Einstellungstab | Leere GeoPackage-Tabelle `projekt_info` als Ansatz |
| Zusammenarbeit | VLN-API für zentralen Plan41-Datenbestand vorhanden | GeoPackage lokal; Postgres/Oracle nur als offene Konzeptfrage erwähnt |
| Veröffentlichung | Gitea-Releases und stable/testing/unstable-Feeds | Keine Release- oder Repository-Integration |
| Umfang | Rund 29.500 Python-Zeilen in den drei Hauptplugins einschließlich Tests | 2.491 Python-Zeilen einschließlich generierter Tests |
| Reife | Aktive Entwicklung mit vielen Fachfunktionen, aber unstable und rote Tests | Machbarkeitsnachweis Version 0.1 |
## Was der Alternativansatz konkret umsetzt
### 1. Erzeugen einer Fachschale
Aus `geopackage_schema.json` werden sechs Layer bzw. Tabellen erzeugt:
- `wka_punkte`
- `wka_linien`
- `gewaesser_linien`
- `einzugsgebiete_flaechen`
- `projekt_info`
- `verfahrensumring`
Die Konfiguration enthält Layername, Titel, Gruppe, Geometrietyp und Namen einer QML-Stildatei. Eine Tabelle `fachschale_info` speichert die Schema-Version, aktuell `0.0.1`. Die Layer werden in einem nach GeoPackage und Version benannten QGIS-Layerbaum gruppiert.
### 2. Einfaches Schema-Update
Beim Laden eines GeoPackages vergleicht das Plugin dessen gespeicherte Version mit der im Plugin enthaltenen JSON-Version. Bei Abweichung können fehlende Tabellen und optional konfigurierte Felder ergänzt werden. Vorhandene Tabellen und Daten sollen erhalten bleiben.
### 3. Import des Verfahrensumrings
Eine Vektordatei wird in den vorbereiteten Layer `verfahrensumring` importiert. Dabei werden:
- Geometrietypen geprüft,
- Geometrien in das Ziel-CRS transformiert,
- Single- zu Multi-Geometrien angehoben,
- fehlende Attribute dynamisch ergänzt,
- Features in Batches geschrieben.
### 4. Konfigurierter Webdienstimport
Ein Dialog zeigt 23 Dienste aus vier Kategorien. Ausgewählte Dienste werden in das jeweils konfigurierte Ziellayer geschrieben. Optional soll der Verfahrensumring als räumliche Begrenzung dienen. Vor dem lokalen Filter wird eine um 500 Meter vergrößerte BBOX an den Dienst gegeben.
## Inhaltliche Überschneidung der Dienstekataloge
Der Alternativansatz greift große Teile derselben LUIS-Dienste auf wie die heutige Plan41-Linkliste:
- 18 der 23 URLs stimmen exakt mit Einträgen der 44-zeiligen AG_QGIS-Linkliste überein.
- Zwei weitere WRRL-Themen entsprechen inhaltlich vorhandenen Einträgen, verwenden aber eine abweichende URL mit explizitem Layer `/0`.
- Drei Denkmalschutz-WFS-Layer (`L1`, `L2`, `L3`) sind im Alternativansatz zusätzlich enthalten.
Die Benennung mehrerer ArcGIS-Layer widerspricht sich jedoch zwischen den Ansätzen. Beispiele:
- dieselbe WKA-URL `/2` heißt in AG_QGIS „Windkraftanlagen-vorBetrieb“, im Alternativansatz „Windkraftanlagen (Linien)“;
- Einzugsgebiets-Layer `/0` und `/1` werden einmal als Haupt-/Teileinzugsgebiete, einmal als Linien-/Flächenebene interpretiert;
- Natura-2000-Layer werden einmal nach fachlichem Inhalt, einmal nach vermeintlicher Geometriefamilie benannt.
Vor einer Übernahme muss daher die tatsächliche Bedeutung und Geometrie jedes Server-Layers gegen dessen Dienstmetadaten geprüft werden. Die Zuordnung darf nicht allein aus der Layernummer abgeleitet werden.
## Die wesentlichen konzeptionellen Unterschiede
### Fachbedeutung statt bloßer Dienstkopie
Der Alternativansatz möchte Layer langfristig nach stabilen Fachbedeutungen organisieren. Das ist konzeptionell stärker als ein GeoPackage, dessen Tabellen unmittelbar aus wechselnden Dienstnamen entstehen. Es schafft einen möglichen Datenvertrag zwischen Plugins, Projekten und Auswertungen.
Der aktuelle Prototyp erreicht diesen Anspruch allerdings noch nicht vollständig: Beim Dienstimport werden sämtliche neu gefundenen Quellfelder automatisch und als Textspalten in das Ziellayer übernommen. Dadurch bestimmt weiterhin der externe Dienst das lokale Schema, ohne dass sich die deklarierte Schema-Version ändert. Die im Konzept ausdrücklich aufgeworfene Frage „Welche Attribute benötigen wir wirklich?“ ist somit noch nicht gelöst.
### Deklaratives Schema statt fest verdrahteter Workflows
Neue Layer lassen sich im Alternativansatz grundsätzlich per JSON hinzufügen. AG_QGIS benötigt für neue Fachfunktionen meist Code in einem Fachplugin. Umgekehrt kann JSON nur Struktur und Zuordnung beschreiben; es ersetzt keine Fachlogik wie:
- ALKIS-Suche nach Verfahrensnummer/Landkreis,
- Beteiligungsabgleich,
- Knickpunkt- und Umringsprüfung,
- Flächenvergleich und Korrektur,
- Drucklayout und Atlas,
- VLN-API-Synchronisation.
### Monolith gegen Modulsystem
Ein einzelnes Plugin ist zunächst leichter zu installieren und kann eine einheitliche Fachschale erzwingen. Mit wachsendem Funktionsumfang drohen jedoch starke Kopplung und ein großer gemeinsamer Release-Zyklus. Das bestehende Plattform-/Fachplugin-Modell trennt Verantwortlichkeiten besser, leidet derzeit aber unter einer nur implizit deklarierten Basisabhängigkeit und unabhängigen Versionsständen.
### GeoPackage als Primärmodell
Im Alternativansatz ist das GeoPackage die maßgebliche Projektinstanz. In AG_QGIS ist es derzeit eher ein wichtiges Arbeits- und Ausgabeformat neben QGIS-Projektvariablen, Memory-Layern, ALKIS-Diensten und der VLN-API.
Diese Festlegung berührt unmittelbar die Mehrbenutzerfrage. Ein lokales GeoPackage eignet sich gut für einen Bearbeiter und transportierbare Verfahren, aber nicht für gleichzeitiges verteiltes Schreiben. AG_QGIS besitzt mit der VLN-API bereits einen Ansatz für zentrale Zusammenarbeit, den der Alternativansatz nicht abbildet.
### Umgang mit Geometrietypen
Das Konzept erwägt, Fachbedeutungen nicht unnötig nach Geometrietyp aufzuteilen. Für QGIS-/GeoPackage-Vektorlayer ist ein eindeutig deklarierter Geometrietyp jedoch der robuste Normalfall. Wenn eine Fachbedeutung tatsächlich Punkte, Linien und Flächen enthalten kann, ist die im aktuellen Plan41-API-Modul verwendete Aufteilung nach Geometriefamilie technisch sinnvoll. Vereinfachen sollte man daher nur nach fachlicher Prüfung, nicht als allgemeine Regel.
## Stärken des Alternativansatzes
1. **Explizites Ziel-Datenmodell:** Er macht die bisher offene Frage nach einer organisationsweit stabilen Fachschale konkret.
2. **Version im Datenbestand:** Ein Verfahren kann erkennen, zu welchem Schema es gehört.
3. **Konfiguration statt Code für Katalogdaten:** Layergruppen, Dienstzuordnungen und Styles sind prinzipiell außerhalb der Fachlogik pflegbar.
4. **Additive Weiterentwicklung:** Fehlende Tabellen und Felder können ergänzt werden, ohne vorhandene Fachdaten pauschal neu aufzubauen.
5. **Klarer Anwenderablauf:** Fachschale anlegen, Umring importieren, Dienste auswählen und laden.
6. **Datennahe Layerorganisation:** Die QGIS-Layergruppen werden reproduzierbar aus dem Schema aufgebaut.
7. **Neue Quellen:** Die drei Denkmalschutz-WFS-Ebenen sind ein möglicher fachlicher Zusatz zur heutigen Linkliste.
## Grenzen und Risiken des Prototyps
### Fachliche und konfigurative Lücken
1. Von 23 konfigurierten Diensten besitzen nur vier ein passendes Ziellayer im sechs Layer umfassenden Schema. Die übrigen 19 Importe scheitern planmäßig.
2. Der Ordner `styles` ist leer, obwohl fünf Schemaeinträge QML-Dateien referenzieren. Die im Konzept gezeigte Ausgestaltung ist im übergebenen Stand nicht reproduzierbar.
3. `projekt_info` enthält keine definierten Fachfelder. Projekteigenschaften werden noch nicht gespeichert oder bearbeitet.
4. Der Schemaentwurf enthält überhaupt keine `fields`-Definitionen. Attribute entstehen ausschließlich beiläufig beim Import.
5. Diensttitel und angenommene Geometrien widersprechen teilweise dem heutigen AG_QGIS-Katalog und müssen fachlich validiert werden.
### Schema- und Datensicherheit
1. Die Updatefunktion ist keine echte Migrationsengine. Sie kennt keine geordneten Schritte, Umbenennungen, Typänderungen, Löschungen, Rollbacks oder Migrationshistorie.
2. Jede Versionsabweichung löst ein Update aus; auch ein GeoPackage mit neuerer Version würde auf die im Plugin enthaltene Version zurückgestempelt.
3. Fehler beim Anlegen einzelner Layer werden teilweise nur protokolliert; anschließend kann die neue Gesamtversion trotzdem gespeichert und Erfolg gemeldet werden.
4. Die automatische Erkennung identifiziert jedes geladene OGR-GeoPackage als potenzielle Fachschale. Dadurch kann das Plugin auch für fachfremde GeoPackages ein Update anbieten oder beim manuellen Update Tabellen ergänzen.
5. Beim Neuanlegen wird eine vorhandene, nicht geladene Zieldatei vor dem erfolgreichen Neuaufbau gelöscht. Backup und transaktionaler Ersatz fehlen.
6. Beim Dienstimport wird das Ziellayer vollständig geleert und ersetzt. Liefert der Dienst dagegen null Treffer, kehrt die Funktion vor dem Leeren zurück; alte Daten bleiben dann unbemerkt erhalten.
### Räumliche Verarbeitung
1. Der Dialog verspricht „Geometrien exakt auf Verfahrensumring zuschneiden“. Implementiert wird aber nur ein `intersects`-Filter gegen einen um einen Meter gepufferten Umring; die Geometrien selbst werden nicht geschnitten.
2. Das Standard-GeoPackage wird in EPSG:25832 erzeugt, während WFS-Verbindungen fest EPSG:25833 anfordern und AG_QGIS für Plan41 ebenfalls EPSG:25833 verwendet. Das kann transformiert werden, muss aber als verbindliche LNO-Entscheidung geklärt werden.
3. Neu erkannte Dienstfelder werden per OGR pauschal als Text angelegt. Zahlen-, Datums- und Boolesche Typen gehen als Datenmodell verloren.
4. Mehrere gleichzeitig geladene Fachschalen sind nicht eindeutig adressiert: ohne aktiven Layer wird der erste Pfad aus einer ungeordneten Menge verwendet; der Verfahrensumring wird nur über den sichtbaren Layernamen gesucht.
### Auslieferung und Qualitätssicherung
1. `Makefile` und `pb_tool.cfg` paketieren nur die drei Python-Dateien im Wurzelverzeichnis. Die zwingend benötigten Ordner `core`, `gui`, `config` und `styles` fehlen in `EXTRA_DIRS`; ein so gebautes Plugin startet nicht.
2. Der Ordner ist kein Git-Repository. Versionshistorie, nachvollziehbare Herkunft und ein reproduzierbares Paket fehlen.
3. Die Metadata sind weitgehend Plugin-Builder-Platzhalter (`about`, Tracker, Repository, Homepage) und geben QGIS 3.0 als Minimum an, obwohl der Code deutlich neuere APIs verwendet.
4. Es gibt keine expliziten Abhängigkeits- oder Umgebungsangaben für QGIS/GDAL/OGR.
5. Die fünf Tests stammen überwiegend unverändert aus dem QGIS Plugin Builder. Es gibt keine Tests für GeoPackage-Erzeugung, Migration, Dienstimport oder Vektorimport.
6. Das Makefile unterdrückt Testfehler ausdrücklich mit `|| true`; ein fehlerhafter Testlauf kann den Build daher nicht stoppen.
7. Die aktuelle Umgebung besitzt kein importierbares QGIS-Python-Modul. Deshalb waren nur ein erfolgreicher Metadata-Test und eine statische Syntaxprüfung aller 16 Python-Dateien möglich, kein realer Funktionstest.
8. Generierte `__pycache__`-Dateien liegen im übergebenen Quellordner; eine Bereinigung und `.gitignore` fehlen.
## Empfohlene gemeinsame Zielarchitektur
Die beste Kombination wäre: **bestehende modulare Fachplugins behalten, die Fachschale als gemeinsamen versionierten Datenvertrag ergänzen.**
```mermaid
flowchart TD
QGIS --> BASIS[sn_basis: Plattform und gemeinsame Dienste]
BASIS --> SCHEMA[Schema-Registry und Migrationen]
BASIS --> CATALOG[zentraler Dienstekatalog]
BASIS --> GPKG[versionierte Verfahrens-Fachschale.gpkg]
VG[sn_verfahrensgebiet] --> BASIS
P41[sn_plan41] --> BASIS
WID[sn_widmung] --> BASIS
FLT[sn_flt / Topografie] --> BASIS
VG --> GPKG
P41 --> GPKG
WID --> GPKG
CATALOG --> P41
GPKG <--> API[VLN-API / spätere Mehrbenutzer-Datenbank]
```
Dabei sollte gelten:
- `sn_basis` stellt Schemaerkennung, Migration, Backup, Transaktionen und einen gemeinsamen Layerkatalog bereit.
- Jedes Fachplugin kann einen eigenen, versionierten Schemaabschnitt und eigene Migrationen beitragen.
- Das GeoPackage besitzt eine eindeutige Fachschalen-Kennung, nicht nur eine Version. Fremde GeoPackages werden nie automatisch verändert.
- Schemaänderungen erfolgen über geordnete Migrationen (`0.1 -> 0.2 -> 0.3`) mit Protokoll, Validierung und Rollback/Backup.
- Felder, Typen, Pflichtwerte, Schlüssel und Relationen werden explizit definiert. Externe Dienstattribute werden über ein konfiguriertes Mapping übernommen, nicht pauschal als Text.
- Die zentrale `Linkliste` wird zur einzigen Dienstquelle. Daraus können JSON für die UI und bei Bedarf XLSX/XML/YAML generiert werden.
- Dienstkonfiguration und Zielschema werden automatisiert gegeneinander validiert: kein Dienst ohne Ziellayer, kein Geometriekonflikt, keine fehlende Style-Datei.
- Verfahrensgebiet, Plan41, Druck und Prüfungen bleiben Fachlogik in ihren bestehenden Plugins.
- Lokales GeoPackage und zentrale VLN-API erhalten klar getrennte Rollen: Offline-/Arbeitskopie einerseits, Mehrbenutzer- und Austauschsystem andererseits.
## Bewertung der Übernahmeideen
| Idee aus dem Alternativansatz | Bewertung | Empfohlene Verwendung |
|---|---|---|
| Versioniertes GeoPackage | Sehr wertvoll | Als gemeinsamer Datenvertrag in `sn_basis` neu und migrationssicher umsetzen |
| Deklaratives Layerschema | Wertvoll | Um Felder, Typen, Schlüssel, CRS und Zuständigkeit je Fachplugin erweitern |
| JSON-Dienstekatalog | Wertvoll | Mit dem bestehenden `Linkliste`-Repository zusammenführen, keine dritte Quelle schaffen |
| Dienst → festes Ziellayer | Wertvoll | Nur mit validiertem Attribut- und Geometriemapping |
| Reproduzierbarer Layerbaum | Gut übernehmbar | Als gemeinsame Ladefunktion in `sn_basis` |
| Ein einziges Gesamtplugin | Nicht empfohlen | Bestehende Fachplugin-Trennung beibehalten |
| Einfacher Versionsvergleich | Für Produktion unzureichend | Durch echte inkrementelle Migrationen ersetzen |
| Automatische Erweiterung aller GPKGs | Gefährlich | Nur explizit markierte Fachschalen zulassen |
| Alle Quellattribute dynamisch übernehmen | Nicht empfohlen | Fachlich ausgewählte und typisierte Zielfelder definieren |
| Lokales GeoPackage als alleinige Wahrheit | Nur für Einzelplatz geeignet | Mit VLN-API bzw. später PostgreSQL für Mehrbenutzerfälle kombinieren |
## Empfohlene nächste Entscheidung
Vor weiterer Implementierung sollte fachlich entschieden werden, ob eine **verbindliche, versionierte Verfahrens-Fachschale** das gemeinsame Zielbild ist. Wenn ja, sollte nicht der Prototyp direkt in AG_QGIS kopiert werden. Sinnvoller ist eine kleine gemeinsame Spezifikation mit:
1. Fachbedeutungen und verantwortlichem Fachplugin,
2. Layern und Geometrietypen,
3. benötigten Attributen, Typen, Schlüsseln und Relationen,
4. verbindlichem CRS,
5. lokaler versus zentraler Datenhoheit,
6. Migrations- und Abwärtskompatibilitätsregeln,
7. kanonischem Dienstekatalog.
Auf dieser Grundlage kann die starke Grundidee des Alternativansatzes in die bestehende, deutlich weiter entwickelte AG_QGIS-Architektur integriert werden, ohne die bereits vorhandenen Fachfunktionen und die Release-Struktur neu bauen zu müssen.
+221
View File
@@ -0,0 +1,221 @@
# Projektanalyse: Aufbau und Zusammenspiel der Komponenten
Momentaufnahme vom 02.09.2026 (alle Repos auf ihrem Default-Branch, siehe `00_gesamtueberblick_2026-09-02.md`).
Zielgruppe: neue Entwickler und KI-Agenten, die das Gesamtbild brauchen, bevor sie einzelne Dateien öffnen.
Die lebenden Referenzen dazu sind `../architektur/modulkarte.md` und `../architektur/schnittstellen.md`.
## 1. Kurzfassung
- Das Vorhaben ist eine **Plugin-Familie** für QGIS: `sn_basis` als Plattform (UI-Rahmen, Wrapper, Prüfer, Datenmodule,
Druck) und Fachplugins, die sich beim Start in Menü und Toolbar von `sn_basis` einhängen. Produktiv ausgebaut sind
`sn_verfahrensgebiet` und `sn_plan41`; `sn_widmung` ist ein Gerüst, drei weitere Repos sind Platzhalter.
- Die Architekturidee ist tragfähig: Wrapper-Schicht mit Mock-Modus, einheitliches Prüfmodell, gemeinsame Tabs,
klare Registrierung der Fachplugins. Sie ist aber **nur im Code** dokumentiert und an einigen Stellen aufgeweicht
(Fachlogik in UI-Dateien, Rückwärtsimport, doppelte Variablennamen, zwei Linklisten).
- Die Codequalität entspricht dem Entstehungsprozess: fachlich fundiert, in KI-erzeugten Fragmenten gewachsen,
mit sehr großen Dateien (zehn Dateien über 600 Zeilen, Spitze 1 674), verbreiteten `try/except: pass`-Mustern,
Debug-`print`, auskommentiertem Code und uneinheitlicher Namensgebung.
- Es gibt **umfangreiche Tests im Mock-Modus** (332 Tests), die ohne QGIS in unter einer Sekunde laufen. Sie sind
der wichtigste Hebel für KI-gestützte Arbeit, aber derzeit nicht vollständig grün und nicht in CI.
- Prozessseitig existiert bereits ein funktionierender Kern: Feature-Branches, Pull Requests auf `unstable`,
Issues mit Labels, dreistufiger Release-Prozess mit Gitea Actions. Es fehlen Test-CI, Abhängigkeitsdeklaration,
Dokumentation und ein organisationsweiter Versionsstand.
## 2. Bestand
| Repository | Paket | Rolle | Reife | Python-Dateien / Tests |
|---|---|---|---|---|
| `Plugin_SN_Basis` | `sn_basis` | Plattform und Bibliothek | aktiv, unstable | 40 / 13 Testdateien (87 Tests) |
| `Plugin_SN_Verfahrensgebiet` | `sn_verfahrensgebiet` | Fachplugin Verfahrensgebiet | aktiv, unstable | 17 / 12 (201 Tests) |
| `Plugin_SN_Plan41` | `sn_plan41` | Fachplugin Plan § 41 | aktiv, unstable | 10 / 5 (44 Tests) |
| `Plugin_SN_Widmung` | `sn_widmung` | Fachplugin Widmung | Gerüst | 5 / 0 |
| `Plugin_SN_FLT`, `_Topografie`, `_Template` | | Platzhalter | leer | 0 |
| `Plugin_Test_Action` | | Testplugin für Release | Werkzeug | 1 |
| `Repository` | | Plugin-Feeds, Release-Orchestrierung | Werkzeug | |
| `Linkliste` | | Dienstekatalog CSV → XML/JSON/YAML | Daten | |
| `Defaults` | | Beispiel-Verfahrensdaten, ein QML | Daten, unreferenziert | |
Beteiligte laut Git-Historie: Daniel Helbig (LK Meißen, Hauptentwickler der Fachlogik), Michael Otto (LK Mittelsachsen,
Release-Infrastruktur, Linkliste, seit 08/2026 auch Fachlogik), Erik Sefkow (VLN Sachsen, VLN-API), Issue-Autoren
`22munzke` und „Kai“ (Copilot-Nutzer, Issue Basis #36). Entwicklungsschub 0304/2026 (92 Commits in `Plugin_SN_Basis`),
seitdem gleichmäßig 1011 Commits pro Monat.
Der Ordner `Alternativansatz/` (außerhalb dieses Workspace) enthält einen unabhängigen Prototyp „LNO-Fachschale“;
seine Bewertung steht in `00_vergleich_alternativansatz_2026-09-02.md`. Kurz: die Idee eines versionierten
GeoPackage-Datenvertrags ist wertvoll, der Prototyp selbst ist kein Ersatz für die Plugin-Familie.
## 3. Architektur zur Laufzeit
```mermaid
flowchart TB
subgraph QGIS["QGIS 3.40 (Qt5) / QGIS 4 (Qt6)"]
subgraph BASIS["sn_basis Plattform"]
NAV["ui: Navigation, DockManager, BaseDockWidget, PrintTab, SettingsTab"]
MOD["modules: Pruefmanager, pruef_ergebnis, *pruefer, DataGrabber, Datenabruf, LayerLoader, Datenschreiber, print_*"]
FUN["functions: qt_/qgiscore_/qgisui_/dialog_/message_/variable_wrapper, verfahrensgebiet_manager"]
NAV --> MOD --> FUN
end
VG["sn_verfahrensgebiet"] --> NAV
VG --> MOD
VG --> FUN
P41["sn_plan41"] --> NAV
P41 --> MOD
P41 --> FUN
WID["sn_widmung (Gerüst)"] --> NAV
MOD -. Lazy-Import Listenauswerter .-> P41
FUN --> PROJ[("QGIS-Projekt: Variablen sn_*, Layer")]
end
VG --> ALKIS["ALKIS-WFS Sachsen"]
P41 --> LUIS["LUIS/RAPIS ArcGIS REST, WFS"]
P41 --> VLN["VLN-Karten-API v2"]
P41 --> GPKG[("Verfahrens-GeoPackage")]
```
### Schichten in `sn_basis`
1. **functions/ Wrapper.** Jede Qt-, QGIS- und OS-Abhängigkeit geht durch ein Wrapper-Modul, das bei fehlendem QGIS
Mock-Klassen bereitstellt. Das ist die Grundlage dafür, dass Tests und KI-Agenten ohne QGIS arbeiten können, und
gleichzeitig die Brücke zwischen Qt5 und Qt6. `qgiscore_wrapper.py` (1 122 Zeilen) und `qt_wrapper.py` (796)
enthalten zusätzlich Fachhelfer (`create_buffer_layer`, `polygon_zu_linie`), die dort nicht hingehören.
2. **modules/ Prüfen, Laden, Schreiben, Drucken.** Einheitliches Ergebnisobjekt `pruef_ergebnis` mit Aktionscode;
`Pruefmanager` entscheidet über Dialoge oder Standardantworten. Die Datenpipeline (`DataGrabber``Datenabruf`
`LayerLoader``Datenschreiber`) ist generisch gedacht, wird aber nur von Plan41 vollständig genutzt.
Die Druckmodule (2 800 Zeilen) sind der größte zusammenhängende Block.
3. **ui/ Rahmen.** Navigation (Menü/Toolbar), DockManager (nur ein Dock sichtbar), BaseDockWidget (Tabs aus
Klassenattribut), gemeinsame Tabs Druck und Projekteigenschaften.
### Fachplugins
Beide großen Fachplugins folgen demselben Muster: `main.py` (Registrierung, Services), `ui/dockwidget.py` (Tabs),
ein großer Arbeits-Tab (`working_tab.py` 873 Zeilen bzw. `tab_a_ui.py` + `tab_a_logic.py` 1 872 Zeilen) und
Fachfunktionen in `functions/` bzw. `modules/`. Der Verfahrensgebiet-Tab enthält viel Ablaufsteuerung, die eigentlich
in die Fachfunktionen gehört; Plan41 hat Logik und UI bereits getrennt.
## 4. Zusammenspiel im Detail
### 4.1 Start und Registrierung
QGIS instanziiert alle Plugins; die Ladereihenfolge ist nicht garantiert. `sn_basis.initGui()` baut die Navigation und
ruft anschließend `initGui()` **aller** Plugins mit Präfix `sn_` nach. Ein Fachplugin registriert in `initGui()` eine
Action mit Priorität (Verfahrensgebiet 10, Plan41 20, Widmung 30), erzeugt beim Klick sein Dock (`sn_dock_<name>`) und
übergibt es an `DockManager.show()`. Sequenzdiagramm und Vertrag: `schnittstellen.md` Abschnitt 12.
Bewertung: funktioniert, ist aber fragil. Die Abhängigkeit von `sn_basis` ist in keiner `metadata.txt` deklariert;
QGIS kann die Fachplugins ohne Basis aktivieren, und sie scheitern dann still. Mehrfaches `initGui()` erzeugt neue
Action-Objekte, alte Referenzen bleiben im Fachplugin bis zum nächsten `run()`.
### 4.2 Gemeinsamer Zustand
Der Zustand lebt in **QGIS-Projektvariablen** mit Präfix `sn_` (Tabelle in `schnittstellen.md` Abschnitt 3) und in
**Layer-Anzeigenamen**. Beides ist stringbasiert und ohne zentrale Definition. Befund:
- 20 verschiedene Schlüssel; die zehn Einstellungsfelder sind in `SettingsLogic.VARIABLEN` gelistet, alle anderen
verteilt im Code.
- Zwei Schreibweisen für denselben Zweck (`verfahrensgebiet_layer` / `verfahrensgebietslayer`) und ein doppeltes Präfix
(`sn_sn_abdeckungslayer`).
- Layer werden über Namen wie „Verfahrensgebiet“ oder `BROR_LAYER_NAME` gefunden; der `verfahrensgebiet_manager`
verfolgt den Verfahrensgebiet-Layer über Projektsignale und schreibt seine ID in die Variable.
### 4.3 Datenflüsse
**Verfahrensgebiet aus ALKIS** (Diagramme im Repo unter `docs/`): Projektvariablen (VKZ/Landkreis) → WFS-Suche
`BauRaumOderBodenordnungsrecht` → Auswahl → Layer → Gemarkungen (vereinfachter WFS) → Flurstücke je Gemarkung
(SF-WFS, BBOX-Batches) → Flächenvergleich → Detailprüfung (Grenzpunkte, Knickpunkte). Viele Dialoge, blockierende
Requests im QGIS-Prozess, manuell gepumpte Fortschrittsdialoge.
**Verfahrensgebiet aus Shape/ZIP:** Mehrfachauswahl → Entpacken → `FBSchl`-Klassifikation → Vereinigung je Gebietstyp
→ Abgleich mit BROR → gleicher weiterer Ablauf.
**Plan41-Fachdaten:** `assets/Linkliste.xlsx``DataGrabber` (Klassifikation) → `Listenauswerter` (Validierung; lebt in
`sn_plan41`, wird von `sn_basis` lazy importiert) → `Datenabruf` (WFS/REST/OGR mit Raumfilter) → `LayerLoader`
(Filter, Stil) → `Datenschreiber` (GeoPackage, Laden, HTML-Protokoll).
**Plan41 ↔ VLN-API:** Login → Verfahren → GeoJSON → Memory-Layer je Geometriefamilie → Bearbeitung → Upload als
eine FeatureCollection (ersetzt Serverbestand).
**Druck:** PrintTab (in jedem Fachplugin-Dock eingebettet) → `PrintLogic` (Bereich, Maßstab, Atlas) → `PrintLayout`
(§ 38/§ 41) bzw. `PrintLayoutBaufreigabe`.
### 4.4 Release und Verteilung
Tag `v<JJ.M.N>-u|-t|` im Plugin-Repo → Repo-Workflow sendet Payload an `Repository` (Branch `hidden/workflows`) →
zentraler Workflow baut **den Kanal-Branch**, erzeugt `metadata.txt` aus dem Template, packt das ZIP unter dem in Gitea
hinterlegten Paketnamen, legt ein Release an und aktualisiert `plugins-<kanal>.xml`. Der Kanal `stable` ist leer und
hat keinen Branch. Details und Fallstricke: `schnittstellen.md` Abschnitt 9, `fallstricke.md`.
### 4.5 Nebenrepositories
`Linkliste` erzeugt automatisch drei Formate aus einer CSV, wird aber von keinem Plugin gelesen. `Defaults` wird
nirgends referenziert. Beide sind Kandidaten für die Konsolidierung (eine Dienstquelle, generierte Laufzeitdatei).
## 5. Qualitätsbefund
### 5.1 Struktur und Stil
| Beobachtung | Beispiele | Auswirkung für KI-gestützte Arbeit |
|---|---|---|
| Sehr große Dateien | zehn Dateien > 600 Zeilen (Modulkarte) | teure Kontexte, ungenaue Änderungen; Agenten müssen per `grep` navigieren |
| Fachlogik in UI-Klassen | `working_tab.py`, `print_tab.py` | schwer testbar, Änderungen streuen |
| Wrapper enthält Fachhelfer | `qgiscore_wrapper.create_buffer_layer`, `polygon_zu_linie` | Schichtgrenze verwischt |
| Rückwärtsabhängigkeit | `sn_basis.DataGrabber``sn_plan41.Listenauswerter` | Basis ist nicht eigenständig; seit PR #57 nur noch lazy |
| Stringbasierter Zustand | Projektvariablen, Layernamen | Tippfehler brechen Abläufe still; siehe Doppelnamen |
| Breite `try/except Exception: pass` | `base_dockwidget`, `dockmanager`, viele Fachfunktionen | Fehler werden verschluckt, Ursachensuche teuer |
| Debug-Reste | `print()` in `navigation.py`, `main.py`, `tab_a_logic.py`; `TEST ACTION` im Menü | Nutzer sehen Entwicklungsballast |
| Auskommentierter Code | `Verfahrensgebiet.run()` doppelt (alt/neu) | Unsicherheit, welche Fassung gilt |
| Uneinheitliche Namen | `schreibe_Daten`, `lade_Layer` neben `create_layer`; deutsche/englische Mischung | Stil je Datei beibehalten (Regel), Vereinheitlichung im Refactoring |
| Fehlende Typen/Docstrings in Teilen | Widmung, ältere Funktionen | Agenten raten Signaturen |
| Personenbezogene IDE-Pfade | `.vscode/settings.json` | irritiert neue Entwickler |
Das Muster ist typisch für **schrittweise per Chatbot erzeugten Code**: jedes Fragment ist für sich plausibel und
gut kommentiert, die Gesamtstruktur wurde aber nie gegen ein Zielbild geprüft. Positiv hervorzuheben: die Wrapper-Idee,
das Prüfmodell, der Mock-Modus und die konsequente deutsche Dokumentation in Docstrings und Changelogs.
### 5.2 Tests
Ausführung im Mock-Modus mit Python 3.14 am 02.09.2026 (`python scripts/run_tests.py`):
| Paket | Gesamt | Grün | Fehlschläge | Fehler | Übersprungen |
|---|---:|---:|---:|---:|---:|
| `sn_basis` | 87 | 87 | 0 | 0 | 0 |
| `sn_plan41` | 44 | 43 | 0 | 1 | 0 |
| `sn_verfahrensgebiet` | 201 | 157 | 15 | 21 | 8 |
Die roten Verfahrensgebiet-Tests betreffen überwiegend `verfahrensgebiet_shape_komplett`, `flurstueck_alkis`,
`grenzpunkt_alkis`, `gemarkung_alkis` und `working_tab`: Tests erwarten die Einzeldatei-/Einzellayer-Schnittstellen
vor dem Umbau auf Mehrfachauswahl. Es handelt sich also überwiegend um **veraltete Tests, nicht um Regressionen**;
das muss aber je Test entschieden werden. Es gibt keine CI, die Tests bei Push/PR ausführt; die vorhandenen Actions
dienen nur Releases.
### 5.3 Dokumentation
Fünf README-Dateien sind leer oder zweizeilig. Wertvoll sind die Mermaid-Abläufe in `Plugin_SN_Verfahrensgebiet/docs/`,
das ERD in `Plugin_SN_Plan41/doc/`, die Changelogs und die Docstrings. Es gibt keine Beschreibung der Gesamtarchitektur,
der Verträge, der Entwicklungsumgebung oder des Arbeitsablaufs genau das, was Agenten und neue Entwickler brauchen.
### 5.4 Prozess
- Branch-Modell `feature/*` → PR → `unstable` → PR → `testing` wird gelebt (57 PRs in Basis).
- Issues mit `Bug`/`Feature`/`Diskussion`; ein Teil sind Diskussionen ohne Auftragscharakter.
- Kein Manifest für kompatible Versionsstände; `sn_basis` und Fachplugins werden unabhängig getaggt.
- Keine Abhängigkeitsdeklaration (`openpyxl`, Basisplugin).
- Individuelle Werkzeuge: VS Code + Copilot (mit lokalem `/memories`), diverse Chatbots; künftig Claude Code und Codex.
## 6. Risiken mit Blick auf die nächste Phase
1. **Rote Tests werden zur Gewohnheit.** Ohne Baseline und CI verlieren Tests ihre Aussagekraft, und Agenten „reparieren“
Tests statt Code. → Baseline festhalten, neue rote Tests verbieten, Rot→Grün als eigene Aufgaben.
2. **Kontextexplosion.** Zehn Dateien über 600 Zeilen und keine Modulkarte führen dazu, dass jeder Agent das Projekt
neu erkundet. → Modulkarte, Schnittstellen, Fallstricke als Pflichtlektüre; Regeln zum gezielten Lesen.
3. **Wissensinseln.** Erkenntnisse liegen in Copilot-Memories, Chat-Verläufen und Köpfen. → Versionierte Wissensbasis.
4. **Unbeabsichtigte API-Brüche in `sn_basis`.** Jede Änderung trifft drei Fachplugins in drei Repos. → Regel „grep vor
Änderung“, später Manifest kompatibler Stände.
5. **Release-Fehler.** Kanal-Branch statt Tag, Template-Fehler, Workspace-Dateien im ZIP. → Skill `sn-release`, Excludes.
6. **Refactoring zu früh.** Ein Umbau bei roten Tests und ohne dokumentierte Verträge zerstört Wissen. → Reihenfolge:
Vorbereitung → grüne Basis → Verträge festschreiben → Refactoring in Scheiben.
## 7. Konsequenzen für die Vorbereitung
Aus der Analyse folgen die Bausteine des Konzepts (`../konzept/02_zusammenarbeit_ki_agenten.md`):
ein Workspace-Meta-Repository mit kanonischer `AGENTS.md`, Modulkarte und Schnittstellen als Pflichtkontext,
eine versionierte Wissensbasis als geteiltes Gedächtnis, Skills für wiederkehrende Abläufe, ein werkzeugunabhängiger
Testlauf mit Baseline und ein leichter, aber verbindlicher Arbeitszyklus über Gitea-Issues und Pull Requests.
+139
View File
@@ -0,0 +1,139 @@
# Modulkarte
Lebende Referenz: **welche Datei tut was und wie groß ist sie**. Zweck: Agenten und Entwickler lesen gezielt statt ganze
Dateien zu laden. Zeilenzahlen Stand 02.09.2026 (`unstable`). Bei Änderungen an Modulzuschnitt bitte mitpflegen.
Legende: ⚠ = über 600 Zeilen, nie komplett laden, per `grep -n "def "` navigieren.
## sn_basis (Repository `Plugin_SN_Basis`) Plattform
### Einstieg und UI-Rahmen
| Datei | Zeilen | Zweck |
|---|---:|---|
| `__init__.py` | 20 | `classFactory``BasisPlugin` |
| `main.py` | 64 | `BasisPlugin`: baut `Navigation`, startet Verfahrensgebiet-Listener, ruft `initGui()` aller `sn_*`-Plugins nach, räumt beim Unload auf |
| `ui/navigation.py` | 127 | Menü und Toolbar „LNO Sachsen“; `add_action(text, callback, tooltip, priority)`, `finalize_menu_and_toolbar()`, `set_active_plugin()`. Enthält noch Debug-`print` und eine `TEST ACTION` |
| `ui/dockmanager.py` | 86 | `DockManager.show(dock)`: entfernt alle anderen `sn_dock_*`-Docks, zeigt das neue rechts an |
| `ui/base_dockwidget.py` | 126 | `BaseDockWidget`: Titel „LNO Sachsen \| <Untertitel>“, Tabs aus Klassenattribut `tabs`, ruft `cleanup()` der Tabs beim Schließen, setzt Toolbar-Action zurück |
| `ui/tabs/settings_tab.py` | 158 | Tab „Projekteigenschaften“: Benutzer- und Projektfelder → Projektvariablen (`SettingsLogic`) |
| `ui/tabs/print_tab.py` ⚠ | 709 | Tab „Druck“: Druckbereich, Maßstab, Format, Themen, Legende, Atlas; nutzt `print_logic` |
### functions/ Wrapper-Schicht (Qt/QGIS/OS mit Mock-Fallback)
| Datei | Zeilen | Zweck |
|---|---:|---|
| `functions/__init__.py` | 50 | Re-Export vieler Wrapper (`from sn_basis.functions import QgsProject, info, ...`) |
| `functions/qt_wrapper.py` ⚠ | 796 | Qt-Symbole: PyQt6 primär, PyQt5-Fallback, sonst Mock. `QT_VERSION` (0/5/6), Enum-Aliase (`DockAreaRight`, …), `exec_dialog()` |
| `functions/qgiscore_wrapper.py` ⚠ | 1 122 | `qgis.core`-Symbole und Konstanten; ab Zeile ~255 Mock-Klassen; Helfer `network_head`, `get_layer_extent`, `create_buffer_layer`, `layer_exists_in_gpkg`, `polygon_zu_linie` |
| `functions/qgisui_wrapper.py` | 258 | `iface`-Zugriffe: Hauptfenster, Docks, Menüs, Toolbars, Layoutdesigner; Mock-`iface` |
| `functions/dialog_wrapper.py` | 393 | Dialoge: `ask_yes_no`, `show_info_dialog`, `ask_text`, Überschreiben/Anhängen/Abbrechen, `ProgressDialog`/`create_progress_dialog` |
| `functions/message_wrapper.py` | 85 | MessageBar: `info`, `warning`, `error`, `success`, `push_message` |
| `functions/variable_wrapper.py` | 120 | `get_variable(key, scope)`, `set_variable(...)` mit Präfix `sn_`; Mock-Speicher |
| `functions/settings_logic.py` | 50 | `SettingsLogic.VARIABLEN`: Liste der gespeicherten Projektvariablen, `load()/save()` |
| `functions/verfahrensgebiet_manager.py` | 217 | Zentrale Verfolgung des Verfahrensgebiet-Layers über Projektsignale; `get/set_verfahrensgebiet_layer_id`, `setup_/teardown_verfahrensgebiet_listener` |
| `functions/ly_*_wrapper.py` | 3298 | Layer-Helfer: Existenz, Geometrie/Featurezahl, Metadaten (Typ, CRS, Felder, Quelle), Stil (`apply_style`), Sichtbarkeit |
| `functions/os_wrapper.py` | 102 | Pfade, Home, Groß-/Kleinschreibung des Dateisystems |
| `functions/sys_wrapper.py` | 106 | `get_plugin_root`, `join_path`, `file_exists`, `read_text`, `add_to_sys_path`, `getattr_safe` |
| `functions/landkreise.py` | 48 | Sächsische Landkreise aus `data/landkreise.json` (`LANDKREISE`, `ALLE_LANDKREISE`) |
| `functions/laenderkennung.py` | 52 | ALKIS-Länderkennung je Aufrufer-Präfix |
### modules/ Prüfen, Laden, Schreiben, Drucken
| Datei | Zeilen | Zweck |
|---|---:|---|
| `modules/pruef_ergebnis.py` | 196 | `pruef_ergebnis(ok, meldung, aktion, kontext)` und `PruefAktion`-Literal (Fehler-/Entscheidungscodes) |
| `modules/Pruefmanager.py` | 295 | Entscheidungsinstanz: `verarbeite(ergebnis)` fragt Nutzer oder entscheidet still (`ui_modus != "qgis"`); `report_error`, `frage_ja_nein`, `frage_text` |
| `modules/Dateipruefer.py` | 267 | Prüft Dateipfade/GeoPackages (existiert, erzeugen, überschreiben, anhängen) |
| `modules/layerpruefer.py` | 183 | Prüft QGIS-Layer (vorhanden, Geometrietyp, CRS, Felder, editierbar) |
| `modules/linkpruefer.py` | 136 | Prüft URLs/lokale Links (Erreichbarkeit) |
| `modules/stilpruefer.py` | 82 | Prüft QML-Stildateien |
| `modules/Datenbankpruefer.py` | 40 | Prüft SQLite/GeoPackage-Datenbanken |
| `modules/DataGrabber.py` | 363 | Klassifiziert Quelle (Excel, GeoPackage, Dienst) und liefert normalisierte Zeilen; Excel-Pfad nutzt **Lazy-Import** `sn_plan41.modules.listenauswerter` |
| `modules/excel_importer.py` | 92 | Excel lesen (`openpyxl`, harte Abhängigkeit) |
| `modules/Datenabruf.py` ⚠ | 636 | `Datenabruf.datenabruf(...)`: WFS, ArcGIS REST, OGR laden; Raumfilter Verfahrensgebiet/Puffer/ohne |
| `modules/LayerLoader.py` | 422 | `create_layer(provider, link, thema)`, `apply_style`, `filter_by_extent`, `filter_by_layer`, `add_to_project` |
| `modules/Datenschreiber.py` | 448 | `schreibe_Daten(...)` in GeoPackage, `lade_Layer(...)` ins Projekt, `schreibe_log(...)` HTML-Protokoll |
| `modules/print_logic.py` ⚠ | 1 321 | `PrintLogic`, `AtlasConfig`, `RectangleCaptureTool`: Druckbereich, Maßstab, Atlas, Layoutaufruf |
| `modules/print_layout.py` ⚠ | 1 078 | `PrintLayout`: Layoutaufbau für § 38 / § 41 (Karte, Legende, Gitter, Schriftfeld) |
| `modules/print_layout_baufreigabe.py` | 403 | Layout „Anlage zur Baufreigabe“ (feste DIN A3/A4 quer) |
### Sonstiges
| Pfad | Zweck |
|---|---|
| `tests/` | 13 Testdateien, `unittest`, Mock-Modus; `run_tests.py` (farbiger Runner), `test_qgis.bat` (Coverage in OSGeo4W) |
| `styles/` | QML-Stile (Verfahrensgebiet, LSG, Denkmalschutz, Flurstücksbeschriftung) |
| `data/landkreise.json` | Landkreise mit Kennzahl |
| `assets/plan-qgis340ExitCrashFix.prompt.md` | KI-Planungsnotiz zum Exit-Crash (historisch) |
| `.gitea/workflows/release.yaml`, `.gitea/templates/metadata.template` | Release-Auslöser, Vorlage für `metadata.txt` |
## sn_verfahrensgebiet (Repository `Plugin_SN_Verfahrensgebiet`) Fachplugin
| Datei | Zeilen | Zweck |
|---|---:|---|
| `main.py` | 80 | `Verfahrensgebiet`: registriert Action (Priorität 10), erzeugt `DockWidget` mit `Pruefmanager` und `DataGrabber` |
| `ui/dockwidget.py` | 60 | Tabs `WorkingTab`, `PrintTab`, `SettingsTab`; injiziert Services per `set_services()` |
| `ui/tabs/working_tab.py` ⚠ | 873 | Tab „Bearbeitung“: Buttons und Handler für ALKIS/Shape laden, Gemarkungen, Flurstücke, Grenzpunkte, Beteiligung, Statistik; hält viel Ablauflogik |
| `functions/verfahrensgebiet_alkis.py` | 359 | WFS-Suche `BauRaumOderBodenordnungsrecht` per Verfahrensnummer/Landkreis, `LoadStatus` |
| `functions/verfahrensgebiet_alkis_komplett.py` ⚠ | 661 | Vollständiger Ladeablauf aus ALKIS: BROR → Gemarkungen → Flurstücke → Flächenvergleich → Detailprüfung |
| `functions/verfahrensgebiet_shape_komplett.py` ⚠ | 978 | Ladeablauf aus Shape/ZIP (Mehrfachauswahl, `FBSchl`-Klassifikation, Abgleich mit BROR) siehe `docs/aus_shape_laden_technisch.md` im Repo |
| `functions/verfahrensgebiet_dialoge.py` | 300 | Dialoge: BROR-Kombinationen, Landkreis-Objekte, Landkreisauswahl |
| `functions/verfahrensgebiet_edit_service.py` | 359 | Verfahrensgebiet aus Flurstücksauswahl bilden, ergänzen, entfernen, ersetzen |
| `functions/gemarkung_alkis.py` | 229 | Gemarkungen aus vereinfachtem ALKIS-WFS |
| `functions/flurstueck_alkis.py` | 259 | Flurstücke je Gemarkung aus ALKIS-SF-WFS (BBOX, Batches) |
| `functions/grenzpunkt_alkis.py` ⚠ | 1 674 | Grenzpunkte/`PunktortTA` gemarkungsweise, GML-Parsing, URL-sichere Batches |
| `functions/knickpunkt_pruefung.py` ⚠ | 810 | Knickpunktbasierte BROR-Prüfung, Ergebnislayer mit Kategorien |
| `functions/umringspruefung.py` | 312 | Umringsprüfung (Variante 1) |
| `functions/umringstatistik_grenzpunkte.py` | 374 | Statistik Grenzpunkte am Umring |
| `functions/beteiligung_pruefung.py` | 453 | Abgleich BI-Server-CSV mit ALKIS-Flurstücken |
| `tests/` | 12 Dateien | Mock-Modus, aktuell 157/201 grün (Baseline) |
| `docs/` | 3 Dateien | Ablaufdiagramme (Mermaid) für ALKIS- und Shape-Laden |
| `styles/` | 6 QML | BROR, Gemarkungen, Beteiligung, Verfahrensgebiet |
## sn_plan41 (Repository `Plugin_SN_Plan41`) Fachplugin
| Datei | Zeilen | Zweck |
|---|---:|---|
| `main.py` | 71 | `Plan41`: Action mit Priorität 20, Dock mit `Pruefmanager`/`DataGrabber` |
| `ui/dockwidget.py` | 65 | Tabs `TabA` (Daten), `TabB` (Beispiel), `PrintTab`, `SettingsTab` |
| `ui/tab_a_ui.py` ⚠ | 633 | UI von Tab A: Verfahrens-GeoPackage, Linkliste, Raumfilter, VLN-API-Bereich |
| `ui/tab_a_logic.py` ⚠ | 1 239 | Fachlogik Tab A: Prüf-Pipeline (Datei, Linkliste, Stil, Layer), Abruf über `DataGrabber`/`Datenabruf`, Schreiben, Protokoll; `_SilentPruefmanagerProxy` |
| `ui/tabs/tab_b.py` | 27 | Beispiel-Tab ohne Funktion |
| `modules/listenauswerter.py` | 142 | Validiert Linklistenzeilen (Schema `ident, gruppe, kartenebene, inhalt, link, provider, stildatei`) wird von `sn_basis.DataGrabber` lazy importiert |
| `modules/vln_api_client.py` | 249 | HTTP-Client VLN-Karten-API v2 (Login, `X-API-Key`, Verfahren, Plan41-Datensatz, Upload) |
| `modules/vln_api_logic.py` | 379 | Ablauflogik der API-Integration in Tab A |
| `modules/vln_layer_manager.py` | 178 | GeoJSON ↔ Memory-Layer nach Geometriefamilie (Punkt/Linie/Fläche), EPSG:25833 |
| `assets/` | 33 Dateien | `Linkliste.xlsx` (44 Dienste) und QML-Stile |
| `doc/Datenbank_ERD.md` | 57 | Ziel-Datenmodell (Maßnahmen, Konten, Ausbauarten, Akteure) Entwurf, nicht implementiert |
| `tests/` | 5 Dateien | 43/44 grün (Baseline) |
## sn_widmung (Repository `Plugin_SN_Widmung`) Gerüst
`main.py` (Priorität 30), `ui/dockwidget.py`, `ui/tabs/tab_a.py`, `tab_b.py`: Registrierung funktioniert, Tabs enthalten nur Platzhalter. Keine Tests.
Dient als **minimales Muster** für ein neues Fachplugin (Skill `sn-neues-fachplugin`).
## Platzhalter und Infrastruktur
| Repository | Inhalt |
|---|---|
| `Plugin_SN_FLT`, `Plugin_SN_Topografie`, `Plugin_SN_Template` | nur Release-Workflow, Metadata-Vorlage (teils noch `Plugin_SN_Neu`), leere README |
| `Plugin_Test_Action` | Minimalplugin zum Testen der Release-Pipeline |
| `Repository` | `plugins.xml` (stable, leer), `plugins-testing.xml`, `plugins-unstable.xml`; Branches `hidden/workflows` (zentrale Release-Orchestrierung) und `hidden/templates` |
| `Linkliste` | `linkliste.csv` → Gitea Action erzeugt XML/JSON/YAML |
| `Defaults` | Beispiel-Verfahrensdaten (JSON) und `styles/verfahrensgebiet.qml`; nicht vom Code referenziert |
## Größte Dateien (Kandidaten für das spätere Refactoring)
| Datei | Zeilen |
|---|---:|
| `sn_verfahrensgebiet/functions/grenzpunkt_alkis.py` | 1 674 |
| `sn_basis/modules/print_logic.py` | 1 321 |
| `sn_plan41/ui/tab_a_logic.py` | 1 239 |
| `sn_basis/functions/qgiscore_wrapper.py` | 1 122 |
| `sn_basis/modules/print_layout.py` | 1 078 |
| `sn_verfahrensgebiet/functions/verfahrensgebiet_shape_komplett.py` | 978 |
| `sn_verfahrensgebiet/ui/tabs/working_tab.py` | 873 |
| `sn_verfahrensgebiet/functions/knickpunkt_pruefung.py` | 810 |
| `sn_basis/functions/qt_wrapper.py` | 796 |
| `sn_basis/ui/tabs/print_tab.py` | 709 |
+128
View File
@@ -0,0 +1,128 @@
# Schnittstellen und Verträge
Lebende Referenz der **impliziten Verträge** zwischen den Komponenten. Wer eine dieser Stellen ändert, ändert eine
Schnittstelle und muss alle Nutzer mit anpassen. Stand 02.09.2026.
## 1. Plugin-Lebenszyklus: sn_basis ↔ Fachplugin
```mermaid
sequenceDiagram
participant QGIS
participant B as sn_basis.BasisPlugin
participant F as Fachplugin (z. B. Verfahrensgebiet)
QGIS->>B: classFactory(iface) / initGui()
B->>B: Navigation().init_ui() (Menü + Toolbar "LNO Sachsen")
B->>B: setup_verfahrensgebiet_listener()
loop für jedes Plugin in qgis.utils.plugins mit Namen sn_* (außer sn_basis)
B->>F: initGui()
F->>B: basis.ui.add_action(name, run, tooltip, priority)
end
B->>B: finalize_menu_and_toolbar() (sortiert nach priority)
QGIS->>F: Nutzer klickt Action → run()
F->>F: DockWidget(...) mit objectName "sn_dock_<name>"
F->>B: DockManager.show(dock) (entfernt andere sn_dock_*)
F->>B: basis.ui.set_active_plugin(action)
QGIS->>F: unload() → removeDockWidget, basis.ui.remove_action(action)
```
Vertrag für ein Fachplugin (Muster: `Plugin_SN_Widmung/main.py`):
- Paketname beginnt mit `sn_` (nur dann ruft `sn_basis` das `initGui()` nach).
- `initGui()` holt `plugins.get("sn_basis")`; ohne `basis.ui` nichts tun (QGIS lädt Plugins in nicht garantierter Reihenfolge; `sn_basis` ruft später nach).
- `add_action(text, callback, tooltip="", priority=100)``QAction` (checkable, in exklusiver Gruppe). Vergebene Prioritäten: Verfahrensgebiet 10, Plan41 20, Widmung 30.
- Dock-Objektname `sn_dock_<pluginname klein>`; nur ein `sn_dock_*` ist gleichzeitig sichtbar.
- `unload()` muss Dock entfernen und Action abmelden, sonst bleiben Reste in Menü/Toolbar.
## 2. DockWidget und Tabs
- Fachplugin-Dock erbt von `sn_basis.ui.base_dockwidget.BaseDockWidget`; Klassenattribut `tabs = [TabKlasse, ...]`.
- Tab-Klasse: `QWidget`-Unterklasse mit Klassenattribut `tab_title`; Konstruktor `Tab(parent)`.
Optional `set_services(pruefmanager=..., data_grabber=...)` (wird von Fachplugin-Docks aufgerufen) und `cleanup()` (wird beim Schließen des Docks aufgerufen Signale trennen!).
- Gemeinsame Tabs aus `sn_basis`: `ui.tabs.print_tab.PrintTab` (Druck), `ui.tabs.settings_tab.SettingsTab` (Projekteigenschaften).
- `dock.action` = Referenz auf die Toolbar-Action; `BaseDockWidget.closeEvent` setzt sie zurück.
## 3. Projektvariablen (gemeinsamer Zustand im QGIS-Projekt)
Zugriff nur über `sn_basis.functions.variable_wrapper.get_variable(key, scope="project")` / `set_variable(...)`.
Der Wrapper setzt das Präfix `sn_` selbst; Schlüssel **ohne** Präfix übergeben.
| Schlüssel (ohne `sn_`) | Gesetzt von | Gelesen von | Bedeutung |
|---|---|---|---|
| `amt`, `behoerde`, `landkreis_user`, `sachgebiet` | SettingsTab | Druck (Schriftfeld) | benutzerspezifische Angaben |
| `bezeichnung`, `name`, `verfahrensnummer`, `gemeinden`, `landkreise_proj`, `landkreis_kennzahl` | SettingsTab | Verfahrensgebiet (ALKIS-Suche), Druck | Verfahrensangaben; `verfahrensnummer` = VKZ |
| `verfahrensgebiet_layer` | `verfahrensgebiet_manager`, WorkingTab | alle Fachplugins, Druck | Layer-ID des aktuellen Verfahrensgebiets |
| `verfahrens_db`, `tab_a_verfahrens_db` | Plan41 Tab A | Plan41 | Pfad des Verfahrens-GeoPackages |
| `linkliste`, `tab_a_linkliste` | Plan41 Tab A | Plan41 | Pfad der Linkliste (XLSX) |
| `tab_a_layer_id`, `Raumfilter` | Plan41 Tab A | Plan41 | Raumfilter-Einstellung |
| `druckrechteck_xmin/xmax/ymin/ymax` | PrintTab/PrintLogic | Druck | gespeicherter Druckbereich |
| `sn_abdeckungslayer` ⚠ | PrintTab/PrintLogic | Druck (Atlas) | Atlas-Abdeckungslayer. **Schlüssel enthält bereits `sn_` → tatsächliche Variable `sn_sn_abdeckungslayer`**; funktioniert, ist aber inkonsistent |
| `verfahrensgebietslayer` ⚠ | `print_logic.py` | `print_logic.py` | **Zweite Schreibweise neben `verfahrensgebiet_layer`** vermutlich Altlast, beim Refactoring zusammenführen |
Benutzer-/Maschinendaten außerhalb des Projekts: `QSettings` in `sn_plan41` (`mail`, `api_key` der VLN-API).
## 4. Prüf- und Entscheidungsmodell
- `pruef_ergebnis(ok: bool, meldung, aktion: PruefAktion, kontext)` ist das einheitliche Rückgabeobjekt aller Prüfer (`Dateipruefer`, `Layerpruefer`, `Linkpruefer`, `Stilpruefer`, `Datenbankpruefer`) und vieler Fachfunktionen.
- `PruefAktion` ist ein `Literal` mit Codes wie `datei_nicht_gefunden`, `layer_leer`, `datei_existiert_ueberschreiben`, `verfahrensgebiet_ergaenzen`. Neue Codes dort ergänzen, nicht als freie Strings verwenden.
- `Pruefmanager(ui_modus="qgis")`: `verarbeite(ergebnis)` entscheidet je Aktion (Dialog, Meldung, Standardwert). Mit `ui_modus != "qgis"` (Tests) werden Standardantworten ohne Dialog geliefert.
- Fachplugins erhalten **eine** `Pruefmanager`- und **eine** `DataGrabber`-Instanz aus `main.py` (Konstruktor) und reichen sie an Tabs weiter.
## 5. Datenpipeline Plan41 (Fachdaten aus Linkliste)
```mermaid
flowchart LR
XLSX[assets/Linkliste.xlsx] --> DG[DataGrabber.run]
DG -->|Excel| LA[sn_plan41.Listenauswerter.validate_rows]
LA --> DA[Datenabruf.datenabruf]
VG[(Verfahrensgebiet / Puffer)] --> DA
DA --> LL[LayerLoader: create_layer, filter_by_layer/extent, apply_style]
LL --> DS[Datenschreiber.schreibe_Daten → GeoPackage]
DS --> P[Datenschreiber.lade_Layer + schreibe_log HTML]
```
- Erwartetes Spaltenschema der XLSX: `ident, gruppe, kartenebene, inhalt, link, provider, stildatei`. `provider` ∈ WFS, REST (ArcGIS), OGR.
- Das zentrale Repository `Linkliste` (`linkliste.csv`: `id;gruppe;ebene;inhalt;url;provider;stil`) ist **nicht** angebunden → zwei Wahrheiten, siehe `fallstricke.md`.
- Die einzige Rückwärtsabhängigkeit `sn_basis → sn_plan41` ist der Lazy-Import von `Listenauswerter` in `DataGrabber._process_excel_source` (seit PR #57 abgesichert, damit `sn_verfahrensgebiet` ohne `sn_plan41` lädt).
## 6. Verfahrensgebiet-Abläufe (Fachplugin)
- Zentrale Kennung des Verfahrens: Projektvariable `verfahrensnummer` (VKZ) und/oder `landkreis_kennzahl`.
- ALKIS-Quellen: vereinfachtes ALKIS (`.../public_alkis/vereinf/wfs`) für Gemarkungen/BROR, ALKIS-SF (`.../public_alkis/sf/wfs`) für Flurstücke und `PunktortTA`.
- Layer-Namenskonventionen (String-Konstanten in den Modulen): `BROR_LAYER_NAME`, `FLST_LAYER_NAME`, `JOINED_LAYER_NAME`, „Verfahrensgebiet“, „Neuordnungsgebiet (NoBIS)“, „Neuvermessungsgebiet (NoBIS)“. Andere Module finden Layer **über den Namen** Umbenennungen sind Schnittstellenänderungen.
- Ablaufdokumentation mit Diagrammen: `Plugin_SN_Verfahrensgebiet/docs/`.
## 7. VLN-Karten-API (Plan41)
- Basis-URL `https://api.flurneuordnung-sachsen.de/v2`; Login per E-Mail/Passwort, danach Header `X-API-Key`.
- Laden: Verfahren (VKZ) → Plan41-GeoJSON → Memory-Layer je Geometriefamilie (`vln_layer_manager`). Upload: alle Teillayer → eine FeatureCollection, `PUT` ersetzt den Serverbestand der VKZ vollständig.
- Arbeits-CRS EPSG:25833.
## 8. Mock-Modus (Tests ohne QGIS)
- Jeder Wrapper versucht den Import von `qgis`/`PyQt6`/`PyQt5`; schlägt das fehl, definiert er `_Mock*`-Klassen. Kennzeichen: `qgiscore_wrapper.QGIS_AVAILABLE`, `qt_wrapper.QT_VERSION == 0`.
- Tests importieren Module über den Paketnamen (`from sn_basis...`), deshalb müssen die Repos unter ihrem Paketnamen erreichbar sein → `scripts/run_tests.py` erledigt das über ein temporäres Verzeichnis.
- Tests, die echtes Layer- oder Netzwerkverhalten brauchen, laufen nur in QGIS (`tests/test_qgis.bat`, Windows).
## 9. Release-Vertrag
```mermaid
flowchart LR
T["Tag v26.9.1-u auf Plugin-Repo"] --> W1["Plugin-Workflow release.yaml"]
W1 -->|"workflow_dispatch + Payload"| W2["Repository, Branch hidden/workflows"]
W2 --> C["checkout Branch = Kanal<br/>(unstable / testing / stable)"]
C --> M["metadata.template → metadata.txt:<br/>version, experimental, repository-URL"]
M --> Z["rsync ohne .git .gitea .gitignore __pycache__ → ZIP_FOLDER.zip"]
Z --> R["Gitea-Release + Asset"]
R --> X["plugins-&lt;kanal&gt;.xml aktualisieren"]
```
- Tagformat `v<JJ.M.N>-u` (unstable), `-t` (testing), ohne Suffix (stable). Version = Jahr.Monat.Laufnummer.
- **Gebaut wird der Kopf des Kanal-Branches, nicht der getaggte Commit.** Tag also nur auf dem aktuellen Kopf von `unstable` bzw. `testing` setzen.
- Paketname im ZIP kommt aus der Gitea-Repo-Variable `ZIP_FOLDER` (z. B. `sn_basis`).
- `metadata.txt` im Repo ist nur ein Abbild des letzten Builds; gepflegt wird `.gitea/templates/metadata.template`.
- Release-Body = Block über dem ersten `---` in `changelog.txt`.
- Neue Workspace-Dateien (`AGENTS.md`, `CLAUDE.md`, `.agents/`, `.claude/`, `docs/` der Repos) landen derzeit im ZIP → Excludes im zentralen Workflow ergänzen (offener Punkt in `STATUS.md`).
## 10. Linkliste-Repository
`linkliste.csv` (Semikolon, Spalten `id;gruppe;ebene;inhalt;url;provider;stil`) → Gitea Action → `linkliste.xml/json/yaml` auf `main`. Die Plan41-Laufzeit nutzt stattdessen die mitgelieferte `assets/Linkliste.xlsx` mit abweichenden Spaltennamen.
@@ -0,0 +1,19 @@
# 0001 `AGENTS.md` ist die kanonische Anweisungsdatei
Status: vorgeschlagen · Datum: 2026-09-02 · Beteiligte: AG QGIS (Vorschlag aus der Vorbereitungsphase)
## Kontext
Entwickler nutzen unterschiedliche Agenten (Claude Code, Codex, Copilot). Claude Code liest `CLAUDE.md`, Codex und
Copilot lesen `AGENTS.md`. Getrennte Dateien driften auseinander.
## Entscheidung
Es gibt genau eine Anweisungsdatei je Ordner: `AGENTS.md`. `CLAUDE.md` enthält nur `@AGENTS.md` und wenige
Claude-spezifische Zeilen. Eine Copilot-Zeigerdatei ist optional (`docs/vorlagen/copilot-instructions.md`).
`AGENTS.md` bleibt unter 150 Zeilen; Abläufe gehören in Skills, Beschreibungen in `docs/`.
## Folgen
Änderungen an Regeln passieren an einer Stelle. Claude Code fragt beim ersten Import einmal nach Zustimmung.
Windows-Symlinks werden vermieden.
## Verworfene Alternativen
Symlink `CLAUDE.md → AGENTS.md` (Windows-Rechte); je Werkzeug eigene Datei (Drift); Inhalt in README (zu lang, falsche Zielgruppe).
@@ -0,0 +1,19 @@
# 0002 Workspace-Meta-Repository mit eigenständigen Klonen
Status: vorgeschlagen · Datum: 2026-09-02 · Beteiligte: AG QGIS
## Kontext
Elf Repositories in einer Gitea-Organisation; Agenten arbeiten ordnerbasiert und brauchen den Gesamtkontext
(Basis ↔ Fachplugins). Der Release-Prozess hängt an einzelnen Repos und Tags. Ein Monorepo wäre ein Umbau.
## Entscheidung
Ein neues Repository `AG_QGIS/Workspace` enthält Anweisungen, Wissensbasis, Skills, Skripte und das Manifest
`workspace.toml`. Die übrigen Repositories werden als Unterordner in den Workspace geklont und per `.gitignore`
ausgeschlossen. Agenten werden im Workspace-Root gestartet. Jedes Plugin-Repo erhält zusätzlich eine kurze `AGENTS.md`.
## Folgen
Keine Änderung an Repos, Branches oder Releases. Das Manifest kann später kompatible Versionsstände festhalten.
Entwickler müssen das Setup-Skript einmal ausführen. Die Monorepo-Frage bleibt für die Refactoring-Phase offen.
## Verworfene Alternativen
Monorepo jetzt (Release-Umbau, Historie); Git-Submodule (fehleranfällig für Einsteiger); nur per-Repo-Dateien (kein Gesamtkontext).
@@ -0,0 +1,20 @@
# 0003 Mock-Tests mit Baseline sind Pflicht vor jedem PR
Status: vorgeschlagen · Datum: 2026-09-02 · Beteiligte: AG QGIS
## Kontext
332 Tests laufen ohne QGIS in unter einer Sekunde, sind aber nicht alle grün (sn_plan41 1 Fehler, sn_verfahrensgebiet
36 rot) und laufen nicht in CI. Ohne Nachweis ist KI-erzeugter Code nicht bewertbar.
## Entscheidung
`python scripts/run_tests.py <paket> --baseline` ist vor jedem Commit/PR Pflicht. Die Baseline
(`docs/status/testbaseline.json`) listet bekannte rote Tests; neue rote Tests sind nicht erlaubt. Die Baseline wird nur
durch Maintainer verändert. Rot→Grün wird als eigene Issues abgearbeitet. Ein CI-Workflow (Vorlage in `docs/vorlagen/`)
führt denselben Lauf bei Push/PR auf `unstable` aus.
## Folgen
Sofortige Wirkung ohne vorheriges Grünmachen. Tests dürfen nicht durch Anpassen der Erwartung „repariert“ werden,
außer das Verhalten wurde bewusst geändert (im PR begründet). QGIS-Verhalten braucht weiterhin manuelle Tests.
## Verworfene Alternativen
Erst alles grün, dann Regel (verzögert die Zusammenarbeit); Tests ignorieren (kein Nachweis).
@@ -0,0 +1,22 @@
# 0004 Geteiltes Wissen liegt versioniert in `docs/`, nicht im Werkzeug-Gedächtnis
Status: vorgeschlagen · Datum: 2026-09-02 · Beteiligte: AG QGIS (greift Issue Basis #36 auf)
## Kontext
Claude Auto-Memory, Copilot `/memories` und Codex-Notizen sind lokal und werkzeuggebunden. Erkenntnisse gehen
zwischen Personen und Werkzeugen verloren.
## Entscheidung
Teilbares Wissen wird in der Wissensbasis des Workspace geführt: `docs/status/STATUS.md` (Stand),
`docs/wissen/fallstricke.md` (Erkenntnisse), `docs/architektur/*` (Verträge, Modulkarte),
`docs/entscheidungen/` (Warum). Jede Aufgabe endet mit einer Übergabenotiz (Skill `sn-uebergabe`). Werkzeug-
Gedächtnisse sind Notizzettel und dürfen nichts enthalten, was andere brauchen.
## Folgen
Pflegeaufwand von wenigen Minuten je Aufgabe; dafür entfällt wiederholte Erkundung. Die Vorschläge aus Issue #36
(Copilot-Instructions je Plugin) werden durch werkzeugneutrale `AGENTS.md` ersetzt; bestehende Copilot-Memories
werden destilliert.
## Verworfene Alternativen
Gemeinsames Memory-Verzeichnis per Netzlaufwerk (werkzeugspezifisch, nicht versioniert, Konflikte); Wiki auf Gitea
(nicht im Kontext der Agenten, getrennt vom Code).
@@ -0,0 +1,19 @@
# 0005 Grundlegendes Refactoring erst nach grüner Baseline und festgeschriebenen Verträgen
Status: vorgeschlagen · Datum: 2026-09-02 · Beteiligte: AG QGIS
## Kontext
Der Code ist gewachsen (zehn Dateien über 600 Zeilen, implizite Verträge, rote Tests). Ein Umbau jetzt würde Wissen
zerstören, das nur im Code steht, und wäre nicht durch Tests abgesichert.
## Entscheidung
Reihenfolge: (1) Workspace und Wissensbasis, (2) Tests mit Baseline und CI, Rot→Grün, (3) Verträge festschreiben
(Abhängigkeiten, Variablen, Linkliste, Manifest), (4) Refactoring-Konzept mit Zielarchitektur und Scheiben,
erst dann (5) Umbau. Bis dahin gilt „kein Refactoring nebenbei“; strukturelle Änderungen brauchen ein Issue.
## Folgen
Fachliche Weiterentwicklung bleibt möglich. Die Fachschale-Idee des Alternativansatzes wird im Refactoring-Konzept
bewertet, nicht vorher eingebaut.
## Verworfene Alternativen
Sofortiges Refactoring (Risiko); Neuentwicklung auf Basis des Alternativansatzes (verwirft 29 500 Zeilen Fachlogik).
@@ -0,0 +1,28 @@
# 0006 Das Workspace-Repository heißt „Projektwissen"
Status: vorgeschlagen · Datum: 2026-09-04 · Beteiligte: AG QGIS · Ergänzt 0002
## Kontext
Entscheidung 0002 legte ein Meta-Repository unter dem Arbeitstitel `AG_QGIS/Workspace` fest. Beim Anlegen auf
Gitea zeigte sich, dass „Workspace" beschreibt, *wie* der Ordner benutzt wird, nicht *was* er enthält. Für
Einsteiger und für die Behörden, die 2026 dazukommen, ist der Inhalt entscheidend: Regeln, Wissensbasis,
Entscheidungen, Anleitungen. Der Name steht in Klon-Befehlen, in der Gitea-Übersicht und in jeder Anleitung.
## Entscheidung
Das Repository heißt **`AG_QGIS/Projektwissen`**. Der lokale Ordner, in den geklont wird, heißt weiterhin
**`AG_QGIS/`**, weil dort neben dem Projektwissen auch alle Plugin-Klone liegen:
git clone .../AG_QGIS/Projektwissen.git AG_QGIS
Der Begriff „Workspace" bleibt für den *Ordner* zulässig (auch der Dateiname `workspace.toml` bleibt), bezeichnet
aber nie das Repository. Die README löst die Verwechslung einmal ausdrücklich auf.
## Folgen
Alle Klon-Befehle in README, Leitfäden, Skills und der CI-Vorlage zeigen auf `Projektwissen.git`. Vorhandene
lokale Klone sind nicht betroffen, weil sich der Ordnername nicht ändert. `workspace.toml` behält seinen Namen,
damit `run_tests.py`, `setup_workspace.py`, `hallo.py`, `gitea.py` und `testkatalog.py` unverändert bleiben.
## Verworfene Alternativen
`Workspace` beibehalten (beschreibt die Nutzung, nicht den Inhalt); `Wissensbasis` (zu eng — es liegen auch
Skripte und Skills darin); Ordner ebenfalls in `Projektwissen` umbenennen (bricht alle vorhandenen lokalen
Einrichtungen und jeden Pfad in der bestehenden Doku, ohne inhaltlichen Gewinn).
+61
View File
@@ -0,0 +1,61 @@
# 0007 Branchmodell: drei Kanäle, `stable` als stabiler Kanal, Rückführungspflicht
Status: vorgeschlagen · Datum: 2026-09-04 · Beteiligte: AG QGIS
## Kontext
Es gab eine erste Beschreibung des Git-Vorgehens (Wiki-Export „Git-Konzept"), die drei Branches
`main`/`testing`/`unstable` beschrieb und den Branch-Schutz auf GitHub erklärte. Der Abgleich mit den
Repositories am 04.09.2026 ergab vier Abweichungen:
1. **Ein stabiler Kanal-Branch existiert in keinem Plugin-Repository.** Vorhanden sind `unstable`, `testing`
und in zwei Repos ein nicht mehr bespieltes `oldstable`. Der Skill `sn-release` vermerkt entsprechend
„stable derzeit ohne Branch".
2. **Der Branchname ist nicht frei wählbar.** Der Release-Workflow in `Repository`, Branch `hidden/workflows`
(Michael Otto, 19.06.2026), klont das Plugin-Repository und führt `git checkout "$CHANNEL"` aus. `$CHANNEL`
wird im Workflow des Plugin-Repos (Michael Otto, 18.03.2026) aus dem Tag-Suffix abgeleitet und ist
`unstable`, `testing` oder `stable`. **Der Branch muss also genauso heißen wie der Kanal.** Ein Branch `main`
würde den stabilen Release-Pfad brechen.
3. Der Branch-Schutz wurde für GitHub beschrieben, wir arbeiten mit Gitea 1.27.
4. Branch-Namen sind uneinheitlich: `bug/crash_on_exit`, `feature/Drucklayout-überarbeiten` (Umlaut),
`alkis-nur-laden-stop` und `fix-fbschl-feldname` (ohne Präfix).
Zusätzlich fehlte die Antwort auf die praktisch wichtigste Frage: Wo fängt ein Fix an, der einen
Release-Kandidaten in `testing` betrifft?
## Entscheidung
Die Kette lautet `feature/*``unstable``testing``stable`. Der stabile Kanal heißt **`stable`** und wird
angelegt; er entspricht dem Feed `plugins.xml` und dem Tag ohne Suffix. `oldstable` wird als Altlast archiviert.
`stable` statt `main`, weil die vorhandene Release-Infrastruktur den Branchnamen aus dem Kanalnamen ableitet.
`main` wäre möglich, würde aber eine Änderung an fremder, funktionierender Infrastruktur samt Testrelease
erfordern — ohne fachlichen Gewinn. Der Name ist im Projekt ohnehin durchgängig verankert: `CHANNEL`-Variable,
Tag-Logik, `plugins.xml`.
**Der Einstiegspunkt eines Fixes richtet sich nach dem Kanal, in dem der Fehler auftritt**, nicht nach seiner
Dringlichkeit. Ein Fix an `testing` oder `stable` ist erst abgeschlossen, wenn er per Pull Request in alle
darunterliegenden Kanäle zurückgeführt wurde. Ohne Rückführung verschwindet der Fix beim nächsten Hochstufen.
Branch-Namen: `feature/`, `bugfix/`, `hotfix/`, `doku/`, `release/`, jeweils mit Issue-Nummer, ASCII, klein,
Bindestriche. Die drei Kanäle sind auf Gitea gegen direkten Push geschützt.
**Push, Merge und Tag bleiben bei Menschen.** KI-Agenten bereiten vor; die entsprechenden Kommandos sind in
`.claude/settings.json` technisch gesperrt, damit die Regel nicht nur auf Zuruf gilt.
Vollständige Fassung: `docs/leitfaden/05_git_arbeitsweise.md`.
## Folgen
Der Maintainer muss `stable` je Plugin-Repository anlegen und die Schutzregeln setzen
(`docs/konzept/08_repo_einrichten.md`). Bis dahin endet die Kette bei `testing` und der stabile Feed bleibt leer.
Jeder Fix an `testing` kostet einen zweiten Pull Request — das ist gewollt und billiger als ein
wiederauftauchender Fehler. Vier verwaiste Branches im Bestand sind zu prüfen und zu löschen.
Wer den Branch später doch `main` nennen will, muss zuerst `git checkout "$CHANNEL"` in
`Repository:hidden/workflows` um eine Kanal→Branch-Zuordnung erweitern und das mit einem Testrelease auf
`Plugin_Test_Action` absichern. Diese Entscheidung wäre dann zu ersetzen.
## Verworfene Alternativen
Den stabilen Kanal `main` nennen (vertraute Git-Konvention, bricht aber den Release-Workflow, solange die
Infrastruktur nicht mit angepasst wird — Eingriff in fremde, funktionierende Automatisierung ohne fachlichen
Gewinn); bei zwei Kanälen bleiben (dann gibt es dauerhaft kein produktionsreifes Ziel und `plugins.xml` bleibt
leer); Fixes grundsätzlich in `unstable` beginnen und nach `testing` vorziehen (Cherry-Picks über mehrere Kanäle
sind fehleranfälliger als der umgekehrte Weg).
@@ -0,0 +1,37 @@
# 0008 Werkzeugunterstützung auf Claude Code fokussiert
Status: vorgeschlagen · Datum: 2026-09-04 · Beteiligte: AG QGIS · Ergänzt 0001
## Kontext
Der Leitfaden beschrieb fünf Werkzeug-Varianten (Claude Code CLI und VS Code, Codex CLI und VS Code, GitHub
Copilot) plus zwei Desktop-Apps und die reinen Chat-Oberflächen. Jede Variante brauchte eigene
Einrichtungsschritte, eigene Prüfschritte und eigene Fußnoten zu Skills und Anweisungsdateien. In der Praxis
wird Claude Code verwendet. Der Rest kostete Pflege, verwirrte Einsteiger und veraltete zwischen zwei
Anbieter-Releases.
## Entscheidung
`docs/leitfaden/04_werkzeuge_einrichten.md` beschreibt nur noch **Claude Code**, in zwei Formen: CLI und
VS-Code-Erweiterung. Die Vorlage `docs/vorlagen/copilot-instructions.md` entfällt.
**Die Dateistruktur bleibt werkzeugneutral.** Entscheidung 0001 gilt unverändert: `AGENTS.md` ist kanonisch,
`CLAUDE.md` ist ein Zeiger, Skills liegen in `.agents/skills/` und werden nach `.claude/skills/` gespiegelt.
Fokussiert wird die *Dokumentation*, nicht die *Struktur*.
Zusätzlich kommen Claude-Code-spezifische Dateien hinzu, die die Regeln durchsetzen statt sie nur zu
beschreiben: `.claude/settings.json` (Sperre für `git push`, `git merge`, `git tag`) und `.claude/commands/`
(Slash-Befehl `/hallo`). Beides ergänzt die neutralen Dateien, ersetzt sie nicht — die eigentliche Arbeit steckt
in `scripts/hallo.py` und ist auch ohne Claude Code nutzbar.
## Folgen
Der Leitfaden wird deutlich kürzer und bleibt aktuell. Wer ein anderes Werkzeug nutzen will, kann das weiterhin
tun — `AGENTS.md` und `.agents/skills/` sind der Standard, den auch andere Agenten lesen. Nur die
Schritt-für-Schritt-Anleitung dafür pflegen wir nicht mehr. Kommt ein zweites Werkzeug ins Team, wird diese
Entscheidung ersetzt.
`.vscode/` entfällt im Repository: es versionierte persönliche Pfade. Die brauchbaren Einstellungen stehen jetzt
als kopierbarer Block im Leitfaden, mit dem Hinweis, sie in die Benutzereinstellungen zu übernehmen.
## Verworfene Alternativen
Alle Varianten weiterpflegen (Aufwand ohne Nutzen, veraltet schneller als sie gelesen werden); vollständig auf
Claude Code umstellen und `AGENTS.md` durch `CLAUDE.md` ersetzen (Bindung an einen Anbieter, ohne dass es etwas
vereinfacht — die Spiegelung ist ein Skriptaufruf).
@@ -0,0 +1,43 @@
# 0009 Tests sind Teil der Definition of Done, und sie werden für Menschen dokumentiert
Status: vorgeschlagen · Datum: 2026-09-04 · Beteiligte: AG QGIS · Ergänzt 0003
## Kontext
Entscheidung 0003 machte den Baseline-Lauf vor jedem Pull Request zur Pflicht. Sie verhindert *Rückschritte*,
verlangt aber keine *neuen* Tests. Bei überwiegend KI-erzeugtem Code ist das zu wenig: Ein Agent kann
überzeugend erklären, dass etwas funktioniert, ohne dass es funktioniert.
Der am 04.09.2026 erstmals erzeugte Testkatalog zeigt dazu einen zweiten Befund: Von 332 Testfällen haben nur
100 (30 %) eine Beschreibung — `sn_basis` null von 87, `sn_plan41` einen von 44. Ohne Beschreibung ist von außen
nicht erkennbar, was ein Test prüft. Damit kann niemand beurteilen, ob die Abdeckung **fachlich** sinnvoll ist,
und genau das ist bei einer Fachanwendung die entscheidende Frage. Ein grüner Testlauf, dessen Inhalt niemand
kennt, ist ein Gefühl, kein Nachweis.
## Entscheidung
1. **Neue Funktionalität ohne Test wird nicht gemergt.** Mindestens ein Test je neuem beobachtbarem Verhalten.
Bei einer Fehlerbehebung zuerst der Test, der den Fehler zeigt.
2. **Jeder Test hat einen deutschen Docstring**, dessen erste Zeile mit `Prüft, dass …` beginnt. Diese Zeile ist
für die meisten Leser der einzige Kontakt mit dem Test.
3. **Der Testkatalog wird erzeugt, nicht gepflegt.** `scripts/testkatalog.py` liest die Testdateien per `ast`
(ohne Import, ohne QGIS, ohne Abhängigkeiten) und schreibt `docs/tests/testkatalog.md`. Er wird mitcommittet;
`--pruefen` meldet, wenn er veraltet ist.
4. **Fachlich wichtige Abläufe bekommen eine Testkonzept-Seite** in `docs/tests/` nach
`docs/vorlagen/testkonzept.md`: ein Mermaid-Diagramm des fachlichen Ablaufs, eine Tabelle
Schritt → deckender Test → bewusst nicht getestet mit Begründung, und die bekannten Lücken.
Damit kann eine Fachperson ohne Programmierkenntnisse prüfen, ob die Tests das Richtige prüfen.
Die Docstring-Pflicht gilt für **neue und geänderte** Tests. Die 232 bestehenden Tests ohne Beschreibung werden
nicht auf einen Schlag nachgezogen; der Abschnitt „Lücken" des Katalogs ist die Arbeitsliste, die nebenbei
abgearbeitet wird.
## Folgen
Pull Requests werden etwas größer und etwas langsamer. Dafür wird zum ersten Mal überprüfbar, ob die Testsuite
fachlich das Richtige abdeckt — bisher war nur bekannt, wie viele Tests grün sind. Der Katalog macht Lücken
sichtbar, statt sie in Testdateien zu verstecken. Die Testkonzept-Seiten sind zusätzlicher Pflegeaufwand und
lohnen nur für Abläufe, die fachlich wirklich wichtig sind — nicht für jede Funktion.
## Verworfene Alternativen
Nur den generierten Katalog (zeigt was getestet wird, aber nicht ob es genügt — genau die Prüfung, um die es
geht, fiele weg); nur handgeschriebene Testkonzepte (laufen dem Code hinterher, dieselbe Drift, die die
Wissensbasis sonst vermeidet); Testabdeckung in Prozent als Ziel (misst durchlaufene Zeilen, nicht fachliche
Vollständigkeit, und verleitet zu Tests ohne Aussage).
+36
View File
@@ -0,0 +1,36 @@
# Entscheidungen (ADR)
Eine Datei je Entscheidung, fortlaufend nummeriert, nie nachträglich geändert. Wird eine Entscheidung ersetzt,
bekommt die alte den Status „ersetzt durch NNNN“ und die neue verweist zurück.
Vorlage:
```markdown
# NNNN Titel
Status: vorgeschlagen | angenommen | ersetzt durch NNNN · Datum: JJJJ-MM-TT · Beteiligte: …
## Kontext
Warum stand die Entscheidung an? Welche Randbedingungen?
## Entscheidung
Was wurde entschieden (ein Satz), danach die wichtigsten Details.
## Folgen
Was wird dadurch einfacher, was schwerer, was muss getan werden?
## Verworfene Alternativen
Kurz, mit Grund.
```
| Nr. | Titel | Status |
|---|---|---|
| 0001 | `AGENTS.md` ist die kanonische Anweisungsdatei | vorgeschlagen |
| 0002 | Workspace-Meta-Repository mit eigenständigen Klonen | vorgeschlagen |
| 0003 | Mock-Tests mit Baseline sind Pflicht vor jedem PR | vorgeschlagen |
| 0004 | Geteiltes Wissen liegt versioniert in `docs/`, nicht im Werkzeug-Gedächtnis | vorgeschlagen |
| 0005 | Grundlegendes Refactoring erst nach grüner Baseline und festgeschriebenen Verträgen | vorgeschlagen |
| 0006 | Das Workspace-Repository heißt „Projektwissen" | vorgeschlagen |
| 0007 | Branchmodell: drei Kanäle, `stable` als stabiler Kanal, Rückführungspflicht | vorgeschlagen |
| 0008 | Werkzeugunterstützung auf Claude Code fokussiert | vorgeschlagen |
| 0009 | Tests sind Teil der Definition of Done, und sie werden für Menschen dokumentiert | vorgeschlagen |
@@ -0,0 +1,188 @@
# Konzept: Gemeinsame KI-gestützte Entwicklung über mehrere Behörden und Werkzeuge
Stand 02.09.2026. Vorschlag zur Abnahme durch die AG QGIS. Die Umsetzung liegt als Struktur bereits in diesem
Workspace vor; was das Team anders entscheidet, wird hier und in `docs/entscheidungen/` nachgezogen.
## 1. Ausgangslage und Ziele
**Randbedingungen**
- Mehrere Entwickler in verschiedenen Flurbereinigungsbehörden, überwiegend Fachleute ohne Software-Ausbildung.
- Jeder nutzt eigene KI-Agenten: Claude Code, OpenAI Codex, teils GitHub Copilot in VS Code (Issue Basis #36).
*Nachtrag 04.09.2026: Das Team arbeitet mit Claude Code; die Doku beschreibt nur noch dieses Werkzeug, die
Dateistruktur bleibt werkzeugneutral (ADR 0008).*
Alle arbeiten ordnerbasiert und lesen Anweisungsdateien aus dem Repository.
- Elf Repositories in einer Gitea-Organisation, eigener Release-Prozess, Zielplattform Windows/OSGeo4W mit QGIS 3.40
und 4. Die Agenten laufen meist ohne QGIS.
- Der Code ist gewachsen und wird später grundlegend refaktoriert; bis dahin soll die Arbeit trotzdem sicher und
effizient sein.
**Ziele**
1. Jeder Agent und jeder Entwickler bekommt **denselben Projektkontext**, unabhängig vom Werkzeug.
2. Wissen fließt **zurück ins Repository**, nicht in Chatverläufe oder lokale Gedächtnisse.
3. **Tokensparsamkeit**: Kontext wird schichtweise geladen; Verifikation (Tests) ist billig und schnell.
4. **Verbindlicher, leichter Arbeitszyklus**, der für Fachleute in einer Stunde erlernbar ist.
5. Vorbereitung des Refactorings, ohne es vorwegzunehmen.
## 2. Leitprinzipien
- **Eine Wahrheit, versioniert.** Alles, was mehr als eine Person wissen muss, steht in Git. Werkzeugspezifische
Dateien sind nur dünne Zeiger auf die kanonische Quelle.
- **Kontext in Schichten (progressive Offenlegung).** Immer geladen: eine kurze Anweisungsdatei. Auf Abruf: Modulkarte,
Schnittstellen, Fallstricke. Bei Bedarf: Skills mit Ablaufwissen. Nur wenn nötig: der Code selbst.
- **Verträge vor Code.** Agenten arbeiten sicher, wenn sie die impliziten Regeln kennen (Wrapper, Variablen, Dock,
Release). Deshalb sind Verträge ausdrücklich dokumentiert und in Regeln übersetzt.
- **Verifikation ist Pflicht und billig.** Mock-Tests in Sekunden, Baseline gegen Verschlechterung, QGIS-Tests dort,
wo Mocks nicht reichen.
- **Kleine Schritte, sichtbare Übergaben.** Ein Issue, ein Branch, ein PR, eine Übergabenotiz. Keine Sammel-PRs.
- **Menschen entscheiden, Agenten arbeiten.** Fachliche Entscheidungen und Merges bleiben bei Personen; Agenten
liefern Vorschläge mit Nachweis.
## 3. Struktur des Projekts: drei Varianten
| Variante | Beschreibung | Vorteile | Nachteile |
|---|---|---|---|
| **A. Workspace-Meta-Repository mit Klonen** (empfohlen, umgesetzt) | Neues Repo `AG_QGIS/Projektwissen` mit `AGENTS.md`, `docs/`, Skills, Skripten, Manifest. Plugin-Repos werden als Unterordner geklont (per `.gitignore` ausgenommen). | Kein Eingriff in bestehende Repos und Release-Prozess; ein Ordner = ganzer Projektkontext; sofort nutzbar; Meta-Repo versioniert die Wissensbasis; Manifest hält kompatible Stände fest | Zwei Ebenen von Git; Entwickler müssen im Workspace-Root starten; per-Repo-Dateien bleiben nötig für Einzelklone |
| B. Monorepo | Alle Plugins in ein Repository überführen | Ein Kontext, ein PR über Plugin-Grenzen, einfache Verträge | Release-Prozess, Tags und Feeds müssten umgebaut werden; Historie-Migration; das ist Teil des Refactorings, nicht der Vorbereitung |
| C. Nur per-Repo-Dateien | `AGENTS.md` in jedem Plugin-Repo, keine Meta-Ebene | Minimal | Gesamtkontext fehlt genau dort, wo er gebraucht wird (Basis ↔ Fachplugin); Wissensbasis müsste dupliziert werden |
**Empfehlung A**, mit per-Repo-`AGENTS.md` als Ergänzung (Vorlage in `docs/vorlagen/AGENTS.repo.md`). Variante B bleibt als
Option für die Refactoring-Phase offen; der Workspace bereitet sie vor (Manifest, Verträge, Tests).
## 4. Anweisungsdateien: kanonisch `AGENTS.md`
| Werkzeug | liest | Lösung |
|---|---|---|
| Claude Code (genutzt) | `CLAUDE.md` (mit `@`-Importen), Skills in `.claude/skills/` | `CLAUDE.md` = `@AGENTS.md` + wenige Claude-Zeilen; Skills werden gespiegelt |
| andere (Codex, Copilot, Cursor, Gemini CLI, …) | überwiegend `AGENTS.md` und Agent-Skills-Standard | direkt, ohne dass wir etwas ändern müssten wir dokumentieren die Einrichtung aber nicht (ADR 0008) |
Alternativen und warum nicht: getrennte Dateien je Werkzeug (Drift), Symlink `CLAUDE.md → AGENTS.md` (unter Windows
Administratorrechte nötig; Import ist gleichwertig).
**Inhalt und Grenzen der `AGENTS.md`:** unter 150 Zeilen; nur Fakten, die in jeder Sitzung gelten (Regeln, Befehle,
Leseliste, Arbeitsablauf in Kurzform). Alles Prozedurale wandert in Skills, alles Beschreibende in `docs/`.
Sie wird bei jeder Sitzung in den Kontext geladen und ist damit die teuerste Datei des Projekts.
**Per-Repo-`AGENTS.md`** (kurz, ≤ 40 Zeilen): Rolle des Repos, Aufbau, Zusatzregeln, Testbefehl, Verweis auf den
Workspace. Nötig, weil Codex Anweisungen nur bis zur Wurzel des jeweiligen Git-Repos sucht und weil Entwickler
gelegentlich nur ein Repo klonen. Für Claude Code liegt daneben eine `CLAUDE.md` mit `@AGENTS.md`.
## 5. Bausteine der Wissensbasis
```
AG_QGIS/ Klon von Projektwissen (Meta)
├── AGENTS.md, CLAUDE.md Schicht 1: immer geladen
├── workspace.toml Manifest: Repos, Branches, Paketnamen, Rollen
├── scripts/ setup_workspace.py, run_tests.py, link_qgis_profile.py
├── .agents/skills/<name>/SKILL.md Schicht 3: Ablaufwissen auf Abruf
├── docs/status/STATUS.md Schicht 2: aktueller Stand (Pflichtlektüre)
├── docs/architektur/ Modulkarte, Schnittstellen (lebend)
├── docs/wissen/ Fallstricke, Glossar, externe Dienste (geteiltes Gedächtnis)
├── docs/entscheidungen/ ADRs: warum etwas so ist
├── docs/vorlagen/ Issue, PR, Übergabe, Repo-AGENTS.md, CI-Workflow
├── docs/analyse|konzept|leitfaden Momentaufnahmen und stabile Texte
└── Plugin_SN_*/ … eigenständige Klone, je mit AGENTS.md + CLAUDE.md
```
| Baustein | Zweck | Pflegeregel |
|---|---|---|
| `STATUS.md` | Was ist gerade los, wer arbeitet woran, offene Baustellen | bei jedem Merge/Übergabe |
| `modulkarte.md` | gezieltes Lesen statt Vollzugriff; Kennzeichnung großer Dateien | bei neuen/verschobenen Modulen |
| `schnittstellen.md` | die impliziten Verträge explizit | vor jeder Änderung daran |
| `fallstricke.md` | nicht ableitbares Wissen mit Datum und Quelle | sobald etwas Zeit gekostet hat |
| `glossar.md`, `externe_dienste.md` | Fachbegriffe, URLs, Abhängigkeiten | nach Bedarf |
| `entscheidungen/NNNN-*.md` | eine Entscheidung je Datei: Kontext, Entscheidung, Folgen, Status | neue Entscheidung = neue Datei; alte nie ändern, nur ersetzen |
| Skills | wiederkehrende Abläufe: Aufgabe bearbeiten, Tests, Übergabe, neues Fachplugin, Release | wenn sich ein Ablauf ändert |
| `testbaseline.json` | Liste bekannter roter Tests | nur mit Absprache (`--update-baseline`) |
### Shared Memory: was wohin
| Art von Wissen | Beispiel | Ort |
|---|---|---|
| Regel, die immer gilt | „Qt nur über Wrapper“ | `AGENTS.md` |
| Vertrag zwischen Komponenten | Prioritäten der Actions, Variablennamen | `schnittstellen.md` |
| Stolperfalle | „Release baut Kanal-Branch, nicht Tag“ | `fallstricke.md` |
| Warum-Entscheidung | „AGENTS.md kanonisch“ | `entscheidungen/` |
| Ablauf | Release-Schritte | Skill |
| Zwischenstand einer Aufgabe | „Test X ist rot, weil …“ | Übergabenotiz im PR und `STATUS.md` |
| Persönliche Vorlieben, lokale Pfade | Editor, OSGeo-Pfad | lokales Tool-Gedächtnis, `CLAUDE.local.md` (gitignored) |
Die werkzeugeigenen Gedächtnisse (Claude Auto-Memory) bleiben erlaubt, gelten aber als
**Notizzettel**. Die Regel in `AGENTS.md` und im Skill `sn-uebergabe` verpflichtet dazu, Teilbares in die Wissensbasis
zu überführen. Das greift den Vorschlag aus Issue Basis #36 auf und ersetzt die dort vorgesehenen
`copilot-instructions.md` je Plugin durch die werkzeugneutralen `AGENTS.md`.
## 6. Tokensparsamkeit: konkrete Maßnahmen
| Maßnahme | Wirkung | Umsetzung |
|---|---|---|
| Kurze `AGENTS.md`, Rest auf Abruf | jede Sitzung startet mit wenigen tausend Token statt Vollkontext | erledigt |
| Modulkarte mit Zeilenzahlen und Warnung ab 600 Zeilen | Agent liest Abschnitte statt Dateien | erledigt; Regel in `AGENTS.md` |
| Schnittstellen-Dokument | keine Erkundung von drei Repos, um einen Vertrag zu verstehen | erledigt |
| Fallstricke | keine Wiederholung bekannter Irrwege | erledigt, wächst |
| Skills statt wiederholter Erklärungen | Ablauf wird nur geladen, wenn er gebraucht wird | 5 Skills erledigt |
| `run_tests.py` mit Zusammenfassung und Baseline | Sekunden statt Minuten; keine Rohausgaben im Kontext | erledigt |
| Übergabenotiz je Aufgabe | nächste Sitzung beginnt bei Schritt 5 statt 1 | Vorlage + Skill erledigt |
| Issues mit Abnahmekriterien | weniger Rückfragen, weniger Fehlversuche | Vorlage erledigt |
| Unteragenten für breite Suchen (Claude Code) | Hauptkontext bleibt schlank | Hinweis in `CLAUDE.md` |
| Refactoring großer Dateien | dauerhaft kleinere Lesekosten | spätere Phase |
Anti-Muster, die vermieden werden: ganze Dateien in den Chat kopieren; „Lies das ganze Projekt“; Testrohausgaben
einfügen; parallele Erkundung durch mehrere Personen ohne Übergabe; Erkenntnisse nur im Chat.
## 7. Arbeitszyklus und Rollen
```mermaid
flowchart LR
I[Issue mit Abnahmekriterien] --> B[Branch von unstable]
B --> A[Agent-Auftrag mit Kontextverweisen]
A --> U[Umsetzung in kleinen Schritten]
U --> T[run_tests.py --baseline\n+ QGIS-Test bei UI]
T --> P[PR mit Übergabenotiz]
P --> R[Review durch Mensch]
R -->|Änderungen| U
R -->|Merge| S[STATUS.md, Wissen, Changelog]
S --> REL[Release bei Bedarf]
```
| Rolle | Aufgaben |
|---|---|
| **Maintainer** (je Repo eine Person, Vorschlag: bisherige Hauptentwickler) | Merge-Recht, Release, Pflege von `AGENTS.md`/Verträgen, Baseline-Änderungen |
| **Entwickler** | Issues formulieren, Agenten steuern, PRs mit Nachweis liefern, Wissensbasis pflegen |
| **Agent** | umsetzen, testen, dokumentieren; keine Merges, keine Tags, keine Entscheidungen über Verträge |
| **AG QGIS** | Entscheidungen zu Verträgen, Datenmodell, Refactoring, Alternativansatz |
Review-Regeln: KI-erzeugter Code wird immer von einer Person gelesen; PRs unter etwa 400 geänderten Zeilen; jede
Vertragsänderung braucht ein Issue mit Label `Schnittstelle`.
## 8. Werkzeugspezifika
**Claude Code:** im Workspace-Root starten; `/context` zeigt, ob `CLAUDE.md` und `AGENTS.md` geladen sind. Skills
liegen nach `python scripts/setup_workspace.py --nur-skills` in `.claude/skills/`. Beim Arbeiten in einem
Plugin-Ordner lädt Claude die dortige `CLAUDE.md` automatisch dazu. Auto-Memory ist lokal; Regel siehe Abschnitt 5.
**Gitea:** Labels `Bug`, `Feature`, `Diskussion` ergänzen um `Schnittstelle`, `Tests`, `Doku`, `Refactoring`.
Issue- und PR-Vorlagen aus `docs/vorlagen/` in `.gitea/` der Repos übernehmen (optional).
## 9. Einführung in Phasen
| Phase | Inhalt | Ergebnis |
|---|---|---|
| **0 Projektwissen veröffentlichen** | Repo `AG_QGIS/Projektwissen` anlegen, diesen Ordner pushen, Team liest Konzept und Leitfaden, Entscheidungen bestätigen (ADR-Status auf „angenommen“) | ab sofort gemeinsamer Kontext |
| **1 Repos anbinden** | per-Repo `AGENTS.md`/`CLAUDE.md` per PR in Basis, Plan41, Verfahrensgebiet, Widmung; Release-Excludes ergänzen; Metadata-Templates korrigieren | Agenten arbeiten auch in Einzelklonen korrekt |
| **2 Tests absichern** | Baseline committen, CI-Workflow aus Vorlage in die drei Plugin-Repos, Rot→Grün als Issues je Testdatei | Tests haben wieder Aussagekraft |
| **3 Verträge festschreiben** | Abhängigkeiten deklarieren, Variablennamen bereinigen (kleiner, gezielter Eingriff), Linkliste-Entscheidung, Manifest kompatibler Versionen | stabile Basis für den Umbau |
| **4 Refactoring planen** | Zielarchitektur (inkl. Fachschale-Idee), Scheiben, Reihenfolge, Monorepo-Frage | eigenes Konzept |
Phase 0 und 1 sind in wenigen Stunden erledigt; Phase 2 ist die erste echte Teamaufgabe mit Agenten und zugleich das
Training des Arbeitszyklus.
## 10. Entscheidungen, die das Team treffen muss
1. Name und Ort des Meta-Repositories (entschieden: `AG_QGIS/Projektwissen`, ADR 0006).
2. Bestätigung der ADRs 00010005 (`docs/entscheidungen/`).
3. Maintainer je Repository.
4. Bestätigung der ADRs 00060009 (Reponame, Branchmodell, Werkzeugfokus, Testpflicht).
5. Linkliste: zentrale CSV als einzige Quelle (Empfehlung) oder XLSX im Plugin.
6. Zeitpunkt des Refactorings (Empfehlung: nach Phase 3) und ob die Fachschale-Idee Teil des Zielbilds wird.
+244
View File
@@ -0,0 +1,244 @@
# Plan: automatisierte Tests gegen echtes QGIS
Stand 04.09.2026. Vorschlag zur Abstimmung im Team. Ergänzt [`../leitfaden/06_tests.md`](../leitfaden/06_tests.md)
um die zweite Testebene. Recherchestand und Quellen am Ende.
## 1. Worum es geht
Heute laufen 332 Tests im Mock-Modus — ohne QGIS, in unter einer Sekunde. Das deckt Logik, Parsing und
Prüfregeln ab. Es deckt **nicht** ab, was erst im Zusammenspiel mit QGIS passiert: ob ein Layer wirklich entsteht,
ob ein WFS-Ergebnis verarbeitbar ist, ob ein Drucklayout eine Seite erzeugt, ob ein Dock sich sauber wieder
abmeldet. Genau dort sitzen die Fehler, die heute nur durch manuelles Klicken auffallen — und die zwischen Qt5
und Qt6 unterschiedlich ausfallen.
Es gibt seit Jahren einen etablierten Weg, QGIS-Plugins ohne Bildschirm und ohne menschliche Interaktion gegen
eine echte QGIS-Instanz zu testen. Dieser Plan beschreibt, wie wir dort in vier Stufen hinkommen.
> **Der Mock-Modus bleibt.** Er wird nicht ersetzt. Er ist die schnelle Ebene, die bei jedem Commit läuft;
> QGIS-Tests sind die langsame Ebene, die bei jedem Pull Request läuft. Wer die Mocks abschafft, tauscht
> Sekunden gegen Minuten und verliert die Wrapper-Disziplin, die den Qt5/Qt6-Code zusammenhält.
## 2. Vorab: unsere Zielversionen stimmen nicht mehr
Recherchestand 04.09.2026, geprüft am QGIS-Release-Fahrplan und an den Docker-Tags:
| | Version | Status |
|---|---|---|
| LTR | **3.44** | aktueller Langzeit-Release |
| Stable | **4.2** | wird am 25.09.2026 zum LTR |
| 3.40 | 3.40.15 (Januar 2026) | **End of Life**, keine Updates mehr |
`workspace.toml` sagt `qgis_min = "3.40"`, alle `.gitea/templates/metadata.template` sagen
`qgisMinimumVersion=3.40`. Das ist keine Testfrage, sondern eine Produktentscheidung: **Welche QGIS-Versionen
unterstützen wir?** Sie muss vor Stufe 2 fallen, weil sie die Test-Matrix bestimmt. Vorschlag zur Diskussion:
`3.44` als Minimum, `4.2` als zweites Matrix-Ziel, `3.40` nur solange Bestandsinstallationen es erzwingen.
## 3. Wie es technisch funktioniert
Der Kern ist `qgis.testing.start_app()` aus dem QGIS-Quellbaum. Es baut eine vollständige `QgsApplication` mit
registrierten Datenprovidern auf, in einem temporären Profil, und ist idempotent — mehrfacher Aufruf schadet
nicht. Plugins lädt es bewusst **nicht**; das eigene Plugin instanziiert der Test selbst.
```python
import os
if not os.getenv("DISPLAY"):
os.environ["QT_QPA_PLATFORM"] = "offscreen" # muss VOR dem App-Start gesetzt sein
from qgis.testing import start_app, unittest
from qgis.testing.mocked import get_iface
QGIS_APP = start_app()
IFACE = get_iface() # echtes QMainWindow, echter QgsMapCanvas, echter LayerTree
```
`get_iface()` liefert kein selbstgebautes Mock, sondern ein `QgisInterface`-Objekt mit echtem Hauptfenster,
echtem Kartenfenster und echtem Layerbaum. Das ist deutlich realitätsnäher als alles, was wir selbst nachbauen
könnten, und es kostet uns keine Pflege.
`QT_QPA_PLATFORM=offscreen` ist der Schalter, der das Ganze ohne Bildschirm laufen lässt — auf dem CI-Server
genauso wie unter Windows.
## 4. Der Stufenplan
```mermaid
flowchart LR
S1["<b>Stufe 1</b><br/>start_app() lokal<br/><i>klein</i>"]
S2["<b>Stufe 2</b><br/>Gitea Actions<br/>Matrix 3.44 / 4.2<br/><i>mittel</i>"]
S3["<b>Stufe 3</b><br/>pytest-qgis<br/><i>klein</i>"]
S4["<b>Stufe 4</b><br/>Render-Vergleich<br/>Plan41<br/><i>groß</i>"]
S1 --> S2 --> S3 --> S4
style S1 fill:#e8f6ec,stroke:#2f9e44
style S2 fill:#e6f2ff,stroke:#2b7dd9
style S3 fill:#e6f2ff,stroke:#2b7dd9
style S4 fill:#fff4e6,stroke:#d9822b
```
### Stufe 1 — QGIS-Tests lokal, ohne CI
**Ziel:** Eine erste echte QGIS-Suite je Plugin, die ein Entwickler auf seinem Windows-Rechner starten kann.
- Neuer Ordner `tests/qgis/` je Plugin-Repo, getrennt von den Mock-Tests in `tests/`. Beide Suiten bleiben
unabhängig lauffähig.
- Start unter Windows über die OSGeo4W-Umgebung:
`C:\OSGeo4W\bin\python-qgis-ltr.bat -m pytest tests\qgis -v`.
Ein normales `python -m pytest` findet die QGIS-Bindings nicht.
- Die vorhandenen Skripte `tests/test_qgis.bat` und `tests/start_osgeo4w_qgis.bat` werden dabei ersetzt. Sie
enthalten heute fest verdrahtete Pfade (`D:\OSGeo`, `D:\QGISQT5`) und funktionieren nur auf einem Rechner.
- Erste Kandidaten, weil dort der Mock-Modus am wenigsten aussagt: Layer anlegen und stylen (`sn_basis`),
Verarbeitung einer WFS-Antwort (`sn_verfahrensgebiet`), Erzeugen eines Drucklayouts (`sn_plan41`).
**Aufwand:** klein. Kein neues Werkzeug, keine Infrastruktur.
**Nutzen:** sofort, aber nur für die, die es lokal ausführen.
### Stufe 2 — Gitea Actions mit Docker
**Ziel:** Jeder Pull Request wird automatisch gegen zwei QGIS-Generationen geprüft, ohne dass jemand eine zweite
QGIS-Version installiert.
Das ist der größte Hebel des ganzen Plans. Der Qt5/Qt6-Unterschied ist in `fallstricke.md` ein Dauerthema, und
er lässt sich mit Bordmitteln kaum abdecken — hier fällt er automatisch auf.
Die offiziellen Images `qgis/qgis` bringen QGIS samt Bindings mit. Verifizierte Tags (Stand 04.09.2026):
`3.44`, `3.44.14`, `4.0`, `4.2`, `4.2.2`, `ltr`, `stable`. QGIS 4 gibt es nur auf Qt6-Distributionen
(Debian trixie, Ubuntu questing) — kein `4.x-noble`.
```yaml
name: QGIS-Tests
on:
pull_request:
branches: [unstable, testing, stable]
jobs:
qgis:
runs-on: ubuntu-latest # Label muss auf einen Docker-fähigen act_runner zeigen
strategy:
fail-fast: false
matrix:
qgis: ["3.44", "4.2"]
steps:
- uses: actions/checkout@v4
- name: Tests in QGIS ${{ matrix.qgis }}
run: |
docker run --rm -v "$PWD":/src -w /src \
-e QT_QPA_PLATFORM=offscreen \
qgis/qgis:${{ matrix.qgis }} \
bash -c "python3 -m venv /venv --system-site-packages \
&& /venv/bin/pip install -q pytest \
&& /venv/bin/pytest tests/qgis -v"
```
**Zu klären vor der Umsetzung:**
- Der vorhandene Gitea-Runner trägt das Label `alpine-latest` (siehe `docs/vorlagen/gitea_workflow_tests.yaml`).
Für Docker-in-Docker braucht es einen Runner mit Docker-Zugriff. Das ist eine Frage an den Maintainer der
Runner-Infrastruktur, keine Codefrage.
- Die Fachplugins brauchen im Container auch `sn_basis`. Das Muster steckt schon in
`docs/vorlagen/gitea_workflow_tests.yaml`: die Nachbar-Repos dazuklonen und unter ihren Paketnamen ablegen.
**Aufwand:** mittel, im Wesentlichen Infrastruktur.
**Nutzen:** hoch und dauerhaft.
### Stufe 3 — `pytest-qgis`
**Ziel:** Die bekannten Stolperfallen von QGIS-Tests nicht selbst lösen müssen.
`pytest-qgis` (Version 4.1.1, August 2026, Python ≥ 3.10, QGIS ≥ 3.34, Qt6-tauglich seit 3.0.0) bringt mit:
| Was | Warum wichtig |
|---|---|
| Fixture `qgis_app` | startet die App einmal je Sitzung |
| Fixture `qgis_iface` | patcht `qgis.utils.iface` automatisch — sonst ist es `None` |
| Fixture `qgis_new_project` | leeres Projekt je Test; sonst wirken Layer aus Test A in Test B nach |
| automatisches Layer-Aufräumen | verhindert die bekannten Abstürze beim Beenden |
| Fixture `qgis_processing` | Processing-Registry initialisiert |
| `--qgis_disable_exit` | Notausgang, falls es beim Beenden doch kracht |
**Aufwand:** klein — etwa eine halbe Stunde plus Umstellung der bis dahin geschriebenen Tests.
**Empfehlung:** erst nach Stufe 2, damit die Umstellung einmal an einer laufenden CI verifiziert wird.
### Stufe 4 — Render-Vergleich für Plan41
**Ziel:** Erkennen, wenn sich das Aussehen eines Plans ungewollt ändert.
Für ein Plugin, dessen Zweck das Zeichnen von Plänen nach § 41 FlurbG ist, ist das fachlich der wertvollste
Testtyp — und der teuerste. Mit einem verifizierten Fallstrick:
> `QgisTestCase.render_layout_check()` und `render_map_settings_check()` suchen Referenzbilder über
> `setControlPathPrefix()` in einem zur Compile-Zeit einkompilierten Pfad des QGIS-Quellbaums. In einer
> installierten QGIS-Version existiert dieser Pfad nicht. Die bequemen Helfer sind für Plugins **nicht** nutzbar.
Gangbar sind stattdessen:
- `QgsLayoutExporter.renderPageToImage()` und ein eigener, toleranter Bildvergleich, oder
- `QgsRenderChecker` direkt, das als einziges `setControlImagePath()` anbietet.
**Bedingungen, ohne die es nur Ärger macht:**
- Referenzbilder **im CI-Container** erzeugen, nie auf einem Entwickler-Windows. Schriftrendering und
Antialiasing unterscheiden sich zwischen den Plattformen und erzeugen sonst dauerhaft rote Tests.
- Toleranz einplanen (Farbabweichung und Anteil abweichender Pixel), nicht auf Byte-Gleichheit prüfen.
- Mit **zwei bis drei** Referenzplänen anfangen, nicht mit fünfzig.
**Aufwand:** groß. Erst angehen, wenn Stufen 13 stabil laufen.
## 5. Was wir bewusst nicht machen
| Ansatz | Warum nicht |
|---|---|
| YAML-Testframework für Processing-Algorithmen | An `nose2`, `utilities.py` und CTest im QGIS-Quellbaum gebunden. Für externe Plugins nur mit Portierungsaufwand nutzbar. Stattdessen `processing.run(...)` und `assertLayersEqual`. |
| `qgis_setup.sh` / `qgis_testrunner.sh` (Plugin im echten QGIS laden) | Kein pytest-Reporting; Erfolg wird über String-Suche in der Standardausgabe erkannt. Nur nötig, wenn der Plugin-Ladevorgang selbst geprüft werden soll. |
| `pytest-xdist` (Parallelisierung) | Bei unserer Teamgröße und Suite-Laufzeit unnötige Komplexität. |
| Eigenes Docker-Image bauen | `qgis/qgis` plus `pip install` im Job reicht. |
| `pb_tool` | Deployment-Helfer, kein Testwerkzeug. Lokal Geschmackssache. |
## 6. Bekannte Fallstricke
Wandern bei der Umsetzung nach `docs/wissen/fallstricke.md`:
- **`QT_QPA_PLATFORM` muss vor dem Erzeugen der `QgsApplication` gesetzt sein.** Danach wirkt es nicht mehr.
- **`qgis.utils.iface` ist nach `start_app()` `None`.** Entweder `pytest-qgis` verwenden oder selbst
`qgis.utils.iface = get_iface()` setzen — und darauf achten, keine Module zu früh zu importieren.
- **`QgsProject.instance()` ist global.** Ohne Aufräumen zwischen den Tests beeinflussen sie sich gegenseitig.
- **Abstürze beim Beenden** sind ein bekanntes QGIS-Thema, wenn Layer- oder Canvas-Objekte noch referenziert
werden. Gegenmittel: `pytest-qgis` oder `--qgis_disable_exit`.
- **`offscreen` hat keinen Fenstermanager.** Fokus, `activeWindow()` und Drag-and-drop verhalten sich anders.
Für Widget-Tests notfalls `xvfb-run -a` davorsetzen (nur Linux).
- **Modale Dialoge blockieren den Test.** Reihenfolge der Gegenmittel: den modalen Aufruf per `monkeypatch`
ersetzen; `open()` statt `exec()` verwenden; erst als letztes `QTimer.singleShot` mit einem Klick-Handler.
- **Locale.** Die Container laufen mit `LANG=C.UTF-8`, Entwickler-Windows meist auf Deutsch. Tests, die
formatierte Zahlen oder Datumsangaben vergleichen, brechen sonst nur an einem der beiden Orte.
- **QGIS 4 / PyQt6.** `exec_()` gibt es nicht mehr, Enum-Kurzformen (`Qt.UserRole`) auch nicht. Das trifft
Testcode genauso wie Produktivcode. Für den Umstieg gibt es das Prüfwerkzeug `pyqgis4-checker`.
## 7. Vorschlag zur Reihenfolge
1. **Entscheidung zu den Zielversionen** (Abschnitt 2) — blockiert Stufe 2, eigenes Issue.
2. **Stufe 1** an einem Plugin als Machbarkeitsnachweis, am besten `sn_basis` (keine Abhängigkeiten).
3. **Runner-Frage klären** — hat der Gitea-Runner Docker?
4. **Stufe 2** für `sn_basis`, dann für die Fachplugins.
5. **Stufe 3**, sobald mehr als eine Handvoll QGIS-Tests existiert.
6. **Stufe 4** als eigenes Vorhaben mit eigenem Konzept.
Die Stufen 1 bis 3 sind zusammen etwa der Aufwand, den heute ein größeres Fach-Issue kostet. Stufe 4 ist ein
eigenes Vorhaben.
## Quellen
Recherchestand 04.09.2026.
- `qgis.testing` (`start_app`, `QgisTestCase`): https://github.com/qgis/QGIS/blob/master/python/testing/__init__.py
- `qgis.testing.mocked.get_iface()`: https://github.com/qgis/QGIS/blob/master/python/testing/mocked.py
- Docker-Images und Test-Helfer: https://github.com/qgis/QGIS/blob/master/.docker/README.md
- Tag-Liste: https://hub.docker.com/r/qgis/qgis/tags
- Release-Fahrplan (LTR-Termine): https://qgis.org/resources/roadmap/
- Unit-Testing und Render-Checks: https://docs.qgis.org/testing/en/docs/developers_guide/unittesting.html
- `QgsRenderChecker` vs. `QgsMultiRenderChecker`: https://github.com/qgis/QGIS/blob/master/src/core/qgsrenderchecker.h
- `pytest-qgis`: https://github.com/GispoCoding/pytest-qgis · https://pypi.org/project/pytest-qgis/
- Modale Dialoge im Test: https://pytest-qt.readthedocs.io/en/latest/note_dialogs.html
- Migration zu Qt6/QGIS 4: https://plugins.qgis.org/docs/migrate-qgis4/ · https://github.com/qgis/pyqgis4-checker
- Gitea Actions, Runner-Labels: https://docs.gitea.com/usage/actions/act-runner/
Vorbilder mit echter QGIS-Testsuite, als Muster zum Abschauen:
- `3liz/lizmap-plugin` — Matrix über drei QGIS-Versionen, eigener `start_app()`-Ersatz, dem hiesigen Setup am nächsten
- `opengisch/qfieldsync` — Dialog-Tests gegen echtes QGIS, Matrix inklusive QGIS 4
- `opengisch/qgis_plugins_test_demo` — kleines Referenz-Repo, unter anderem ein Drucklayout-Test
+125
View File
@@ -0,0 +1,125 @@
# Repository „Projektwissen" auf Gitea einrichten
Prüfliste für den Maintainer. Stand 04.09.2026. Einmalig auszuführen; danach ist dieses Dokument nur noch
Nachweis, was wie eingestellt wurde.
Alles hier ist **Menschenarbeit**. Ein KI-Agent kann die Texte vorbereiten, aber weder Repositories anlegen noch
pushen noch Schutzregeln setzen (ADR 0007).
## 1. Repository anlegen
Gitea → Organisation `AG_QGIS`**Neues Repository**.
| Einstellung | Wert |
|---|---|
| Name | `Projektwissen` |
| Beschreibung | Gemeinsame Wissensbasis, Regeln und Werkzeuge der QGIS-Plugin-Entwicklung LNO Sachsen |
| Sichtbarkeit | öffentlich (wie die übrigen Repos der Organisation) |
| `.gitignore`, Lizenz, README | **nicht** anlegen lassen — bringt der erste Push mit |
| Issues | an |
| Wiki | aus — die Doku liegt versioniert im Repo (ADR-Begründung in `docs/wissen/externe_dienste.md`) |
| Standard-Branch | wird beim ersten Push gesetzt |
## 2. Ersten Stand pushen
Der Ordner `AG_QGIS/` ist bereits als Git-Repository vorbereitet, mit einem Commit, der genau die Dateien
dieses Repositories enthält (die Plugin-Klone sind per `.gitignore` ausgenommen).
```bash
cd <…>/AG_QGIS
git status # erwartet: nichts Unversioniertes außer den Plugin-Klonen
git log --oneline # erwartet: der vorbereitete erste Commit
git remote add origin https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Projektwissen.git
git push -u origin unstable
```
Wir beginnen bewusst auf `unstable`, damit auch dieses Repository dem Kanal-Modell aus
[`../leitfaden/05_git_arbeitsweise.md`](../leitfaden/05_git_arbeitsweise.md) folgt.
**Vor dem Push prüfen:**
- [ ] **Autor des Commits stimmt.** `git log -1 --format='%an <%ae>'` der vorbereitete Commit trägt die
Identität, die auf dem vorbereitenden Rechner in `git config --global` eingetragen war. Ist das nicht die
dienstliche Adresse, vor dem Push korrigieren:
```bash
git config user.name "Vorname Nachname"
git config user.email "dienstliche@adresse"
git commit --amend --reset-author --no-edit
```
- [ ] `git status` zeigt keine Plugin-Ordner (`Plugin_SN_*`, `Repository`, `Linkliste`, `Defaults`)
- [ ] `entwuerfe/` ist nicht enthalten
- [ ] keine Zugangsdaten, keine Tokens, keine echten Verfahrensdaten
- [ ] `python scripts/run_tests.py` läuft
- [ ] `python scripts/testkatalog.py --pruefen` meldet „aktuell"
## 3. Branches anlegen
```bash
git switch -c testing unstable && git push -u origin testing
git switch -c stable unstable && git push -u origin stable
git switch unstable
```
Danach in Gitea unter **Einstellungen → Allgemein** den Standard-Branch auf `unstable` setzen — dort landet die
tägliche Arbeit.
## 4. Branch-Schutz setzen
**Einstellungen → Branches** (englisch: *Settings → Branches*), dort eine neue Branch-Schutzregel anlegen — je
einmal für `stable`, `testing` und `unstable`:
| Einstellung | Wert |
|---|---|
| Branch-Name | `stable` / `testing` / `unstable` |
| Push deaktivieren | an |
| Merge-Freigabe erforderlich | mindestens 1 |
| Veraltete Freigaben verwerfen | an |
| Statusprüfungen erforderlich | erst aktivieren, wenn CI läuft (Abschnitt 6) |
## 5. Dasselbe für die Plugin-Repositories
Dort fehlt bisher der stabile Kanal. Je Repo (`Plugin_SN_Basis`, `Plugin_SN_Plan41`,
`Plugin_SN_Verfahrensgebiet`, `Plugin_SN_Widmung`, `Plugin_SN_FLT`, `Plugin_SN_Topografie`,
`Plugin_SN_Template`):
- [ ] `stable` anlegen — aus `testing`, wo es das gibt, sonst aus `unstable`. **Der Name muss exakt `stable`
lauten**, weil der Release-Workflow `git checkout "$CHANNEL"` ausführt (ADR 0007).
- [ ] Branch-Schutz für `stable`, `testing`, `unstable` wie in Abschnitt 4
- [ ] `oldstable` prüfen (`Plugin_SN_Basis`, `Plugin_SN_Plan41`): enthält er noch etwas Nötiges? Sonst löschen.
- [ ] Verwaiste Branches prüfen und löschen: `feature/Drucklayout-überarbeiten`, `print_layout` (Basis),
`bug/crash_on_exit` (Plan41), `alkis-nur-laden-stop`, `fix-fbschl-feldname` (Verfahrensgebiet)
Erst danach funktioniert ein Release auf dem stabilen Kanal (Tag ohne Suffix).
## 6. Optional: Tests in CI
Vorlage: [`../vorlagen/gitea_workflow_tests.yaml`](../vorlagen/gitea_workflow_tests.yaml) → als
`.gitea/workflows/tests.yaml` in `Plugin_SN_Basis`, `Plugin_SN_Plan41` und `Plugin_SN_Verfahrensgebiet`.
Sie klont das Projektwissen und die Nachbar-Repos und führt den Baseline-Lauf aus.
Voraussetzung: ein Gitea-Runner mit dem Label `alpine-latest` (wie beim Release-Workflow). Läuft das, in
Abschnitt 4 „Statusprüfungen erforderlich" aktivieren.
Der Ausbau zu Tests gegen echtes QGIS steht in
[`07_qgis_tests_plan.md`](07_qgis_tests_plan.md) und braucht einen Runner mit Docker.
## 7. Team informieren
- [ ] Link zum Repository und zur [`README.md`](../../README.md) verteilen
- [ ] Auf die Umstellung hinweisen: der Klon-Befehl heißt jetzt
`git clone .../Projektwissen.git AG_QGIS` (Ordnername unverändert)
- [ ] Um Durchsicht der Entscheidungen 00060009 bitten; nach Zustimmung den Status aller ADR von
„vorgeschlagen" auf „angenommen" setzen
- [ ] Jede Person trägt sich in `docs/status/STATUS.md` unter „Wer arbeitet woran" ein
## 8. Nach der Einrichtung zu klären
Diese Punkte liegen als Entwürfe bzw. offene Baustellen in `docs/status/STATUS.md`:
1. **QGIS-Mindestversion.** Steht auf 3.40, das seit Januar 2026 End of Life ist
([`07_qgis_tests_plan.md`](07_qgis_tests_plan.md) Abschnitt 2). Betrifft `workspace.toml` und alle
`.gitea/templates/metadata.template`.
2. **`.vscode/` aus den Plugin-Repos entfernen.** Liegt in vier Repos mit fest verdrahteten Pfaden eines
einzelnen Entwicklers. Je ein kleiner Pull Request.
3. **Release-Excludes ergänzen** (`Repository:hidden/workflows`): `AGENTS.md`, `CLAUDE.md`, `.agents/`,
`.claude/`, `docs/`, `tests/`, `.vscode/`.
+203
View File
@@ -0,0 +1,203 @@
# Arbeitsweise für Entwickler: So arbeiten wir mit KI-Agenten an den LNO-Plugins
Für Kolleginnen und Kollegen, die neu einsteigen. Fachwissen wird vorausgesetzt, Software-Erfahrung nicht.
Wir arbeiten mit Claude Code (ADR 0008); die Dateien bleiben werkzeugneutral. Die Kurzfassung für den Agenten
selbst ist `AGENTS.md`, dieser Leitfaden erklärt das Warum und das Wie. Git im Detail:
`05_git_arbeitsweise.md`. Tests im Detail: `06_tests.md`.
## 1. In zehn Minuten startklar
**Voraussetzungen:** Git, Python ≥ 3.11 (ist in OSGeo4W enthalten: `python-qgis.bat` oder das OSGeo-Python), Zugang
zu Gitea (`entwicklung.flurneuordnung-sachsen.de`), Claude Code (CLI oder VS-Code-Erweiterung).
Für QGIS-Tests: QGIS 3.44 LTR oder 4 mit einem eigenen Profil (z. B. `dev`), in dessen Plugin-Ordner die Pakete
`sn_basis`, `sn_verfahrensgebiet`, `sn_plan41` liegen (Verknüpfungen auf die Klone).
```bash
git clone https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Projektwissen.git AG_QGIS
cd AG_QGIS
python scripts/setup_workspace.py # klont die Plugin-Repos, spiegelt Skills
python scripts/run_tests.py # muss laufen: 3 Suiten, Zusammenfassung
```
> **Solange `AG_QGIS/Projektwissen` auf Gitea noch nicht angelegt ist** (Phase 0 im Konzept), schlägt der Clone mit
> „Repository not found“ fehl, obwohl die Anmeldung gelingt. Übergangslösung: den Ordner `AG_QGIS` aus dem
> Projektverzeichnis auf dem Netzlaufwerk (`Projekte\QGIS Plan 41\AG_QGIS`) komplett kopieren, darin
> `python scripts/setup_workspace.py` ausführen (erneuert den Skill-Spiegel und prüft die Klone) und weiter wie oben.
Dann das Werkzeug **im Ordner `AG_QGIS/` starten**:
| Form | Start | Prüfen |
|---|---|---|
| Claude Code CLI | `claude` im Ordner `AG_QGIS` | `/context` zeigt `CLAUDE.md` und `AGENTS.md` unter „Memory files“; `/hallo` ist im Menü |
| Claude Code in VS Code | Ordner `AG_QGIS` öffnen, Panel starten | dasselbe; Diffs erscheinen im Editor |
Einrichtung Schritt für Schritt: `04_werkzeuge_einrichten.md`.
Erste Frage an den Agenten zum Warmwerden: „Fasse `docs/status/STATUS.md` zusammen und nenne die drei wichtigsten
Regeln aus `AGENTS.md`.“ Antwortet er korrekt, ist die Einrichtung fertig.
## 2. Der Arbeitszyklus
```mermaid
flowchart TD
A["1 Issue auf Gitea (Vorlage docs/vorlagen/issue.md)"] --> B["2 Branch von unstable: feature/<nr>-<kurzname>"]
B --> C["3 Auftrag an den Agenten (Abschnitt 3)"]
C --> D["4 Umsetzung in kleinen Schritten, Zwischenstände committen"]
D --> E["5 python scripts/run_tests.py <paket> --baseline"]
E -->|rot| D
E -->|grün| F["6 Bei UI/QGIS-Verhalten: Test in QGIS, Ergebnis notieren"]
F --> G["7 changelog.txt oben ergänzen"]
G --> H["8 PR auf unstable (Vorlage docs/vorlagen/pull_request.md) mit Übergabenotiz"]
H --> I["9 Review durch eine Kollegin/einen Kollegen"]
I -->|Änderungswünsche| D
I -->|Merge| J["10 STATUS.md und Wissensbasis nachziehen"]
```
Jeder Schritt ist klein. Ein PR sollte an einem Tag entstehen und in einer Viertelstunde reviewbar sein. Wenn das
nicht geht, ist das Issue zu groß: aufteilen.
**Branch-Namen:** `feature/<issue-nr>-<kurzname>`, `bugfix/<issue-nr>-<kurzname>`, `doku/<kurzname>`.
**Commit-Nachrichten:** deutsch, Präsens, erste Zeile ≤ 72 Zeichen, Issue-Nummer am Ende (`Shape-Import: FBSchl.-Variante akzeptieren (#41)`).
## 3. Wie man einem Agenten einen Auftrag gibt
Der Agent liest `AGENTS.md` automatisch. Was er **nicht** weiß: was genau gewollt ist, wo es endet, und was fachlich
richtig ist. Ein guter Auftrag hat vier Teile:
1. **Ziel und Issue:** „Bearbeite Issue Verfahrensgebiet #41: …“ oder das Ziel in zwei Sätzen.
2. **Kontext-Zeiger statt Erklärungen:** „Betroffen ist `functions/verfahrensgebiet_shape_komplett.py`, Ablauf in
`docs/aus_shape_laden_technisch.md`. Lies zuerst `docs/architektur/schnittstellen.md` Abschnitt 6.“
3. **Grenzen:** „Keine Änderung am ALKIS-Ablauf. Keine neuen Abhängigkeiten. Kein Refactoring.“
4. **Abnahme:** „Fertig ist es, wenn `run_tests.py sn_verfahrensgebiet --baseline` keine neuen roten Tests zeigt, ein
Test für den Fall `FBSchl.` existiert und die Übergabenotiz geschrieben ist.“
Beispiel eines guten Auftrags (das Issue wurde am 02.09.2026 tatsächlich so in PR #45 gelöst; hier als Muster):
> Nutze den Skill `sn-aufgabe-bearbeiten` für Issue Verfahrensgebiet #41. Der Shape-Import soll zusätzlich das Feld
> `FBSchl.` (mit Punkt) akzeptieren, wie es manche DAVID-Exporte liefern. Betroffen: `_ermittle_gebietstyp_aus_fbschl`
> in `functions/verfahrensgebiet_shape_komplett.py`. Nicht ändern: die Bedeutung der Schlüssel `901/10110F` und
> `901/10120F`. Schreibe einen Test im Mock-Modus für beide Feldnamen. Am Ende: Übergabenotiz nach Vorlage.
Beispiel eines schlechten Auftrags: „Schau dir das Shape-Laden an und mach es besser.“ Das erzeugt lange Erkundung,
unklare Änderungen und einen PR, den niemand reviewen kann.
**Während der Arbeit:** Rückfragen des Agenten beantworten, fachlich prüfen, was er behauptet, und ihn bei
Abschweifungen zurückholen („Nur Issue #41“). Wenn er anfängt, Tests zu ändern statt Code, nach der Begründung fragen.
**Was der Agent nicht darf:** mergen, taggen/releasen, Baseline ändern, Verträge (Abschnitt `schnittstellen.md`)
ändern ohne Issue, Zugangsdaten anfassen.
## 4. Die Regeln und warum es sie gibt
| Regel (aus `AGENTS.md`) | Warum |
|---|---|
| Qt/QGIS nur über Wrapper | Ein Code für Qt5 und Qt6; Tests ohne QGIS; ein Ort für Kompatibilitätsfixe |
| Projektvariablen nur über `get_variable`/`set_variable`, Schlüssel ohne `sn_` | Einheitliches Präfix; Mock-Speicher in Tests; Doppelpräfixe vermeiden |
| Fachlogik ins Fachplugin, `sn_basis` importiert keine Fachplugins | Basis muss allein laden können; sonst Startfehler wie Issue Basis #57 |
| `grep` vor Änderungen an `sn_basis` | Drei Fachplugins in drei Repos hängen daran |
| Mock-Tests vor jedem Commit, keine neuen roten Tests | Tests sind der einzige schnelle Nachweis für Agentenarbeit |
| Kein Refactoring nebenbei | Umbauten ohne Plan zerstören Verträge, die nur im Code stehen; eigene Phase |
| Deutsch in Doku, Commits, Issues | Team und Fachbegriffe sind deutsch; Agenten folgen der Sprache der Dateien |
| Keine Zugangsdaten, keine echten Verfahrensdaten | Öffentliche Repos, Datenschutz |
| Übergabenotiz | Die nächste Person oder der nächste Agent soll nicht von vorn beginnen |
## 5. Tests
**Mock-Modus (immer):** `python scripts/run_tests.py <paket> --baseline`. Läuft ohne QGIS in Sekunden, verlinkt die
Repos unter ihren Paketnamen in ein temporäres Verzeichnis und fasst zusammen. `--baseline` vergleicht mit
`docs/status/testbaseline.json` und meldet **NEU ROT** (nicht erlaubt) und **neu grün** (bitte Baseline-Update im PR
vorschlagen). `--verbose` zeigt die volle Ausgabe eines Laufs, wenn man einen Fehler verstehen will.
**QGIS-Tests (bei UI, Layern, WFS, Layouts):** Windows, OSGeo4W-Shell, im Plugin-Repo `tests\test_qgis.bat`
(Pfad zu `python-qgis.bat` anpassen) oder QGIS starten und den Ablauf manuell durchspielen. Ergebnis im PR unter
„Nachweis“ eintragen: was geprüft wurde, mit welchen Daten, was herauskam.
**Neue Tests:** `unittest`, Datei `tests/test_<modul>.py`, Mock-Modus, ohne Netzwerk. Muster: vorhandene Tests des
Pakets. Ein Test pro Verhalten, deutsche Methodennamen sind üblich (`test_zip_fehler_gibt_none_und_warning`).
**Rote Tests reparieren:** Erst klären, ob der Test veraltet ist oder der Code falsch. Bei veralteten Tests den Test
an das neue Verhalten anpassen und in der Übergabenotiz begründen. Niemals Tests löschen oder mit `skip` stilllegen,
um grün zu werden.
## 6. Wissen festhalten
| Situation | Was tun |
|---|---|
| Etwas hat überraschend Zeit gekostet | Eintrag in `docs/wissen/fallstricke.md` (Rubrik, Datum, Quelle) |
| Ein Vertrag wurde geändert oder entdeckt | `docs/architektur/schnittstellen.md` anpassen |
| Modul neu, verschoben, gewachsen | `docs/architektur/modulkarte.md` anpassen |
| Team hat etwas entschieden | neue Datei `docs/entscheidungen/NNNN-<thema>.md` nach Vorlage |
| Aufgabe abgegeben oder pausiert | Übergabenotiz im PR (Vorlage) und Zeile in `STATUS.md` |
| Fachbegriff musste erklärt werden | `docs/wissen/glossar.md` |
Der Skill `sn-uebergabe` führt den Agenten am Ende jeder Aufgabe durch genau diese Liste. Das eigene Gedächtnis des
Werkzeugs (Claude Auto-Memory) ist ein Notizzettel; teilbares Wissen gehört in die Dateien oben.
## 7. Was liegt wo, und was liest der Agent
| Datei | Wird gelesen |
|---|---|
| `AGENTS.md` (Projektwissen) | ja, über `CLAUDE.md` (`@AGENTS.md`) kanonisch, ADR 0001 |
| `CLAUDE.md` | ja, enthält nur den Verweis und wenige Claude-spezifische Zeilen |
| `Plugin_*/AGENTS.md` | über `Plugin_*/CLAUDE.md`, wenn in dem Ordner gearbeitet wird |
| `.agents/skills/` | kanonischer Ort; gelesen wird der Spiegel `.claude/skills/` |
| `.claude/commands/` | Slash-Befehle, z. B. `/hallo` |
| `.claude/settings.json` | Berechtigungen sperrt `git push`, `merge`, `tag` |
| `docs/` | auf Abruf, nicht automatisch |
| `CLAUDE.local.md` | ja, lokal, nicht versioniert |
## 8. Release in Kürze
Nur Maintainer. Kanal-Branch (`unstable` oder `testing`) auf den gewünschten Stand bringen, `changelog.txt`-Block
prüfen, Tag `v<JJ.M.N>-u` bzw. `-t` auf dem **Kopf des Kanal-Branches** setzen und pushen. Der Workflow baut den
Branch, erzeugt `metadata.txt`, legt das Release an und aktualisiert den Feed. Details und Prüfliste: Skill `sn-release`.
## 9. Häufige Fragen
**Der Agent findet `sn_basis` nicht.** Er hat nicht `scripts/run_tests.py` benutzt oder wurde im falschen Ordner
gestartet. Tests immer über das Skript; Werkzeug im Workspace-Root starten.
**Der Agent will `qgis.core` direkt importieren.** Auf den Wrapper verweisen; fehlt ein Symbol, zuerst im Wrapper mit
Mock-Fallback ergänzen (siehe Muster in `qgiscore_wrapper.py`, Zeilen 73260).
**Der Agent „repariert“ rote Tests durch Anpassen der Erwartung.** Nach Begründung fragen; nur akzeptieren, wenn das
Verhalten bewusst geändert wurde und das Issue es abdeckt.
**Der Agent hat eine Datei komplett umgeschrieben.** Diff prüfen, auf das Issue zurückführen, Rest verwerfen.
Regel „kein Refactoring nebenbei“ zitieren.
**Zwei Personen arbeiten am selben Modul.** In `STATUS.md` steht, wer woran arbeitet; vorher dort eintragen und
kleine PRs machen. Konflikte in `unstable` löst der Maintainer mit beiden.
**Wie kommt das Plugin aus dem Workspace ohne Release in mein QGIS?** Die Klone im Workspace verknüpfen:
`python scripts/link_qgis_profile.py --profil dev`. QGIS lädt dann `Plugin_SN_Basis` als `sn_basis` direkt aus der
Arbeitskopie. Details in `04_werkzeuge_einrichten.md` Abschnitt 2.5.
**Windows und Symlinks.** `setup_workspace.py` und `run_tests.py` nutzen Junctions oder Kopien, wenn Symlinks nicht
erlaubt sind. Nach Änderungen an Skills das Setup-Skript erneut ausführen, falls es „Kopie“ gemeldet hat.
## 10. Checklisten
**Vor dem PR**
- [ ] Issue verlinkt, Branch von `unstable`
- [ ] `python scripts/run_tests.py <paket> --baseline` ohne NEU ROT
- [ ] Bei UI/QGIS-Verhalten: Test in QGIS dokumentiert
- [ ] Keine direkten Qt/QGIS-Importe, keine Debug-`print`, kein auskommentierter Code
- [ ] `changelog.txt` ergänzt
- [ ] Übergabenotiz im PR; Wissensbasis nachgezogen, falls nötig
- [ ] Diff gelesen: nur, was das Issue verlangt
**Review**
- [ ] Fachlich richtig? (Verhalten, Begriffe, Dienste)
- [ ] Verträge unberührt oder per Issue abgestimmt?
- [ ] Tests vorhanden und sinnvoll (nicht nur angepasst)?
- [ ] Umfang überschaubar (< 400 Zeilen)?
- [ ] Nach dem Merge: `STATUS.md`
**Sitzungsende (auch bei Abbruch)**
- [ ] Zwischenstand committet oder gestasht und in der Übergabenotiz benannt
- [ ] Offene Fragen im Issue notiert
- [ ] `STATUS.md`-Zeile aktualisiert
+243
View File
@@ -0,0 +1,243 @@
# Werkzeuge einrichten: von null bis zur ersten Aufgabe
Stand 04.09.2026. Ergänzt [`03_arbeitsweise.md`](03_arbeitsweise.md) um die konkrete Einrichtung. Wer Abschnitt 2
durchgearbeitet hat, kann beide Varianten aus Abschnitt 3 und 4 nutzen und jederzeit zwischen ihnen wechseln —
sie greifen auf dieselben Dateien zu.
## 1. Zwei Varianten
Wir arbeiten mit **Claude Code**. Es gibt zwei Formen, dieselbe Software zu bedienen:
| Variante | Form | Wofür geeignet |
|---|---|---|
| **A · Claude Code CLI** | Terminal | lange, eigenständige Aufgaben; Skripte; Arbeiten über mehrere Repos |
| **B · Claude Code in VS Code** | Erweiterung mit Chatpanel | Einstieg; Diffs im Editor sehen und einzeln annehmen |
Beide lesen `CLAUDE.md` (und darüber `AGENTS.md`), beide finden die Skills unter `.claude/skills/`, beide
arbeiten über Git mit Gitea. **Empfehlung für Einsteiger: Variante B**, weil man jede Änderung als Diff sieht,
bevor sie übernommen wird.
**Nicht genutzt und nicht unterstützt:** Cloud-Agenten (Claude Code Web, Codex Cloud, Copilot Coding Agent) —
sie setzen GitHub voraus, wir arbeiten mit Gitea. Reine Chat-Oberflächen ohne Ordnerzugriff (claude.ai,
chatgpt.com) sind für Fachfragen brauchbar, aber **nicht** für Änderungen an den Repos: Code-Fragmente per
Kopieren einzubauen ist genau die Arbeitsweise, die zur heutigen Struktur geführt hat. Wer nur Chat hat,
formuliert Issues nach [`docs/vorlagen/issue.md`](../vorlagen/issue.md) und lässt sie von jemandem mit Agent
umsetzen.
## 2. Gemeinsame Grundlage (einmal pro Rechner)
### 2.1 Konten und Zugänge
- **Gitea-Konto** auf `entwicklung.flurneuordnung-sachsen.de` mit Schreibrecht in der Organisation `AG_QGIS`
(beim Maintainer anfragen). Für HTTPS-Push ein **Zugriffstoken** anlegen:
Gitea → Einstellungen → Anwendungen → Token, Berechtigung `repository: read/write`.
- **Claude-Konto**: Abo (Pro, Max, Team, Enterprise) oder Console-Konto. Dienstliche E-Mail verwenden;
die Abrechnung klärt die Behörde.
- **Behördennetz:** ausgehendes HTTPS zu `claude.ai` und `api.anthropic.com` muss möglich sein. Bei Proxy die
Variablen `HTTPS_PROXY` und `HTTP_PROXY` setzen; bei Problemen mit der Zertifikatsprüfung die IT ansprechen.
### 2.2 Git
- **Windows:** [Git for Windows](https://git-scm.com/downloads/win) mit den Standardoptionen. Enthält Git Bash
und den Credential Manager, der sich das Gitea-Token nach dem ersten Push merkt. Claude Code setzt Git for
Windows voraus.
- **macOS/Linux:** Git über Xcode-Tools bzw. die Paketverwaltung.
- Einmalig einstellen:
```bash
git config --global user.name "Vorname Nachname"
git config --global user.email "dienstliche@adresse"
```
### 2.3 Python
Benötigt: **Python ≥ 3.11** und das Paket `openpyxl` für `scripts/run_tests.py`.
- **Windows mit QGIS:** am einfachsten die OSGeo4W-Shell mitnutzen — das dortige `python` ist das QGIS-Python.
Darin `python -m pip install openpyxl`.
- **Alternativ:** Python von python.org installieren, beim Setup „Add to PATH" anhaken, dann
`pip install openpyxl`.
- Prüfen: `python --version` und `python -c "import openpyxl"` müssen beide ohne Fehler durchlaufen.
Die übrigen Skripte (`hallo.py`, `gitea.py`, `testkatalog.py`, `setup_workspace.py`) brauchen **nur die
Standardbibliothek** — sie laufen auch ohne `openpyxl`.
### 2.4 Projektwissen holen
```bash
git clone https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Projektwissen.git AG_QGIS
cd AG_QGIS
python scripts/setup_workspace.py # klont alle Plugin-Repos, spiegelt die Skills
python scripts/run_tests.py # erwartet: drei Suiten mit den Baseline-Zahlen
```
Der Zielordner `AG_QGIS` ist bewusst anders benannt als das Repository: darin liegen später nicht nur das
Projektwissen, sondern auch alle Plugin-Klone nebeneinander.
> **Solange `AG_QGIS/Projektwissen` auf Gitea noch nicht existiert**, schlägt der Clone mit „Repository not
> found" fehl, obwohl die Anmeldung gelingt. Übergangslösung: den Ordner `AG_QGIS` aus dem Projektverzeichnis
> auf dem Netzlaufwerk (`Projekte\QGIS Plan 41\AG_QGIS`) kopieren und darin `python scripts/setup_workspace.py`
> ausführen.
### 2.5 QGIS für manuelle Tests
Empfohlen, für den Start nicht zwingend.
**Warum das nötig ist:** QGIS lädt Plugins nur aus dem Plugin-Ordner eines Profils, und zwar unter dem
**Paketnamen** (`sn_basis`), nicht unter dem Repo-Namen (`Plugin_SN_Basis`). Die Klone sind genau der Inhalt, der
sonst per Release-ZIP in QGIS landet. Statt zu kopieren oder ein Release zu bauen, wird der Klon in das Profil
**verknüpft**; QGIS arbeitet dann direkt auf der Arbeitskopie.
- QGIS 3.44 LTR (Qt5) und nach Möglichkeit QGIS 4.2 (Qt6) installieren. Eigenes Profil `dev` anlegen
(Einstellungen → Benutzerprofile → Neues Profil) und einmal damit starten. Im Profil `dev` die Plugins
**nicht** aus dem Release-Feed installieren.
- Verknüpfungen anlegen — unter Windows als Junction, ohne Administratorrechte:
```
python scripts/link_qgis_profile.py --profil dev # alle Plugins
python scripts/link_qgis_profile.py --profil dev --status # prüfen
python scripts/link_qgis_profile.py --profil dev --entfernen
```
Liegt das Profil woanders (OSGeo4W-Installation, Netzprofil):
`--pfad "<…>\profiles\dev\python\plugins"`.
- In QGIS: Erweiterungen → Verwalten → Einstellungen → „Auch experimentelle Erweiterungen anzeigen".
Dann zuerst „LNO Sachsen | Plugin Basisfunktionen" (`sn_basis`) aktivieren, danach die Fachplugins.
Menü und Toolbar „LNO Sachsen" erscheinen.
- Nach jeder Codeänderung: Plugin „Plugin Reloader" (offizielles QGIS-Repository) → `sn_basis` neu laden, oder
QGIS neu starten. Änderungen in `functions/` und `modules/` brauchen meist den Neustart, weil Module gecacht
werden.
> Die QGIS-Mindestversion des Projekts steht noch auf 3.40, das seit Januar 2026 End of Life ist.
> Siehe [`../konzept/07_qgis_tests_plan.md`](../konzept/07_qgis_tests_plan.md) Abschnitt 2.
## 3. Variante A: Claude Code CLI
1. **Installieren.**
Windows (PowerShell): `irm https://claude.ai/install.ps1 | iex`
macOS/Linux: `curl -fsSL https://claude.ai/install.sh | bash`
Node.js wird nicht benötigt; der Installer aktualisiert sich selbst.
2. **Anmelden:** `claude` starten, im Browser mit dem Claude-Konto anmelden.
3. **Starten:** im Ordner `AG_QGIS` (nicht in einem Plugin-Unterordner) `claude` eingeben. Beim ersten Mal den
Import `@AGENTS.md` bestätigen.
4. **Prüfen:** `/context` listet `CLAUDE.md` und `AGENTS.md` unter „Memory files". `/hallo` und die Skills
(`sn-aufgabe-bearbeiten`, `sn-tests`, `sn-ticket`, …) erscheinen im Menü.
5. **Berechtigungen:** Der Standardmodus fragt vor Änderungen. Für Routinearbeit ist „Accept edits" praktikabel.
`--dangerously-skip-permissions` niemals außerhalb einer Sandbox.
Das Repo verbietet über `.claude/settings.json` ohnehin `git push`, `git merge` und `git tag` —
siehe [`05_git_arbeitsweise.md`](05_git_arbeitsweise.md) Abschnitt 6.
6. **Erste Aufgabe:** Abschnitt 6 dieser Seite.
Bei Arbeit in einem Plugin-Unterordner lädt Claude zusätzlich dessen `CLAUDE.md`. Für breite Codesuchen den
Explore-Unteragenten nutzen (steht in `CLAUDE.md`). Die Auto-Memory ist lokal und wird nicht geteilt; Teilbares
gehört nach `docs/` — Skill `sn-uebergabe`.
## 4. Variante B: Claude Code in VS Code
1. **VS Code installieren**, dann **den Ordner `AG_QGIS` öffnen** (Datei → Ordner öffnen) — nicht einen
Plugin-Unterordner.
2. **Erweiterung installieren:** `Strg+Shift+X` → „Claude Code" (Herausgeber Anthropic) → Installieren.
Die Erweiterung bringt eine eigene CLI-Kopie mit; die Installation aus Variante A ist nur nötig, wenn man
`claude` zusätzlich im integrierten Terminal nutzen will.
3. **Anmelden:** Symbol in der Aktivitätsleiste → Sign in → Browser-Anmeldung.
4. **Python-Interpreter wählen:** `Strg+Shift+P` → „Python: Select Interpreter" → das Python aus Abschnitt 2.3.
5. **Arbeiten:** Panel öffnen. Markierter Code wird automatisch mitgesendet (`Alt+K` fügt einen Verweis ein).
Diffs erscheinen im Editor und werden einzeln angenommen oder verworfen. Den Berechtigungsmodus wählt man
unten im Eingabefeld; Pläne lässt man vor der Umsetzung im Plan-Modus prüfen.
### 4.1 Empfohlene Erweiterungen
| Erweiterung | Kennung | Wofür |
|---|---|---|
| Claude Code | `anthropic.claude-code` | der Agent selbst |
| Python | `ms-python.python` | Syntax, Interpreterwahl, Debugger |
| Markdown Preview Mermaid Support | `bierner.markdown-mermaid` | zeigt die Mermaid-Diagramme in `docs/` in der Vorschau |
### 4.2 Persönliche Einstellungen
**Diese Einstellungen gehören in die Benutzereinstellungen, nicht ins Repository.** Das Repo enthält bewusst
keinen `.vscode/`-Ordner: er würde persönliche Pfade (OSGeo-Installation, QGIS-Profil, Benutzername) versionieren
und bei jedem eine andere Datei erzeugen. Wer sie haben will, kopiert sie sich in die eigene `settings.json`
(`Strg+Shift+P` → „Preferences: Open User Settings (JSON)").
```jsonc
{
// Aufgeräumte Datei- und Suchansicht
"files.exclude": {
"**/__pycache__": true,
"**/.sn_testlauf": true
},
"search.exclude": {
"**/.git": true,
"**/__pycache__": true,
"Plugin_SN_Plan41/assets/**": true
},
// Einheitliche Dateiform — die Repos sind auf UTF-8 und LF eingestellt
"files.encoding": "utf8",
"files.eol": "\n",
"[python]": { "editor.tabSize": 4, "editor.insertSpaces": true },
"[markdown]": { "editor.wordWrap": "on" },
// Tests laufen über scripts/run_tests.py, nicht über den VS-Code-Testrunner:
// die Pakete müssen dafür erst unter ihren Paketnamen verlinkt werden.
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": false
}
```
Wer zusätzlich Code-Vervollständigung für die QGIS-API möchte, ergänzt die eigenen Pfade — **nur lokal**:
```jsonc
{
"python.analysis.extraPaths": [
"C:/OSGeo4W/apps/qgis/python",
"C:/Users/<benutzer>/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins"
]
}
```
## 5. Einschränkungen und Datenschutz
- **Gitea statt GitHub.** Alle Cloud-Agenten setzen GitHub voraus und entfallen damit. Lokale Agenten arbeiten
problemlos mit Gitea über Git. Issues und Pull Requests entstehen im Gitea-Browser; lesend geht die API auch
ohne Anmeldung — dafür gibt es `scripts/gitea.py` und den Skill `sn-ticket`.
- **Code geht an den Anbieter.** Die Repos sind öffentlich; trotzdem gilt: keine Zugangsdaten, keine
Verfahrensdaten, keine personenbezogenen Daten in Prompts, Dateien oder Testdaten. API-Schlüssel (VLN) nie in
den Chat kopieren.
- **Der Mensch drückt ab.** Agenten dürfen nicht pushen, mergen, taggen oder irgendetwas auf dem Gitea-Server
verändern. Das ist in `.claude/settings.json` technisch hinterlegt, nicht nur als Bitte formuliert.
- **Windows-Symlinks.** Die Skripte fallen auf Junctions oder Kopien zurück, wenn Symlinks nicht erlaubt sind.
Nach Änderungen an Skills `python scripts/setup_workspace.py --nur-skills` erneut ausführen.
## 6. Von null zur ersten Aufgabe (etwa eine Stunde)
1. Abschnitt 2 durcharbeiten; `python scripts/run_tests.py` zeigt drei Suiten.
2. Variante A oder B einrichten. Prüfung: der Agent nennt auf Nachfrage die Regeln aus `AGENTS.md`.
3. `/hallo` ausführen — holt den Stand aller Klone.
4. `python scripts/gitea.py tickets` — offene Tickets ansehen und eines auswählen. Für den Anfang eignet sich
auch ein einzelner roter Test aus `docs/status/testbaseline.json` oder ein Eintrag in `fallstricke.md`.
5. In `docs/status/STATUS.md` unter „Wer arbeitet woran" eintragen (kleiner Pull Request oder Nachricht an den
Maintainer).
6. Auftrag nach dem Muster in [`03_arbeitsweise.md`](03_arbeitsweise.md) Abschnitt 3 geben, mit dem Skill
`sn-aufgabe-bearbeiten`.
7. Ergebnis prüfen: `python scripts/run_tests.py <paket> --baseline` ohne `NEU ROT`, Diff gelesen.
8. Branch pushen (selbst!), Pull Request im Gitea-Browser nach
[`docs/vorlagen/pull_request.md`](../vorlagen/pull_request.md), Übergabenotiz aus dem Skill `sn-uebergabe`.
9. Review durch eine Kollegin oder einen Kollegen; nach dem Merge `STATUS.md` nachziehen.
## 7. Prüfliste „Einrichtung fertig"
- [ ] `git --version` und `python --version` (≥ 3.11) laufen, `python -c "import openpyxl"` ohne Fehler
- [ ] `AG_QGIS/` mit allen Repos vorhanden, `python scripts/setup_workspace.py` ohne Fehler
- [ ] `python scripts/run_tests.py` zeigt drei Suiten mit den Baseline-Zahlen
- [ ] `python scripts/hallo.py` läuft und zeigt den Stand aller Klone
- [ ] `python scripts/gitea.py tickets` listet die offenen Tickets
- [ ] Claude Code im Ordner `AG_QGIS` gestartet; `/context` zeigt `CLAUDE.md` und `AGENTS.md`
- [ ] Gitea-Token gespeichert (Test: `git fetch` in einem Plugin-Repo ohne Passwortabfrage)
- [ ] Optional: QGIS-Profil `dev` mit Verknüpfungen, `sn_basis` lädt ohne Fehler
## Quellen
- Claude Code: [VS-Code-Erweiterung](https://code.claude.com/docs/en/vs-code) ·
[Gedächtnis und CLAUDE.md](https://code.claude.com/docs/en/memory) ·
[Skills](https://code.claude.com/docs/en/skills) ·
[Einstellungen und Berechtigungen](https://code.claude.com/docs/en/settings)
- [Agent-Skills-Standard](https://agentskills.io/home)
+282
View File
@@ -0,0 +1,282 @@
# Git-Arbeitsweise — verbindlich für alle Repositories
Stand 04.09.2026. Diese Regeln gelten für **jedes** Repository der Organisation `AG_QGIS`, für Menschen und für
KI-Agenten gleichermaßen. Die Kurzfassung für den Agenten steht in `AGENTS.md`; hier steht das Vollständige mit
Begründung. Wer eine Abweichung braucht, macht ein Issue auf — nicht einfach einen Push.
> **Die goldene Regel**
>
> In `stable`, `testing` und `unstable` wird nicht entwickelt. Diese Branches nehmen ausschließlich fertige Arbeit
> per Pull Request entgegen. Entwickelt wird in kurzlebigen Branches, die nach dem Merge gelöscht werden.
>
> **Und: Der Mensch drückt ab.** Agenten bereiten alles vor. Push, Merge und Tag macht eine Person mit ihrem
> eigenen Zugang.
## 1. Die drei Kanäle
Jedes Plugin-Repository hat drei geschützte Branches. Sie entsprechen genau den drei QGIS-Plugin-Feeds im
Repository `Repository` (`plugins.xml`, `plugins-testing.xml`, `plugins-unstable.xml`).
| Branch | Bedeutung | Feed | Tag-Suffix | Direkter Push |
|---|---|---|---|---|
| `stable` | stabile Releases, produktionsreif | `plugins.xml` | ohne Suffix | nein |
| `testing` | Release-Kandidaten, wird getestet | `plugins-testing.xml` | `-t` | nein |
| `unstable` | fertige Features, noch nicht vollständig getestet | `plugins-unstable.xml` | `-u` | nein |
```mermaid
flowchart LR
F["feature/123-kurzname<br/>bugfix/456-kurzname"] -->|"Pull Request"| U["unstable<br/><i>Arbeitsstand</i>"]
U -->|"Pull Request"| T["testing<br/><i>Release-Kandidat</i>"]
T -->|"Pull Request"| M["stable<br/><i>stabil</i>"]
U -.->|"Tag v26.9.1-u"| RU(["Feed unstable"])
T -.->|"Tag v26.9.1-t"| RT(["Feed testing"])
M -.->|"Tag v26.9.1"| RM(["Feed stable"])
style U fill:#fff4e6,stroke:#d9822b
style T fill:#e6f2ff,stroke:#2b7dd9
style M fill:#e8f6ec,stroke:#2f9e44
```
**Stand heute (04.09.2026):** `stable` existiert in den Plugin-Repositories noch **nicht** und muss vom Maintainer
angelegt werden (siehe `docs/konzept/08_repo_einrichten.md`). Bis dahin endet die Kette bei `testing`, und der
stabile Feed bleibt leer. Der Branch `oldstable` in `Plugin_SN_Basis` und `Plugin_SN_Plan41` ist eine Altlast aus
der Zeit davor; er wird nicht mehr bespielt und nach dem Anlegen von `stable` archiviert oder gelöscht.
> **Warum `stable` und nicht `main`?** Der Branchname ist hier nicht frei wählbar. Der Release-Workflow im
> Repository `Repository` (Branch `hidden/workflows`) klont das Plugin-Repo und führt `git checkout "$CHANNEL"`
> aus. `$CHANNEL` kommt aus dem Tag-Suffix und ist `unstable`, `testing` oder `stable`. **Der Branch muss also
> genauso heißen wie der Kanal.** Ein Branch `main` würde den stabilen Release-Pfad brechen, solange die
> Infrastruktur nicht mit angepasst wird (ADR 0007).
## 2. Der normale Weg: ein Feature
```bash
# 1. Kanal-Branch aktualisieren (macht auch /hallo bzw. python scripts/hallo.py)
cd Plugin_SN_Verfahrensgebiet
git switch unstable
git pull --ff-only
# 2. Eigenen Branch abzweigen — immer von unstable, nie von stable oder testing
git switch -c feature/41-fbschl-feldname
# 3. In kleinen Schritten arbeiten und committen
git add functions/verfahrensgebiet_shape_komplett.py tests/test_shape_komplett.py
git commit -m "Shape-Import akzeptiert Feldnamen FBSchl. mit Punkt (#41)"
# 4. Tests
python ../scripts/run_tests.py sn_verfahrensgebiet --baseline
# 5. Push — durch den Menschen, mit dessen Zugangsdaten
git push -u origin feature/41-fbschl-feldname
```
Danach im Gitea-Browser einen **Pull Request nach `unstable`** öffnen, nach der Vorlage
`docs/vorlagen/pull_request.md`, mit Übergabenotiz (Skill `sn-uebergabe`).
Ein Pull Request sollte an einem Tag entstehen und in einer Viertelstunde reviewbar sein. Mehr als etwa
400 geänderte Zeilen heißt: das Issue war zu groß.
## 3. Branch-Namen
| Präfix | Beispiel | Wofür |
|---|---|---|
| `feature/` | `feature/41-fbschl-feldname` | neue Funktion |
| `bugfix/` | `bugfix/57-basis-laedt-ohne-plan41` | Fehlerbehebung |
| `hotfix/` | `hotfix/61-absturz-beim-beenden` | dringender Fix auf `stable` |
| `doku/` | `doku/git-arbeitsweise` | nur Dokumentation, ohne Issue zulässig |
| `release/` | `release/26.9.1` | Release-Vorbereitung, nur Maintainer |
**Regeln für den Namen:** Kleinbuchstaben, ASCII, Bindestriche statt Leerzeichen. Keine Umlaute, kein `ß`, keine
Punkte. Issue-Nummer direkt nach dem Präfix, damit Gitea den Branch dem Ticket zuordnet.
Gegenbeispiele aus dem Bestand, die es künftig nicht mehr geben soll:
| Vorhanden | Problem | Richtig wäre |
|---|---|---|
| `feature/Drucklayout-überarbeiten` | Umlaut, Großbuchstabe, keine Issue-Nummer | `feature/37-drucklayout` |
| `bug/crash_on_exit` | Präfix `bug/` statt `bugfix/`, Unterstriche, keine Nummer | `bugfix/61-absturz-beim-beenden` |
| `alkis-nur-laden-stop` | gar kein Präfix | `feature/43-alkis-stop-abfrage` |
| `fix-fbschl-feldname` | Präfix `fix-` gibt es nicht | `bugfix/45-fbschl-feldname` |
## 4. Wo fange ich bei einem Fix an?
Das ist die Frage, bei der am meisten schiefgeht. Sie hängt nicht davon ab, wie dringend der Fehler ist, sondern
davon, **in welchem Kanal er sitzt**.
```mermaid
flowchart TD
A{"In welchem Kanal<br/>tritt der Fehler auf?"}
A -->|"nur in unstable"| B["Branch von unstable<br/>PR nach unstable"]
A -->|"in einem testing-Kandidaten"| C["Branch von testing<br/>PR nach testing"]
A -->|"in einem stable-Release"| D["Branch von stable<br/>PR nach stable"]
B --> B2(["fertig"])
C --> C2["Rückführung:<br/>PR testing → unstable"]
D --> D2["Rückführung:<br/>PR stable → testing"]
D2 --> D3["Rückführung:<br/>PR testing → unstable"]
C2 --> E(["fertig"])
D3 --> E
style C fill:#e6f2ff,stroke:#2b7dd9
style D fill:#ffe9e9,stroke:#d94b2b
style C2 fill:#fff4e6,stroke:#d9822b
style D2 fill:#fff4e6,stroke:#d9822b
style D3 fill:#fff4e6,stroke:#d9822b
```
| Fehler sitzt in | Branch von | Pull Request nach | Danach zwingend |
|---|---|---|---|
| nur `unstable` | `unstable` | `unstable` | — |
| `testing`-Kandidat | **`testing`** | `testing` | Rückführung `testing``unstable` |
| `stable`-Release | **`stable`** | `stable` | Rückführung nach `testing` **und** `unstable` |
### Warum die Rückführung nicht optional ist
Ein Fix, der nur in `testing` landet, ist beim nächsten Hochstufen wieder weg: dann wird `unstable` nach `testing`
gemergt, und `unstable` kennt den Fix nicht. Der Fehler kommt zurück, und niemand versteht warum. Deshalb gehört
zu jedem Fix, der nicht in `unstable` beginnt, ein zweiter Pull Request — sofort, nicht „später".
```bash
# Fix in einem testing-Kandidaten
git switch testing
git pull --ff-only
git switch -c bugfix/61-absturz-beim-beenden
# ... beheben, testen, committen ...
git push -u origin bugfix/61-absturz-beim-beenden
# PR nach testing öffnen und mergen lassen
# Rückführung, direkt im Anschluss
git switch testing && git pull --ff-only
git switch -c doku/rueckfuehrung-61-nach-unstable testing
git push -u origin doku/rueckfuehrung-61-nach-unstable
# PR nach unstable öffnen: "Rückführung Fix #61 aus testing"
```
Führt die Rückführung zu Konflikten, weil `unstable` inzwischen weit vorausgelaufen ist, wird der Fix dort neu
angewendet (`git cherry-pick <commit>`) — auch das als eigener Branch mit eigenem Pull Request.
## 5. Tags und Releases
Version ist `JJ.M.N` — Jahr, Monat, laufende Nummer im Monat. Beispiel: `26.9.1` ist das erste Release im
September 2026. Jeder Kanal zählt getrennt hoch.
| Kanal | Tag | Wird gebaut aus |
|---|---|---|
| unstable | `v26.9.1-u` | Kopf von `unstable` |
| testing | `v26.9.1-t` | Kopf von `testing` |
| stable | `v26.9.1` | Kopf von `stable` |
**Achtung:** Der Workflow baut den **Kopf des Kanal-Branches**, nicht den getaggten Commit. Vor dem Taggen also
sicherstellen, dass der Branch genau den gewünschten Stand hat.
Tags setzt ausschließlich ein Maintainer. Ablauf und Prüfliste: Skill `sn-release`.
## 6. Was der Agent darf und was nicht
| Der Agent darf | Der Agent darf nicht |
|---|---|
| `git fetch`, `git pull --ff-only`, `git status`, `git log`, `git diff` | `git push` in jeder Form |
| lokale Branches anlegen und wechseln | `git merge`, `git rebase` |
| `git add`, `git commit` auf einem Arbeitsbranch | `git tag` |
| Tests laufen lassen, `changelog.txt` ergänzen | Pull Requests mergen |
| Issue- und PR-Texte als Entwurf vorbereiten | `git reset --hard`, `git push --force`, `git clean` |
| Tickets über `scripts/gitea.py` lesen | irgendetwas auf dem Gitea-Server verändern |
| | `docs/status/testbaseline.json` ändern |
Das ist nicht nur eine Bitte. `.claude/settings.json` im Repo verbietet die entsprechenden Kommandos technisch;
der Agent bekommt eine Ablehnung statt einer Ausführung. Wer die Regel für einen Einzelfall aussetzen will, tut
das bewusst und lokal, nicht im Repo.
**Warum so streng:** Ein Push trägt den Namen einer Person und ihre Verantwortung. Ein Agent kann fachlich nicht
beurteilen, ob ein Stand veröffentlichungsreif ist — und ein versehentlicher Push auf einen Kanal-Branch trifft
sofort alle anderen Behörden.
## 7. Branch-Schutz auf Gitea einrichten (Maintainer)
Gitea 1.27, je Repository unter **Einstellungen → Branches** (englisch: *Settings → Branches*), dort eine neue
Branch-Schutzregel anlegen. Für jeden der drei Kanäle eine Regel:
| Einstellung | Wert | Wirkung |
|---|---|---|
| Branch-Name | `stable` bzw. `testing`, `unstable` | betrifft genau diesen Branch |
| Push deaktivieren | an | kein direkter Push, nur Merge über Pull Request |
| Merge-Freigabe erforderlich | mindestens 1 | Review durch eine andere Person ist Pflicht |
| Statusprüfungen erforderlich | an, sobald CI läuft | Tests müssen grün sein (Vorlage `docs/vorlagen/gitea_workflow_tests.yaml`) |
| Veraltete Freigaben verwerfen | an | ein neuer Push macht das Review ungültig |
Zusätzlich den Standard-Branch auf `unstable` belassen — dort landet die tägliche Arbeit, und ein neu geklontes
Repo steht damit sofort richtig.
## 8. Aufräumen
Nach dem Merge wird der Arbeitsbranch gelöscht — in Gitea per Schaltfläche direkt am gemergten Pull Request,
lokal so:
```bash
git switch unstable
git pull --ff-only
git branch -d feature/41-fbschl-feldname
git fetch --prune
```
Nicht gelöschte Branches sammeln sich an und niemand weiß später, ob sie noch etwas enthalten. Im Bestand liegen
davon derzeit vier herum (`feature/Drucklayout-überarbeiten`, `print_layout`, `alkis-nur-laden-stop`,
`fix-fbschl-feldname`) — die sind zu prüfen und dann zu entfernen.
## 9. Commit-Nachrichten
Deutsch, Präsens, erste Zeile höchstens 72 Zeichen, Issue-Nummer am Ende in Klammern.
```
Shape-Import akzeptiert Feldnamen FBSchl. mit Punkt (#41)
DAVID-Exporte liefern das Feld teils mit, teils ohne Punkt. Die Suche
prüft jetzt beide Schreibweisen, die Bedeutung der Schlüssel 901/10110F
und 901/10120F bleibt unverändert.
```
Was die Nachricht beantworten muss: **was** ändert sich und **warum**. Das *Wie* steht im Diff.
Nicht brauchbar: „Fix", „Update", „Änderungen", „wip".
## 10. Sitzungsbeginn
Vor dem ersten Handgriff — auch wenn es „nur schnell etwas" ist:
```bash
python scripts/hallo.py # oder in Claude Code: /hallo
```
Das holt für alle Klone den Stand vom Server (`git fetch`), aktualisiert die Kanal-Branches per Fast-Forward,
wo das gefahrlos möglich ist, und meldet alles Übrige, ohne es anzufassen. Es merged nicht, es stasht nicht, und
es fasst keine ungesicherten Änderungen an.
Ohne diesen Schritt entstehen Konflikte, die niemand braucht — und im schlimmsten Fall wird ein Fehler behoben,
den jemand anderes vor drei Tagen schon behoben hat.
## 11. Häufige Fehler
**„Ich habe direkt auf `unstable` committet."** Solange nichts gepusht ist, ist das reparabel:
`git switch -c feature/<nr>-<name>` legt einen Branch auf dem aktuellen Stand an, danach
`git switch unstable && git reset --hard origin/unstable`. Der `reset` ist hier zulässig, weil er nur die lokale
Kopie auf den Serverstand zurücksetzt — trotzdem vorher `git log` prüfen.
**„Mein Branch ist hinter `unstable`."** Vor dem Pull Request aktualisieren:
`git switch unstable && git pull --ff-only`, dann zurück auf den Arbeitsbranch und `git merge unstable`.
Kein `rebase` auf bereits gepushten Branches — das schreibt Historie um, die andere schon haben.
**`git pull` will einen Merge-Commit machen."** Dann ist der lokale Branch vorausgelaufen. `--ff-only` bricht
in dem Fall bewusst ab. Erst klären, was die eigenen Commits sind, statt blind zu mergen.
**„Der Fix ist wieder weg."** Fast immer die fehlende Rückführung aus Abschnitt 4.
**„Ich habe im falschen Repo gearbeitet."** Passiert, weil alle Klone nebeneinander liegen.
`git status` zeigt oben den Pfad; `python scripts/hallo.py` zeigt alle Repos auf einmal.
## Siehe auch
- `AGENTS.md` — Kurzfassung der harten Regeln
- `docs/leitfaden/03_arbeitsweise.md` — der Arbeitszyklus vom Issue bis zum Merge
- `docs/leitfaden/06_tests.md` — was vor dem Pull Request grün sein muss
- `docs/entscheidungen/0007-branchmodell.md` — warum das Modell so aussieht
- `docs/konzept/08_repo_einrichten.md``stable` anlegen, Branch-Schutz setzen
- Skill `sn-release` — Tags und Releases
+196
View File
@@ -0,0 +1,196 @@
# Tests: was geprüft wird, wie man es liest, was Pflicht ist
Stand 04.09.2026. Für alle, die an den LNO-Plugins mitarbeiten — auch für Fachleute ohne Programmiererfahrung.
Der Abschnitt 7 ist ausdrücklich für Sie geschrieben: dort geht es darum, zu **beurteilen**, ob eine Testsuite
fachlich das Richtige prüft. Dafür muss man keinen Code lesen können.
## 1. Warum das hier ernster genommen wird als sonst
Ein großer Teil des Codes entsteht mit KI-Agenten. Ein Agent kann überzeugend erklären, dass etwas funktioniert,
ohne dass es funktioniert. Er kann auch einen roten Test „reparieren", indem er die Erwartung an das falsche
Verhalten anpasst. Tests sind der einzige schnelle, nachprüfbare Nachweis, den ein Review hat.
Deshalb gilt: **Neue Funktionalität ohne Test wird nicht gemergt.** Und: Tests werden nicht gelöscht, nicht
übersprungen und nicht in ihrer Erwartung angepasst, um grün zu werden — außer das Verhalten wurde bewusst
geändert, und der Pull Request begründet es.
## 2. Die drei Ebenen
```mermaid
flowchart TB
M["<b>Manueller Test in QGIS</b><br/>wenige · Minuten bis Stunden · durch Menschen<br/><i>Bedienbarkeit, Optik, Zusammenspiel mit echten Daten</i>"]
Q["<b>QGIS-Tests</b> — geplant, siehe konzept/07<br/>einige Dutzend · Minuten · automatisch<br/><i>Layer, WFS, Drucklayouts, Docks gegen echtes QGIS</i>"]
K["<b>Mock-Tests</b> — heute 332 Stück<br/>viele · unter einer Sekunde · automatisch, ohne QGIS<br/><i>Logik, Parsing, Prüfregeln, Fehlerbehandlung</i>"]
M --- Q --- K
style M fill:#ffe9e9,stroke:#d94b2b
style Q fill:#e6f2ff,stroke:#2b7dd9
style K fill:#e8f6ec,stroke:#2f9e44
```
Je weiter unten, desto schneller, billiger und häufiger. Ein Fehler soll möglichst weit unten auffallen — dort
ist er in Sekunden zu finden statt in einer halben Stunde Klickerei.
**Mock-Tests** laufen ohne QGIS, weil aller Zugriff auf QGIS und Qt über die Wrapper in `sn_basis.functions`
läuft. Fehlt QGIS, liefern die Wrapper Ersatzobjekte. Das ist der Grund für die Wrapper-Regel in `AGENTS.md`
sie ist nicht Selbstzweck, sondern das, was diese Ebene überhaupt möglich macht.
**QGIS-Tests** gibt es heute noch nicht automatisiert. Der Stufenplan steht in
[`docs/konzept/07_qgis_tests_plan.md`](../konzept/07_qgis_tests_plan.md).
**Manuelle Tests** bleiben unersetzbar für alles, was ein Mensch sehen muss: Sieht der Plan richtig aus? Ist der
Dialog bedienbar? Stimmen die Beschriftungen fachlich?
## 3. Was gehört auf welche Ebene
| Was geändert wurde | Mock-Test | QGIS-Test | Manuell |
|---|---|---|---|
| Rechenregel, Prüflogik, Schlüsselableitung | **Pflicht** | | |
| Einlesen einer Datei (Shape, Excel, CSV, ZIP) | **Pflicht** | wenn Geometrie entsteht | |
| Fehlerbehandlung, Sonderfälle, leere Eingaben | **Pflicht** | | |
| Aufruf eines Webdienstes (WFS, VLN, ALKIS) | **Pflicht** (Antwort nachgestellt) | empfohlen | bei neuem Dienst |
| Layer anlegen, stylen, in den Baum hängen | soweit möglich | **Pflicht** | |
| Drucklayout, Planausgabe | Logik ja | **Pflicht** | **Pflicht** (Aussehen) |
| Dialog, Dock, Tab, Signalverdrahtung | Logik dahinter | empfohlen | **Pflicht** |
| Menü, Toolbar, Plugin-Ladevorgang | | empfohlen | **Pflicht** |
| Nur Dokumentation | | | |
Faustregel: **Alles, was ohne sichtbare Oberfläche entscheidbar ist, gehört in einen Mock-Test.** Wenn sich etwas
schlecht testen lässt, liegt das meistens daran, dass Fachlogik und Oberfläche vermischt sind — dann ist die
Trennung die eigentliche Aufgabe, nicht der Test.
## 4. Die Pflicht im Detail
**Bei neuer Funktionalität:** mindestens ein Test je neuem beobachtbarem Verhalten. Nicht je Funktion — je
*Verhalten*. Eine Funktion, die drei Fälle unterscheidet, braucht drei Tests.
**Bei Änderungen an bestehender Logik:**
1. Vorher `python scripts/run_tests.py <paket> --baseline` laufen lassen und die Zahl merken.
2. Ändern.
3. Erneut laufen lassen. Keine Zeile `NEU ROT`.
4. Die Tests, die das geänderte Verhalten abdecken, erweitern oder anpassen — mit Begründung im Pull Request.
**Bei einer Fehlerbehebung:** zuerst einen Test schreiben, der den Fehler zeigt (also rot ist), dann beheben.
Sonst weiß niemand, ob der Fehler wirklich weg ist oder nur nicht mehr auffällt.
## 5. Wie ein guter Test aussieht
```python
def test_fbschl_mit_punkt_wird_erkannt(self):
"""Prüft, dass der Shape-Import auch das Feld 'FBSchl.' mit Punkt akzeptiert.
Manche DAVID-Exporte liefern den Feldnamen mit Punkt, andere ohne.
Beide müssen zum selben Gebietstyp führen.
"""
layer = self._layer_mit_feldern({"FBSchl.": "901/10110F"})
self.assertEqual(_ermittle_gebietstyp_aus_fbschl(layer), "Verfahrensgebiet")
```
Drei Anforderungen, alle drei verbindlich:
1. **Der Name sagt, was geprüft wird**, auf Deutsch, aus fachlicher Sicht:
`test_fbschl_mit_punkt_wird_erkannt` — nicht `test_case_3` oder `test_import`.
2. **Ein deutscher Docstring, erste Zeile in der Form `Prüft, dass …`.** Diese Zeile landet im Testkatalog
(Abschnitt 6) und ist für viele Leser der einzige Kontakt mit dem Test. Wenn nötig, danach ein Absatz mit dem
fachlichen Hintergrund.
3. **Ein Verhalten je Test.** Wenn im Namen ein „und" steht, sind es zwei Tests.
Weiter gilt: `unittest`, Datei `tests/test_<modul>.py`, kein Netzwerkzugriff, keine echten Verfahrensdaten, keine
Abhängigkeit von der Reihenfolge der Tests. Muster liefern die vorhandenen Tests des jeweiligen Pakets.
## 6. Der Testkatalog
```bash
python scripts/testkatalog.py
```
erzeugt [`docs/tests/testkatalog.md`](../tests/testkatalog.md): jeder Testfall aller Pakete mit seiner
Docstring-Zeile und seinem Status. Die Datei wird aus dem Code erzeugt und ist damit immer aktuell — sie wird
**nicht von Hand bearbeitet**.
Wozu das gut ist: Man sieht auf zwei Seiten, was die Suite abdeckt, ohne eine einzige Testdatei zu öffnen. Der
Abschnitt „Lücken" am Ende listet alle Tests ohne Docstring — das ist die Arbeitsliste, um den Katalog
vollständig lesbar zu machen.
Nach dem Hinzufügen von Tests den Katalog neu erzeugen und mit committen. `python scripts/testkatalog.py --pruefen`
meldet, wenn er veraltet ist.
## 7. Testkonzepte: prüfen, ob die Tests fachlich sinnvoll sind
Der Katalog beantwortet „was wird getestet". Er beantwortet **nicht** „ist das genug". Dafür gibt es je
wichtigem Fachablauf eine eigene Seite in [`docs/tests/`](../tests/), nach der Vorlage
[`docs/vorlagen/testkonzept.md`](../vorlagen/testkonzept.md).
Jede dieser Seiten hat denselben Aufbau:
1. **Ein Mermaid-Diagramm des fachlichen Ablaufs** — so, wie ein Sachbearbeiter ihn beschreiben würde.
Keine Klassennamen, keine Dateipfade.
2. **Eine Tabelle: Schritt → welcher Test deckt ihn ab → was bewusst nicht getestet wird, und warum.**
3. **Ein Abschnitt „Bekannte Lücken"** mit Issue-Nummer, wo etwas fehlt.
So können Sie als Fachperson prüfen, ohne Code zu lesen:
- Kommt jeder Schritt Ihres Ablaufs in der Tabelle vor? Fehlt einer, fehlt wahrscheinlich auch der Test.
- Sind die **Sonderfälle** dabei, die in der Praxis wirklich vorkommen — der leere Export, die doppelte
Gemarkung, der Datensatz aus dem Nachbarlandkreis?
- Steht bei „bewusst nicht getestet" eine Begründung, die Sie nachvollziehen können? „Zu aufwendig" ist keine.
- Beschreibt der Test das *Ergebnis* oder nur, dass etwas *durchläuft*? „Prüft, dass der Import nicht abstürzt"
ist deutlich weniger wert als „Prüft, dass aus 901/10110F der Gebietstyp Verfahrensgebiet wird".
Ein Beispiel liegt in [`docs/tests/verfahrensgebiet-shape-import.md`](../tests/verfahrensgebiet-shape-import.md).
Wenn Sie eine Lücke finden: Issue aufmachen (`python scripts/gitea.py entwurf issue Verfahrensgebiet` legt einen
Entwurf an). Eine gefundene Testlücke ist ein vollwertiges Ticket.
## 8. Die Baseline
Nicht alle Tests sind grün. Der Ist-Stand steht in [`docs/status/testbaseline.json`](../status/testbaseline.json)
und wirkt als Sperrklinke: **bekannte rote Tests dürfen bleiben, neue nicht dazukommen.**
```bash
python scripts/run_tests.py sn_verfahrensgebiet --baseline
```
- `NEU ROT` — nicht erlaubt. Ursache beheben, bevor der Pull Request aufgemacht wird.
- `neu grün` — schön. Im Pull Request nennen und eine Baseline-Aktualisierung vorschlagen.
- `unverändert gegenüber Baseline` — in Ordnung.
Die Baseline ändert **nur ein Maintainer** mit `--update-baseline`, nach Absprache. Ein Agent fasst sie nicht an.
Das Grünmachen der bekannten roten Tests läuft über eigene Issues, nicht nebenbei.
## 9. Wenn ein Test rot ist
Erst die Frage beantworten: **Ist der Test veraltet oder ist der Code falsch?**
| Befund | Was zu tun ist |
|---|---|
| Der Code verhält sich falsch | Code beheben. Der Test bleibt, wie er ist. |
| Das Verhalten wurde bewusst geändert (Issue deckt es ab) | Test anpassen, im Pull Request begründen, `changelog.txt` prüfen |
| Der Test prüft etwas, das es nicht mehr gibt | Test entfernen — mit Begründung und nur, wenn das Verhalten wirklich entfallen ist |
| `ModuleNotFoundError: sn_basis` | Nicht über `scripts/run_tests.py` gestartet, oder Klone fehlen |
| `AttributeError: _MockQgs… has no attribute` | Mock im Wrapper ergänzen — nicht den Test umgehen |
| Der Test hängt oder braucht Netz | Der Test ist falsch gebaut. Dienstantwort nachstellen. |
**Niemals** ein roter Test wird durch `@unittest.skip` oder durch Löschen grün gemacht. Das ist keine Lösung,
das ist das Verstecken des Problems vor dem nächsten Leser.
Details zum Deuten einzelner Fehler: Skill `sn-tests`.
## 10. Vor dem Pull Request
- [ ] `python scripts/run_tests.py <paket> --baseline` ohne `NEU ROT`
- [ ] Neue Funktionalität hat mindestens einen Test je neuem Verhalten
- [ ] Jeder neue Test hat einen deutschen Docstring `"""Prüft, dass …"""`
- [ ] `python scripts/testkatalog.py` neu erzeugt und mitcommittet
- [ ] Betrifft die Änderung einen dokumentierten Fachablauf: die Seite in `docs/tests/` nachgezogen
- [ ] Bei UI-, Layer-, WFS- oder Layoutverhalten: manueller QGIS-Test durchgeführt und im Pull Request
unter „Wie geprüft" beschrieben — oder ausdrücklich als offener Punkt benannt
## Siehe auch
- Skill `sn-tests` — Tests ausführen und Fehler deuten
- [`docs/konzept/07_qgis_tests_plan.md`](../konzept/07_qgis_tests_plan.md) — Ausbau zu echten QGIS-Tests
- [`docs/entscheidungen/0003-mock-tests-baseline-pflicht.md`](../entscheidungen/0003-mock-tests-baseline-pflicht.md)
- [`docs/entscheidungen/0009-tests-definition-of-done.md`](../entscheidungen/0009-tests-definition-of-done.md)
- [`docs/architektur/schnittstellen.md`](../architektur/schnittstellen.md) — die Wrapper, die den Mock-Modus tragen
+88
View File
@@ -0,0 +1,88 @@
# Status des Projekts
Kurz und aktuell. Wird bei jedem Merge und jeder Übergabe gepflegt (Skill `sn-uebergabe`). Älteres wandert nach unten
in „Erledigt" oder wird gelöscht. **Stand: 04.09.2026.**
## Aktuelle Phase
**Vorbereitung der gemeinsamen KI-gestützten Entwicklung.** Wissensbasis, Regeln, Skills, Leitfäden und Werkzeuge
liegen als Vorschlag vor (dieses Repository, künftig `AG_QGIS/Projektwissen`). Das grundlegende Refactoring ist eine
spätere Phase und beginnt erst, wenn die Test-Baseline grün ist und das Projektwissen vom Team abgenommen wurde.
## Wer arbeitet woran
| Person / Behörde | Schwerpunkt | Stand |
|---|---|---|
| Daniel Helbig (LK Meißen) | Fachfunktionen Verfahrensgebiet, Plan41, VLN-API | aktiv |
| Michael Otto (LK Mittelsachsen) | Release-Infrastruktur, Linkliste, zuletzt Verfahrensgebiet (Shape-Laden, Beteiligung) | aktiv |
| 22munzke | Druck/Layout-Anforderungen (Issues Basis #37#44) | Issues |
| weitere Behörden | Einstieg über dieses Repository | ab 09/2026 |
## Offene Baustellen (priorisiert)
1. **Repository `AG_QGIS/Projektwissen` auf Gitea anlegen** und diesen Ordner pushen.
Prüfliste: `docs/konzept/08_repo_einrichten.md`. Danach Teamabnahme der Entscheidungen 00060009.
2. **`stable` in den Plugin-Repositories anlegen** und Branch-Schutz für `stable`, `testing`, `unstable` setzen.
Der stabile Kanal existiert heute nicht; ein Tag ohne Suffix würde fehlschlagen (ADR 0007,
`docs/konzept/08_repo_einrichten.md` Abschnitt 5).
3. **QGIS-Mindestversion klären.** `qgis_min = "3.40"` in `workspace.toml` und `qgisMinimumVersion=3.40` in allen
`metadata.template` zeigen auf eine Version, die seit Januar 2026 End of Life ist. Aktueller LTR ist 3.44,
Stable 4.2 wird am 25.09.2026 LTR. Produktentscheidung, blockiert die CI-Matrix
(`docs/konzept/07_qgis_tests_plan.md` Abschnitt 2).
4. **Test-Baseline grün machen.** Ist-Stand Mock-Modus laut `docs/status/testbaseline.json`:
sn_basis 87/87 grün, sn_plan41 43/44 (1 Fehler in `test_tab_a_ui`), sn_verfahrensgebiet 157/201
(36 rot, 8 übersprungen; meist veraltete Tests nach dem Mehrfachauswahl-Umbau).
Probelauf am 03.09.2026 mit frischen Klonen: `sn_verfahrensgebiet` 154/201, **drei neue rote Tests** in
`test_verfahrensgebiet_alkis_komplett` durch PR #43 („Nur-laden-Option durch Stop-Abfrage ersetzt").
Entscheidung des Maintainers: Tests anpassen oder Baseline mit `--update-baseline` nachziehen.
5. **Testbeschreibungen nachziehen.** Von 332 Testfällen haben nur 100 (30 %) einen Docstring — `sn_basis`
keinen einzigen von 87, `sn_plan41` einen von 44. Ohne Beschreibung ist von außen nicht erkennbar, was ein
Test prüft. Arbeitsliste: Abschnitt „Lücken" in `docs/tests/testkatalog.md`. Pflicht für neue und geänderte
Tests gilt ab sofort (ADR 0009).
6. **Testlücken beim Shape-Import schließen.** Beim Erstellen von `docs/tests/verfahrensgebiet-shape-import.md`
gefunden und gegen `origin/unstable` geprüft: `_ermittle_gebietstyp_aus_fbschl` hat **keinen** Test — obwohl
PR #44/#45 genau diese Funktion tolerant gemacht hat. Ebenso ungetestet: mehrere Shapefile-Sätze in einer
ZIP-Datei, Fehlersammlung über mehrere Dateien. Issues anzulegen.
7. **CI für Tests** in den drei Plugin-Repos einrichten (Vorlage `docs/vorlagen/gitea_workflow_tests.yaml`),
Push/PR auf `unstable`.
8. **`.vscode/` aus den Plugin-Repos entfernen.** Liegt noch in `Plugin_SN_Basis`, `Plugin_SN_Plan41`,
`Plugin_SN_Verfahrensgebiet` und `Plugin_SN_Widmung`, mit fest verdrahteten Pfaden eines einzelnen
Entwicklers (`C:/Users/helbi`, `D:/OSGeo`). Im Projektwissen ist er am 04.09.2026 entfernt worden; die
brauchbaren Einstellungen stehen jetzt in `docs/leitfaden/04_werkzeuge_einrichten.md` Abschnitt 4.2.
9. **Release-Excludes ergänzen** (`Repository:hidden/workflows`): `AGENTS.md`, `CLAUDE.md`, `.agents/`,
`.claude/`, `docs/`, `tests/`, `.vscode/`.
10. **Metadata-Templates korrigieren** (Plan41 → eigenes Repo, FLT → `Plugin_SN_FLT`, Widmung-Version).
11. **Abhängigkeiten deklarieren** (`openpyxl`, `requests`; Basis-Abhängigkeit in `metadata.template` als
`plugin_dependencies`).
12. **Linkliste zusammenführen** (Entscheidung: zentrale CSV als einzige Quelle, XLSX generieren).
13. Fachlich offen: Planstand-Versionierung (Plan41 #3), Fachschale aus dem Alternativansatz
(`docs/analyse/00_vergleich_alternativansatz_2026-09-02.md`). Shape-Import mit abweichendem
`FBSchl`-Feldnamen ist seit PR Verfahrensgebiet #45 umgesetzt; QGIS-Test mit echtem DAVID-Export steht aus.
**Hinweis zum lokalen Stand (04.09.2026):** Die Klone `Plugin_SN_Verfahrensgebiet` (4 Commits: PR #43, PR #45)
und `Repository` (Release 26.8.4-unstable) liegen hinter `origin`. Aktualisieren mit `python scripts/hallo.py`.
## Zuletzt gemergt (unstable)
- 02.09.2026 Verfahrensgebiet #45: FBSchl-Feldsuche tolerant gegenüber abweichenden Feldnamen; #43: Nur-laden-Option
bei ALKIS-Laden durch Stop-Abfrage ersetzt (Release 26.8.4-unstable).
- 02.09.2026 Verfahrensgebiet #42: Shape-Ladevorgang mit Mehrfachauswahl, ZIP-Entpacken, `FBSchl`-Prüfung.
- 31.08.2026 Basis #57: DataGrabber lädt ohne `sn_plan41` (Lazy-Import).
- 31.08.2026 Basis #55 / Verfahrensgebiet #40: Mehrfachauswahl ALKIS, Landkreis-Fallback, Beteiligungsprüfung portiert.
## Erledigt (Vorbereitungsphase)
- 04.09.2026 **Git-Vorgaben verbindlich festgeschrieben:** `docs/leitfaden/05_git_arbeitsweise.md` mit Kanal-Kette,
Fix-Einstieg nach Kanal, Rückführungspflicht, Branch-Namen, Gitea-Branch-Schutz. Technisch abgesichert über
`.claude/settings.json` (ADR 0007).
- 04.09.2026 **Ticketzugriff:** `scripts/gitea.py` (lesend, ohne Token) und Skill `sn-ticket`. Schreibende Aktionen
nur als Entwurf in `entwuerfe/`; abgesendet wird vom Menschen.
- 04.09.2026 **Sitzungsbeginn:** `scripts/hallo.py` und Slash-Befehl `/hallo` — Stand aller Klone holen,
Fast-Forward wo gefahrlos, nie mergen oder stashen.
- 04.09.2026 **Tests ausgebaut:** `docs/leitfaden/06_tests.md`, generierter `docs/tests/testkatalog.md`
(`scripts/testkatalog.py`), Vorlage und erste Testkonzept-Seite mit Mermaid (ADR 0009).
- 04.09.2026 **QGIS-Testplan** recherchiert und als Stufenplan festgehalten (`docs/konzept/07_qgis_tests_plan.md`).
- 04.09.2026 **Werkzeuge entschlackt** auf Claude Code (CLI und VS Code), `.vscode/` entfernt (ADR 0008).
Repository heißt künftig `Projektwissen` (ADR 0006); README als Einstieg neu aufgebaut.
- 02.09.2026 Gesamtüberblick und Vergleich Alternativansatz erstellt (`docs/analyse/00_*`).
- 02.09.2026 Workspace-Struktur, AGENTS.md, Skills, Modulkarte, Schnittstellen, Fallstricke, Leitfaden angelegt.
+65
View File
@@ -0,0 +1,65 @@
{
"sn_basis": {
"gesamt": 87,
"fehlschlaege": 0,
"fehler": 0,
"uebersprungen": 0,
"gruen": 87,
"rot": []
},
"sn_verfahrensgebiet": {
"gesamt": 201,
"fehlschlaege": 15,
"fehler": 21,
"uebersprungen": 8,
"gruen": 157,
"rot": [
"test_abweichung_abbrechen_gibt_shape_vg_zurueck (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_abweichung_abbrechen_gibt_shape_vg_zurueck)",
"test_abweichung_detailpruefung_ruft_knickpunkt_pruefung_auf (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_abweichung_detailpruefung_ruft_knickpunkt_pruefung_auf)",
"test_backwards_compatible_no_new_params (sn_verfahrensgebiet.tests.test_flurstueck_alkis.TestFlurstueckAlkis.test_backwards_compatible_no_new_params)",
"test_bbox_embedded_in_wfs_uri (sn_verfahrensgebiet.tests.test_flurstueck_alkis.TestFlurstueckAlkis.test_bbox_embedded_in_wfs_uri)",
"test_bror_als_erstes_argument_beim_flaechenvergleich (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_bror_als_erstes_argument_beim_flaechenvergleich)",
"test_bror_geladen_und_zurueckgegeben_wenn_vkz_gesetzt (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_bror_geladen_und_zurueckgegeben_wenn_vkz_gesetzt)",
"test_bror_wird_bei_abbruch_aus_projekt_entfernt (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_bror_wird_bei_abbruch_aus_projekt_entfernt)",
"test_bror_wird_vor_shape_vg_ins_projekt_eingefuegt (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_bror_wird_vor_shape_vg_ins_projekt_eingefuegt)",
"test_dialog_abgebrochen_gibt_none_zurueck (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestLadeVgAusZip.test_dialog_abgebrochen_gibt_none_zurueck)",
"test_erfolg_gibt_memory_layer_zurueck (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestLadeVgAusZip.test_erfolg_gibt_memory_layer_zurueck)",
"test_existing_layer_keep_no (sn_verfahrensgebiet.tests.test_flurstueck_alkis.TestFlurstueckAlkis.test_existing_layer_keep_no)",
"test_existing_layer_reload_yes (sn_verfahrensgebiet.tests.test_flurstueck_alkis.TestFlurstueckAlkis.test_existing_layer_reload_yes)",
"test_extent_derived_from_vg_layer (sn_verfahrensgebiet.tests.test_grenzpunkt_alkis.TestGrenzpunktAlkis.test_extent_derived_from_vg_layer)",
"test_fallback_attribute (sn_verfahrensgebiet.tests.test_gemarkung_alkis.TestExtractSchluesselUndName.test_fallback_attribute)",
"test_fallback_auf_temp_enriched_wenn_bror_ladeversagen (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_fallback_auf_temp_enriched_wenn_bror_ladeversagen)",
"test_flaechen_uebereinstimmung_gibt_info_und_kein_dialog (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_flaechen_uebereinstimmung_gibt_info_und_kein_dialog)",
"test_grenzpunkt_batch_empty (sn_verfahrensgebiet.tests.test_grenzpunkt_alkis.TestGrenzpunktAlkis.test_grenzpunkt_batch_empty)",
"test_handle_grenz_alkis_none_result_sets_no_haken (sn_verfahrensgebiet.tests.test_working_tab.TestWorkingTab.test_handle_grenz_alkis_none_result_sets_no_haken)",
"test_handle_grenz_alkis_success_sets_haken (sn_verfahrensgebiet.tests.test_working_tab.TestWorkingTab.test_handle_grenz_alkis_success_sets_haken)",
"test_handle_verf_alkis_success_path (sn_verfahrensgebiet.tests.test_working_tab.TestWorkingTab.test_handle_verf_alkis_success_path)",
"test_invalider_qgslayer_gibt_none_und_warning (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestLadeVgAusZip.test_invalider_qgslayer_gibt_none_und_warning)",
"test_kein_shp_in_zip_gibt_none_und_warning (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestLadeVgAusZip.test_kein_shp_in_zip_gibt_none_und_warning)",
"test_leere_geometrien_geben_none_und_warning (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestLadeVgAusZip.test_leere_geometrien_geben_none_und_warning)",
"test_no_gml_ids_returns_pta_only (sn_verfahrensgebiet.tests.test_grenzpunkt_alkis.TestGrenzpunktAlkis.test_no_gml_ids_returns_pta_only)",
"test_punktort_ta_none_gibt_shape_vg_zurueck_und_zeigt_fehler (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_punktort_ta_none_gibt_shape_vg_zurueck_und_zeigt_fehler)",
"test_shape_vg_sofort_ins_projekt_nach_laden (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_shape_vg_sofort_ins_projekt_nach_laden)",
"test_shape_vg_wird_bei_abbruch_aus_projekt_entfernt (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestVerfahrensgebietShapeKomplett.test_shape_vg_wird_bei_abbruch_aus_projekt_entfernt)",
"test_sql_filter_contains_all_schluessel (sn_verfahrensgebiet.tests.test_flurstueck_alkis.TestFlurstueckAlkis.test_sql_filter_contains_all_schluessel)",
"test_standard_attribute (sn_verfahrensgebiet.tests.test_gemarkung_alkis.TestExtractSchluesselUndName.test_standard_attribute)",
"test_success_returns_both_layers (sn_verfahrensgebiet.tests.test_grenzpunkt_alkis.TestGrenzpunktAlkis.test_success_returns_both_layers)",
"test_success_returns_layer_and_first_status (sn_verfahrensgebiet.tests.test_flurstueck_alkis.TestFlurstueckAlkis.test_success_returns_layer_and_first_status)",
"test_verfahrensnummer_nicht_gesetzt_ja_fuehrt_fort (sn_verfahrensgebiet.tests.test_working_tab.TestHandleVerfShape.test_verfahrensnummer_nicht_gesetzt_ja_fuehrt_fort)",
"test_verfahrensnummer_nicht_gesetzt_nein_bricht_ab (sn_verfahrensgebiet.tests.test_working_tab.TestHandleVerfShape.test_verfahrensnummer_nicht_gesetzt_nein_bricht_ab)",
"test_vorgabe_namen_appears_in_label (sn_verfahrensgebiet.tests.test_flurstueck_alkis.TestFlurstueckAlkis.test_vorgabe_namen_appears_in_label)",
"test_vorgabe_schluessel_prefills_default_text (sn_verfahrensgebiet.tests.test_flurstueck_alkis.TestFlurstueckAlkis.test_vorgabe_schluessel_prefills_default_text)",
"test_zip_fehler_gibt_none_und_warning (sn_verfahrensgebiet.tests.test_verfahrensgebiet_shape_komplett.TestLadeVgAusZip.test_zip_fehler_gibt_none_und_warning)"
]
},
"sn_plan41": {
"gesamt": 44,
"fehlschlaege": 0,
"fehler": 1,
"uebersprungen": 0,
"gruen": 43,
"rot": [
"test_set_services_initializes_logic_and_injects_data_grabber (sn_plan41.tests.test_tab_a_ui.TestTabAUI.test_set_services_initializes_logic_and_injects_data_grabber)"
]
}
}
File diff suppressed because it is too large Load Diff
+105
View File
@@ -0,0 +1,105 @@
# Testkonzept: Verfahrensgebiet aus Shape laden
Plugin: `sn_verfahrensgebiet` · Stand: 04.09.2026 · Pflege: bei jeder Änderung an diesem Ablauf
Beispielseite nach [`../vorlagen/testkonzept.md`](../vorlagen/testkonzept.md). Sie zeigt, wie eine Fachperson
ohne Blick in den Quelltext beurteilen kann, ob die Tests das Richtige prüfen —
siehe [`../leitfaden/06_tests.md`](../leitfaden/06_tests.md) Abschnitt 7.
## Worum es fachlich geht
Ein Verfahrensgebiet liegt als Shape-Export aus NoBIS bzw. DAVID vor, meist als ZIP-Datei. Der Anwender wählt
diese Datei aus; das Plugin liest daraus das Neuordnungs- und/oder Neuvermessungsgebiet ein, gleicht die Fläche
mit den ALKIS-Flurstücken ab und meldet Abweichungen. Eine ZIP-Datei kann mehrere Shapefile-Sätze enthalten —
je einen für Neuordnungs- und Neuvermessungsgebiet. Welcher Satz was ist, entscheidet der Wert im Feld `FBSchl`.
Wenn dieser Schritt schiefgeht, arbeitet das gesamte weitere Verfahren auf einem falschen Gebiet.
## Der Ablauf
```mermaid
flowchart TD
A["1 Anwender wählt ZIP oder SHP"] --> B["2 ZIP entpacken,<br/>alle .shp darin finden"]
B --> C["3 Jede Datei als Layer laden<br/>und auf Gültigkeit prüfen"]
C --> D{"4 Feld FBSchl<br/>vorhanden und bekannt?"}
D -->|"901/10110F"| E["Neuordnungsgebiet"]
D -->|"901/10120F"| F["Neuvermessungsgebiet"]
D -->|"fehlt / unbekannt"| G["Datei überspringen,<br/>Fehler sammeln"]
E --> H["5 Flächen desselben Typs<br/>zu einem Layer verschmelzen"]
F --> H
G --> H
H --> I["6 ALKIS-Flurstücke laden,<br/>Flächen vergleichen"]
I --> J{"7 Flächen gleich?"}
J -->|ja| K["Hinweis: stimmt überein"]
J -->|nein| L["Nachfrage:<br/>Detailprüfung oder abbrechen"]
L --> M["8 Knickpunktprüfung"]
K --> N["9 Layer bleibt im Projekt"]
M --> N
style D fill:#fff4e6,stroke:#d9822b
style G fill:#ffe9e9,stroke:#d94b2b
```
## Was geprüft wird
Alle Tests liegen in `Plugin_SN_Verfahrensgebiet/tests/test_verfahrensgebiet_shape_komplett.py`.
| # | Schritt | Wird geprüft durch | Ebene |
|---|---|---|---|
| 1 | Auswahl abgebrochen | `test_dialog_abgebrochen_gibt_none_zurueck` | Mock |
| 2 | ZIP nicht entpackbar | `test_zip_fehler_gibt_none_und_warning` | Mock |
| 2 | Kein `.shp` im ZIP | `test_kein_shp_in_zip_gibt_none_und_warning` | Mock |
| 2 | `.shp` im Wurzel- bzw. Unterordner finden | `test_findet_shp_in_root`, `test_findet_shp_in_unterordner` | Mock |
| 2 | Groß-/Kleinschreibung der Dateiendung | `test_case_insensitive_erkennung` | Mock |
| 3 | Layer ungültig | `test_invalider_qgslayer_gibt_none_und_warning` | Mock |
| 3 | Keine Features / leere Geometrien | `test_keine_features_gibt_none`, `test_leere_geometrien_geben_none` | Mock |
| **4** | **Feld `FBSchl` auswerten** | **— keiner** | **—** |
| 5 | Verschmelzen zum Einzel-Feature, Attribute übernehmen | `test_attribute_von_erstem_feature_werden_gesetzt`, `test_erfolg_gibt_memory_layer_zurueck` | Mock |
| 6 | ALKIS liefert nichts / keine Auswahl | `test_flst_none_aborts`, `test_keine_flurstuecke_selektiert_gibt_warning` | Mock |
| 7 | Flächen stimmen überein | `test_flaechen_uebereinstimmung_gibt_info_und_kein_dialog` | Mock |
| 7 | Abweichung, Anwender bricht ab | `test_abweichung_abbrechen_gibt_shape_vg_zurueck` | Mock |
| 8 | Abweichung, Detailprüfung | `test_abweichung_detailpruefung_ruft_knickpunkt_pruefung_auf` | Mock |
| 8 | PunktortTA nicht verfügbar | `test_punktort_ta_none_gibt_shape_vg_zurueck_und_zeigt_fehler` | Mock |
| 9 | Layer bleibt bei Erfolg, verschwindet bei Abbruch | `test_shape_vg_bleibt_im_projekt_bei_erfolg`, `test_shape_vg_wird_bei_abbruch_aus_projekt_entfernt` | Mock |
| 9 | Temporärer Layer wird immer entfernt | `test_temp_layer_wird_stets_entfernt` | Mock |
| — | Sieht das Ergebnis in QGIS richtig aus? | manueller Test | manuell |
Ebene: `Mock` (ohne QGIS, Sekunden) · `QGIS` (gegen echtes QGIS) · `manuell` (Mensch in QGIS).
Zusätzlich ist das Zusammenspiel mit dem BROR-Layer (Bau-, Raum- oder Bodenordnungsrecht) abgedeckt, wenn eine
VKZ gesetzt ist: `test_bror_geladen_und_zurueckgegeben_wenn_vkz_gesetzt`,
`test_bror_wird_vor_shape_vg_ins_projekt_eingefuegt`, `test_verfahrensgebiet_alkis_nicht_aufgerufen_wenn_vkz_leer`,
`test_fallback_auf_temp_enriched_wenn_bror_ladeversagen`.
## Fachliche Sonderfälle
| Sonderfall | Kommt vor bei | Wird geprüft durch |
|---|---|---|
| ZIP enthält mehrere Shapefile-Sätze | Export mit Neuordnungs- **und** Neuvermessungsgebiet | — |
| Eine von mehreren Dateien im ZIP ist fehlerhaft | gemischte Exporte; die übrigen sollen trotzdem verarbeitet werden | — |
| Feldname `FBSchl.` mit Punkt | manche DAVID-Exporte | — |
| Feldname in abweichender Groß-/Kleinschreibung | ältere Exporte | — |
| Feld `FBSchl` fehlt ganz | Shape ist gar kein Verfahrensgebiet | — |
| Unbekannter Wert in `FBSchl` | neuer Schlüssel, den wir noch nicht kennen | — |
| ZIP nicht entpackbar, kein `.shp` enthalten | beschädigter oder falscher Export | ja (siehe Tabelle oben) |
| Fläche weicht von ALKIS ab | Regelfall bei Grenzänderungen | ja |
## Bewusst nicht getestet
| Was | Warum | Wie stattdessen abgesichert |
|---|---|---|
| Geometrisches Verschmelzen (`unaryUnion`) | QGIS-Funktion, nicht unser Code; im Mock-Modus gibt es keine echte Geometrie | manueller Test in QGIS; künftig QGIS-Test (Stufe 1, `../konzept/07_qgis_tests_plan.md`) |
| Aussehen und Beschriftung der Dialoge | nicht sinnvoll automatisiert prüfbar | manueller Test |
| Echte ALKIS-WFS-Antworten | kein Netzwerk in Tests; Dienstgrenzen siehe `../wissen/fallstricke.md` | nachgestellte Antworten im Test, plus manueller Test bei Dienständerungen |
## Bekannte Lücken
| Lücke | Auswirkung | Issue |
|---|---|---|
| **Schritt 4 (`_ermittle_gebietstyp_aus_fbschl`) hat keinen einzigen Test** — weder für die gültigen Werte `901/10110F` und `901/10120F`, noch für fehlendes Feld, unbekannten Wert, `FBSchl.` mit Punkt oder abweichende Groß-/Kleinschreibung | Ein Shape wird stillschweigend dem falschen Gebietstyp zugeordnet oder grundlos abgewiesen. Genau dieser Fall war Gegenstand von PR #44/#45 — die tolerante Feldsuche wurde eingebaut, aber nie durch einen Test abgesichert. | anzulegen |
| Mehrere Shapefile-Sätze in einer ZIP-Datei werden nicht als Fall getestet | Der dokumentierte Hauptzweck der Mehrfachverarbeitung ist ungeprüft; ein Regressionsfehler bliebe unbemerkt | anzulegen |
| Fehlersammlung über mehrere Dateien (eine fehlerhafte Datei bricht die übrigen nicht ab) ist nicht getestet | Ein Export mit einer defekten Datei könnte komplett abgewiesen werden, statt die guten Dateien zu verarbeiten | anzulegen |
> Diese drei Lücken wurden am 04.09.2026 beim Erstellen dieser Seite gefunden — durch den bloßen Abgleich des
> fachlichen Ablaufs mit der Testliste. Sie sind gegen `origin/unstable` geprüft, nicht nur gegen den lokalen
> Klon. Das ist der Zweck dieser Seite.
+21
View File
@@ -0,0 +1,21 @@
# <paketname> (Repository <Repo-Name>)
Teil des AG_QGIS-Projektwissens. Übergeordnete Regeln, Modulkarte, Schnittstellen, Fallstricke und Skills liegen im
Workspace: `../AGENTS.md` und `../docs/` (wenn dieses Repo als Unterordner des Workspace geklont ist).
Einzelklon? Projektwissen holen: https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Projektwissen
## Rolle
<Ein bis drei Sätze: Was tut dieses Plugin, wovon hängt es ab, wer hängt davon ab.>
## Aufbau
<Ordner mit Einzeiler; große Dateien (> 600 Zeilen) mit Zeilenzahl nennen.>
## Zusätzliche Regeln
<Nur, was über den Workspace hinausgeht.>
## Tests
`python ../scripts/run_tests.py <paketname> --baseline` (Mock-Modus). In QGIS: `tests/test_qgis.bat` (Windows).
## Release
Version `JJ.M.N`, Tag `v<Version>-u` (unstable) / `-t` (testing) auf dem Kopf des Kanal-Branches. `metadata.txt` nicht
von Hand pflegen (wird aus `.gitea/templates/metadata.template` erzeugt). `changelog.txt`: neue Einträge oben vor dem ersten `---`.
+3
View File
@@ -0,0 +1,3 @@
@AGENTS.md
Beim Arbeiten im Workspace (`../AGENTS.md` vorhanden) gelten dessen Regeln und Skills zusätzlich.
+51
View File
@@ -0,0 +1,51 @@
# Vorlage für .gitea/workflows/tests.yaml in Plugin_SN_Basis, Plugin_SN_Plan41, Plugin_SN_Verfahrensgebiet.
# Führt die Mock-Modus-Tests bei Push/PR auf unstable und testing aus.
# Voraussetzung: Gitea-Runner mit Label alpine-latest (wie beim Release-Workflow), Projektwissen-Repo öffentlich lesbar.
# Der Lauf klont das Projektwissen und die abhängigen Plugin-Repos (unstable) und testet den aktuellen Checkout dieses Repos.
name: Tests (Mock-Modus)
run-name: "Tests | ${{ github.ref_name }}"
on:
push:
branches: [unstable, testing]
pull_request:
branches: [unstable, testing]
jobs:
mock-tests:
runs-on: alpine-latest
defaults:
run:
shell: bash
steps:
- name: Abhängigkeiten installieren
shell: sh
run: apk add --no-cache bash git python3 py3-openpyxl
- name: Workspace und Repos holen
env:
GITEA: ${{ vars.RELEASE_URL }} # z. B. entwicklung.flurneuordnung-sachsen.de
run: |
git clone --depth 1 "https://${GITEA}/AG_QGIS/Projektwissen.git" ws
cd ws
for r in Plugin_SN_Basis Plugin_SN_Plan41 Plugin_SN_Verfahrensgebiet; do
git clone --depth 1 --branch unstable "https://${GITEA}/AG_QGIS/$r.git" "$r"
done
# Dieses Repo im geprüften Stand einsetzen
NAME="${GITHUB_REPOSITORY##*/}"
rm -rf "$NAME"
git clone "https://${GITEA}/${GITHUB_REPOSITORY}.git" "$NAME"
cd "$NAME" && git checkout "${GITHUB_SHA}" && cd ..
- name: Tests mit Baseline
run: |
cd ws
NAME="${GITHUB_REPOSITORY##*/}"
PAKET=$(python3 - "$NAME" <<'PY'
import sys, tomllib
m = tomllib.load(open("workspace.toml", "rb"))
print(next(r["paket"] for r in m["repos"] if r["name"] == sys.argv[1]))
PY
)
python3 scripts/run_tests.py "$PAKET" --baseline
+19
View File
@@ -0,0 +1,19 @@
## Ziel
<Was soll am Ende möglich sein? Ein Satz aus Nutzersicht.>
## Betroffen
Plugin: sn_basis | sn_verfahrensgebiet | sn_plan41 | sn_widmung
Module/Dateien (falls bekannt):
Berührt Schnittstellen (`docs/architektur/schnittstellen.md`)? nein | ja: <welche>
## Fachlicher Hintergrund
<Begriffe, Regeln, Beispieldaten (keine echten Verfahrensdaten). Verweise auf Glossar.>
## Abnahmekriterien
- [ ] <beobachtbares Verhalten 1>
- [ ] <beobachtbares Verhalten 2>
- [ ] Mock-Test vorhanden, `run_tests.py --baseline` ohne NEU ROT
- [ ] QGIS-Test: <nötig / nicht nötig>
## Nicht Teil dieses Issues
<Abgrenzung, damit der Agent nicht abschweift.>
+18
View File
@@ -0,0 +1,18 @@
## Was
<Zwei Sätze. Issue: #…>
## Wie geprüft
- `python scripts/run_tests.py <paket> --baseline`: <Ergebniszeile>
- QGIS-Test: <QGIS-Version, Ablauf, Ergebnis> | nicht nötig, weil …
## Änderungen an Verträgen
keine | <welche, mit Verweis auf Issue und `schnittstellen.md`-Anpassung>
## Übergabenotiz
<nach docs/vorlagen/uebergabe.md>
## Checkliste
- [ ] Nur Änderungen zum Issue
- [ ] Keine direkten Qt/QGIS-Importe, keine Debug-`print`
- [ ] `changelog.txt` ergänzt
- [ ] Wissensbasis nachgezogen (oder „nichts Neues“)
+65
View File
@@ -0,0 +1,65 @@
<!--
Vorlage für eine Testkonzept-Seite unter docs/tests/<ablauf>.md.
Zweck: Eine Fachperson soll ohne Blick in den Quelltext beurteilen können, ob die Tests
fachlich das Richtige prüfen. Deshalb: fachliche Sprache, keine Klassennamen im Diagramm.
Erläuterung: docs/leitfaden/06_tests.md Abschnitt 7.
Diesen Kommentar beim Ausfüllen entfernen.
-->
# Testkonzept: <Name des fachlichen Ablaufs>
Plugin: `sn_<paket>` · Stand: <TT.MM.JJJJ> · Pflege: bei jeder Änderung an diesem Ablauf
## Worum es fachlich geht
<Zwei bis vier Sätze aus Sicht der Sachbearbeitung: Was macht der Anwender, was soll dabei herauskommen,
warum ist das wichtig. Keine Dateinamen, keine Funktionsnamen.>
## Der Ablauf
```mermaid
flowchart TD
A["1 <Was der Anwender tut>"] --> B["2 <Was das Plugin daraufhin tut>"]
B --> C{"<Fachliche Entscheidung>"}
C -->|"<Fall a>"| D["3a <Ergebnis>"]
C -->|"<Fall b>"| E["3b <Ergebnis>"]
D --> F["4 <Endzustand>"]
E --> F
```
## Was geprüft wird
| # | Schritt | Wird geprüft durch | Ebene |
|---|---|---|---|
| 1 | <Schritt aus dem Diagramm> | `test_<name>` in `tests/test_<modul>.py` | Mock |
| 2 | <Schritt> | `test_<name>` | Mock |
| 3a | <Fall a> | `test_<name>` | Mock |
| 3b | <Fall b> | — | — |
| 4 | <Endzustand> | manueller Test in QGIS | manuell |
Ebene: `Mock` (ohne QGIS, Sekunden) · `QGIS` (gegen echtes QGIS) · `manuell` (Mensch in QGIS).
Erläuterung der Ebenen: `docs/leitfaden/06_tests.md` Abschnitt 2.
## Fachliche Sonderfälle
Die Fälle, die in der Praxis wirklich vorkommen — hier entscheidet sich, ob die Suite etwas taugt.
| Sonderfall | Kommt vor bei | Wird geprüft durch |
|---|---|---|
| <z. B. leerer Export> | <Situation in der Praxis> | `test_<name>` |
| <z. B. abweichender Feldname> | <Situation> | `test_<name>` |
| <z. B. Datensatz aus dem Nachbarlandkreis> | <Situation> | — |
## Bewusst nicht getestet
| Was | Warum | Wie stattdessen abgesichert |
|---|---|---|
| <Aspekt> | <nachvollziehbarer Grund — „zu aufwendig" ist keiner> | <manueller Test, Review, anderer Test> |
## Bekannte Lücken
| Lücke | Auswirkung | Issue |
|---|---|---|
| <Was fehlt> | <Was im schlimmsten Fall unbemerkt bleibt> | #<nr> |
<Keine Lücken bekannt? Dann hier „Derzeit keine bekannten Lücken." und das Datum der letzten Durchsicht.>
+10
View File
@@ -0,0 +1,10 @@
## Übergabenotiz
**Aufgabe:** <Issue-Link und Ziel in einem Satz>
**Stand:** fertig | in Arbeit | blockiert <wo genau>
**Geändert:** <Dateien mit Kurzgrund, keine Diffs>
**Nachweis:** `run_tests.py <paket> --baseline`: <Ergebniszeile>; QGIS-Test: <was, womit, Ergebnis | nicht nötig, weil …>
**Entscheidungen/Annahmen:** <fachliche Annahmen, die jemand bestätigen sollte>
**Offen / nächste Schritte:** <konkret, in Reihenfolge>
**Wissensbasis:** <Einträge in fallstricke/schnittstellen/modulkarte/STATUS | „nichts Neues“>
**Kontext für die nächste Sitzung:** <die zwei bis drei Dateien/Abschnitte, mit denen es weitergeht>
+26
View File
@@ -0,0 +1,26 @@
# Externe Dienste und Abhängigkeiten
| Dienst / Ressource | URL / Ort | Verwendung | Hinweise |
|---|---|---|---|
| Gitea 1.27 (Quellcode, Issues, Releases, Actions) | https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS | alle Repositories | API **lesend ohne Anmeldung** (`/api/v1/...`) dafür `scripts/gitea.py` und Skill `sn-ticket`. Wiki nicht genutzt (Doku liegt versioniert im Repo). Labels sind **organisationsweit** definiert (`/orgs/AG_QGIS/labels`), nicht je Repo `/repos/.../labels` liefert deshalb eine leere Liste. Vorhanden: `Bug`, `Diskussion`, `Feature`, `Konflikt`, `QGIS 3.44`. Meilensteine gibt es keine (geprüft 04.09.2026). Schreibend nur durch Menschen. |
| QGIS-Plugin-Feeds | `Repository/raw/branch/main/plugins*.xml` | Installation in QGIS | stable leer; testing und unstable befüllt |
| ALKIS vereinfacht (WFS) | https://geodienste.sachsen.de/aaa/public_alkis/vereinf/wfs | Gemarkungen, BauRaumOderBodenordnungsrecht | POST/GML-Filter blockiert, URL-Länge begrenzt |
| ALKIS SF (WFS) | https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs | Flurstücke, PunktortTA | Feature-Limits, deshalb Batches |
| VLN-Karten-API v2 | https://api.flurneuordnung-sachsen.de/v2 | Plan41-Datensatz je VKZ laden/hochladen | Login E-Mail/Passwort → `X-API-Key`; Zugang über VLN Sachsen |
| LUIS Sachsen (ArcGIS REST) | https://luis.sachsen.de/arcgis/rest/services/... | Fachdaten laut Linkliste (Umwelt, Wasser, Naturschutz, Energie) | 44 Einträge in `assets/Linkliste.xlsx` |
| RAPIS | laut Linkliste | Bauleitplanung, Raumordnung | |
| NoBIS / DAVID | Fachverfahren der Behörden (kein Webdienst) | Shape-Exporte des Verfahrensgebiets | Feldname `FBSchl` erwartet |
| BI-Server | behördenintern | CSV-Export Beteiligte | |
## Python-Abhängigkeiten (ohne Manifest im Projekt)
| Paket | Art | Verwendung |
|---|---|---|
| `qgis`, `PyQt5`/`PyQt6` | Laufzeit (QGIS) | über Wrapper; im Mock-Modus entbehrlich |
| `openpyxl` | **hart** | Excel-Import in `sn_basis.modules.excel_importer` |
| `requests` | optional | Fallback im Netzwerkzugriff |
| Standardbibliothek | | `sqlite3`, `zipfile`, `tempfile`, `csv`, `json`, `xml`, `urllib` |
Für das Projektwissen selbst: Python ≥ 3.11 (`tomllib`) und Git. Die Skripte `hallo.py`, `gitea.py`,
`testkatalog.py`, `setup_workspace.py` und `link_qgis_profile.py` brauchen **nur die Standardbibliothek**;
allein `run_tests.py` setzt `openpyxl` voraus (weil die Testsuiten es importieren).
+57
View File
@@ -0,0 +1,57 @@
# Fallstricke und Erkenntnisse
Gemeinsames Gedächtnis: Dinge, die man **nicht aus dem Code ablesen kann** und die schon einmal Zeit gekostet haben.
Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik. Veraltetes löschen, nicht auskommentieren.
## Umgebung und Werkzeuge
- **QGIS 3.40 ist seit Januar 2026 End of Life.** Aktueller LTR ist 3.44, Stable 4.2 (wird 25.09.2026 LTR). `workspace.toml` und alle `metadata.template` stehen noch auf `3.40` offene Produktentscheidung, siehe `docs/konzept/07_qgis_tests_plan.md` Abschnitt 2. (04.09.2026, QGIS-Release-Fahrplan)
- **Zwei Qt-Welten.** QGIS 3.40/3.44 laufen mit Qt5/PyQt5, QGIS 4 mit Qt6/PyQt6. `qt_wrapper` lädt PyQt6 zuerst, dann PyQt5, sonst Mock. Enum-Zugriffe unterscheiden sich (`Qt.AlignmentFlag.AlignTop` statt `Qt.AlignTop`), deshalb nur Wrapper-Aliase verwenden. (09/2026, Code)
- **Mock-Modus ist kein QGIS.** `python scripts/run_tests.py` läuft in Sekunden, prüft aber nur Logik gegen Mock-Klassen. Geometrieoperationen, WFS, Layouts und Signale müssen in QGIS geprüft werden (`tests/test_qgis.bat` unter Windows). Manuelle QGIS-Tests im PR dokumentieren. (09/2026)
- **`openpyxl` ist eine harte Abhängigkeit** von `sn_basis` (Excel-Import) und unter Linux-QGIS oft nicht installiert (Issue Verfahrensgebiet #21). `requests` ist nur optionaler Fallback. Es gibt keine `requirements.txt`. (09/2026)
- **QGIS lädt Plugins in nicht garantierter Reihenfolge.** Fachplugins dürfen in `initGui()` nicht davon ausgehen, dass `sn_basis` fertig ist; `sn_basis` ruft `initGui()` aller `sn_*` nach. (Code `main.py`)
- **Exit-Crash unter QGIS 3.40** entstand durch nicht getrennte Projektsignale beim Unload. Regel: jeder Tab/Manager, der Signale verbindet, bietet `cleanup()`/`teardown_...()` und wird beim Schließen aufgerufen. (Basis, `assets/plan-qgis340ExitCrashFix.prompt.md`)
- **`.vscode/` gehört nicht ins Repository.** Im Projektwissen wurde er am 04.09.2026 entfernt; brauchbare Einstellungen stehen jetzt in `docs/leitfaden/04_werkzeuge_einrichten.md` Abschnitt 4.2 zum Kopieren in die Benutzereinstellungen. In `Plugin_SN_Basis`, `Plugin_SN_Plan41`, `Plugin_SN_Verfahrensgebiet` und `Plugin_SN_Widmung` liegt er noch und enthält Pfade eines einzelnen Entwicklers (`D:/OSGeo`, `C:/Users/helbi`) nicht als Vorgabe verstehen. (09/2026)
## QGIS-Tests (geplant, siehe `docs/konzept/07_qgis_tests_plan.md`)
- **`QT_QPA_PLATFORM=offscreen` muss gesetzt sein, bevor die `QgsApplication` erzeugt wird.** Danach wirkt es nicht mehr. (04.09.2026, Recherche)
- **`qgis.utils.iface` ist nach `start_app()` `None`.** Entweder `pytest-qgis` verwenden (patcht automatisch) oder selbst `qgis.utils.iface = get_iface()` setzen und keine Module zu früh importieren. (04.09.2026)
- **`QgsProject.instance()` ist global.** Ohne Aufräumen zwischen den Tests beeinflussen sie sich gegenseitig. (04.09.2026)
- **Abstürze beim Beenden** sind ein bekanntes QGIS-Thema, wenn Layer- oder Canvas-Objekte noch referenziert werden. Gegenmittel: `pytest-qgis` oder `--qgis_disable_exit`. (04.09.2026)
- **`QgisTestCase.render_layout_check()` ist für Plugins nicht nutzbar**: es sucht Referenzbilder in einem einkompilierten Pfad des QGIS-Quellbaums. Nur `QgsRenderChecker` bietet `setControlImagePath()`. Referenzbilder immer im CI-Container erzeugen, nie unter Windows (Font- und Antialiasing-Unterschiede). (04.09.2026)
- **Modale Dialoge blockieren den Test.** Reihenfolge der Gegenmittel: modalen Aufruf per `monkeypatch` ersetzen, `open()` statt `exec()`, erst zuletzt `QTimer.singleShot`. (04.09.2026)
- **Unter Windows nur `python-qgis.bat`/`python-qgis-ltr.bat` verwenden** ein normales `python -m pytest` findet die QGIS-Bindings nicht. (04.09.2026)
## Release und Versionen
- **Der Release-Workflow baut den Kopf des Kanal-Branches**, nicht den getaggten Commit (`git checkout "$CHANNEL"` in `Repository:hidden/workflows`). Tag nur setzen, wenn der Kanal-Branch auf dem gewünschten Stand ist. (09/2026)
- **`metadata.txt` nie von Hand pflegen.** Sie wird beim Release aus `.gitea/templates/metadata.template` erzeugt. Die im Repo liegende Datei ist ein Abbild des letzten Builds und kann irreführende Versionen/URLs zeigen (Issue Verfahrensgebiet #26). (09/2026)
- **Stable-Kanal ist leer**, ein Branch dafür existiert nicht; ein Tag ohne Suffix würde derzeit fehlschlagen. Der Branch muss **exakt `stable`** heißen, weil der Release-Workflow in `Repository:hidden/workflows` `git checkout "$CHANNEL"` ausführt und `$CHANNEL` aus dem Tag-Suffix stammt. Anlegen: `docs/konzept/08_repo_einrichten.md` (ADR 0007). (04.09.2026, Workflow gelesen)
- **Repo-Name ≠ Paketname.** `Plugin_SN_Basis` wird als `sn_basis` installiert (Gitea-Variable `ZIP_FOLDER`). Importe im Code verwenden immer den Paketnamen.
- **Bekannte Metadata-Fehler:** Plan41-Template verweist auf `Plugin_Test_Action` als Repository; FLT-Template auf `Plugin_SN_Neu`; Widmung trägt Version `4711-unstable`. (Gesamtüberblick 09/2026)
- **Workspace-Dateien in Release-ZIPs.** Der rsync-Schritt schließt nur `.git`, `.gitea`, `.gitignore`, `__pycache__` aus. `AGENTS.md`, `CLAUDE.md`, `.agents/`, `.claude/`, `docs/`, `tests/` werden mit ausgeliefert, bis die Excludes ergänzt sind (offener Punkt).
## Daten und Dienste
- **WFS-Beschränkungen der sächsischen ALKIS-Dienste:** POST/GML-Filter werden blockiert, URL-Länge ist begrenzt, serverseitige Feature-Limits. Deshalb arbeiten Flurstücks- und Grenzpunktabrufe mit BBOX, gemarkungsweisen Batches und Nutzerdialogen. Nicht „vereinfachen“, ohne das gegen den Dienst zu testen. (Verfahrensgebiet, Code)
- **Zwei Linklisten.** Zentrale `Linkliste/linkliste.csv` (Spalten `id;gruppe;ebene;inhalt;url;provider;stil`) und die vom Plugin genutzte `Plugin_SN_Plan41/assets/Linkliste.xlsx` (`ident, gruppe, kartenebene, inhalt, link, provider, stildatei`) sind nicht verbunden. Änderungen an Diensten in **beiden** nachziehen, bis die Zusammenführung entschieden ist. (09/2026)
- **ArcGIS-REST-Layer nicht nach Nummer deuten.** Gleiche Layer-URLs werden in der Linkliste und im Alternativansatz unterschiedlich benannt; Bedeutung und Geometrie immer gegen die Dienstmetadaten prüfen. (Vergleich Alternativansatz 09/2026)
- **CRS:** Plan41 und VLN-API arbeiten in EPSG:25833. Der Alternativansatz erzeugt EPSG:25832. Für neue Layer 25833 verwenden, bis eine Entscheidung dokumentiert ist.
- **Shape-Import erwartet Feld `FBSchl`** mit `901/10110F` (Neuordnungsgebiet) oder `901/10120F` (Neuvermessungsgebiet). Exporte mit abweichendem Feldnamen (`FBSchl.`) wurden bis 26.8.3 abgewiesen; seit PR Verfahrensgebiet #45 (02.09.2026, 26.8.4-unstable) ist die Feldsuche tolerant. Neue Exportvarianten trotzdem gegen die Erkennung testen.
- **VLN-Upload ersetzt den Serverbestand der VKZ vollständig** (`PUT`). Vor dem Upload sicherstellen, dass alle Teillayer geladen sind.
- **API-Key liegt in `QSettings`** (Registry/Konfigurationsdatei), kein sicherer Schlüsselspeicher. Niemals in Logs oder Tests ausgeben.
## Code-Konventionen, die man kennen muss
- **Projektvariablen-Schlüssel ohne `sn_` übergeben.** `set_variable("sn_abdeckungslayer", ...)` erzeugt `sn_sn_abdeckungslayer` (Altlast in Druckmodulen; funktioniert nur, weil Lesen und Schreiben denselben Fehler machen).
- **Zwei Schreibweisen für den Verfahrensgebiet-Layer:** `verfahrensgebiet_layer` (Manager, Fachplugins) und `verfahrensgebietslayer` (`print_logic.py`). Bis zur Bereinigung nicht auf Gleichheit vertrauen.
- **Layer werden über Anzeigenamen gefunden** (`BROR_LAYER_NAME` usw.). Umbenennungen in der UI brechen Abläufe.
- **`print()` ist im Code verbreitet**, aber kein Logging. Neue Ausgaben über `message_wrapper` (Nutzer) oder gar nicht; Debug-`print` vor dem PR entfernen.
- **Testrunner pro Repo (`tests/run_tests.py`) und Workspace-Runner unterscheiden sich.** Für PR-Nachweise zählt `scripts/run_tests.py --baseline`.
- **Tests der Verfahrensgebiet-Suite sind teils veraltet** (erwarten Einzeldatei-/Einzellayer-Schnittstellen vor der Mehrfachauswahl). Vor dem Fixen prüfen, ob der Test oder der Code falsch ist; siehe Baseline.
## Zusammenarbeit
- **Tool-Gedächtnis ist lokal.** Die Claude-Auto-Memory sieht niemand sonst. Geteiltes Wissen nur in diesem Ordner (siehe Issue Basis #36 „shared-memory-architektur“, Kai).
- **Issues sind teils Diskussionen**, nicht Aufträge (Label `Diskussion`). Vor der Umsetzung Konsens im Issue abwarten oder im PR als Annahme kennzeichnen.
+36
View File
@@ -0,0 +1,36 @@
# Glossar
Fachbegriffe und Abkürzungen, die im Code, in Issues und in Gesprächen vorkommen. Für Entwickler und Agenten ohne
Flurbereinigungs-Hintergrund. Ergänzen, wenn ein Begriff im Issue erklärt werden musste.
| Begriff | Bedeutung |
|---|---|
| **LNO** | Ländliche Neuordnung; Sammelbegriff für Flurbereinigungsverfahren in Sachsen. Name des gemeinsamen Menüs „LNO Sachsen“ |
| **FlurbG** | Flurbereinigungsgesetz. § 38: Vorarbeiten und gemeinschaftliche Anlagen; § 41: Wege- und Gewässerplan mit landschaftspflegerischem Begleitplan (Kern von `sn_plan41`) |
| **Plan 41 / Plan41** | Wege- und Gewässerplan nach § 41 FlurbG; Maßnahmen (Linien/Flächen/Punkte) mit Konten, Ausbauarten, Akteuren |
| **Verfahren** | Ein Flurbereinigungsverfahren; identifiziert über die **VKZ** |
| **VKZ / Verfahrensnummer** | Verfahrenskennzahl (sechsstellig), Projektvariable `sn_verfahrensnummer` |
| **Verfahrensgebiet (VG)** | Flächenumring des Verfahrens; zentraler Layer, dessen ID in `sn_verfahrensgebiet_layer` gehalten wird |
| **ALKIS** | Amtliches Liegenschaftskatasterinformationssystem; Quelle für Flurstücke, Gemarkungen, Grenzpunkte über WFS |
| **BROR / BROBOR** | ALKIS-Objektart `BauRaumOderBodenordnungsrecht`; enthält Verfahrensgebiete als Objekte; Suche per VKZ oder Landkreis |
| **Gemarkung** | Katasterbezirk; Flurstücke werden gemarkungsweise abgerufen (Gemarkungsschlüssel) |
| **Flurstück (Flst)** | Katasterparzelle; Flurstückskennzeichen (mit Länderkennung) |
| **PunktortTA** | ALKIS-Punktorte (Grenzpunkte) im Tatsächlichen Ausgleich; Basis der Grenzpunkt- und Knickpunktprüfung |
| **Knickpunkt** | Eckpunkt des Verfahrensgebietsumrings; Prüfung, ob zu jedem Knickpunkt ein Grenzpunkt existiert |
| **NoBIS** | Fachverfahren (Neuordnungs-Bodenordnungs-Informationssystem); liefert Shape-Exporte mit `FBSchl` |
| **FBSchl** | Fachbedeutungsschlüssel im NoBIS-Shape: `901/10110F` Neuordnungsgebiet, `901/10120F` Neuvermessungsgebiet |
| **Neuordnungsgebiet (NO)** | Gebiet der Bodenordnung (Flurstücksbeteiligung maßgeblich) |
| **Neuvermessungsgebiet (NV)** | Gebiet, in dem Grenzen neu vermessen werden |
| **Beteiligung / Beteiligungsprüfung** | Abgleich der im BI-Server geführten beteiligten Flurstücke mit ALKIS |
| **BI-Server** | Berichtssystem, aus dem CSV-Exporte der Beteiligten kommen (Spalten `FLA_Verfahren-VEF_VKZF`, `FLA-Flurstuecke-Flurstueck`, `FLA_Beteiligung`) |
| **Linkliste** | Katalog externer Fachdienste (LUIS, RAPIS, …) für Plan41; zentral als CSV, im Plugin als XLSX |
| **LUIS** | Landesumweltinformationssystem Sachsen (ArcGIS-REST-Dienste) |
| **RAPIS** | Raumplanungsinformationssystem Sachsen |
| **VLN-API / VLN-Manager** | Zentrale Karten-API (`api.flurneuordnung-sachsen.de/v2`) zum Austausch von Plan41-Daten je VKZ |
| **Verfahrens-DB** | Verfahrens-GeoPackage, in das Fachdaten geschrieben werden (Projektvariable `sn_verfahrens_db`) |
| **Fachschale** | Begriff des Alternativansatzes: versioniertes GeoPackage mit definiertem Layerschema |
| **Baufreigabe** | Drucklayout „Anlage zur Baufreigabe“ (DIN A3/A4 quer) |
| **Atlas** | QGIS-Serienkarte über einen Abdeckungslayer (`sn_abdeckungslayer`) |
| **Kanal** | Release-Kanal: stable, testing, unstable (QGIS-Plugin-Feeds in `Repository`) |
| **Mock-Modus** | Betrieb der Wrapper ohne QGIS/Qt für Tests |
| **AG QGIS** | Arbeitsgruppe QGIS der sächsischen Flurbereinigungsbehörden; Gitea-Organisation `AG_QGIS` |
+623
View File
@@ -0,0 +1,623 @@
#!/usr/bin/env python3
"""Lesender Zugriff auf das Gitea-Ticketsystem der Organisation aus workspace.toml.
Fragt die öffentlich lesbare Gitea-API ab (kein Token, keine Anmeldung, keine
Installation nur Standardbibliothek) und gibt Issues, Pull Requests und
Suchtreffer token-sparend für KI-Agenten und Entwickler aus. Läuft auf Linux,
macOS und Windows mit jedem Python >= 3.11.
WICHTIGE PROJEKTREGEL: Agenten dürfen auf dem Gitea-Server nichts verändern.
Dieses Skript stellt ausschließlich GET-Anfragen. Schreibende Aktionen (neues
Issue, neuer Kommentar) werden nur als Entwurfsdatei unter entwuerfe/ vorbereitet.
Ein Mensch prüft den Text und sendet ihn selbst über die Weboberfläche ab.
Aufrufe (aus dem Workspace-Root):
python scripts/gitea.py tickets # offene Issues ALLER Repos
python scripts/gitea.py tickets Basis # nur ein Repo (Kurzname genügt)
python scripts/gitea.py tickets --geschlossen # geschlossene statt offene
python scripts/gitea.py tickets --label Bug # nach Label filtern
python scripts/gitea.py ticket Basis 48 # ein Issue mit allen Kommentaren
python scripts/gitea.py suche "FBSchl" # Volltext über Titel, Text und Kommentare,
# offene und geschlossene Tickets
python scripts/gitea.py pulls # offene Pull Requests
python scripts/gitea.py entwurf issue Basis # Entwurfsdatei anlegen
python scripts/gitea.py entwurf kommentar Basis 48 # Entwurfsdatei für einen Kommentar
Exit-Code: 0 = in Ordnung, 1 = Gitea nicht erreichbar / nicht gefunden,
2 = falsche Angabe (Repo unbekannt oder mehrdeutig).
"""
from __future__ import annotations
import argparse
import json
import shutil
import tomllib
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "workspace.toml"
ENTWURFSORDNER = ROOT / "entwuerfe"
VORLAGE_ISSUE = ROOT / "docs" / "vorlagen" / "issue.md"
BENUTZERKENNUNG = "AG_QGIS-Projektwissen/1.0"
ZEITLIMIT = 20 # Sekunden je Anfrage
SEITENGROESSE = 50 # Gitea liefert sonst nur wenige Einträge je Seite
MAX_SEITEN = 20 # Sicherheitsgrenze gegen Endlosschleifen
LEER = "-"
class GiteaFehler(Exception):
"""Netz-, HTTP- oder Formatfehler beim Zugriff auf die API (Exit-Code 1)."""
class NichtGefunden(GiteaFehler):
"""HTTP 404: Repo oder Issue existiert nicht bzw. hat kein Ticketsystem."""
class EingabeFehler(Exception):
"""Unbekannter oder mehrdeutiger Repo-Name (Exit-Code 2)."""
# ---------------------------------------------------------------- Manifest ---
def lade_manifest() -> dict:
with MANIFEST.open("rb") as f:
return tomllib.load(f)
def zugangsdaten(manifest: dict) -> tuple[str, str]:
"""Basis-URL und Organisation aus [workspace] nie hart kodieren."""
ws = manifest.get("workspace", {})
basis = str(ws.get("gitea", "")).rstrip("/")
org = str(ws.get("org", ""))
if not basis or not org:
raise GiteaFehler("In workspace.toml fehlen [workspace] gitea oder org.")
return basis, org
def repo_namen(manifest: dict) -> list[str]:
return [r["name"] for r in manifest.get("repos", []) if r.get("name")]
def loese_repo(kurzname: str, namen: list[str]) -> str:
"""Löst 'Basis' zu 'Plugin_SN_Basis' auf.
Reihenfolge: exakter Name gewinnt, dann Groß-/Kleinschreibung egal, dann
eindeutige Übereinstimmung am Namensende, dann eindeutiger Teilstring.
"""
if kurzname in namen:
return kurzname
klein = kurzname.lower()
for pruefung in (
lambda n: n.lower() == klein,
lambda n: n.lower().endswith(klein),
lambda n: klein in n.lower(),
):
treffer = [n for n in namen if pruefung(n)]
if len(treffer) == 1:
return treffer[0]
if len(treffer) > 1:
raise EingabeFehler(
f"Repo-Name '{kurzname}' ist mehrdeutig. Gemeint sein könnte: "
+ ", ".join(treffer)
)
raise EingabeFehler(
f"Repo '{kurzname}' steht nicht in workspace.toml. Bekannt: " + ", ".join(namen)
)
# ------------------------------------------------------------------- API ------
def baue_url(basis: str, pfad: str, parameter: dict[str, str] | None = None) -> str:
url = f"{basis}/api/v1{pfad}"
if parameter:
url += "?" + urllib.parse.urlencode(parameter)
return url
def hole_json(url: str) -> object:
"""Eine einzelne GET-Anfrage. Schreibende Methoden gibt es hier bewusst nicht."""
anfrage = urllib.request.Request(
url, method="GET",
headers={"User-Agent": BENUTZERKENNUNG, "Accept": "application/json"},
)
try:
with urllib.request.urlopen(anfrage, timeout=ZEITLIMIT) as antwort:
roh = antwort.read()
except urllib.error.HTTPError as fehler:
if fehler.code == 404:
raise NichtGefunden("Repo/Issue nicht gefunden: " + url) from None
raise GiteaFehler(
f"Gitea antwortet mit HTTP {fehler.code} ({fehler.reason}) auf {url}"
) from None
except TimeoutError:
raise GiteaFehler(
f"Gitea nicht erreichbar: Zeitlimit von {ZEITLIMIT} s überschritten. "
"Netzverbindung oder Proxy prüfen."
) from None
except urllib.error.URLError as fehler:
raise GiteaFehler(
f"Gitea nicht erreichbar: {fehler.reason}. Netzverbindung oder Proxy prüfen."
) from None
except OSError as fehler:
raise GiteaFehler(
f"Gitea nicht erreichbar: {fehler}. Netzverbindung oder Proxy prüfen."
) from None
try:
return json.loads(roh.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
raise GiteaFehler(f"Unerwartete Antwort (kein JSON) von {url}") from None
def hole_liste(basis: str, pfad: str, parameter: dict[str, str]) -> list[dict]:
"""Blättert über die Seiten, bis eine leere oder unvollständige Seite kommt."""
ergebnisse: list[dict] = []
for seite in range(1, MAX_SEITEN + 1):
abfrage = dict(parameter, limit=str(SEITENGROESSE), page=str(seite))
daten = hole_json(baue_url(basis, pfad, abfrage))
if not isinstance(daten, list):
# Gitea liefert bei deaktiviertem Ticketsystem ein Objekt statt einer Liste.
raise NichtGefunden("Repo/Issue nicht gefunden: " + baue_url(basis, pfad, abfrage))
ergebnisse.extend(daten)
if len(daten) < SEITENGROESSE:
break
else:
print(f"Hinweis: Sicherheitsgrenze von {MAX_SEITEN} Seiten erreicht Liste evtl. unvollständig.")
return ergebnisse
def hole_issues(basis: str, org: str, repo: str, state: str, art: str = "issues") -> list[dict]:
"""Issues EINES Repos. art='issues' blendet Pull Requests aus, 'pulls' zeigt nur sie."""
pfad = f"/repos/{urllib.parse.quote(org)}/{urllib.parse.quote(repo)}/issues"
try:
return hole_liste(basis, pfad, {"state": state, "type": art})
except NichtGefunden:
raise NichtGefunden(
f"Repo/Issue nicht gefunden: {org}/{repo} Repo existiert nicht oder "
"hat auf Gitea kein Ticketsystem."
) from None
def hole_issues_org(basis: str, org: str, state: str, art: str = "issues",
suchtext: str | None = None) -> list[dict]:
"""Issues ALLER Repos der Organisation über den Sammelendpunkt.
Spart pro Aufruf zehn Einzelabfragen; Repos ohne Ticketsystem fallen dabei
automatisch weg. Mit suchtext sucht Gitea serverseitig (Titel, Text und
Kommentare).
"""
parameter = {"owner": org, "state": state, "type": art}
if suchtext:
parameter["q"] = suchtext
return hole_liste(basis, "/repos/issues/search", parameter)
def sammle_issues(basis: str, org: str, repo: str | None, namen: list[str], state: str,
art: str = "issues", suchtext: str | None = None) -> list[tuple[str, list[dict]]]:
"""Holt die Einträge und gruppiert sie in der Reihenfolge aus workspace.toml."""
if suchtext:
# Auch für ein einzelnes Repo über den Sammelendpunkt: nur er durchsucht
# zusätzlich die Kommentare.
eintraege = hole_issues_org(basis, org, state, art, suchtext)
if repo:
eintraege = [e for e in eintraege
if (e.get("repository") or {}).get("name") == repo]
elif repo:
eintraege = hole_issues(basis, org, repo, state, art)
else:
eintraege = hole_issues_org(basis, org, state, art)
return gruppiere_nach_repo(eintraege, namen)
def gruppiere_nach_repo(eintraege: list[dict], namen: list[str]) -> list[tuple[str, list[dict]]]:
nach_repo: dict[str, list[dict]] = {}
for eintrag in eintraege:
name = (eintrag.get("repository") or {}).get("name") or "(unbekanntes Repo)"
nach_repo.setdefault(name, []).append(eintrag)
reihenfolge = [n for n in namen if n in nach_repo]
reihenfolge += sorted(n for n in nach_repo if n not in namen)
return [(n, nach_repo[n]) for n in reihenfolge]
def enthaelt(eintrag: dict, suchtext: str) -> bool:
"""Lokaler Volltextvergleich über Titel und Text (ohne Kommentare)."""
klein = suchtext.lower()
return klein in (eintrag.get("title") or "").lower() or klein in (eintrag.get("body") or "").lower()
def hole_kommentare(basis: str, org: str, repo: str, nummer: int) -> list[dict]:
pfad = f"/repos/{urllib.parse.quote(org)}/{urllib.parse.quote(repo)}/issues/{nummer}/comments"
return hole_liste(basis, pfad, {})
def hole_issue(basis: str, org: str, repo: str, nummer: int) -> dict:
pfad = f"/repos/{urllib.parse.quote(org)}/{urllib.parse.quote(repo)}/issues/{nummer}"
try:
daten = hole_json(baue_url(basis, pfad))
except NichtGefunden:
raise NichtGefunden(f"Repo/Issue nicht gefunden: {repo} #{nummer}") from None
if not isinstance(daten, dict) or "number" not in daten:
raise NichtGefunden(f"Repo/Issue nicht gefunden: {repo} #{nummer}")
return daten
# ------------------------------------------------------------- Formatierung ---
def terminalbreite(vorgabe: int | None = None) -> int:
breite = vorgabe or shutil.get_terminal_size((100, 24)).columns
return max(60, min(breite, 200))
def kuerze(text: str, breite: int) -> str:
text = " ".join((text or "").split())
if len(text) <= breite:
return text
return text[: max(1, breite - 1)] + ""
def zeitpunkt(wert: str | None) -> datetime | None:
if not wert:
return None
try:
return datetime.fromisoformat(wert.replace("Z", "+00:00"))
except ValueError:
return None
def datum(wert: str | None) -> str:
zeit = zeitpunkt(wert)
return zeit.strftime("%Y-%m-%d") if zeit else LEER
def datum_zeit(wert: str | None) -> str:
zeit = zeitpunkt(wert)
return zeit.strftime("%Y-%m-%d %H:%M") if zeit else LEER
def alter_tage(wert: str | None) -> int:
zeit = zeitpunkt(wert)
if not zeit:
return 0
jetzt = datetime.now(timezone.utc)
return max(0, (jetzt - zeit.astimezone(timezone.utc)).days)
def labelnamen(eintrag: dict) -> list[str]:
return [l.get("name", "") for l in (eintrag.get("labels") or [])]
def zuweisung(eintrag: dict) -> list[str]:
namen = [a.get("login", "") for a in (eintrag.get("assignees") or [])]
if not namen and eintrag.get("assignee"):
namen = [eintrag["assignee"].get("login", "")]
return [n for n in namen if n]
def mehrzahl(anzahl: int, einzahl: str, mehrzahlform: str) -> str:
return f"{anzahl} {einzahl if anzahl == 1 else mehrzahlform}"
def anzahl_tickets(anzahl: int, geschlossen: bool, pull: bool = False) -> str:
"""Zählbare Wortform für die Summenzeile, z.B. '1 offenes Ticket'."""
if pull:
wort = ("offener Pull Request", "offene Pull Requests")
if geschlossen:
wort = ("geschlossener Pull Request", "geschlossene Pull Requests")
elif geschlossen:
wort = ("geschlossenes Ticket", "geschlossene Tickets")
else:
wort = ("offenes Ticket", "offene Tickets")
return mehrzahl(anzahl, *wort)
def zustandskuerzel(eintrag: dict) -> str:
return "[offen]" if eintrag.get("state") == "open" else "[geschl.]"
def autor(eintrag: dict) -> str:
nutzer = eintrag.get("user") or {}
login = nutzer.get("login", "unbekannt")
voll = (nutzer.get("full_name") or "").strip()
return f"{voll} ({login})" if voll and voll != login else login
def tabellenkopf(breiten: dict[str, int]) -> str:
return (" " + "Nr".rjust(breiten["nr"])
+ " " + "Titel".ljust(breiten["titel"])
+ " " + "Labels".ljust(breiten["labels"])
+ " " + "Zugewiesen".ljust(breiten["zug"])
+ " " + "Alter".rjust(breiten["alter"]))
def spaltenbreiten(breite: int, eintraege: list[dict]) -> dict[str, int]:
nr = max(4, max((len(str(e.get("number", 0))) + 1 for e in eintraege), default=4))
labels, zug, alter = 16, 11, 5
titel = breite - (nr + labels + zug + alter + 10)
return {"nr": nr, "titel": max(20, titel), "labels": labels, "zug": zug, "alter": alter}
def tabellenzeile(eintrag: dict, breiten: dict[str, int], zusatz: str = "") -> str:
zeile = (" " + ("#" + str(eintrag.get("number", 0))).rjust(breiten["nr"])
+ " " + kuerze(eintrag.get("title", ""), breiten["titel"]).ljust(breiten["titel"])
+ " " + kuerze(", ".join(labelnamen(eintrag)) or LEER, breiten["labels"]).ljust(breiten["labels"])
+ " " + kuerze(", ".join(zuweisung(eintrag)) or LEER, breiten["zug"]).ljust(breiten["zug"])
+ " " + f"{alter_tage(eintrag.get('created_at'))}T".rjust(breiten["alter"]))
return zeile + ("\n" + " " * (breiten["nr"] + 4) + zusatz if zusatz else "")
def drucke_gruppen(gruppen: list[tuple[str, list[dict]]], breite: int, leermeldung: str,
zusatz=None) -> tuple[int, int]:
"""Gibt die nach Repo gruppierte Tabelle aus; Spaltenkopf nur einmal."""
gruppen = [(repo, eintraege) for repo, eintraege in gruppen if eintraege]
if not gruppen:
print("\n" + leermeldung)
return 0, 0
breiten = spaltenbreiten(breite, [e for _, eintraege in gruppen for e in eintraege])
print(tabellenkopf(breiten))
gesamt = 0
for repo, eintraege in gruppen:
gesamt += len(eintraege)
print(f"\n{repo} ({len(eintraege)})")
for eintrag in sorted(eintraege, key=lambda e: -e.get("number", 0)):
print(tabellenzeile(eintrag, breiten, zusatz(eintrag, breiten) if zusatz else ""))
return gesamt, len(gruppen)
def passt_zum_label(eintrag: dict, label: str | None) -> bool:
if not label:
return True
return any(label.lower() == name.lower() for name in labelnamen(eintrag))
# ---------------------------------------------------------------- Befehle -----
def befehl_tickets(basis: str, org: str, repo: str | None, namen: list[str],
args: argparse.Namespace) -> int:
state = "closed" if args.geschlossen else "open"
bezeichnung = "geschlossene" if args.geschlossen else "offene"
gefunden = sammle_issues(basis, org, repo, namen, state)
breite = terminalbreite(args.breite)
gruppen = [(name, [i for i in issues if passt_zum_label(i, args.label)])
for name, issues in gefunden]
filter_text = f", Label '{args.label}'" if args.label else ""
print(f"{org} {bezeichnung} Tickets{filter_text} (Stand {datetime.now():%Y-%m-%d})")
gesamt, repos_mit = drucke_gruppen(gruppen, breite, "Keine passenden Tickets gefunden.")
print(f"\nSumme: {anzahl_tickets(gesamt, args.geschlossen)} in {repos_mit} von "
f"{mehrzahl(1 if repo else len(namen), 'Repo', 'Repos')} | Alter in Tagen"
f" | Details: python scripts/gitea.py ticket <Repo> <Nr>")
return 0
def befehl_ticket(basis: str, org: str, repo: str, nummer: int) -> int:
eintrag = hole_issue(basis, org, repo, nummer)
kommentare = hole_kommentare(basis, org, repo, nummer)
art = "Pull Request" if eintrag.get("pull_request") else "Issue"
status = "offen" if eintrag.get("state") == "open" else "geschlossen"
print(f"# {repo} #{eintrag['number']} {eintrag.get('title', '')}\n")
print(f"- Art: {art}")
print(f"- Status: {status}" + (f" (geschlossen am {datum(eintrag.get('closed_at'))})"
if eintrag.get("closed_at") else ""))
print(f"- Autor: {autor(eintrag)}")
print(f"- Angelegt: {datum_zeit(eintrag.get('created_at'))}"
f" (vor {alter_tage(eintrag.get('created_at'))} Tagen)"
f", zuletzt geändert: {datum_zeit(eintrag.get('updated_at'))}")
print(f"- Labels: {', '.join(labelnamen(eintrag)) or 'keine'}")
print(f"- Zugewiesen: {', '.join(zuweisung(eintrag)) or 'niemandem'}")
if eintrag.get("milestone"):
print(f"- Meilenstein: {eintrag['milestone'].get('title', '')}")
print(f"- URL: {eintrag.get('html_url', '')}\n")
print("## Beschreibung\n")
print((eintrag.get("body") or "").strip() or "_(kein Text)_")
print(f"\n## Kommentare ({len(kommentare)})")
if not kommentare:
print("\n_(keine)_")
for nr, kommentar in enumerate(kommentare, start=1):
print(f"\n### {nr}. {autor(kommentar)} {datum_zeit(kommentar.get('created_at'))}\n")
print((kommentar.get("body") or "").strip() or "_(kein Text)_")
return 0
def fundstelle(eintrag: dict, begriff: str, breite: int) -> str:
"""Kurzer Textausschnitt um die erste Fundstelle im Text."""
text = " ".join((eintrag.get("body") or "").split())
stelle = text.lower().find(begriff.lower())
if stelle < 0:
if begriff.lower() in (eintrag.get("title") or "").lower():
return "(Treffer im Titel)"
return "(Treffer in einem Kommentar)"
start = max(0, stelle - breite // 3)
ausschnitt = text[start : start + breite]
return ("" if start else "") + ausschnitt + ("" if start + breite < len(text) else "")
def befehl_suche(basis: str, org: str, repo: str | None, namen: list[str],
args: argparse.Namespace) -> int:
# Gesucht wird standardmäßig in offenen UND geschlossenen Tickets: die Antwort
# auf eine Fachfrage steht oft in einem längst geschlossenen Ticket.
state = "open" if args.offen else ("closed" if args.geschlossen else "all")
gefunden = sammle_issues(basis, org, repo, namen, state, suchtext=args.begriff)
if not any(issues for _, issues in gefunden):
# Zweiter Versuch ohne Server-Suche: Gitea findet je nach Index nicht jeden Teilstring.
gefunden = [(name, [i for i in issues if enthaelt(i, args.begriff)])
for name, issues in sammle_issues(basis, org, repo, namen, state)]
breite = terminalbreite(args.breite)
gruppen = [(name, [i for i in issues if passt_zum_label(i, args.label)])
for name, issues in gefunden]
def ausschnitt(eintrag: dict, breiten: dict[str, int]) -> str:
zustand = "" if state != "all" else zustandskuerzel(eintrag) + " "
return zustand + fundstelle(eintrag, args.begriff, breite - breiten["nr"] - 10)
zustaende = {"open": "nur offene", "closed": "nur geschlossene", "all": "offene und geschlossene"}
print(f"Suche nach '{args.begriff}' in {org} ({zustaende[state]} Tickets,"
f" Titel, Text und Kommentare)")
gesamt, _ = drucke_gruppen(gruppen, breite, "Keine Treffer.", ausschnitt)
print(f"\nSumme: {gesamt} Treffer | Details: python scripts/gitea.py ticket <Repo> <Nr>")
return 0
def befehl_pulls(basis: str, org: str, repo: str | None, namen: list[str],
args: argparse.Namespace) -> int:
state = "closed" if args.geschlossen else "open"
bezeichnung = "geschlossene" if args.geschlossen else "offene"
gefunden = sammle_issues(basis, org, repo, namen, state, art="pulls")
breite = terminalbreite(args.breite)
gruppen = [(name, [p for p in pulls if passt_zum_label(p, args.label)])
for name, pulls in gefunden]
print(f"{org} {bezeichnung} Pull Requests (Stand {datetime.now():%Y-%m-%d})")
gesamt, _ = drucke_gruppen(gruppen, breite, "Keine passenden Pull Requests gefunden.",
lambda eintrag, breiten: f"von {autor(eintrag)}")
print(f"\nSumme: {anzahl_tickets(gesamt, args.geschlossen, pull=True)} in "
f"{mehrzahl(1 if repo else len(namen), 'Repo', 'Repos')}")
return 0
def befehl_entwurf(basis: str, org: str, repo: str, art: str, nummer: int | None) -> int:
"""Legt eine Entwurfsdatei an. Abgeschickt wird sie ausschließlich von Hand."""
zeitstempel = datetime.now().strftime("%Y%m%d-%H%M%S")
web = f"{basis}/{org}/{repo}"
if art == "issue":
ziel = ENTWURFSORDNER / f"{zeitstempel}-issue-{repo}.md"
kopf = f"# Entwurf: neues Issue in {org}/{repo}"
vorlage = (VORLAGE_ISSUE.read_text(encoding="utf-8").strip()
if VORLAGE_ISSUE.exists()
else "## Ziel\n<Was soll am Ende möglich sein?>\n\n## Abnahmekriterien\n- [ ] <…>")
quelle = ("docs/vorlagen/issue.md" if VORLAGE_ISSUE.exists()
else "Notvorlage (docs/vorlagen/issue.md fehlt)")
koerper = f"Titel: <Kurzer, sprechender Titel>\n\n{vorlage}"
absenden = (f"1. Seite öffnen: {web}/issues/new\n"
"2. Titelzeile und Text unterhalb von `--- ENTWURF ---` von Hand einfügen"
" (die Zeile `Titel:` selbst nicht mitkopieren).\n"
"3. Labels, Zuweisung und Meilenstein setzen und das Issue anlegen.")
hinweis = f"Vorlage: {quelle}"
else:
eintrag = hole_issue(basis, org, repo, int(nummer))
ziel = ENTWURFSORDNER / f"{zeitstempel}-kommentar-{repo}-{nummer}.md"
kopf = f"# Entwurf: Kommentar zu {org}/{repo} #{nummer}"
koerper = "<Kommentartext hier eintragen>"
absenden = (f"1. Seite öffnen: {web}/issues/{nummer}\n"
"2. Text unterhalb von `--- ENTWURF ---` in das Kommentarfeld einfügen.\n"
"3. Kommentar abschicken.")
hinweis = (f"Ticket: #{nummer} {eintrag.get('title', '')}"
f" (Status: {'offen' if eintrag.get('state') == 'open' else 'geschlossen'})")
inhalt = "\n".join([
kopf,
"",
f"- Ziel-Repo: {org}/{repo} ({web})",
f"- {hinweis}",
f"- Angelegt: {datetime.now():%Y-%m-%d %H:%M} von scripts/gitea.py",
"",
"> **Dieser Entwurf wird NICHT automatisch abgeschickt.** KI-Agenten verändern auf",
"> Gitea nichts. Ein Mensch prüft den Text und stellt ihn selbst über die",
"> Weboberfläche ein.",
"",
"--- ENTWURF ---",
"",
koerper,
"",
"--- ENDE ENTWURF ---",
"",
"## Absenden (von Hand)",
"",
absenden,
"",
])
ENTWURFSORDNER.mkdir(parents=True, exist_ok=True)
ziel.write_text(inhalt, encoding="utf-8")
print(f"Entwurf angelegt: {ziel.relative_to(ROOT)}")
print("Bitte prüfen, ergänzen und selbst auf Gitea einstellen das Skript sendet nichts.")
return 0
# ------------------------------------------------------------------ main ------
def main() -> int:
breite_eltern = argparse.ArgumentParser(add_help=False)
breite_eltern.add_argument("--breite", type=int, help="Terminalbreite erzwingen (Standard: automatisch)")
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
unter = ap.add_subparsers(dest="befehl", required=True)
p = unter.add_parser("tickets", parents=[breite_eltern], help="Issues auflisten (Standard: alle Repos, offen)")
p.add_argument("repo", nargs="?", help="Repo oder Kurzname (z.B. Basis); ohne Angabe: alle")
p.add_argument("--geschlossen", action="store_true", help="geschlossene statt offener Tickets")
p.add_argument("--label", help="nur Tickets mit diesem Label (z.B. Bug)")
p = unter.add_parser("ticket", help="ein Issue mit allen Kommentaren als Markdown")
p.add_argument("repo", help="Repo oder Kurzname (z.B. Basis)")
p.add_argument("nummer", type=int, help="Issue-Nummer")
p = unter.add_parser("suche", parents=[breite_eltern], help="Volltextsuche über Titel und Text")
p.add_argument("begriff", help="Suchbegriff (Groß-/Kleinschreibung egal)")
p.add_argument("repo", nargs="?", help="Repo oder Kurzname; ohne Angabe: alle")
p.add_argument("--offen", action="store_true", help="nur offene Tickets (Standard: offene und geschlossene)")
p.add_argument("--geschlossen", action="store_true", help="nur geschlossene Tickets")
p.add_argument("--label", help="zusätzlich nach Label filtern")
p = unter.add_parser("pulls", parents=[breite_eltern], help="Pull Requests auflisten")
p.add_argument("repo", nargs="?", help="Repo oder Kurzname; ohne Angabe: alle")
p.add_argument("--geschlossen", action="store_true", help="geschlossene statt offener PRs")
p.add_argument("--label", help="nur PRs mit diesem Label")
p = unter.add_parser("entwurf", help="Entwurfsdatei für einen Menschen vorbereiten (sendet nichts)")
p.add_argument("art", choices=["issue", "kommentar"], help="Art des Entwurfs")
p.add_argument("repo", help="Repo oder Kurzname (z.B. Basis)")
p.add_argument("nummer", nargs="?", type=int, help="Issue-Nummer (nur bei 'kommentar')")
args = ap.parse_args()
try:
manifest = lade_manifest()
basis, org = zugangsdaten(manifest)
namen = repo_namen(manifest)
if not namen:
raise GiteaFehler("workspace.toml enthält keine [[repos]]-Einträge.")
if args.befehl == "entwurf" and args.art == "kommentar" and args.nummer is None:
print("Für 'entwurf kommentar' wird die Issue-Nummer gebraucht, "
"z.B. python scripts/gitea.py entwurf kommentar Basis 48")
return 2
repo = loese_repo(args.repo, namen) if getattr(args, "repo", None) else None
if args.befehl == "tickets":
return befehl_tickets(basis, org, repo, namen, args)
if args.befehl == "ticket":
return befehl_ticket(basis, org, repo, args.nummer)
if args.befehl == "suche":
return befehl_suche(basis, org, repo, namen, args)
if args.befehl == "pulls":
return befehl_pulls(basis, org, repo, namen, args)
if args.befehl == "entwurf":
return befehl_entwurf(basis, org, repo, args.art, args.nummer)
except EingabeFehler as fehler:
print(fehler)
return 2
except NichtGefunden as fehler:
print(str(fehler))
return 1
except GiteaFehler as fehler:
print(str(fehler))
return 1
except KeyboardInterrupt:
print("Abgebrochen.")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+391
View File
@@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""Sitzungsbeginn: zeigt den Stand aller Klone und aktualisiert die Kanal-Branches.
Für jedes Repository aus workspace.toml und für den Workspace-Ordner selbst, sofern
er ein Git-Repository ist holt das Skript den Stand vom Server (git fetch --prune),
ermittelt Branch, ungesicherte Änderungen und den Abstand zu origin und macht genau
dann einen fast-forward-Pull, wenn das gefahrlos ist:
* das Arbeitsverzeichnis ist sauber (git status --porcelain leer),
* der aktuelle Branch ist der Kanal-Branch aus workspace.toml,
* es gibt einen Upstream,
* wir sind hinter origin und haben keine eigenen Commits davor.
In allen anderen Fällen wird nur berichtet und nichts verändert. Am Ende stehen eine
Tabelle (eine Zeile je Repository) und, falls nötig, konkrete Handlungsempfehlungen.
Absolut verboten und deshalb nirgends im Code enthalten:
git merge, git rebase, git stash, git reset, git checkout/switch auf einen anderen
Branch, git push, git tag sowie jedes Verwerfen von Änderungen.
Ungesicherte oder unversionierte Dateien werden nie angefasst. Die einzige schreibende
Git-Operation des Skripts ist `git pull --ff-only` unter den vier Bedingungen oben.
Aufrufe (aus dem Workspace-Root):
python scripts/hallo.py # fetch + fast-forward-Pull + Bericht
python scripts/hallo.py --nur-pruefen # fetch + Bericht, kein Pull
python scripts/hallo.py --ohne-netz # nur lokaler Stand, kein fetch
python scripts/hallo.py --repo Plugin_SN_Basis --repo Repository
Der Kanal-Branch des Workspace-Repos selbst kann in workspace.toml unter
[workspace] branch = "..." gesetzt werden; ohne Eintrag gilt sein aktueller Branch.
Exit-Code: 0 = nichts Kritisches, 1 = ein Repo-Ordner fehlt oder ein Git-Befehl ist
unerwartet gescheitert, 2 = Aufruffehler (unbekanntes Repo, workspace.toml unlesbar).
"""
from __future__ import annotations
import argparse
import os
import subprocess
import textwrap
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "workspace.toml"
BREITE = 100 # Zielbreite der Tabelle (lesbar im 100-Zeichen-Terminal)
FETCH_TIMEOUT = 60 # Sekunden je git fetch, damit ein hängender Server nicht blockiert
# Git ohne interaktive Rückfragen: sonst wartet ein fetch ewig auf eine Passworteingabe.
GIT_UMGEBUNG = dict(os.environ, GIT_TERMINAL_PROMPT="0", GCM_INTERACTIVE="never")
def git(*args: str, cwd: Path, timeout: int | None = None) -> tuple[int, str]:
"""Führt git aus und liefert (Rückgabecode, Ausgabe). Wirft nie eine Ausnahme."""
try:
p = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True,
errors="replace", env=GIT_UMGEBUNG, timeout=timeout)
except FileNotFoundError:
return 127, "git wurde nicht gefunden (steht git im PATH?)"
except subprocess.TimeoutExpired:
return 124, f"Zeitüberschreitung nach {timeout} s"
return p.returncode, (p.stdout + p.stderr).strip()
def kurz(text: str, breite: int = 70) -> str:
"""Letzte, gekürzte Zeile einer Git-Ausgabe für einzeilige Meldungen."""
zeilen = [z for z in text.splitlines() if z.strip()]
letzte = zeilen[-1].strip() if zeilen else ""
return letzte if len(letzte) <= breite else letzte[: breite - 1] + ""
def kuerze(text: str, breite: int) -> str:
return text if len(text) <= breite else text[: max(1, breite - 1)] + "~"
def umbrechen(zeile: str) -> str:
"""Bricht Fließtext auf BREITE um; Befehlszeilen (vier Leerzeichen Einzug) bleiben ganz."""
if zeile.startswith(" ") or len(zeile) <= BREITE:
return zeile
einzug = " " * (len(zeile) - len(zeile.lstrip()))
return textwrap.fill(zeile.strip(), width=BREITE, initial_indent=einzug,
subsequent_indent=einzug)
def commits(anzahl: int) -> str:
return f"{anzahl} Commit" if anzahl == 1 else f"{anzahl} Commits"
def aenderungen(anzahl: int) -> str:
return f"{anzahl} ungesicherte Änderung" if anzahl == 1 else f"{anzahl} ungesicherte Änderungen"
@dataclass
class Befund:
"""Zustand eines Klons wird erst gefüllt, dann als Tabellenzeile ausgegeben."""
name: str
kanal: str
pfad: Path
ist_wurzel: bool = False
vorhanden: bool = True
lesbar: bool = True
branch: str = "-"
geaendert: int = 0
hinter: int = 0
vor: int = 0
upstream: bool = False
remote: bool = True
offline: bool = False
aktualisiert: bool = False
zustand: str = "-"
aktion: str = "-"
kritisch: bool = False
hinweise: list[str] = field(default_factory=list)
def befehl(self, rest: str) -> str:
"""Kopierbarer Git-Befehl, aufzurufen aus dem Workspace-Root."""
return f"git {rest}" if self.ist_wurzel else f"git -C {self.name} {rest}"
@property
def abstand(self) -> str:
return "-" if not (self.vorhanden and self.upstream) else f"{self.hinter}/{self.vor}"
def zustand_ermitteln(b: Befund) -> str:
"""Ein Klartextwort für die Spalte Zustand (ohne Emoji, auch für KI-Agenten lesbar)."""
if b.geaendert:
return f"geaendert ({b.geaendert})"
if b.branch == "HEAD":
return "detached HEAD"
if b.branch != b.kanal:
return "fremder branch"
if not b.upstream:
return "kein upstream"
if b.offline:
return "offline"
return "sauber"
def einlesen(b: Befund, ohne_netz: bool) -> None:
"""Nur lesende Git-Befehle: fetch, rev-parse, status, rev-list."""
if not b.pfad.is_dir():
b.vorhanden, b.zustand, b.aktion, b.kritisch = False, "fehlt", "Ordner fehlt", True
return
if not (b.pfad / ".git").exists():
b.vorhanden, b.zustand, b.aktion, b.kritisch = False, "kein git-repo", "Ordner ohne .git", True
return
_, remotes = git("remote", cwd=b.pfad)
b.remote = bool(remotes.strip())
if not b.remote:
b.hinweise.append("kein Remote konfiguriert kein Abgleich mit origin möglich.")
elif not ohne_netz:
rc, ausgabe = git("fetch", "--prune", "--quiet", cwd=b.pfad, timeout=FETCH_TIMEOUT)
if rc != 0:
b.offline = True
b.hinweise.append(f"origin nicht erreichbar: {kurz(ausgabe)}")
rc, branch = git("rev-parse", "--abbrev-ref", "HEAD", cwd=b.pfad)
if rc != 0:
b.lesbar, b.zustand, b.aktion, b.kritisch = False, "git-fehler", f"FEHLER: {kurz(branch, 30)}", True
b.hinweise.append(f"git rev-parse fehlgeschlagen: {kurz(branch)}")
return
b.branch = branch
if not b.kanal: # Workspace-Repo ohne Vorgabe in workspace.toml
b.kanal = branch
rc, status = git("status", "--porcelain", cwd=b.pfad)
if rc != 0:
b.lesbar, b.zustand, b.aktion, b.kritisch = False, "git-fehler", f"FEHLER: {kurz(status, 30)}", True
b.hinweise.append(f"git status fehlgeschlagen: {kurz(status)}")
return
b.geaendert = len([z for z in status.splitlines() if z.strip()])
rc, _ = git("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}", cwd=b.pfad)
b.upstream = rc == 0
if b.upstream:
rc, zaehl = git("rev-list", "--left-right", "--count", "HEAD...@{upstream}", cwd=b.pfad)
teile = zaehl.split()
if rc == 0 and len(teile) == 2 and all(t.isdigit() for t in teile):
b.vor, b.hinter = int(teile[0]), int(teile[1])
else:
b.kritisch = True
b.hinweise.append(f"Abstand zu origin nicht bestimmbar: {kurz(zaehl)}")
b.zustand = zustand_ermitteln(b)
def abgleichen(b: Befund, nur_pruefen: bool, ohne_netz: bool) -> None:
"""Entscheidet über den fast-forward-Pull. Bei jedem Zweifel: nichts tun, nur melden."""
if not (b.vorhanden and b.lesbar):
return
if b.geaendert:
b.aktion = "uebersprungen: Aenderungen vorhanden"
return
if b.branch == "HEAD":
b.aktion = "uebersprungen: detached HEAD"
return
if b.branch != b.kanal:
b.aktion = "uebersprungen: fremder branch"
return
if not b.upstream:
b.aktion = "uebersprungen: kein upstream"
return
if b.offline: # ohne erfolgreichen fetch ist der Abstand nur geraten
b.aktion = "uebersprungen: offline"
return
if b.vor and b.hinter:
b.aktion = "uebersprungen: divergiert"
return
if b.vor:
eigene = "1 eigener Commit" if b.vor == 1 else f"{b.vor} eigene Commits"
b.aktion = f"uebersprungen: {eigene}"
return
if not b.hinter:
b.aktion = "bereits aktuell"
return
if nur_pruefen or ohne_netz:
b.aktion = f"hinter origin ({commits(b.hinter)})"
return
rc, ausgabe = git("pull", "--ff-only", "--quiet", cwd=b.pfad)
if rc == 0:
b.aktion = f"aktualisiert ({commits(b.hinter)})"
b.aktualisiert, b.hinter = True, 0
b.zustand = zustand_ermitteln(b)
else:
b.aktion, b.kritisch = "FEHLER: pull fehlgeschlagen", True
b.hinweise.append(f"git pull --ff-only fehlgeschlagen: {kurz(ausgabe)}")
def tabelle_ausgeben(befunde: list[Befund]) -> None:
"""Eine Zeile je Repository, insgesamt hoechstens BREITE Zeichen breit."""
spalten = [
("Repo", 34, [b.name for b in befunde]),
("Branch", 14, [b.branch for b in befunde]),
("Zustand", 16, [b.zustand for b in befunde]),
("hinter/vor", 10, [b.abstand for b in befunde]),
]
breiten = [min(hoechst, max([len(kopf)] + [len(w) for w in werte]))
for kopf, hoechst, werte in spalten]
platz = max(20, BREITE - (sum(breiten) + len(breiten)))
b_aktion = min(platz, max([len("Aktion")] + [len(b.aktion) for b in befunde]))
def zeile(felder: list[str]) -> str:
gefuellt = [kuerze(w, br).ljust(br) for w, br in zip(felder[:-1], breiten)]
return " ".join(gefuellt) + " " + kuerze(felder[-1], b_aktion)
kopfzeile = zeile(["Repo", "Branch", "Zustand", "hinter/vor", "Aktion"])
print(kopfzeile)
print("-" * (sum(breiten) + len(breiten) + b_aktion))
for b in befunde:
print(zeile([b.name, b.branch, b.zustand, b.abstand, b.aktion]).rstrip())
def empfehlungen(befunde: list[Befund], nur_pruefen: bool, ohne_netz: bool) -> list[str]:
"""Konkrete, kopierbare Handlungsempfehlungen nur für das, was wirklich ansteht."""
zeilen: list[str] = []
fehlend = [b.name for b in befunde if b.zustand == "fehlt"]
if fehlend:
zeilen.append(f"Repository-Ordner fehlen: {', '.join(fehlend)}. Klonen mit:")
zeilen.append(" python scripts/setup_workspace.py")
ohne_git = [b.name for b in befunde if b.zustand == "kein git-repo"]
if ohne_git:
zeilen.append(f"Ordner ohne .git (kein Klon): {', '.join(ohne_git)}. Von Hand prüfen und "
"wegräumen, danach: python scripts/setup_workspace.py")
for b in befunde:
if not b.vorhanden:
continue
block: list[str] = []
if b.offline:
block.append("origin nicht erreichbar Netz/VPN prüfen. Die Angaben zu hinter/vor "
"stammen vom letzten erfolgreichen fetch.")
if not b.remote:
block.append("kein Remote konfiguriert gehört der Ordner wirklich zum Workspace?")
if b.geaendert:
block.append(f"{aenderungen(b.geaendert)} dieses Skript fasst sie nicht an. "
"Selbst sichten, committen oder verwerfen:")
block.append(f" {b.befehl('status')}")
if b.branch == "HEAD":
block.append("HEAD ist losgelöst (kein Branch). Vor der Arbeit auf einen Branch wechseln "
"das macht das Skript bewusst nicht selbst.")
elif b.branch != b.kanal:
block.append(f"steht auf Branch '{b.branch}' statt auf dem Kanal-Branch '{b.kanal}'. "
f"Das ist richtig so, solange daran gearbeitet wird: Kanal-Branches werden nie "
f"direkt bearbeitet, die Rückführung läuft über einen Pull Request auf Gitea. "
f"Ist der Branch fertig, den PR stellen; '{b.kanal}' wird von diesem Skript "
f"nur aktualisiert, wenn er ausgecheckt ist.")
if b.upstream and b.vor and b.hinter:
block.append(f"divergiert: {commits(b.vor)} lokal, {commits(b.hinter)} auf origin. "
"Weder Merge noch Rebase noch Reset macht dieses Skript bitte selbst entscheiden.")
elif b.upstream and b.vor:
block.append(f"{commits(b.vor)} lokal, nicht gepusht. Pushen ist Sache des Menschen "
"und passiert nur nach Absprache:")
block.append(f" {b.befehl(f'push origin {b.branch}')}")
if not b.upstream and b.remote and b.branch != "HEAD":
block.append(f"Branch '{b.branch}' hat keinen Upstream. Erst nach Absprache und nur vom "
"Menschen:")
block.append(f" {b.befehl(f'push -u origin {b.branch}')}")
if b.hinter and not b.geaendert and b.branch == b.kanal and not b.vor:
grund = ("fehlendem Netzkontakt" if b.offline else "--nur-pruefen" if nur_pruefen
else "--ohne-netz" if ohne_netz else "einem Hindernis")
block.append(f"{commits(b.hinter)} hinter origin, wegen {grund} nicht geholt. Nachholen:")
block.append(f" python scripts/hallo.py --repo {b.name}")
elif b.hinter and (b.geaendert or b.branch != b.kanal):
block.append(f"Kanal-Branch liegt {commits(b.hinter)} hinter origin erst nach dem "
f"Aufräumen oben aktualisierbar.")
for meldung in b.hinweise:
if meldung.startswith(("origin nicht erreichbar", "kein Remote")):
continue # oben bereits in Klartext ausgegeben
block.append(meldung)
if block:
zeilen.append(f"{b.name}:")
zeilen.extend(f" {z}" if not z.startswith(" ") else z for z in block)
return zeilen
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--nur-pruefen", action="store_true", help="kein Pull, nur Bericht (fetch findet statt)")
ap.add_argument("--ohne-netz", action="store_true", help="kein fetch und kein Pull, nur lokaler Zustand")
ap.add_argument("--repo", action="append", metavar="NAME", help="nur dieses Repo (mehrfach angebbar)")
args = ap.parse_args()
try:
with MANIFEST.open("rb") as f:
manifest = tomllib.load(f)
except (OSError, tomllib.TOMLDecodeError) as fehler:
print(f"workspace.toml nicht lesbar: {fehler}")
return 2
repos = manifest.get("repos", [])
bekannt = [ROOT.name] + [r["name"] for r in repos]
if args.repo:
unbekannt = [n for n in args.repo if n not in bekannt]
if unbekannt:
print(f"Unbekannte Repositories: {', '.join(unbekannt)}")
print(f"Bekannt sind: {', '.join(bekannt)}")
return 2
def gewuenscht(name: str) -> bool:
return args.repo is None or name in args.repo
befunde: list[Befund] = []
wurzel_ist_repo = (ROOT / ".git").exists()
if wurzel_ist_repo and gewuenscht(ROOT.name):
befunde.append(Befund(name=ROOT.name, kanal=manifest.get("workspace", {}).get("branch", ""),
pfad=ROOT, ist_wurzel=True))
befunde += [Befund(name=r["name"], kanal=r["branch"], pfad=ROOT / r["name"])
for r in repos if gewuenscht(r["name"])]
modus = ("ohne Netz kein fetch, kein Pull" if args.ohne_netz
else "nur prüfen fetch, kein Pull" if args.nur_pruefen
else "fetch und fast-forward-Pull")
print(f"Hallo. Workspace: {ROOT}")
print(f"Modus: {modus} | Repositories: {len(befunde)}")
if not wurzel_ist_repo and gewuenscht(ROOT.name):
print(f"Hinweis: '{ROOT.name}' selbst ist noch kein Git-Repository geprüft werden nur die Klone.")
if not args.ohne_netz:
print("Hole den Stand von origin (git fetch --prune) ...")
print()
for b in befunde:
einlesen(b, args.ohne_netz)
abgleichen(b, args.nur_pruefen, args.ohne_netz)
tabelle_ausgeben(befunde)
aktualisiert = sum(1 for b in befunde if b.aktualisiert)
aktuell = sum(1 for b in befunde if b.aktion == "bereits aktuell")
offen = len(befunde) - aktualisiert - aktuell
wort = "Repository" if len(befunde) == 1 else "Repositories"
print(f"\n{len(befunde)} {wort}: {aktualisiert} aktualisiert, {aktuell} bereits aktuell, "
f"{offen} mit Befund.")
if args.ohne_netz:
print("Ohne Netz gelaufen: hinter/vor stammt vom letzten fetch und kann veraltet sein.")
zu_tun = empfehlungen(befunde, args.nur_pruefen, args.ohne_netz)
if zu_tun:
print("\nZu tun")
print("-" * 6)
for zeile in zu_tun:
print(umbrechen(zeile))
return 1 if any(b.kritisch for b in befunde) else 0
if __name__ == "__main__":
raise SystemExit(main())
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Verknüpft die Plugin-Klone des Workspace unter ihren Paketnamen im Plugin-Ordner eines QGIS-Profils.
QGIS lädt die Plugins dann direkt aus der Arbeitskopie ohne Release, ohne Kopieren. Eine Änderung im Klon
ist nach „Plugin neu laden“ (Plugin „Plugin Reloader“) oder einem QGIS-Neustart aktiv.
Aufrufe (aus dem Workspace-Root):
python scripts/link_qgis_profile.py --profil dev # alle Plugins (plattform, fachplugin, geruest)
python scripts/link_qgis_profile.py --profil dev sn_basis sn_plan41 # nur bestimmte Pakete
python scripts/link_qgis_profile.py --pfad "D:/QGIS/profiles/dev/python/plugins" # Plugin-Ordner direkt angeben
python scripts/link_qgis_profile.py --profil dev --status # nur anzeigen
python scripts/link_qgis_profile.py --profil dev --entfernen # Verknüpfungen wieder entfernen
Empfehlung: eigenes QGIS-Profil (z. B. „dev“) verwenden, in dem die Plugins NICHT aus dem Release-Feed installiert sind.
Unter Windows werden Junctions angelegt (keine Administratorrechte nötig), sonst Symlinks.
"""
from __future__ import annotations
import argparse
import os
import sys
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ROLLEN_MIT_PLUGIN = {"plattform", "fachplugin", "geruest"}
def profil_plugin_ordner(profil: str) -> Path:
if sys.platform.startswith("win"):
basis = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
elif sys.platform == "darwin":
basis = Path.home() / "Library" / "Application Support"
else:
basis = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
return basis / "QGIS" / "QGIS3" / "profiles" / profil / "python" / "plugins"
def ist_link(p: Path) -> bool:
if p.is_symlink():
return True
if os.name == "nt" and p.is_dir():
try:
return bool(os.stat(p, follow_symlinks=False).st_file_attributes & 0x400) # REPARSE_POINT (Junction)
except (AttributeError, OSError):
return False
return False
def link_anlegen(ziel: Path, quelle: Path) -> str:
try:
os.symlink(quelle, ziel, target_is_directory=True)
return "Symlink"
except (OSError, NotImplementedError):
if os.name == "nt":
import _winapi # type: ignore[attr-defined]
_winapi.CreateJunction(str(quelle), str(ziel))
return "Junction"
raise
def link_entfernen(p: Path) -> None:
if p.is_symlink():
p.unlink()
else:
os.rmdir(p) # Junction: rmdir entfernt nur den Verweis
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("pakete", nargs="*", help="Paketnamen (Standard: alle Plugins laut workspace.toml)")
ap.add_argument("--profil", default=None, help="Name des QGIS-Profils, z. B. dev")
ap.add_argument("--pfad", type=Path, default=None, help="Plugin-Ordner direkt (überschreibt --profil)")
ap.add_argument("--status", action="store_true", help="nur anzeigen, nichts ändern")
ap.add_argument("--entfernen", action="store_true", help="Verknüpfungen entfernen")
ap.add_argument("--ersetzen", action="store_true",
help="vorhandenen echten Plugin-Ordner (z. B. Release-Installation) nach <name>.vor_link umbenennen und verknüpfen")
args = ap.parse_args()
if args.pfad is None and args.profil is None:
ap.error("--profil <name> oder --pfad <ordner> angeben")
plugin_ordner = args.pfad or profil_plugin_ordner(args.profil)
with (ROOT / "workspace.toml").open("rb") as f:
manifest = tomllib.load(f)
repos = {r["paket"]: r for r in manifest["repos"] if r.get("paket") and r.get("rolle") in ROLLEN_MIT_PLUGIN}
gewuenscht = args.pakete or list(repos)
unbekannt = [p for p in gewuenscht if p not in repos]
if unbekannt:
print(f"Unbekannte oder nicht verknüpfbare Pakete: {unbekannt}. Möglich: {sorted(repos)}")
return 2
print(f"QGIS-Plugin-Ordner: {plugin_ordner}")
if not args.status and not args.entfernen:
plugin_ordner.mkdir(parents=True, exist_ok=True)
elif not plugin_ordner.exists():
print(" (existiert noch nicht QGIS mit diesem Profil einmal starten oder --pfad prüfen)")
rc = 0
for paket in gewuenscht:
quelle = ROOT / repos[paket]["name"]
ziel = plugin_ordner / paket
if not (quelle / "__init__.py").is_file():
print(f"[{paket}] übersprungen: {quelle.name} enthält kein Plugin (__init__.py fehlt)")
continue
if args.status:
if ist_link(ziel):
print(f"[{paket}] verknüpft -> {os.path.realpath(ziel)}")
elif ziel.exists():
print(f"[{paket}] echter Ordner vorhanden (Release-Installation?) nicht verknüpft")
else:
print(f"[{paket}] nicht vorhanden")
continue
if args.entfernen:
if ist_link(ziel):
link_entfernen(ziel)
print(f"[{paket}] Verknüpfung entfernt")
elif ziel.exists():
print(f"[{paket}] ist ein echter Ordner nicht entfernt")
else:
print(f"[{paket}] nichts zu tun")
continue
if ist_link(ziel):
if os.path.realpath(ziel) == os.path.realpath(quelle):
print(f"[{paket}] bereits verknüpft")
continue
link_entfernen(ziel)
elif ziel.exists():
if not args.ersetzen:
print(f"[{paket}] echter Ordner vorhanden: {ziel}\n"
f" -> eigenes Profil verwenden oder mit --ersetzen nach {paket}.vor_link verschieben")
rc = 1
continue
sicherung = ziel.with_name(f"{paket}.vor_link")
if sicherung.exists():
print(f"[{paket}] {sicherung.name} existiert bereits bitte manuell aufräumen")
rc = 1
continue
ziel.rename(sicherung)
print(f"[{paket}] vorhandenen Ordner nach {sicherung.name} verschoben")
try:
art = link_anlegen(ziel, quelle)
print(f"[{paket}] {art} angelegt -> {quelle}")
except OSError as e:
print(f"[{paket}] FEHLER: {e}")
rc = 1
if not args.status and not args.entfernen and rc == 0:
print("\nIn QGIS: Erweiterungen -> Verwalten -> Einstellungen: „Auch experimentelle Erweiterungen anzeigen“ aktivieren,\n"
"dann zuerst „LNO Sachsen | Plugin Basisfunktionen“ (sn_basis) einschalten, danach die Fachplugins.\n"
"Nach Codeänderungen: Plugin „Plugin Reloader“ (offizielles Repository) oder QGIS neu starten.")
return rc
if __name__ == "__main__":
raise SystemExit(main())
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""Führt die unittest-Suiten der sn_*-Plugins im Mock-Modus (ohne QGIS) aus.
Die Plugins werden unter ihrem Paketnamen (sn_basis, sn_plan41, ...) in ein
temporäres Verzeichnis verlinkt, damit die Importe wie im QGIS-Plugin-Ordner
funktionieren. Läuft auf Linux, macOS und Windows mit jedem Python >= 3.11.
Aufrufe (aus dem Workspace-Root):
python scripts/run_tests.py # alle Pakete mit Tests
python scripts/run_tests.py sn_basis # nur ein Paket
python scripts/run_tests.py sn_basis --verbose # volle unittest-Ausgabe
python scripts/run_tests.py --baseline # Vergleich mit docs/status/testbaseline.json
python scripts/run_tests.py --update-baseline # Baseline neu schreiben (nur nach Absprache)
Exit-Code: 0 = alle Suiten grün (bzw. mit --baseline: keine neuen roten Tests), sonst 1.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "workspace.toml"
BASELINE_DEFAULT = ROOT / "docs" / "status" / "testbaseline.json"
def lade_manifest() -> dict:
with MANIFEST.open("rb") as f:
return tomllib.load(f)
def verlinke(quelle: Path, ziel: Path) -> str:
"""Verknüpft quelle als ziel (Symlink, Windows-Junction oder Kopie)."""
try:
os.symlink(quelle, ziel, target_is_directory=True)
return "symlink"
except (OSError, NotImplementedError):
pass
if os.name == "nt":
try:
import _winapi # type: ignore[attr-defined]
_winapi.CreateJunction(str(quelle), str(ziel))
return "junction"
except Exception:
pass
shutil.copytree(
quelle, ziel,
ignore=shutil.ignore_patterns(".git", "__pycache__", "*.pyc"),
)
return "kopie"
def parse_ergebnis(ausgabe: str) -> dict:
m = re.search(r"^Ran (\d+) tests?", ausgabe, re.M)
gesamt = int(m.group(1)) if m else 0
fail = err = skip = 0
m = re.search(r"^FAILED \((.*)\)", ausgabe, re.M)
if m:
for teil in m.group(1).split(","):
k, _, v = teil.strip().partition("=")
if k == "failures":
fail = int(v)
elif k == "errors":
err = int(v)
elif k == "skipped":
skip = int(v)
else:
m = re.search(r"^OK(?: \(skipped=(\d+)\))?", ausgabe, re.M)
if m and m.group(1):
skip = int(m.group(1))
rot = sorted(set(re.findall(r"^(?:FAIL|ERROR): (\S+ \([\w.]+\))", ausgabe, re.M)))
return {
"gesamt": gesamt, "fehlschlaege": fail, "fehler": err,
"uebersprungen": skip, "gruen": gesamt - fail - err - skip, "rot": rot,
}
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("pakete", nargs="*", help="Paketnamen (Standard: alle mit tests=true)")
ap.add_argument("--verbose", "-v", action="store_true", help="volle unittest-Ausgabe anzeigen")
ap.add_argument("--baseline", action="store_true", help="mit Baseline vergleichen")
ap.add_argument("--update-baseline", action="store_true", help="Baseline aus diesem Lauf schreiben")
ap.add_argument("--baseline-datei", type=Path, default=BASELINE_DEFAULT)
ap.add_argument("--json", type=Path, help="Ergebnis als JSON in diese Datei schreiben")
args = ap.parse_args()
manifest = lade_manifest()
repos = {r["paket"]: r for r in manifest["repos"] if r.get("paket")}
testbare = [p for p, r in repos.items() if r.get("tests")]
gewuenscht = args.pakete or testbare
unbekannt = [p for p in gewuenscht if p not in repos]
if unbekannt:
print(f"Unbekannte Pakete: {unbekannt}. Bekannt: {sorted(repos)}")
return 2
# Abhängigkeiten immer mit verlinken (sn_basis wird von allen importiert;
# sn_basis.DataGrabber importiert sn_plan41 lazy).
zu_verlinken: set[str] = set()
for p in gewuenscht:
zu_verlinken.add(p)
zu_verlinken.update(repos[p].get("abhaengigkeiten", []))
zu_verlinken.update(p for p in testbare)
fehlend = [p for p in zu_verlinken if not (ROOT / repos[p]["name"]).is_dir()]
if fehlend:
print(f"Repository-Ordner fehlen: {[repos[p]['name'] for p in fehlend]} "
f"-> zuerst 'python scripts/setup_workspace.py' ausführen.")
return 2
ergebnisse: dict[str, dict] = {}
rc = 0
with tempfile.TemporaryDirectory(prefix="sn_tests_") as tmp:
tmp_pfad = Path(tmp)
for p in sorted(zu_verlinken):
verlinke(ROOT / repos[p]["name"], tmp_pfad / p)
env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1", PYTHONIOENCODING="utf-8", PYTHONPATH=str(tmp_pfad))
for p in gewuenscht:
if not repos[p].get("tests"):
print(f"[{p}] keine Tests vorhanden übersprungen")
continue
cmd = [sys.executable, "-m", "unittest", "discover", "-s", f"{p}/tests", "-t", ".", "-p", "test_*.py"]
proz = subprocess.run(cmd, cwd=tmp_pfad, env=env, capture_output=True, text=True, errors="replace")
ausgabe = proz.stdout + proz.stderr
erg = parse_ergebnis(ausgabe)
ergebnisse[p] = erg
if args.verbose:
print(f"\n{'=' * 70}\n[{p}] {' '.join(cmd)}\n{'=' * 70}\n{ausgabe}")
status = "GRÜN" if not erg["rot"] and erg["gesamt"] else "ROT"
print(f"[{p}] {status}: {erg['gruen']}/{erg['gesamt']} bestanden, "
f"{erg['fehlschlaege']} Fehlschläge, {erg['fehler']} Fehler, {erg['uebersprungen']} übersprungen")
if erg["gesamt"] == 0:
print(f" Keine Tests gefunden oder Import-Fehler. Mit --verbose prüfen.")
rc = 1
# Baseline-Vergleich: verhindert, dass bereits rote Tests "vermehrt" werden.
if args.baseline or args.update_baseline:
alt: dict = {}
baseline_vorhanden = args.baseline_datei.exists()
if baseline_vorhanden:
alt = json.loads(args.baseline_datei.read_text(encoding="utf-8"))
elif not args.update_baseline:
print(f"Keine Baseline unter {args.baseline_datei.relative_to(ROOT)} alle roten Tests gelten als neu.")
neu_rot_gesamt = 0
for p, erg in (ergebnisse.items() if (baseline_vorhanden or not args.update_baseline) else []):
alt_rot = set(alt.get(p, {}).get("rot", []))
neu_rot = sorted(set(erg["rot"]) - alt_rot)
neu_gruen = sorted(alt_rot - set(erg["rot"]))
if neu_rot:
neu_rot_gesamt += len(neu_rot)
print(f"[{p}] NEU ROT ({len(neu_rot)}):")
for t in neu_rot:
print(f" - {t}")
if neu_gruen:
print(f"[{p}] neu grün ({len(neu_gruen)}): {', '.join(neu_gruen)}")
if not neu_rot and not neu_gruen:
print(f"[{p}] unverändert gegenüber Baseline")
if args.update_baseline:
alt.update({p: {k: v for k, v in erg.items()} for p, erg in ergebnisse.items()})
args.baseline_datei.parent.mkdir(parents=True, exist_ok=True)
args.baseline_datei.write_text(json.dumps(alt, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print(f"Baseline geschrieben: {args.baseline_datei.relative_to(ROOT)}")
rc = 1 if neu_rot_gesamt else rc
else:
for erg in ergebnisse.values():
if erg["rot"]:
rc = 1
if rc:
print("\nRote Tests (Kurzliste, Details mit --verbose):")
for p, erg in ergebnisses_sortiert(ergebnisse):
for t in erg["rot"]:
print(f" [{p}] {t}")
if args.json:
args.json.write_text(json.dumps(ergebnisse, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return rc
def ergebnisses_sortiert(ergebnisse: dict[str, dict]):
return sorted(ergebnisse.items())
if __name__ == "__main__":
raise SystemExit(main())
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Richtet den AG_QGIS-Workspace ein bzw. aktualisiert ihn.
1. Klont fehlende Repositories aus workspace.toml in Unterordner (Default-Branch).
2. Holt für vorhandene Klone den Stand vom Server (git fetch) und zeigt Branch,
Vorsprung/Rückstand und ungesicherte Änderungen an. Ein Pull passiert nur
mit --pull und nur bei sauberem Arbeitsverzeichnis (fast-forward).
3. Spiegelt die Skills aus .agents/skills/ nach .claude/skills/ (Claude Code liest
nur dort; der Agent-Skills-Standard und andere Werkzeuge lesen .agents/skills/).
Aufrufe:
python scripts/setup_workspace.py # klonen + Status + Skills
python scripts/setup_workspace.py --pull # zusätzlich fast-forward pull
python scripts/setup_workspace.py --nur-skills
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def git(*args: str, cwd: Path) -> tuple[int, str]:
p = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, errors="replace")
return p.returncode, (p.stdout + p.stderr).strip()
def sync_skills() -> None:
quelle = ROOT / ".agents" / "skills"
ziel = ROOT / ".claude" / "skills"
if not quelle.is_dir():
print("Keine Skills in .agents/skills/ gefunden.")
return
ziel.parent.mkdir(exist_ok=True)
# Alten Spiegel entfernen (Symlink, Junction oder Kopie)
if ziel.is_symlink() or (ziel.exists() and not ziel.is_dir()):
ziel.unlink()
elif ziel.is_dir():
try:
os.rmdir(ziel) # leere Junction/Ordner
except OSError:
shutil.rmtree(ziel)
art = "symlink"
try:
os.symlink(quelle, ziel, target_is_directory=True)
except (OSError, NotImplementedError):
art = "kopie"
if os.name == "nt":
try:
import _winapi # type: ignore[attr-defined]
_winapi.CreateJunction(str(quelle), str(ziel))
art = "junction"
except Exception:
pass
if art == "kopie":
shutil.copytree(quelle, ziel, ignore=shutil.ignore_patterns("__pycache__"))
anzahl = len([p for p in quelle.iterdir() if (p / "SKILL.md").is_file()])
print(f"Skills: {anzahl} aus .agents/skills/ nach .claude/skills/ gespiegelt ({art}).")
if art == "kopie":
print(" Hinweis: Kopie statt Link nach Änderungen an .agents/skills/ dieses Skript erneut ausführen.")
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--pull", action="store_true", help="saubere Klone per fast-forward aktualisieren")
ap.add_argument("--nur-skills", action="store_true", help="nur Skills spiegeln, kein Git")
args = ap.parse_args()
if not args.nur_skills:
with (ROOT / "workspace.toml").open("rb") as f:
manifest = tomllib.load(f)
ws = manifest["workspace"]
basis_url = f"{ws['gitea']}/{ws['org']}"
print(f"Workspace: {ROOT}\nGitea: {basis_url}\n")
for repo in manifest["repos"]:
name, branch = repo["name"], repo["branch"]
pfad = ROOT / name
if not (pfad / ".git").is_dir():
print(f"[{name}] klone Branch {branch} ...")
rc, out = git("clone", "--branch", branch, f"{basis_url}/{name}.git", name, cwd=ROOT)
print(" " + ("ok" if rc == 0 else f"FEHLER: {out}"))
continue
git("fetch", "--quiet", "--prune", cwd=pfad)
_, aktuell = git("rev-parse", "--abbrev-ref", "HEAD", cwd=pfad)
_, dirty = git("status", "--porcelain", cwd=pfad)
n_dirty = len([z for z in dirty.splitlines() if z.strip()])
_, ab = git("rev-list", "--left-right", "--count", f"HEAD...origin/{aktuell}", cwd=pfad)
vor, _, zurueck = ab.partition("\t") if "\t" in ab else ("?", "", "?")
meldung = f"[{name}] Branch {aktuell} (+{vor}/-{zurueck.strip()} zu origin), {n_dirty} ungesicherte Änderungen"
if aktuell != branch:
meldung += f" <- Hinweis: Manifest erwartet {branch}"
if args.pull and n_dirty == 0 and aktuell == branch:
rc, out = git("pull", "--ff-only", "--quiet", cwd=pfad)
meldung += " -> pull " + ("ok" if rc == 0 else f"FEHLER: {out.splitlines()[-1] if out else rc}")
print(meldung)
print()
sync_skills()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+514
View File
@@ -0,0 +1,514 @@
#!/usr/bin/env python3
"""Erzeugt aus den unittest-Suiten der sn_*-Plugins einen lesbaren Testkatalog.
Der Katalog (docs/tests/testkatalog.md) beantwortet die Frage "Was prüft die
Testsuite eigentlich?" auch für Fachleute ohne Programmierkenntnisse. Er
listet je Testdatei jeden Testfall mit seiner Beschreibung (erste Docstring-
Zeile) und seinem Status auf und weist am Ende alle Tests ohne Beschreibung als
Arbeitsliste aus.
Die Testdateien werden mit dem ast-Modul der Standardbibliothek *geparst* und
bewusst NICHT importiert: sie importieren sn_basis und Geschwisterpakete, was
ohne den Verlinkungs-Trick von scripts/run_tests.py fehlschlägt, und optionale
Fremdpakete (z. B. openpyxl) sind nicht überall installiert. Dadurch läuft
dieses Skript mit jedem Python >= 3.11 ohne jede Abhängigkeit, auch ohne QGIS.
Der Status je Testfall stammt aus docs/status/testbaseline.json; dort stehen
unter <paket>.rot Einträge der Form "<methode> (<modulpfad>.<Klasse>.<methode>)".
Determinismus: Die erzeugte Datei wird versioniert, darf also bei unverändertem
Eingang keine Diffs erzeugen. Deshalb steht im Kopf weder Uhrzeit noch
Erzeugungsdatum auch ein reines Tagesdatum würde die Datei bei jedem Lauf an
einem anderen Tag ändern, obwohl sich inhaltlich nichts geändert hat. Statt
dessen weist die Zeile "Stand der Baseline" die verwendete Baseline-Datei über
ihre SHA-256-Kurzprüfsumme aus: Sie ändert sich genau dann, wenn sich der
Eingang ändert. Sortiert wird nach der Reihenfolge in workspace.toml (Pakete),
nach Dateinamen (Testdateien) und nach der Reihenfolge im Quelltext (Klassen
und Testfälle).
Aufrufe (aus dem Workspace-Root):
python scripts/testkatalog.py # docs/tests/testkatalog.md neu schreiben
python scripts/testkatalog.py sn_basis # nur ein Paket
python scripts/testkatalog.py --pruefen # nur prüfen, ob die Datei aktuell ist (CI, vor dem Commit)
python scripts/testkatalog.py --ausgabe /tmp/k.md # anderes Ziel
Exit-Code: 0 = erfolgreich (mit --pruefen: Datei ist aktuell), 1 = Datei veraltet
oder fehlt, 2 = Aufruffehler.
"""
from __future__ import annotations
import argparse
import ast
import difflib
import hashlib
import json
import re
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "workspace.toml"
BASELINE_DEFAULT = ROOT / "docs" / "status" / "testbaseline.json"
AUSGABE_DEFAULT = ROOT / "docs" / "tests" / "testkatalog.md"
FEHLT = "— (Docstring fehlt)"
SPHINX_ROLLE = re.compile(r":(?:mod|func|class|meth|attr|data|obj|ref):`~?([^`]+)`")
SKIP_DEKORATOREN = {"skip", "skipIf", "skipUnless"}
ROLLENTEXT = {
"plattform": "Plattform-Plugin",
"fachplugin": "Fachplugin",
"geruest": "Gerüst",
"platzhalter": "Platzhalter",
"infrastruktur": "Infrastruktur",
"daten": "Daten",
}
@dataclass
class Testfall:
"""Eine einzelne Testmethode (test_*) einer TestCase-Klasse."""
klasse: str
name: str
beschreibung: str
uebersprungen: bool = False
grund: str = ""
erwarteter_fehlschlag: bool = False
status: str = "grün"
@property
def bezeichnung(self) -> str:
return f"{self.klasse}.{self.name}"
@dataclass
class Testgruppe:
"""Eine TestCase-Klasse mit ihren Testfällen."""
name: str
beschreibung: str
faelle: list[Testfall] = field(default_factory=list)
@dataclass
class Testdatei:
"""Eine Datei tests/test_*.py mit allen darin gefundenen Testgruppen."""
pfad: Path
anzeigepfad: str
modulpfad: str
beschreibung: str
gruppen: list[Testgruppe] = field(default_factory=list)
@property
def faelle(self) -> list[Testfall]:
return [f for g in self.gruppen for f in g.faelle]
@dataclass
class Paketkatalog:
"""Alle Testdateien eines Pakets samt Kennzahlen."""
paket: str
ordner: str
rolle: str
dateien: list[Testdatei] = field(default_factory=list)
@property
def faelle(self) -> list[Testfall]:
return [f for d in self.dateien for f in d.faelle]
def lade_manifest() -> dict:
with MANIFEST.open("rb") as f:
return tomllib.load(f)
def erste_zeile(docstring: str | None, dateiname: str = "") -> str:
"""Erste aussagekräftige Zeile eines Docstrings, sonst leer.
Viele Testdateien beginnen ihren Modul-Docstring mit dem eigenen Dateipfad
("sn_basis/tests/test_x.py"). Diese Zeile beschreibt nichts und wird
übersprungen.
"""
if not docstring:
return ""
for zeile in docstring.splitlines():
text = " ".join(zeile.split())
if not text:
continue
if dateiname and text.endswith(".py") and text.split("/")[-1] == dateiname:
continue
# Sphinx-Rollen wie :mod:`sn_basis.x` lesen sich für Fachleute schlecht.
return SPHINX_ROLLE.sub(r"`\1`", text)
return ""
def dekorator_name(knoten: ast.expr) -> str:
"""Punktierter Name eines Dekorators, z. B. 'unittest.skipUnless'."""
ziel = knoten.func if isinstance(knoten, ast.Call) else knoten
teile: list[str] = []
while isinstance(ziel, ast.Attribute):
teile.append(ziel.attr)
ziel = ziel.value
if isinstance(ziel, ast.Name):
teile.append(ziel.id)
return ".".join(reversed(teile))
def lies_skip(dekoratoren: list[ast.expr]) -> tuple[bool, str, bool]:
"""Wertet skip-Dekoratoren aus: (übersprungen, Grund, erwarteter Fehlschlag)."""
uebersprungen = False
erwartet = False
grund = ""
for dek in dekoratoren:
kurz = dekorator_name(dek).rsplit(".", 1)[-1]
if kurz == "expectedFailure":
erwartet = True
elif kurz in SKIP_DEKORATOREN:
uebersprungen = True
argumente = dek.args if isinstance(dek, ast.Call) else []
texte = [a.value for a in argumente if isinstance(a, ast.Constant) and isinstance(a.value, str)]
if texte and not grund:
grund = " ".join(texte[0].split())
if not grund:
grund = f"@unittest.{kurz}"
return uebersprungen, grund, erwartet
def ist_testcase(klasse: ast.ClassDef) -> bool:
"""True für Klassen, die als unittest-TestCase gelten.
Erkannt werden Basisklassen, deren Name auf 'TestCase' endet (unittest.TestCase,
eigene Basisklassen), sowie als Auffangnetz Klassen mit Namen 'Test…', die
mindestens eine test_*-Methode haben.
"""
for basis in klasse.bases:
name = dekorator_name(basis)
if name.rsplit(".", 1)[-1].endswith("TestCase"):
return True
hat_tests = any(
isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef)) and m.name.startswith("test")
for m in klasse.body
)
return klasse.name.startswith("Test") and hat_tests
def lies_testdatei(pfad: Path, paket: str) -> Testdatei:
"""Parst eine Testdatei per ast (ohne sie zu importieren)."""
quelle = pfad.read_text(encoding="utf-8")
baum = ast.parse(quelle, filename=str(pfad))
datei = Testdatei(
pfad=pfad,
anzeigepfad=pfad.relative_to(ROOT).as_posix(),
modulpfad=f"{paket}.tests.{pfad.stem}",
beschreibung=erste_zeile(ast.get_docstring(baum), pfad.name),
)
for knoten in baum.body:
if not isinstance(knoten, ast.ClassDef) or not ist_testcase(knoten):
continue
klassen_skip, klassen_grund, klassen_erwartet = lies_skip(knoten.decorator_list)
gruppe = Testgruppe(name=knoten.name, beschreibung=erste_zeile(ast.get_docstring(knoten)))
for glied in knoten.body:
if not isinstance(glied, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
if not glied.name.startswith("test"):
continue
skip, grund, erwartet = lies_skip(glied.decorator_list)
gruppe.faelle.append(
Testfall(
klasse=knoten.name,
name=glied.name,
beschreibung=erste_zeile(ast.get_docstring(glied)),
uebersprungen=skip or klassen_skip,
grund=grund or (klassen_grund if klassen_skip else ""),
erwarteter_fehlschlag=erwartet or klassen_erwartet,
)
)
if gruppe.faelle:
datei.gruppen.append(gruppe)
return datei
def sammle_paket(paket: str, repo: dict) -> Paketkatalog | None:
"""Liest alle tests/test_*.py eines Pakets; None, wenn kein tests/-Ordner da ist."""
ordner = ROOT / repo["name"] / "tests"
if not ordner.is_dir():
return None
katalog = Paketkatalog(paket=paket, ordner=repo["name"], rolle=repo.get("rolle", ""))
for pfad in sorted(ordner.glob("test_*.py"), key=lambda p: p.name):
katalog.dateien.append(lies_testdatei(pfad, paket))
return katalog
def lies_baseline(pfad: Path) -> tuple[dict[str, set[str]], str]:
"""Rote Tests je Paket als Menge '<modulpfad>.<Klasse>.<methode>' plus Kurzprüfsumme."""
if not pfad.exists():
return {}, ""
rohdaten = pfad.read_bytes()
kurz = hashlib.sha256(rohdaten).hexdigest()[:12]
daten = json.loads(rohdaten.decode("utf-8"))
rot: dict[str, set[str]] = {}
muster = re.compile(r"^(?P<methode>\S+)\s+\((?P<pfad>[\w.]+)\)$")
for paket, eintrag in daten.items():
schluessel: set[str] = set()
for zeile in eintrag.get("rot", []):
treffer = muster.match(zeile.strip())
schluessel.add(treffer.group("pfad") if treffer else zeile.strip())
rot[paket] = schluessel
return rot, kurz
def setze_status(katalog: Paketkatalog, rot: set[str]) -> None:
for datei in katalog.dateien:
for fall in datei.faelle:
voll = f"{datei.modulpfad}.{fall.bezeichnung}"
if fall.uebersprungen:
fall.status = "übersprungen"
elif fall.erwarteter_fehlschlag:
fall.status = "erwarteter Fehlschlag"
elif voll in rot:
fall.status = "rot (Baseline)"
else:
fall.status = "grün"
def zaehle(faelle: list[Testfall]) -> dict[str, int]:
return {
"gesamt": len(faelle),
"gruen": sum(1 for f in faelle if f.status == "grün"),
"rot": sum(1 for f in faelle if f.status == "rot (Baseline)"),
"uebersprungen": sum(1 for f in faelle if f.status in ("übersprungen", "erwarteter Fehlschlag")),
"erwartet": sum(1 for f in faelle if f.status == "erwarteter Fehlschlag"),
"mit_docstring": sum(1 for f in faelle if f.beschreibung),
}
def anteil(teil: int, ganz: int) -> str:
if not ganz:
return "0 von 0 ()"
return f"{teil} von {ganz} ({round(100 * teil / ganz)} %)"
def zelle(text: str) -> str:
"""Macht einen Text tabellensicher (Pipes maskieren, Zeilenumbrüche entfernen)."""
return " ".join(text.split()).replace("|", "\\|") or FEHLT
def status_text(fall: Testfall) -> str:
if fall.status == "übersprungen" and fall.grund:
return f"übersprungen {zelle(fall.grund)}"
return fall.status
def erzeuge_markdown(kataloge: list[Paketkatalog], baseline_pfad: Path, baseline_kurz: str) -> str:
alle = [f for k in kataloge for f in k.faelle]
summe = zaehle(alle)
baseline_anzeige = baseline_pfad.relative_to(ROOT).as_posix() if baseline_pfad.is_relative_to(ROOT) else str(baseline_pfad)
z: list[str] = []
z.append("# Testkatalog der sn_*-Plugins")
z.append("")
z.append("> **Automatisch erzeugt von `scripts/testkatalog.py` nicht von Hand bearbeiten.**")
z.append("> Neu erzeugen: `python scripts/testkatalog.py`. Prüfen (CI, vor dem Commit): `python scripts/testkatalog.py --pruefen`.")
z.append("")
z.append("Dieser Katalog zeigt ohne Blick in den Quelltext, **was** die Testsuite prüft. Er ist die Grundlage")
z.append("für die fachliche Frage, ob die Abdeckung sinnvoll ist. Die Angaben stammen aus den Testdateien")
z.append("selbst (Docstrings) und aus der Testbaseline.")
z.append("")
if baseline_kurz:
z.append(f"Stand der Baseline: `{baseline_anzeige}` (SHA-256 `{baseline_kurz}`)")
else:
z.append(f"Stand der Baseline: `{baseline_anzeige}` **nicht gefunden** alle Testfälle gelten als grün.")
z.append("")
z.append(
f"Gesamt: {len(kataloge)} Pakete, {sum(len(k.dateien) for k in kataloge)} Testdateien, "
f"{summe['gesamt']} Testfälle {summe['gruen']} grün, {summe['rot']} rot (Baseline), "
f"{summe['uebersprungen']} übersprungen."
)
z.append(f"Mit Beschreibung (Docstring): {anteil(summe['mit_docstring'], summe['gesamt'])}.")
z.append("")
z.append("## Zusammenfassung")
z.append("")
z.append("| Paket | Testdateien | Testfälle | grün | rot (Baseline) | übersprungen | mit Beschreibung |")
z.append("| --- | ---: | ---: | ---: | ---: | ---: | ---: |")
for katalog in kataloge:
k = zaehle(katalog.faelle)
z.append(
f"| `{katalog.paket}` | {len(katalog.dateien)} | {k['gesamt']} | {k['gruen']} | "
f"{k['rot']} | {k['uebersprungen']} | {anteil(k['mit_docstring'], k['gesamt'])} |"
)
z.append(
f"| **Summe** | **{sum(len(k.dateien) for k in kataloge)}** | **{summe['gesamt']}** | "
f"**{summe['gruen']}** | **{summe['rot']}** | **{summe['uebersprungen']}** | "
f"**{anteil(summe['mit_docstring'], summe['gesamt'])}** |"
)
z.append("")
if summe["erwartet"]:
z.append(f"In Spalte „übersprungen“ sind {summe['erwartet']} Testfälle mit `@unittest.expectedFailure` enthalten.")
z.append("")
z.append("### Legende")
z.append("")
z.append("- **prüft** erste Zeile des Docstrings des Testfalls. Fehlt sie, steht dort `" + FEHLT + "`.")
z.append("- **grün** der Testfall lief im Baseline-Lauf durch.")
z.append(f"- **rot (Baseline)** der Testfall schlägt bereits fehl und ist in `{baseline_anzeige}` vermerkt; er darf nicht ignoriert, aber auch nicht als neuer Fehler gewertet werden.")
z.append("- **übersprungen** per `@unittest.skip`/`skipIf`/`skipUnless` abgeschaltet; der Grund steht dabei.")
z.append("- Ein Testfall heißt `Testgruppe.test_name`; die Testgruppe fasst zusammengehörige Testfälle einer Datei zusammen.")
z.append("")
for katalog in kataloge:
k = zaehle(katalog.faelle)
rolle = ROLLENTEXT.get(katalog.rolle, katalog.rolle or "Plugin")
z.append(f"## Paket `{katalog.paket}`")
z.append("")
z.append(
f"{rolle}, Ordner `{katalog.ordner}` {len(katalog.dateien)} Testdateien mit {k['gesamt']} Testfällen "
f"({k['gruen']} grün, {k['rot']} rot, {k['uebersprungen']} übersprungen)."
)
z.append("")
if not katalog.dateien:
z.append("Keine Testdateien gefunden.")
z.append("")
continue
for datei in katalog.dateien:
z.append(f"### `{datei.anzeigepfad}`")
z.append("")
z.append(f"Worum es in der Datei geht: {zelle(datei.beschreibung)}")
z.append("")
if not datei.gruppen:
z.append("Keine Testfälle: Die Datei enthält keine `unittest.TestCase`-Klasse sie ist eine reine "
"Hilfsdatei, die beim Testlauf nur importiert wird.")
z.append("")
continue
z.append("Testgruppen:")
z.append("")
for gruppe in datei.gruppen:
z.append(f"- `{gruppe.name}` ({len(gruppe.faelle)} Testfälle): {zelle(gruppe.beschreibung)}")
z.append("")
z.append("| Testfall | prüft | Status |")
z.append("| --- | --- | --- |")
for fall in datei.faelle:
z.append(f"| `{fall.bezeichnung}` | {zelle(fall.beschreibung)} | {status_text(fall)} |")
z.append("")
z.append("## Lücken")
z.append("")
z.append("Arbeitsliste: Diese Testfälle haben keinen Docstring; was sie fachlich prüfen, lässt sich nur")
z.append("aus dem Quelltext erschließen. Jeder Eintrag braucht eine einzeilige Beschreibung im Testfall,")
z.append("dann verschwindet er hier von selbst.")
z.append("")
for katalog in kataloge:
k = zaehle(katalog.faelle)
offen = k["gesamt"] - k["mit_docstring"]
z.append(f"### `{katalog.paket}` {offen} von {k['gesamt']} Testfällen ohne Beschreibung")
z.append("")
if not offen:
z.append("Keine Lücken.")
z.append("")
continue
for datei in katalog.dateien:
luecken = [f for f in datei.faelle if not f.beschreibung]
if not luecken:
continue
hinweis = "" if datei.beschreibung else " auch der Modul-Docstring fehlt"
z.append(f"#### `{datei.anzeigepfad}` ({len(luecken)} von {len(datei.faelle)}){hinweis}")
z.append("")
for fall in luecken:
z.append(f"- `{fall.bezeichnung}`")
z.append("")
return "\n".join(z) + "\n"
def melde_unterschied(vorhanden: bytes, erzeugt: str, ziel: str) -> None:
"""Kurzmeldung, worin sich Datei und erzeugter Stand unterscheiden."""
alt = vorhanden.decode("utf-8", errors="replace").splitlines()
neu = erzeugt.splitlines()
hinzu = entfernt = 0
erste = ""
for zeile in difflib.unified_diff(alt, neu, lineterm="", n=0):
if zeile.startswith("+++") or zeile.startswith("---") or zeile.startswith("@@"):
continue
if zeile.startswith("+"):
hinzu += 1
elif zeile.startswith("-"):
entfernt += 1
if not erste:
erste = zeile[:160]
print(f"{ziel} ist nicht aktuell: {hinzu} Zeilen neu, {entfernt} Zeilen entfallen.")
if erste:
print(f" erste Abweichung: {erste}")
print(" Neu erzeugen mit: python scripts/testkatalog.py")
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("pakete", nargs="*", help="Paketnamen (Standard: alle mit tests=true)")
ap.add_argument("--pruefen", action="store_true",
help="nichts schreiben; Exit-Code 1, wenn die vorhandene Datei nicht dem erzeugten Stand entspricht")
ap.add_argument("--ausgabe", type=Path, default=AUSGABE_DEFAULT, help="Zieldatei (Standard: docs/tests/testkatalog.md)")
ap.add_argument("--baseline-datei", type=Path, default=BASELINE_DEFAULT)
args = ap.parse_args()
manifest = lade_manifest()
repos = {r["paket"]: r for r in manifest["repos"] if r.get("paket")}
testbare = [p for p, r in repos.items() if r.get("tests")]
gewuenscht = args.pakete or testbare
unbekannt = [p for p in gewuenscht if p not in repos]
if unbekannt:
print(f"Unbekannte Pakete: {unbekannt}. Bekannt: {sorted(repos)}")
return 2
# Reihenfolge aus workspace.toml übernehmen das hält die Ausgabe stabil.
reihenfolge = [p for p in repos if p in set(gewuenscht)]
rot_je_paket, baseline_kurz = lies_baseline(args.baseline_datei)
if not baseline_kurz:
print(f"Keine Baseline unter {args.baseline_datei} alle Testfälle werden als grün geführt.")
kataloge: list[Paketkatalog] = []
for paket in reihenfolge:
if not repos[paket].get("tests"):
print(f"[{paket}] laut workspace.toml ohne Tests übersprungen")
continue
katalog = sammle_paket(paket, repos[paket])
if katalog is None:
print(f"[{paket}] kein Ordner {repos[paket]['name']}/tests übersprungen")
continue
setze_status(katalog, rot_je_paket.get(paket, set()))
k = zaehle(katalog.faelle)
print(f"[{paket}] {len(katalog.dateien)} Testdateien, {k['gesamt']} Testfälle "
f"({k['gruen']} grün, {k['rot']} rot, {k['uebersprungen']} übersprungen, "
f"{k['mit_docstring']} mit Docstring)")
if not katalog.dateien:
print(f" Keine Datei tests/test_*.py gefunden.")
kataloge.append(katalog)
if not kataloge:
print("Keine Pakete mit Testdateien gefunden nichts zu tun.")
return 2
text = erzeuge_markdown(kataloge, args.baseline_datei, baseline_kurz)
ziel = args.ausgabe
anzeige = ziel.relative_to(ROOT).as_posix() if ziel.is_relative_to(ROOT) else str(ziel)
if args.pruefen:
if not ziel.exists():
print(f"{anzeige} fehlt. Neu erzeugen mit: python scripts/testkatalog.py")
return 1
if ziel.read_bytes() == text.encode("utf-8"):
print(f"{anzeige} ist aktuell.")
return 0
melde_unterschied(ziel.read_bytes(), text, anzeige)
return 1
ziel.parent.mkdir(parents=True, exist_ok=True)
ziel.write_text(text, encoding="utf-8", newline="\n")
print(f"Testkatalog geschrieben: {anzeige} ({len(text.splitlines())} Zeilen)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+103
View File
@@ -0,0 +1,103 @@
# Manifest des AG_QGIS-Workspace.
# Wird von scripts/setup_workspace.py und scripts/run_tests.py gelesen (Python >= 3.11, tomllib).
# Repo-Name (Gitea) und Paketname (Ordner im QGIS-Plugin-Verzeichnis) sind NICHT identisch.
[workspace]
name = "AG_QGIS"
gitea = "https://entwicklung.flurneuordnung-sachsen.de"
org = "AG_QGIS"
arbeitsbranch = "unstable"
qgis_min = "3.40"
python_min = "3.12"
# rolle: plattform | fachplugin | geruest | platzhalter | infrastruktur | daten
# tests: true = tests/ mit unittest-Suite vorhanden (Mock-Modus ohne QGIS lauffähig)
# abhaengigkeiten: Pakete, die zur Laufzeit/Testzeit importiert werden
[[repos]]
name = "Plugin_SN_Basis"
paket = "sn_basis"
rolle = "plattform"
branch = "unstable"
tests = true
abhaengigkeiten = []
[[repos]]
name = "Plugin_SN_Verfahrensgebiet"
paket = "sn_verfahrensgebiet"
rolle = "fachplugin"
branch = "unstable"
tests = true
abhaengigkeiten = ["sn_basis"]
[[repos]]
name = "Plugin_SN_Plan41"
paket = "sn_plan41"
rolle = "fachplugin"
branch = "unstable"
tests = true
abhaengigkeiten = ["sn_basis"]
[[repos]]
name = "Plugin_SN_Widmung"
paket = "sn_widmung"
rolle = "geruest"
branch = "unstable"
tests = false
abhaengigkeiten = ["sn_basis"]
[[repos]]
name = "Plugin_SN_FLT"
paket = "sn_flt"
rolle = "platzhalter"
branch = "unstable"
tests = false
abhaengigkeiten = ["sn_basis"]
[[repos]]
name = "Plugin_SN_Topografie"
paket = "sn_topografie"
rolle = "platzhalter"
branch = "unstable"
tests = false
abhaengigkeiten = ["sn_basis"]
[[repos]]
name = "Plugin_SN_Template"
paket = "sn_template"
rolle = "platzhalter"
branch = "unstable"
tests = false
abhaengigkeiten = ["sn_basis"]
[[repos]]
name = "Plugin_Test_Action"
paket = "sn_test_action"
rolle = "infrastruktur"
branch = "unstable"
tests = false
abhaengigkeiten = []
[[repos]]
name = "Repository"
paket = ""
rolle = "infrastruktur"
branch = "main"
tests = false
abhaengigkeiten = []
[[repos]]
name = "Linkliste"
paket = ""
rolle = "daten"
branch = "main"
tests = false
abhaengigkeiten = []
[[repos]]
name = "Defaults"
paket = ""
rolle = "daten"
branch = "main"
tests = false
abhaengigkeiten = []