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:
@@ -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 1–2. 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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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/
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 03–04/2026 (92 Commits in `Plugin_SN_Basis`),
|
||||
seitdem gleichmäßig 10–11 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 1–2.
|
||||
|
||||
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.
|
||||
@@ -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` | 32–98 | 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 |
|
||||
@@ -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-<kanal>.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).
|
||||
@@ -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).
|
||||
@@ -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 0001–0005 (`docs/entscheidungen/`).
|
||||
3. Maintainer je Repository.
|
||||
4. Bestätigung der ADRs 0006–0009 (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.
|
||||
@@ -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 1–3 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
|
||||
@@ -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 0006–0009 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/`.
|
||||
@@ -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 73–260).
|
||||
|
||||
**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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 0006–0009.
|
||||
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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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 `---`.
|
||||
@@ -0,0 +1,3 @@
|
||||
@AGENTS.md
|
||||
|
||||
Beim Arbeiten im Workspace (`../AGENTS.md` vorhanden) gelten dessen Regeln und Skills zusätzlich.
|
||||
@@ -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
|
||||
@@ -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.>
|
||||
@@ -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“)
|
||||
@@ -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.>
|
||||
@@ -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>
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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` |
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
@@ -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 = []
|
||||
Reference in New Issue
Block a user