feat(sources): strukturierte Klassifikation (Politik/Medientyp/Reliability/Alignments)

- Neue sources-Spalten: political_orientation (7+2 Stufen), media_type (20),
  reliability (5+1), state_affiliated, country_code, classification_source,
  classified_at sowie proposed_*-Spalten fuer LLM-Vorschlaege.
- Neue source_alignments-Tabelle fuer Mehrfach-Tagging geopolitischer Naehe
  (prorussisch, proiranisch, prowestlich, ...).
- API-Filter: ?political_orientation, ?media_type, ?reliability,
  ?state_affiliated, ?alignment.
- create/update_source nehmen alignments[] entgegen und setzen
  classification_source automatisch auf 'manual' bei Klassifikations-Edits.

Backwards-kompatibel: bestehendes bias/language/category bleibt unveraendert,
Default fuer Bestandsquellen ist classification_source = 'legacy'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Code
2026-05-07 18:21:45 +00:00
Ursprung 7f220a9b65
Commit f8e2f73bc0
3 geänderte Dateien mit 279 neuen und 41 gelöschten Zeilen

Datei anzeigen

@@ -158,7 +158,31 @@ CREATE TABLE IF NOT EXISTS sources (
article_count INTEGER DEFAULT 0,
last_seen_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tenant_id INTEGER REFERENCES organizations(id)
tenant_id INTEGER REFERENCES organizations(id),
language TEXT,
bias TEXT,
political_orientation TEXT DEFAULT 'na',
media_type TEXT DEFAULT 'sonstige',
reliability TEXT DEFAULT 'na',
state_affiliated INTEGER DEFAULT 0,
country_code TEXT,
classification_source TEXT DEFAULT 'legacy',
classified_at TIMESTAMP,
proposed_political_orientation TEXT,
proposed_media_type TEXT,
proposed_reliability TEXT,
proposed_state_affiliated INTEGER,
proposed_country_code TEXT,
proposed_alignments_json TEXT,
proposed_confidence REAL,
proposed_reasoning TEXT,
proposed_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS source_alignments (
source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
alignment TEXT NOT NULL,
PRIMARY KEY (source_id, alignment)
);
CREATE TABLE IF NOT EXISTS notifications (
@@ -611,6 +635,57 @@ async def init_db():
await db.execute("ALTER TABLE sources ADD COLUMN tenant_id INTEGER REFERENCES organizations(id)")
await db.commit()
# Migration: language + bias (Freitext, schon laenger im Einsatz, Schema-Lueck schliessen)
if "language" not in src_columns:
await db.execute("ALTER TABLE sources ADD COLUMN language TEXT")
await db.commit()
if "bias" not in src_columns:
await db.execute("ALTER TABLE sources ADD COLUMN bias TEXT")
await db.commit()
# Migration: strukturierte Klassifikations-Spalten fuer sources
for col, ddl in [
("political_orientation", "ALTER TABLE sources ADD COLUMN political_orientation TEXT DEFAULT 'na'"),
("media_type", "ALTER TABLE sources ADD COLUMN media_type TEXT DEFAULT 'sonstige'"),
("reliability", "ALTER TABLE sources ADD COLUMN reliability TEXT DEFAULT 'na'"),
("state_affiliated", "ALTER TABLE sources ADD COLUMN state_affiliated INTEGER DEFAULT 0"),
("country_code", "ALTER TABLE sources ADD COLUMN country_code TEXT"),
("classification_source", "ALTER TABLE sources ADD COLUMN classification_source TEXT DEFAULT 'legacy'"),
("classified_at", "ALTER TABLE sources ADD COLUMN classified_at TIMESTAMP"),
("proposed_political_orientation", "ALTER TABLE sources ADD COLUMN proposed_political_orientation TEXT"),
("proposed_media_type", "ALTER TABLE sources ADD COLUMN proposed_media_type TEXT"),
("proposed_reliability", "ALTER TABLE sources ADD COLUMN proposed_reliability TEXT"),
("proposed_state_affiliated", "ALTER TABLE sources ADD COLUMN proposed_state_affiliated INTEGER"),
("proposed_country_code", "ALTER TABLE sources ADD COLUMN proposed_country_code TEXT"),
("proposed_alignments_json", "ALTER TABLE sources ADD COLUMN proposed_alignments_json TEXT"),
("proposed_confidence", "ALTER TABLE sources ADD COLUMN proposed_confidence REAL"),
("proposed_reasoning", "ALTER TABLE sources ADD COLUMN proposed_reasoning TEXT"),
("proposed_at", "ALTER TABLE sources ADD COLUMN proposed_at TIMESTAMP"),
]:
if col not in src_columns:
await db.execute(ddl)
await db.commit()
if any(c not in src_columns for c in ("political_orientation", "media_type", "reliability")):
logger.info("Migration: Klassifikations-Spalten zu sources hinzugefuegt")
# Migration: source_alignments-Tabelle (Mehrfach-Tags fuer geopolitische Naehe)
cursor = await db.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='source_alignments'"
)
if not await cursor.fetchone():
await db.executescript(
"""
CREATE TABLE source_alignments (
source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
alignment TEXT NOT NULL,
PRIMARY KEY (source_id, alignment)
);
CREATE INDEX IF NOT EXISTS idx_source_alignments_alignment ON source_alignments(alignment);
"""
)
await db.commit()
logger.info("Migration: source_alignments-Tabelle erstellt")
# Migration: tenant_id fuer notifications
cursor = await db.execute("PRAGMA table_info(notifications)")
notif_columns = [row[1] for row in await cursor.fetchall()]