154 lines
6.6 KiB
Markdown
154 lines
6.6 KiB
Markdown
|
|
# 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.
|