docs(adr 0015) + scripts: 2.0-Cut + Citation-Cross-Block-Fix dokumentieren
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).
This commit is contained in:
parent
88211c5708
commit
d5b8cf4573
@ -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 <tag> <branch>` —
|
||||||
|
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.
|
||||||
@ -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 |
|
| [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 |
|
| [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 |
|
| [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
|
## Wann ADR, wann nicht
|
||||||
|
|
||||||
|
|||||||
91
scripts/migrate-zitate-blocks.py
Executable file
91
scripts/migrate-zitate-blocks.py
Executable file
@ -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()
|
||||||
Loading…
Reference in New Issue
Block a user