gwoe-antragspruefer/docs/adr/0008-ddd-lightweight-migration.md
Dotty Dotter 8f0f6d6e32 refactor(#136): DDD-Lightweight Tag 1-4 (Ports, Adapter, Repositories, Domain-Verhalten)
ADR 0008: Lightweight-Migration ohne Package-Split

- ports/llm_bewerter.py: Protocol + LlmRequest-Dataclass
- adapters/qwen_bewerter.py: Qwen/DashScope-Adapter mit Retry-Loop
- repositories/{antrag,bewertung,abonnement}_repository.py: Protocol + Sqlite-Impl + InMemory-Fake
- analyzer.py refactored: nimmt Optional[LlmBewerter], AsyncOpenAI-Import raus
- models.py: 5 Domain-Methoden auf Bewertung/MatrixEntry
  (ist_ablehnung, hat_fundamental_kritisches_feld, verletzt_score_cap, ...)
- analyzer loggt WARNING wenn LLM Score-Cap-Invariante verletzt

Folge-PR: Callsite-Migration in main.py (~21 direkte database.*-Aufrufe)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:16 +02:00

154 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.