gwoe-antragspruefer/docs/adr/0008-ddd-lightweight-migration.md

154 lines
6.6 KiB
Markdown
Raw Permalink Normal View History

# 0008 — DDD-Lightweight-Migration
| | |
|---|---|
| **Status** | accepted |
| **Datum** | 2026-04-20 |
| **Refs** | Issue #136, `docs/analysen/ddd-bewertung.md` Kap. 7 + 10 |
| **Related** | ADR 0001 (Citation-Binding), ADR 0002 (Adapter-Architektur) |
## Kontext
Nach 18 Monaten Entwicklung verteilt der Code sich auf 23 Dateien in
einem flachen `app/`-Verzeichnis (LOC-Inventar in `ddd-bewertung.md`
Kap. 1.1). Die Analyse in diesem Dokument zeigt fünf konkrete
DDD-Verletzungen (AE, ebenda Kap. 1.2):
- **A** Infrastructure-Leak: `analyze_antrag` instanziiert direkt einen
`AsyncOpenAI`-Client.
- **B** Retry-Loop, JSON-Parsing und Pydantic in einer 60-Zeilen-Kaskade.
- **C** Anämisches Modell: `MatrixEntry` / `Assessment` tragen keine
Invarianten; die Score-Cap-Regel („rating ≤ -4 ⇒ score ≤ 3") lebt nur
im LLM-System-Prompt.
- **D** Kein Repository-Pattern: `database.py` (909 LOC) wird von sechs
Modulen direkt aufgerufen.
- **E** Adapter-Contract nur informell (abhandelbar in späterem PR).
Issue #136 fragte nach einer DDD-Umstellung. Die Analyse zeigte zwei
realistische Optionen (Kap. 7 des Bewertungsdokuments).
## Optionen
### Option A — Voll-DDD mit Package-Split
Getrennte Packages `antragsbewertung/`, `parlamentsintegration/`,
`wahlprogramm_wissensbasis/`, `publikation/`, `benutzer_abo/`,
`monitoring/`, jeweils mit `domain/application/infrastructure/`-
Struktur. Ports und Adapter überall, Anti-Corruption-Layer pro BL.
**Vorteile:** Maximale Testbarkeit, klare Bounded-Contexts, mehrere
Devs können parallel arbeiten, Ubiquitous-Language konsequent.
**Nachteile:** 4-8 Wochen netto Umbau, Test-Suite muss migriert werden,
hohes Regressionsrisiko während der Migration, für Solo-Projekt dieser
Größe ein schlechtes Kosten-Nutzen-Verhältnis.
### Option B — DDD-Lightweight
Drei gezielte DDD-Prinzipien *ohne* Package-Split: Repository-Pattern,
LLM-Port, Domain-Verhalten auf den bestehenden Pydantic-Modellen.
Dateien bleiben flach in `app/` liegen, nur drei neue Unterordner:
`repositories/`, `ports/`, `adapters/`.
**Vorteile:** Adressiert die schmerzhaftesten Punkte (A, B, C, D) in
5-8 Tagen. Keine API-Breaking-Changes. Callsites müssen nicht alle
gleichzeitig migrieren — die Repositories delegieren an die alten
`database.py`-Funktionen, alte Calls laufen weiter.
**Nachteile:** Parlaments-Adapter bleiben als eine 3397-LOC-Datei
(`parlamente.py`). Ubiquitous-Language bleibt halb Deutsch / halb
Englisch. Bounded-Contexts sind nur konzeptuell, nicht physisch
separiert.
## Entscheidung
**Option B**. Konkret:
1. **Repository-Pattern** für `assessments`, `assessment_versions` und
`email_subscriptions``app/repositories/`, drei Module.
2. **LLM-Port + Qwen-Adapter**`app/ports/llm_bewerter.py`,
`app/adapters/qwen_bewerter.py`. `analyze_antrag` nimmt ein
`LlmBewerter`-Argument (Default: `QwenBewerter()`), direkt oder über
FastAPI-`Depends`.
3. **Domain-Verhalten** auf `Assessment` und `MatrixEntry`:
`verletzt_score_cap()`, `ist_ablehnung()`,
`ist_uneingeschraenkt_unterstuetzend()`, `hat_fundamental_kritisches_feld()`,
`MatrixEntry.ist_fundamental_kritisch()`, `MatrixEntry.to_symbol()`.
`analyzer.py` loggt bei Verletzung der Score-Cap-Invariante eine
Warning, wirft aber nicht — das LLM soll lernen, der Produktiv-
betrieb soll nicht brechen.
Nicht Teil dieser Entscheidung (Folge-PRs):
- Migration aller `database.*`-Callsites in `main.py` (21 Stellen)
auf Repository-`Depends`. Der Repository-Layer ist dafür
bereitgelegt; die Umstellung selbst ist mechanisch, aber wegen
1746 LOC in einer Datei mit Merge-Konflikt-Risiko verbunden.
- `ZitatVerifier`-Port (Tag 5 der Roadmap).
- Frozen-Dataclass-Domain-Objekte in `app/domain/` (Tag 6/7).
- Bounded-Context-Package-Split (explizit ausgeschlossen).
## Konsequenzen
### Positiv
- **Tests ohne OpenAI-Stub**. `conftest.py::_stub("openai")` ist nicht
mehr zwingend; Tests reichen einen `FakeLlmBewerter`-Objekt. Der
umgebaute `test_bug_regressions.py::test_analyzer_user_prompt_...`
ist exakt dieses Muster.
- **Adapter-Wechsel trivial**. Ein zweiter LLM-Provider (Claude,
Gemini) ist eine neue Klasse in `app/adapters/`, ohne Änderung in
`analyzer.py`.
- **Server-seitige Score-Cap-Erkennung**. Verstöße gegen die
fundamental-kritisch-Regel aus dem System-Prompt werden jetzt in
Logs sichtbar (`analyzer.py`), können in einem Folge-Issue zu
harten Rejects hochgezogen werden, wenn die Qualitäts-Daten stabil
aussehen.
- **Test-Fakes für DB-Zugriff**. Application-Logik-Tests brauchen
keine SQLite-Datei mehr; `InMemoryAntragRepository` reicht. Die
76 neuen Tests in `tests/test_*_repository.py`, `test_llm_bewerter.py`
und `test_domain_behavior.py` demonstrieren das.
### Negativ
- **Zwei Zugangswege zur DB parallel**. Solange `main.py` noch
`database.get_assessment` direkt aufruft, gibt es doppelten Zugriff.
Folge-PR räumt auf.
- **`analyzer.py` importiert noch `check_missing_programmes` direkt**.
Auch das ist Infrastructure, liegt aber außerhalb der drei
migrierten Schnitte. Nächster Schritt bei Bedarf.
- **Logger-Warning ist weich**. Wer die Logs nicht liest, bemerkt
Score-Cap-Verletzungen nicht. Ein Dashboard-Panel auf
`logger.name == "app.analyzer"` + Severity ≥ WARNING gehört in
die Monitoring-Erweiterung (#135-Folge).
### Folgen für andere ADRs
- **ADR 0001** (Citation-Binding) bleibt unverändert gültig — die
Post-LLM-Rekonstruktion läuft weiter nach `bewerter.bewerte()`.
- **ADR 0002** (Adapter-Architektur für Parlamente) bleibt unverändert;
die 17 Adapter in `parlamente.py` werden hier *nicht* in
`app/adapters/` gezogen. Ein Folge-ADR (0009?) könnte den Package-
Split für Parlaments-Adapter beschließen, wenn die Datei über 5000
LOC wächst (wie in ADR 0002 angekündigt).
- **ADR 0003** (Citation-Property-Tests) bleibt unverändert; die
Tests hängen an der Assessment-Schnittstelle, die sich hier nicht
geändert hat.
## Nach-Migration-Regeln
Zur Orientierung bei neuen Beiträgen:
1. **Kein direktes `database.*` in neuer Application-Logik.** Immer
über das passende Repository.
2. **Kein `AsyncOpenAI` außerhalb `app/adapters/qwen_bewerter.py`.**
Neue LLM-Provider bekommen einen eigenen Adapter im gleichen
Ordner.
3. **Invarianten auf dem Domain-Modell, nicht im Prompt.** Wenn eine
Regel im LLM-System-Prompt steht und vom LLM potenziell verletzt
werden kann, gehört eine Prüfmethode auf `Assessment` oder
`MatrixEntry` und ein Warning-Log in `analyzer.py`.
4. **Tests mit `InMemory*Repository` und `FakeLlmBewerter`.** Keine
Monkeypatches auf `app.analyzer.AsyncOpenAI` mehr; keine
temporären SQLite-Dateien für Unit-Tests.