# 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 (A–E, 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.