From d5b8cf4573636513abe074fc3f296a8e426fc5d0 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Sun, 10 May 2026 13:28:56 +0200 Subject: [PATCH] docs(adr 0015) + scripts: 2.0-Cut + Citation-Cross-Block-Fix dokumentieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0015 fixiert die zwei strukturellen Entscheidungen vom 2.0-Cut: - Prod-Deploy ueber sauberen git-Checkout statt Tar-Upload (loest ADR 0004 in Teilen ab) - Reconstruct_zitate-Zwei-Pass: Zitate werden ueber beide Bloecke hinweg klassifiziert, dann erst geschrieben — Cross-Block-Move statt nur quelle-Korrektur scripts/migrate-zitate-blocks.py: idempotentes String-basiertes Migrations-Skript fuer bestehende Records mit altem Bug-Stand. Nicht LLM-abhaengig, deterministisch. Beim 2.0-Cut auf 22 Assessments angewendet (26 Zitate verschoben). --- ...r-git-checkout-und-citation-cross-block.md | 109 ++++++++++++++++++ docs/adr/index.md | 1 + scripts/migrate-zitate-blocks.py | 91 +++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 docs/adr/0015-prod-deploy-ueber-git-checkout-und-citation-cross-block.md create mode 100755 scripts/migrate-zitate-blocks.py diff --git a/docs/adr/0015-prod-deploy-ueber-git-checkout-und-citation-cross-block.md b/docs/adr/0015-prod-deploy-ueber-git-checkout-und-citation-cross-block.md new file mode 100644 index 0000000..95e525c --- /dev/null +++ b/docs/adr/0015-prod-deploy-ueber-git-checkout-und-citation-cross-block.md @@ -0,0 +1,109 @@ +# 0015 — Prod-Deploy als sauberer git-Checkout + Citation-Block-Reklassifikation + +| | | +|---|---| +| **Status** | accepted | +| **Datum** | 2026-05-10 | +| **Supersedes** | (Anteile von) ADR 0004 (Tar-Upload-Workflow) | +| **Refs** | v2.0.0-Tag, release/2.0-Branch, Commits 770d890–88211c5 | + +## Kontext + +Beim 1.x → 2.0-Release-Cut wurden zwei strukturelle Probleme sichtbar, die +in einer Entscheidung zusammenhängen. + +**Problem 1 — Prod-Repo-Mess.** Der prod-Pfad `/opt/gwoe-antragspruefer` +wurde seit April per Tar-Upload aktualisiert (siehe ADR 0004). Effekt: +Filesystem-Stand war neuer als der HEAD-Commit (4fbdc15 vom 10. April), +19 Files modifiziert, 30+ untracked. `git pull` war nicht mehr möglich, +ohne den Stand zu zerstören. Dev wurde parallel sauber per `git pull` aus +`main` deployed (Cron `auto-deploy.sh`). Die zwei Workflows divergierten. + +**Problem 2 — Citation-Block-Misattribution.** `reconstruct_zitate` (ADR +0001 / Issue #60) hat bei einem Cross-Kind-Fallback-Match nur die +``quelle`` des Zitats korrigiert, das Zitat aber im ursprünglichen Block +belassen. Folge: Im wahlprogramm-Block standen Zitate aus +Grundsatzprogrammen, die quelle stimmte zwar, aber die Block-Zuordnung +suggerierte etwas Falsches. Reproduziert auf Antrag 18/18246 (NRW), +Bewertung GRÜNE. + +## Optionen + +### Workflow 1 — Beide Probleme über separate Migrationen lösen + +**A.1 (Deploy):** Einmaliger Cut mit frischem `git clone`, dann beidseitig +git-pull-basiert. Tar-Upload-Pfad obsolet. + +**A.2 (Deploy):** Tar-Upload retten, indem man HEAD nachzieht und +.gitignore erweitert, sodass Tar-überschriebene Files nicht als Diff +auftauchen. + +**B.1 (Citations):** Zwei-Pass-Verarbeitung in `reconstruct_zitate` — +erst klassifizieren über beide Blöcke hinweg, dann schreiben. Plus +einmaliges String-basiertes Migrations-Skript für die bestehenden 117 +Records. + +**B.2 (Citations):** Ganze Bewertungen neu generieren (LLM-Call), +sobald der Code-Fix lebt. + +## Entscheidung + +**Deploy: A.1.** Einmaliger sauberer git-clone auf prod, danach beide +Umgebungen identisch via `git pull`. Begründung: Der Tar-Mess wäre nie +sauber zu reparieren gewesen, ohne irgendwo eine Annahme zu treffen, was +"echt" ist. Ein frischer Clone setzt den Stand definitiv. Alle Volumes +(`data/`, `reports/`, `backups/`) bleiben unangetastet. + +**Citations: B.1.** Code-Fix mit Zwei-Pass plus einmaliges Migrations-Skript. +Begründung: B.2 wäre nicht-deterministisch (LLM-Fluktuation), würde Tokens +verbrennen und liefert keine bessere Garantie als das deterministische +String-Match auf "Grundsatzprogramm" vs. "Wahlprogramm" im quelle-Label. +22 Assessments wurden migriert, 26 Zitate verschoben. + +## Konsequenzen + +### Deploy + +- `/opt/gwoe-antragspruefer` ist seit dem Cut ein sauberer Checkout von + `release/2.0`. +- Nächster Standard-Deploy: `./scripts/deploy.sh` (Branch-Guard, + Pre-flight, Pre-Deploy-Backup, Health-Check, Uptime-Kuma). +- Major-Cuts: `./scripts/major-release-cut.sh ` — + inkl. Bundle-Fallback bei Gitea-Korruption (war beim 2.0-Cut nötig). +- Alter Pfad als `/opt/gwoe-antragspruefer-YYYYMMDD-HHMMSS-archive` + archiviert. +- ADR 0004 ist in Teilen abgelöst, der Tar-Upload-Pfad gilt nicht mehr. + +### Citation-Binding + +- `reconstruct_zitate` klassifiziert pro Fraktion über beide Blöcke + hinweg, schreibt erst danach in die jeweils passenden Blöcke. +- Test: ``tests/test_embeddings.py::TestReconstructZitate::test_zitat_aus_grundsatzprogramm_landet_im_parteiprogramm_block`` + reproduziert den 18/18246-Fall. +- Migrations-Skript ``scripts/migrate-zitate-blocks.py`` ist idempotent + und kann jederzeit re-run werden, falls weitere Records aus älterem + Code-Stand reinkommen. + +### DB-Wipe-Liste beim Major-Cut + +`scripts/major-release-cut.sh` enthält die Liste der Tabellen, die beim +Cut geleert werden: + +- `assessments`, `assessment_versions` — Bewertungen (Schema-Drift möglich) +- `presse_drafts`, `news_articles` — Cache-Daten +- `auto_rate_runs`, `jobs` — Queue-/Cron-Tracking +- `monitoring_scans`, `monitoring_daily_summary` — Live-Metriken +- `auth_bypass_uses`, `comments`, `merkliste`, `bookmarks`, + `email_subscriptions`, `votes` + +**Erhalten bleiben:** + +- `plenum_vote_results` — kostenlose Re-Ingest-Daten (PDFs werden vom + Server geholt, kein LLM nötig) +- `abgeordnetenwatch_votes`, `abgeordnetenwatch_polls` — re-fetchbar via + `sync_abgeordnetenwatch.py`, aber zeitaufwändig +- `embeddings.db` — extern als Volume, separat gehandhabt + +Beim 3.0-Cut diese Liste prüfen: falls neue User-Daten-Tabellen +hinzukommen (z.B. erweiterte Bookmarks), gehört die Wipe-Entscheidung +dort explizit gemacht. diff --git a/docs/adr/index.md b/docs/adr/index.md index 2a7276d..069ed86 100644 --- a/docs/adr/index.md +++ b/docs/adr/index.md @@ -31,6 +31,7 @@ und Konsequenzen. Format inspiriert von [Michael Nygard](https://cognitect.com/b | [0012](0012-debug-auth-token-bypass.md) | DEBUG_AUTH_TOKEN-Bypass für Diagnose-Sessions auf dev | accepted | 2026-05-06 | | [0013](0013-programme-legislaturen-zeitpunktige-bewertung.md) | Programme + Legislaturen mit zeitpunktiger Bewertung | accepted | 2026-05-08 | | [0014](0014-tour-system-mit-elevenlabs-voice.md) | Tour-System mit ElevenLabs-Voice + Web-Speech-Fallback | accepted | 2026-05-09 | +| [0015](0015-prod-deploy-ueber-git-checkout-und-citation-cross-block.md) | Prod-Deploy als sauberer git-Checkout + Citation-Block-Reklassifikation | accepted | 2026-05-10 | ## Wann ADR, wann nicht diff --git a/scripts/migrate-zitate-blocks.py b/scripts/migrate-zitate-blocks.py new file mode 100755 index 0000000..49b7bb8 --- /dev/null +++ b/scripts/migrate-zitate-blocks.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Retro-Migration: Zitate, deren `quelle`-Label auf das jeweils andere +Programm verweist, wandern in den passenden Block. + +Hintergrund: Vor Commit 4b9c65c hat ``reconstruct_zitate`` bei einem +Cross-Kind-Fallback-Match nur die ``quelle`` korrigiert, das Zitat aber +im urspruenglichen Block belassen. Folge: Im wahlprogramm-Block standen +auch Zitate aus dem Grundsatzprogramm. Der Code-Fix korrigiert das fuer +neue Bewertungen — dieses Skript korrigiert die bestehenden Records. + +Heuristik (string-basiert, ohne LLM/Re-Bewertung): +- quelle enthaelt 'Grundsatzprogramm' (case-insensitive) → parteiprogramm-Block +- quelle enthaelt 'Wahlprogramm' (ohne 'Grundsatz') → wahlprogramm-Block +- sonst: bleibt wo es ist + +Idempotent: doppelter Lauf bewegt nichts mehr. + +Usage (aus dem Container): + docker exec gwoe-antragspruefer python /app/scripts/migrate-zitate-blocks.py # dry-run + docker exec gwoe-antragspruefer python /app/scripts/migrate-zitate-blocks.py --apply # commit +""" +import json +import sqlite3 +import sys + +DRY_RUN = "--apply" not in sys.argv + +db = sqlite3.connect("/app/data/gwoe-antraege.db") +db.row_factory = sqlite3.Row +moved = 0 +touched_assessments = 0 + +rows = db.execute( + "SELECT drucksache, bundesland, wahlprogramm_scores FROM assessments " + "WHERE wahlprogramm_scores IS NOT NULL" +).fetchall() + +for r in rows: + raw = r["wahlprogramm_scores"] + if not raw: + continue + try: + wps = json.loads(raw) + except Exception: + continue + + changed = False + for wp in (wps or []): + wp_blk = wp.get("wahlprogramm") or {} + pp_blk = wp.get("parteiprogramm") or {} + wp_zitate = list(wp_blk.get("zitate") or []) + pp_zitate = list(pp_blk.get("zitate") or []) + + new_wp, new_pp = [], [] + for z in wp_zitate: + q = (z.get("quelle") or "").lower() + if "grundsatzprogramm" in q: + new_pp.append(z) + moved += 1 + else: + new_wp.append(z) + for z in pp_zitate: + q = (z.get("quelle") or "").lower() + if "wahlprogramm" in q and "grundsatz" not in q: + new_wp.append(z) + moved += 1 + else: + new_pp.append(z) + + if new_wp != wp_zitate or new_pp != pp_zitate: + wp_blk["zitate"] = new_wp + wp["wahlprogramm"] = wp_blk + pp_blk["zitate"] = new_pp + wp["parteiprogramm"] = pp_blk + changed = True + + if changed: + touched_assessments += 1 + if not DRY_RUN: + db.execute( + "UPDATE assessments SET wahlprogramm_scores=? WHERE drucksache=?", + (json.dumps(wps, ensure_ascii=False), r["drucksache"]), + ) + +if not DRY_RUN: + db.commit() + +print(f"DRY_RUN={DRY_RUN}") +print(f"Zitate verschoben: {moved}") +print(f"Assessments betroffen: {touched_assessments}") +db.close()