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>
6.6 KiB
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_antraginstanziiert direkt einenAsyncOpenAI-Client. - B Retry-Loop, JSON-Parsing und Pydantic in einer 60-Zeilen-Kaskade.
- C Anämisches Modell:
MatrixEntry/Assessmenttragen 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:
- Repository-Pattern für
assessments,assessment_versionsundemail_subscriptions—app/repositories/, drei Module. - LLM-Port + Qwen-Adapter —
app/ports/llm_bewerter.py,app/adapters/qwen_bewerter.py.analyze_antragnimmt einLlmBewerter-Argument (Default:QwenBewerter()), direkt oder über FastAPI-Depends. - Domain-Verhalten auf
AssessmentundMatrixEntry:verletzt_score_cap(),ist_ablehnung(),ist_uneingeschraenkt_unterstuetzend(),hat_fundamental_kritisches_feld(),MatrixEntry.ist_fundamental_kritisch(),MatrixEntry.to_symbol().analyzer.pyloggt 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 inmain.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 einenFakeLlmBewerter-Objekt. Der umgebautetest_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 inanalyzer.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;
InMemoryAntragRepositoryreicht. Die 76 neuen Tests intests/test_*_repository.py,test_llm_bewerter.pyundtest_domain_behavior.pydemonstrieren das.
Negativ
- Zwei Zugangswege zur DB parallel. Solange
main.pynochdatabase.get_assessmentdirekt aufruft, gibt es doppelten Zugriff. Folge-PR räumt auf. analyzer.pyimportiert nochcheck_missing_programmesdirekt. 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.pywerden hier nicht inapp/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:
- Kein direktes
database.*in neuer Application-Logik. Immer über das passende Repository. - Kein
AsyncOpenAIaußerhalbapp/adapters/qwen_bewerter.py. Neue LLM-Provider bekommen einen eigenen Adapter im gleichen Ordner. - 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
AssessmentoderMatrixEntryund ein Warning-Log inanalyzer.py. - Tests mit
InMemory*RepositoryundFakeLlmBewerter. Keine Monkeypatches aufapp.analyzer.AsyncOpenAImehr; keine temporären SQLite-Dateien für Unit-Tests.