fix(faktencheck): Belegtiefe zaehlt Medienhaeuser, Widerspruch ist keine Widerlegung

Block 2 der Auswertung des Ceuta-Vergleichs vom 01.08.2026. Der Faktencheck der
EU-Fassung zeigte dort drei Fehler, die alle dieselbe Wurzel haben: Der Status
behauptete eine Belegtiefe, die niemand nachgerechnet hat.

1. Die Zaehlung war kaputt. Das Muster fuer die Quellenzaehlung war
   r'https?://' ohne den Rest der Adresse, findall lieferte also eine Liste aus
   "https://"-Bruchstuecken und len(set(...)) ergab immer 1 oder 2. Daher stand
   im Bericht eine Behauptung als BESTAETIGT mit 1 Quelle, obwohl vier Haeuser
   in der Evidenz standen, und der gesamte Faktencheck der Anthropic-Fassung
   wies durchgehend 1 bis 2 Quellen aus. Die Adresse wird jetzt vollstaendig
   erfasst.

2. Gezaehlt wird nach Medienhaus, nicht nach Fundstelle. Sechs France24-Treffer,
   davon vier Videofassungen derselben Meldung, sind ein Haus. Grundlage ist die
   registrierbare Domain aus services/media_registry.py. Weiterleitungsportale
   bekommen keine gemeinsame Identitaet, hinter news.google.com koennen
   beliebige Zeitungen stecken. Nachgerechnet an Lage 53 sinken die Angaben von
   9 auf 6, von 8 auf 4 und von 7 auf 5 Belege.

3. Neue Nachbedingung. pruefe_belegtiefe stuft jeden Fakt ab, dessen Status mehr
   behauptet als vorliegt: confirmed braucht zwei, established drei unabhaengige
   Haeuser. Ein Primaerbeleg (Originalzitat im Interview, amtliche Mitteilung,
   Urteil) traegt allein, dafuer liefert das Modell jetzt "evidence_type". Damit
   verschwindet auch die Unterkonfidenz der Gegenprobe, in der ein ZDF-Interview
   als unbestaetigt galt, obwohl der Kritisierte dort selbst spricht.

4. Die Nachhak-Runde stuft "developing" nicht mehr hoch. Sie hatte die
   Behauptung "Der Ausloeser ist weiterhin unklar" auf BESTAETIGT gesetzt, weil
   developing in der Statusordnung die niedrigste Stufe ist. Eine Unklarheit
   laesst sich nicht bestaetigen. Ausserdem rechnet die Runde die Belegzahl nach
   dem Hochstufen neu und nimmt die Stufe zurueck, wenn die Belege sie nicht
   decken.

5. Widerspruch und Widerlegung sind getrennt. "contradicted" meint laut Auftrag
   Belege, die einander widersprechen, wurde aber als "Widerlegt" angezeigt. Im
   Bericht stand deshalb die sachlich zutreffende Spanne von 50.000 bis 60.000
   Grenzuebertritten als widerlegt, obwohl die Evidenz woertlich sagt "Die
   Zahlen variieren je nach Quelle". Jetzt: contradicted = "Widersprüchlich",
   der neue Status "false" = "Widerlegt" und setzt einen entkraeftenden Beleg
   voraus. Angepasst in Bericht, Oberflaeche, beiden Sprachdateien, Farben und
   Statusordnung.

6. Ein verbindlicher Regelblock haengt an jedem Faktencheck-Auftrag
   (bewertungsregeln()): Unabhaengigkeit heisst verschiedene Medienhaeuser,
   Agenturuebernahmen zaehlen einmal, unterschiedliche Zahlen verschiedener
   Erhebungsstellen sind kein Widerspruch sondern eine Spanne mit Zuordnung,
   und Aussagen ueber den Kenntnisstand ("Der Ausloeser ist unklar") sind keine
   pruefbaren Behauptungen und gehoeren nicht in den Faktencheck.

Die Aenderungen wirken auf beide Modellwege. Die Fixtures in test_eu_factcheck
liefern jetzt Belege aus mehreren Haeusern, weil eine einzelne Quelle keine
Bestaetigung mehr traegt.

Nachgerechnet an beiden gespeicherten Laeufen: kein Fakt wird durch die neue
Nachbedingung abgestuft, die Belegzahlen werden aber durchgehend ehrlicher.

Neu sind 27 Pruefungen, insgesamt laufen 156 ohne Netzzugriff und ohne Kosten.

Offen: Die Oberflaechen-Aenderungen (components.js, app.js, style.css, i18n)
sind noch nicht in den Lokal-Fork portiert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-08-01 22:01:30 +02:00
Ursprung f87d377082
Commit 3495409377
14 geänderte Dateien mit 512 neuen und 66 gelöschten Zeilen

Datei anzeigen

@@ -93,7 +93,7 @@ src/:
stage_runners.py: "Studio-Bausteine: analyze/factcheck einzeln auf vorhandenem Datenbestand, bewusst NEBEN _run_refresh gebaut" stage_runners.py: "Studio-Bausteine: analyze/factcheck einzeln auf vorhandenem Datenbestand, bewusst NEBEN _run_refresh gebaut"
researcher.py: "WebSearch-Recherche (Standard + 4-Phasen-Tiefenrecherche), Feed-/Kanal-/Account-Selektion, Keyword-Extraktion, Google-News-Suchfeeds je Sprache" researcher.py: "WebSearch-Recherche (Standard + 4-Phasen-Tiefenrecherche), Feed-/Kanal-/Account-Selektion, Keyword-Extraktion, Google-News-Suchfeeds je Sprache"
analyzer.py: "Analyse-Agent (Lagebild/Briefing, Erst- + inkrementell, Inline-Zitate, Topic-Filter, Neueste Entwicklungen, Stimmungsbild)" analyzer.py: "Analyse-Agent (Lagebild/Briefing, Erst- + inkrementell, Inline-Zitate, Topic-Filter, Neueste Entwicklungen, Stimmungsbild)"
factchecker.py: "Faktencheck (Erst/Inkrementell/Zwei-Phasen mit Haiku-Triage ab 25 Fakten), Claim-Matching, Dedup, Schutz gegen Massen-Downgrades" factchecker.py: "Faktencheck (Erst/Inkrementell/Zwei-Phasen mit Haiku-Triage ab 25 Fakten), Claim-Matching, Dedup, Schutz gegen Massen-Downgrades. Belegtiefe: zaehle_medienhaeuser zaehlt unabhaengige Verlage statt Fundstellen, pruefe_belegtiefe stuft Fakten ab, deren Status mehr Beleg behauptet als vorliegt (confirmed 2, established 3, Primaerbeleg 1), bewertungsregeln() haengt den verbindlichen Regelblock an jeden Auftrag"
geoparsing.py: "Haiku-basierte Ortsextraktion, Geocoding offline via geonamescache" geoparsing.py: "Haiku-basierte Ortsextraktion, Geocoding offline via geonamescache"
entity_extractor.py: "Netzwerkanalyse (Sonnet). ACHTUNG: aktuell ohne Aufrufer in der App, nur regenerate_relations.py im Repo-Root nutzt es" entity_extractor.py: "Netzwerkanalyse (Sonnet). ACHTUNG: aktuell ohne Aufrufer in der App, nur regenerate_relations.py im Repo-Root nutzt es"
translator.py: "Haiku-Uebersetzung in Batches a 5 (groessere Batches rissen den JSON-Output ab)" translator.py: "Haiku-Uebersetzung in Batches a 5 (groessere Batches rissen den JSON-Output ab)"
@@ -164,6 +164,7 @@ tests/:
test_eu_belegsuche.py: "Gezielte Belegsuche ueber staan, Planung, Grenzen, Entdopplung, Ausfallverhalten" test_eu_belegsuche.py: "Gezielte Belegsuche ueber staan, Planung, Grenzen, Entdopplung, Ausfallverhalten"
test_qc_und_runden.py: "Absicherung der Duplikatpruefung und Abbruch der Suchrunden bei erreichter Treffergrenze" test_qc_und_runden.py: "Absicherung der Duplikatpruefung und Abbruch der Suchrunden bei erreichter Treffergrenze"
test_mehrfachantwort.py: "Antworten, in denen sich das Modell selbst korrigiert und mehrere Fassungen liefert, die ausfuehrlichste muss gewinnen" test_mehrfachantwort.py: "Antworten, in denen sich das Modell selbst korrigiert und mehrere Fassungen liefert, die ausfuehrlichste muss gewinnen"
test_faktencheck_belegtiefe.py: "Belegzaehlung nach Medienhaus, Nachbedingung fuer bestaetigte Fakten, Sperre gegen das Hochstufen von developing, getrennte Bezeichnungen fuer Widerspruch und Widerlegung"
test_quellenausgabe.py: "Ausgabetreue des Quellenverzeichnisses: Nummern aus dem Lagebild statt Zeilennummern, keine Kappung, ein Verlag ein Name, Statistik deckungsgleich mit dem Verzeichnis" test_quellenausgabe.py: "Ausgabetreue des Quellenverzeichnisses: Nummern aus dem Lagebild statt Zeilennummern, keine Kappung, ein Verlag ein Name, Statistik deckungsgleich mit dem Verzeichnis"
``` ```
@@ -175,13 +176,13 @@ incident_typen:
label: "Live-Monitoring" label: "Live-Monitoring"
quellen: "RSS + WebSearch + optional Telegram" quellen: "RSS + WebSearch + optional Telegram"
analyse: "Fliesstext-Lagebild" analyse: "Fliesstext-Lagebild"
faktencheck_status: "confirmed/unconfirmed/contradicted/developing" faktencheck_status: "confirmed/unconfirmed/contradicted/false/developing (contradicted = Belege widersprechen einander, false = nachweislich widerlegt)"
refresh: "Manuell oder automatisch (Intervall konfigurierbar)" refresh: "Manuell oder automatisch (Intervall konfigurierbar)"
research: research:
label: "Recherche" label: "Recherche"
quellen: "Nur WebSearch 4-Phasen-Tiefenrecherche (kein RSS)" quellen: "Nur WebSearch 4-Phasen-Tiefenrecherche (kein RSS)"
analyse: "Strukturiertes Briefing (Ueberblick, Hintergrund, Akteure, Lage, Einschaetzung, Quellenqualitaet)" analyse: "Strukturiertes Briefing (Ueberblick, Hintergrund, Akteure, Lage, Einschaetzung, Quellenqualitaet)"
faktencheck_status: "established/unverified/disputed/developing" faktencheck_status: "established/unverified/disputed/false/developing"
refresh: "Immer manuell, erster Refresh automatisch 3 Durchlaeufe (Multi-Pass)" refresh: "Immer manuell, erster Refresh automatisch 3 Durchlaeufe (Multi-Pass)"
multi_pass: multi_pass:
durchlaeufe: 3 durchlaeufe: 3

Datei anzeigen

@@ -279,6 +279,8 @@ _FACT_STATUS_GROUPS = [
{"confirmed", "established"}), {"confirmed", "established"}),
("Umstritten (Quellen widersprechen sich oder Faktencheck hat Widersprüche dokumentiert):", ("Umstritten (Quellen widersprechen sich oder Faktencheck hat Widersprüche dokumentiert):",
{"contradicted", "disputed"}), {"contradicted", "disputed"}),
("Widerlegt (ein Beleg entkräftet die Aussage, sie darf im Lagebild nicht als Tatsache stehen):",
{"false"}),
("Unbestätigt (nur eine einzelne Quelle, eine unabhängige Bestätigung steht aus):", ("Unbestätigt (nur eine einzelne Quelle, eine unabhängige Bestätigung steht aus):",
{"unconfirmed", "unverified"}), {"unconfirmed", "unverified"}),
("In Entwicklung (laufender Sachverhalt, Stand offen):", ("In Entwicklung (laufender Sachverhalt, Stand offen):",
@@ -287,7 +289,7 @@ _FACT_STATUS_GROUPS = [
_FACT_STATUS_PRIORITY = { _FACT_STATUS_PRIORITY = {
"confirmed": 5, "established": 5, "confirmed": 5, "established": 5,
"contradicted": 4, "disputed": 4, "contradicted": 4, "disputed": 4, "false": 4,
"unconfirmed": 3, "unverified": 3, "unconfirmed": 3, "unverified": 3,
"developing": 1, "developing": 1,
} }

Datei anzeigen

@@ -32,16 +32,18 @@ AUFTRAG:
1. Identifiziere die 5-10 wichtigsten Faktenaussagen aus den Meldungen 1. Identifiziere die 5-10 wichtigsten Faktenaussagen aus den Meldungen
2. Prüfe jeden Claim aktiv per WebSearch gegen mindestens eine weitere unabhängige Quelle 2. Prüfe jeden Claim aktiv per WebSearch gegen mindestens eine weitere unabhängige Quelle
3. Kategorisiere jede Aussage: 3. Kategorisiere jede Aussage:
- "confirmed": Durch 2+ unabhängige seriöse Quellen mit überprüfbarer URL bestätigt - "confirmed": Mindestens 2 unabhängige Medienhäuser mit überprüfbarer URL bestätigen die Aussage
- "unconfirmed": Nur 1 Quelle oder nicht unabhängig verifizierbar - "unconfirmed": Nur ein Medienhaus, oder nicht unabhängig verifizierbar
- "contradicted": Widersprüchliche Informationen aus verschiedenen Quellen - "contradicted": Die Belege widersprechen EINANDER in der Sache, keine Seite ist entschieden
- "false": Ein Beleg widerlegt die Behauptung nachweislich
- "developing": Situation noch unklar, entwickelt sich - "developing": Situation noch unklar, entwickelt sich
4. Markiere WICHTIGE NEUE Entwicklungen mit is_notification: true 4. Markiere WICHTIGE NEUE Entwicklungen mit is_notification: true
Antworte AUSSCHLIESSLICH als JSON-Array. Jedes Element hat: Antworte AUSSCHLIESSLICH als JSON-Array. Jedes Element hat:
- "claim": Die Faktenaussage auf {output_language} - "claim": Die Faktenaussage auf {output_language}
- "status": "confirmed" | "unconfirmed" | "contradicted" | "developing" - "status": "confirmed" | "unconfirmed" | "contradicted" | "false" | "developing"
- "sources_count": Anzahl unabhängiger Quellen mit überprüfbarer URL - "sources_count": Anzahl unabhängiger Medienhäuser mit überprüfbarer URL
- "evidence_type": "primary" | "secondary" | "agency" (siehe Bewertungsregeln)
- "evidence": Begründung MIT konkreten Quellen-URLs als Beleg (z.B. "Bestätigt durch: tagesschau.de (URL), Reuters (URL)") - "evidence": Begründung MIT konkreten Quellen-URLs als Beleg (z.B. "Bestätigt durch: tagesschau.de (URL), Reuters (URL)")
- "is_notification": true/false (nur bei wichtigen Entwicklungen true) - "is_notification": true/false (nur bei wichtigen Entwicklungen true)
@@ -72,16 +74,18 @@ Fokus: "Was sind die gesicherten Fakten zu diesem Thema?"
1. Identifiziere die 5-10 wichtigsten Faktenaussagen aus den Quellen 1. Identifiziere die 5-10 wichtigsten Faktenaussagen aus den Quellen
2. Prüfe jeden Claim aktiv per WebSearch gegen weitere unabhängige Quellen 2. Prüfe jeden Claim aktiv per WebSearch gegen weitere unabhängige Quellen
3. Kategorisiere jede Aussage: 3. Kategorisiere jede Aussage:
- "established": Breit dokumentierter, gesicherter Fakt (3+ unabhängige Quellen mit URL) - "established": Breit dokumentierter Fakt, 3+ unabhängige Medienhäuser mit URL
- "disputed": Umstrittener Sachverhalt, verschiedene Positionen dokumentiert - "disputed": Die Belege widersprechen EINANDER, verschiedene Positionen dokumentiert
- "false": Ein Beleg widerlegt die Behauptung nachweislich
- "unverified": Einzelbehauptung, nicht unabhängig verifizierbar - "unverified": Einzelbehauptung, nicht unabhängig verifizierbar
- "developing": Aktuelle Entwicklung, Faktenlage noch im Fluss - "developing": Aktuelle Entwicklung, Faktenlage noch im Fluss
4. Markiere WICHTIGE Erkenntnisse mit is_notification: true 4. Markiere WICHTIGE Erkenntnisse mit is_notification: true
Antworte AUSSCHLIESSLICH als JSON-Array. Jedes Element hat: Antworte AUSSCHLIESSLICH als JSON-Array. Jedes Element hat:
- "claim": Die Faktenaussage auf {output_language} - "claim": Die Faktenaussage auf {output_language}
- "status": "established" | "disputed" | "unverified" | "developing" - "status": "established" | "disputed" | "false" | "unverified" | "developing"
- "sources_count": Anzahl unabhängiger Quellen mit überprüfbarer URL - "sources_count": Anzahl unabhängiger Medienhäuser mit überprüfbarer URL
- "evidence_type": "primary" | "secondary" | "agency" (siehe Bewertungsregeln)
- "evidence": Begründung MIT konkreten Quellen-URLs als Beleg - "evidence": Begründung MIT konkreten Quellen-URLs als Beleg
- "is_notification": true/false - "is_notification": true/false
@@ -116,16 +120,18 @@ AUFTRAG:
5. Markiere wichtige Statusänderungen und neue Entwicklungen mit is_notification: true 5. Markiere wichtige Statusänderungen und neue Entwicklungen mit is_notification: true
Status-Kategorien: Status-Kategorien:
- "confirmed": 2+ unabhängige seriöse Quellen mit URL - "confirmed": 2+ unabhängige Medienhäuser mit URL
- "unconfirmed": Nur 1 Quelle - "unconfirmed": Nur ein Medienhaus
- "contradicted": Widersprüchliche Informationen - "contradicted": Die Belege widersprechen einander
- "false": Ein Beleg widerlegt die Behauptung nachweislich
- "developing": Situation unklar - "developing": Situation unklar
Antworte AUSSCHLIESSLICH als JSON-Array mit ALLEN Fakten (bestehende aktualisiert + neue). Antworte AUSSCHLIESSLICH als JSON-Array mit ALLEN Fakten (bestehende aktualisiert + neue).
Jedes Element hat: Jedes Element hat:
- "claim": Die Faktenaussage auf {output_language} - "claim": Die Faktenaussage auf {output_language}
- "status": "confirmed" | "unconfirmed" | "contradicted" | "developing" - "status": "confirmed" | "unconfirmed" | "contradicted" | "false" | "developing"
- "sources_count": Anzahl unabhängiger Quellen - "sources_count": Anzahl unabhängiger Medienhäuser
- "evidence_type": "primary" | "secondary" | "agency"
- "evidence": Begründung MIT konkreten Quellen-URLs - "evidence": Begründung MIT konkreten Quellen-URLs
- "is_notification": true/false - "is_notification": true/false
@@ -157,16 +163,18 @@ AUFTRAG:
4. Prüfe neue Claims per WebSearch 4. Prüfe neue Claims per WebSearch
Status-Kategorien: Status-Kategorien:
- "established": 3+ unabhängige Quellen mit URL - "established": 3+ unabhängige Medienhäuser mit URL
- "disputed": Verschiedene Positionen dokumentiert - "disputed": Die Belege widersprechen einander, verschiedene Positionen dokumentiert
- "false": Ein Beleg widerlegt die Behauptung nachweislich
- "unverified": Nicht unabhängig verifizierbar - "unverified": Nicht unabhängig verifizierbar
- "developing": Faktenlage im Fluss - "developing": Faktenlage im Fluss
Antworte AUSSCHLIESSLICH als JSON-Array mit ALLEN Fakten (bestehende aktualisiert + neue). Antworte AUSSCHLIESSLICH als JSON-Array mit ALLEN Fakten (bestehende aktualisiert + neue).
Jedes Element hat: Jedes Element hat:
- "claim": Die Faktenaussage auf {output_language} - "claim": Die Faktenaussage auf {output_language}
- "status": "established" | "disputed" | "unverified" | "developing" - "status": "established" | "disputed" | "false" | "unverified" | "developing"
- "sources_count": Anzahl unabhängiger Quellen - "sources_count": Anzahl unabhängiger Medienhäuser
- "evidence_type": "primary" | "secondary" | "agency"
- "evidence": Begründung MIT konkreten Quellen-URLs - "evidence": Begründung MIT konkreten Quellen-URLs
- "is_notification": true/false - "is_notification": true/false
@@ -237,14 +245,15 @@ ANWEISUNGEN:
Für JEDE Faktenaussage (bestehend UND neu): Für JEDE Faktenaussage (bestehend UND neu):
1. Suche per WebSearch nach unabhängigen Bestätigungen oder Widerlegungen 1. Suche per WebSearch nach unabhängigen Bestätigungen oder Widerlegungen
2. Bewerte den Status: 2. Bewerte den Status:
- "confirmed": Mindestens 2 unabhängige Quellen mit verifizierter URL bestätigen - "confirmed": Mindestens 2 unabhängige Medienhäuser mit verifizierter URL bestätigen
- "unconfirmed": Nicht genug unabhängige Bestätigung - "unconfirmed": Nicht genug unabhängige Bestätigung
- "contradicted": Glaubwürdige Quellen widersprechen - "contradicted": Die Belege widersprechen einander in der Sache
- "false": Ein Beleg widerlegt die Behauptung nachweislich
- "developing": Lage noch im Fluss - "developing": Lage noch im Fluss
3. Dokumentiere die Evidenz mit konkreten, verifizierten URLs 3. Dokumentiere die Evidenz mit konkreten, verifizierten URLs
QUALITÄTSREGELN: QUALITÄTSREGELN:
- "confirmed" NUR bei mindestens 2 unabhängigen Quellen mit ECHTER URL - "confirmed" NUR bei mindestens 2 unabhängigen Medienhäusern mit ECHTER URL
- KEINE Halluzinationen — nur tatsächlich per WebSearch gefundene Quellen - KEINE Halluzinationen — nur tatsächlich per WebSearch gefundene Quellen
- Bestehende Evidenz BEIBEHALTEN, nur neue ergänzen - Bestehende Evidenz BEIBEHALTEN, nur neue ergänzen
- Jeder Claim MUSS eine konkrete Quellen-URL als Beleg enthalten - Jeder Claim MUSS eine konkrete Quellen-URL als Beleg enthalten
@@ -254,8 +263,9 @@ Antworte AUSSCHLIESSLICH als JSON-Array:
{{{{ {{{{
"id": 123, "id": 123,
"claim": "Die Faktenaussage...", "claim": "Die Faktenaussage...",
"status": "confirmed|unconfirmed|contradicted|developing", "status": "confirmed|unconfirmed|contradicted|false|developing",
"sources_count": 3, "sources_count": 3,
"evidence_type": "primary|secondary|agency",
"evidence": "Bestätigt durch: Quelle1 (URL)\\nQuelle2 (URL)", "evidence": "Bestätigt durch: Quelle1 (URL)\\nQuelle2 (URL)",
"is_notification": false "is_notification": false
}}}} }}}}
@@ -283,14 +293,15 @@ ANWEISUNGEN:
Für JEDE Faktenaussage (bestehend UND neu): Für JEDE Faktenaussage (bestehend UND neu):
1. Suche per WebSearch nach unabhängigen Bestätigungen oder Widerlegungen 1. Suche per WebSearch nach unabhängigen Bestätigungen oder Widerlegungen
2. Bewerte den Status: 2. Bewerte den Status:
- "established": 3+ unabhängige Quellen mit verifizierter URL - "established": 3+ unabhängige Medienhäuser mit verifizierter URL
- "unverified": Nicht unabhängig verifizierbar - "unverified": Nicht unabhängig verifizierbar
- "disputed": Verschiedene Positionen dokumentiert - "disputed": Die Belege widersprechen einander, verschiedene Positionen dokumentiert
- "false": Ein Beleg widerlegt die Behauptung nachweislich
- "developing": Faktenlage im Fluss - "developing": Faktenlage im Fluss
3. Dokumentiere die Evidenz mit konkreten, verifizierten URLs 3. Dokumentiere die Evidenz mit konkreten, verifizierten URLs
QUALITÄTSREGELN: QUALITÄTSREGELN:
- "established" NUR bei mindestens 3 unabhängigen Quellen mit ECHTER URL - "established" NUR bei mindestens 3 unabhängigen Medienhäusern mit ECHTER URL
- KEINE Halluzinationen — nur tatsächlich per WebSearch gefundene Quellen - KEINE Halluzinationen — nur tatsächlich per WebSearch gefundene Quellen
- Bestehende Evidenz BEIBEHALTEN, nur neue ergänzen - Bestehende Evidenz BEIBEHALTEN, nur neue ergänzen
@@ -299,8 +310,9 @@ Antworte AUSSCHLIESSLICH als JSON-Array:
{{{{ {{{{
"id": 123, "id": 123,
"claim": "Die Faktenaussage...", "claim": "Die Faktenaussage...",
"status": "established|unverified|disputed|developing", "status": "established|unverified|disputed|false|developing",
"sources_count": 3, "sources_count": 3,
"evidence_type": "primary|secondary|agency",
"evidence": "Bestätigt durch: Quelle1 (URL)\\nQuelle2 (URL)", "evidence": "Bestätigt durch: Quelle1 (URL)\\nQuelle2 (URL)",
"is_notification": false "is_notification": false
}}}} }}}}
@@ -321,11 +333,62 @@ _STOPWORDS = frozenset({
STATUS_PRIORITY = { STATUS_PRIORITY = {
"confirmed": 5, "established": 5, "confirmed": 5, "established": 5,
"contradicted": 4, "disputed": 4, "contradicted": 4, "disputed": 4, "false": 4,
"unconfirmed": 3, "unverified": 3, "unconfirmed": 3, "unverified": 3,
"developing": 1, "developing": 1,
} }
# Status, die eine belegte Bestaetigung behaupten und deshalb an der Zahl
# unabhaengiger Medienhaeuser gemessen werden.
BESTAETIGT_STATUS = {"confirmed": 2, "established": 3}
# Ein Primaerbeleg (Originalzitat im Interview, amtliche Mitteilung, Urteil,
# Pressemitteilung der handelnden Stelle) traegt allein. Mehrfache
# Berichterstattung ueber denselben O-Ton ist keine zusaetzliche Verifikation,
# und ihr Fehlen ist kein Mangel. Ohne diese Ausnahme wurde im Vergleich vom
# 01.08.2026 ein ZDF-Interview als "unbestaetigt" gefuehrt, obwohl der
# Betroffene die Aussage dort selbst getroffen hat.
PRIMAERBELEG_MINDESTZAHL = 1
_BEWERTUNGSREGELN = """
BEWERTUNGSREGELN (verbindlich, sie gehen allen Angaben oben vor):
- Unabhängig heißt: verschiedene Medienhäuser. Mehrere Artikel derselben
Redaktion, Videofassungen derselben Meldung und wortgleiche Übernahmen
derselben Agenturmeldung zählen zusammen als EINE Quelle.
- Weiterleitungen über Sammelportale (news.google.com und ähnliche) sind kein
eigenständiger Beleg. Zähle das Zielmedium, wenn es erkennbar ist.
- "sources_count" ist die Zahl unabhängiger MEDIENHÄUSER, nicht die Zahl der
Fundstellen oder Adressen.
- "evidence_type" beschreibt den besten vorliegenden Beleg:
"primary" = die handelnde oder sprechende Stelle selbst (Originalzitat im
Interview, amtliche Mitteilung, Urteil, Pressemitteilung),
"secondary" = unabhängige eigene Berichterstattung eines Mediums,
"agency" = Agenturmeldung und ihre Übernahmen.
Bei "primary" genügt diese eine Quelle für eine Bestätigung.
- Unterschiedliche Zahlenangaben verschiedener Erhebungsstellen sind KEIN
Widerspruch. Nenne die Spanne mit Zuordnung ("50.000 laut Innenministerium,
60.000 laut Regionalregierung") und bewerte die Aussage nach ihrer
Belegtiefe. Ein Widerspruchs-Status ist nur richtig, wenn die Belege
einander in der Sache widersprechen.
- Der Status für eine nachweisliche Widerlegung ist ausschließlich "false" und
setzt einen Beleg voraus, der die Behauptung entkräftet. Eine fehlende
Bestätigung ist keine Widerlegung.
- Prüfe nur Tatsachenbehauptungen. Aussagen über den Kenntnisstand ("Der
Auslöser ist unklar", "Eine unabhängige Bestätigung steht aus", "Die Lage
bleibt unübersichtlich") sind nicht prüfbar und gehören NICHT in den
Faktencheck. Formuliere stattdessen die prüfbare Tatsache dahinter oder lass
den Punkt weg."""
def bewertungsregeln() -> str:
"""Verbindlicher Regelblock, wird an jeden Faktencheck-Auftrag angehaengt.
Bewusst am Ende des Auftrags und an einer einzigen Stelle gepflegt, damit
die sechs Vorlagen nicht auseinanderlaufen.
"""
return _BEWERTUNGSREGELN
# Zwei-Phasen-Faktencheck: Konfiguration # Zwei-Phasen-Faktencheck: Konfiguration
MAX_FACTS_PER_VERIFY_GROUP = 8 # Max Fakten pro Verifikationsgruppe MAX_FACTS_PER_VERIFY_GROUP = 8 # Max Fakten pro Verifikationsgruppe
TWOPHASE_MIN_FACTS = 25 # Ab dieser Anzahl bestehender Fakten wird der Zwei-Phasen-Ansatz genutzt TWOPHASE_MIN_FACTS = 25 # Ab dieser Anzahl bestehender Fakten wird der Zwei-Phasen-Ansatz genutzt
@@ -363,6 +426,66 @@ Antworte NUR mit einem JSON-Array, ein Objekt je oben genannter Behauptung:
[{{"claim": "Behauptung im Wortlaut wie oben", "status": "{status_werte}", "evidence": "Begruendung mit [S..]"}}]""" [{{"claim": "Behauptung im Wortlaut wie oben", "status": "{status_werte}", "evidence": "Begruendung mit [S..]"}}]"""
_URL_RE = re.compile(r'https?://[^\s\)\]\},;"\'<>]+')
def zaehle_medienhaeuser(urls: list[str]) -> int:
"""Zaehlt unabhaengige Medienhaeuser statt Fundstellen.
Sechs France24-Treffer, davon vier Videofassungen derselben Meldung, sind
ein Haus und kein sechsfacher Beleg. Gezaehlt wird deshalb nach
registrierbarer Domain. Weiterleitungsportale bekommen keine gemeinsame
Identitaet, weil hinter news.google.com beliebige Zeitungen stecken
koennen: dort zaehlt jede Adresse einzeln.
"""
from services import media_registry
haeuser: set[str] = set()
for url in urls or []:
sauber = (url or "").rstrip(".,;:)]}\"'")
if not sauber:
continue
domain = media_registry.registrable_domain(sauber)
if not domain:
continue
haeuser.add(sauber.lower() if media_registry.ist_aggregator(sauber) else domain)
return len(haeuser)
def pruefe_belegtiefe(fact: dict) -> dict:
"""Stuft einen Fakt ab, wenn sein Status mehr Beleg behauptet als vorliegt.
Ohne diese Nachbedingung stand im Bericht vom 01.08.2026 eine Behauptung
als BESTAETIGT mit einer einzigen Quelle, deren Evidenz woertlich mit "Nur
durch Al Jazeera berichtet" begann. Ursache war, dass die Nachhak-Runde den
Status hochsetzte, ohne die Belegzahl neu zu bestimmen.
Ein Primaerbeleg (evidence_type 'primary') traegt allein, siehe
PRIMAERBELEG_MINDESTZAHL.
"""
status = (fact.get("status") or "").strip().lower()
noetig = BESTAETIGT_STATUS.get(status)
if not noetig:
return fact
if (fact.get("evidence_type") or "").strip().lower() == "primary":
noetig = PRIMAERBELEG_MINDESTZAHL
vorhanden = fact.get("sources_count")
if not isinstance(vorhanden, int):
vorhanden = zaehle_medienhaeuser(_URL_RE.findall(fact.get("evidence") or ""))
fact["sources_count"] = vorhanden
if vorhanden < noetig:
neuer = "unconfirmed" if status == "confirmed" else "unverified"
logger.warning(
"Fakt herabgestuft (%s -> %s): %d unabhaengige Quelle(n), noetig waeren %d: '%s...'",
status, neuer, vorhanden, noetig, (fact.get("claim") or "")[:60],
)
fact["status"] = neuer
return fact
def normalize_claim(claim: str) -> str: def normalize_claim(claim: str) -> str:
"""Normalisiert einen Claim fuer Aehnlichkeitsvergleich.""" """Normalisiert einen Claim fuer Aehnlichkeitsvergleich."""
c = claim.lower().strip() c = claim.lower().strip()
@@ -614,15 +737,15 @@ class FactCheckerAgent:
logger.info("EU-Nachhak-Runde: keine zusaetzlichen Belege gefunden, Fakten bleiben unveraendert") logger.info("EU-Nachhak-Runde: keine zusaetzlichen Belege gefunden, Fakten bleiben unveraendert")
return facts, gesamt return facts, gesamt
status_werte = ("established|unverified|disputed|developing" status_werte = ("established|unverified|disputed|false|developing"
if incident_type == "research" if incident_type == "research"
else "confirmed|unconfirmed|contradicted|developing") else "confirmed|unconfirmed|contradicted|false|developing")
prompt = EU_FOLLOWUP_PROMPT_TEMPLATE.format( prompt = EU_FOLLOWUP_PROMPT_TEMPLATE.format(
output_language=output_language, output_language=output_language,
title=title, title=title,
open_claims_text="\n".join(f"- {c}" for c in claims), open_claims_text="\n".join(f"- {c}" for c in claims),
status_werte=status_werte, status_werte=status_werte,
) ) + bewertungsregeln()
try: try:
result, usage = await call_claude(build_eu_prompt(prompt, block), tools=None) result, usage = await call_claude(build_eu_prompt(prompt, block), tools=None)
@@ -656,11 +779,36 @@ class FactCheckerAgent:
alt = treffer.get("status", "developing") alt = treffer.get("status", "developing")
if STATUS_PRIORITY.get(neuer_status, 0) <= STATUS_PRIORITY.get(alt, 0): if STATUS_PRIORITY.get(neuer_status, 0) <= STATUS_PRIORITY.get(alt, 0):
continue continue
# "developing" heisst: der Sachverhalt selbst ist noch offen. Das
# ist keine schwache Beleglage, die sich nachrecherchieren laesst,
# und wird deshalb nicht hochgestuft. Ohne diese Sperre wurde am
# 01.08.2026 die Behauptung "Der Ausloeser ist weiterhin unklar"
# auf BESTAETIGT gesetzt, weil mehrere Quellen die Unklarheit
# beschrieben. Eine Unklarheit laesst sich nicht bestaetigen.
if alt == "developing" and neuer_status not in ("contradicted", "disputed", "false"):
logger.info(
"EU-Nachhak-Runde: '%s...' bleibt developing, %s nicht zulaessig",
claim[:60], neuer_status,
)
continue
treffer["status"] = neuer_status treffer["status"] = neuer_status
belege = str(eintrag.get("evidence", "")).strip() belege = str(eintrag.get("evidence", "")).strip()
if belege: if belege:
bisher = (treffer.get("evidence") or "").strip() bisher = (treffer.get("evidence") or "").strip()
treffer["evidence"] = f"{bisher} Nachrecherche: {belege}".strip() if bisher else belege treffer["evidence"] = f"{bisher} Nachrecherche: {belege}".strip() if bisher else belege
# Die Belegzahl folgt der neuen Evidenz, nicht dem alten Stand.
treffer["sources_count"] = zaehle_medienhaeuser(
_URL_RE.findall(treffer.get("evidence") or "")
)
if eintrag.get("evidence_type"):
treffer["evidence_type"] = eintrag.get("evidence_type")
pruefe_belegtiefe(treffer)
if treffer["status"] != neuer_status:
logger.info(
"EU-Nachhak-Runde: '%s...' Hochstufung auf %s nicht gedeckt, bleibt %s",
claim[:60], neuer_status, treffer["status"],
)
continue
hochgestuft += 1 hochgestuft += 1
logger.info("EU-Nachhak-Runde: '%s...' %s -> %s", claim[:60], alt, neuer_status) logger.info("EU-Nachhak-Runde: '%s...' %s -> %s", claim[:60], alt, neuer_status)
@@ -682,7 +830,7 @@ class FactCheckerAgent:
title=title, title=title,
articles_text=articles_text, articles_text=articles_text,
output_language=output_language, output_language=output_language,
) ) + bewertungsregeln()
try: try:
# EU-Modus: gezielte staan-Belegsuche plus Nachhak-Runde statt WebSearch. # EU-Modus: gezielte staan-Belegsuche plus Nachhak-Runde statt WebSearch.
@@ -733,7 +881,7 @@ class FactCheckerAgent:
articles_text=articles_text, articles_text=articles_text,
existing_facts_text=existing_facts_text, existing_facts_text=existing_facts_text,
output_language=output_language, output_language=output_language,
) ) + bewertungsregeln()
try: try:
# EU-Modus: gezielte Belegsuche zu offenen Fakten plus Nachhak-Runde. # EU-Modus: gezielte Belegsuche zu offenen Fakten plus Nachhak-Runde.
@@ -854,7 +1002,7 @@ class FactCheckerAgent:
facts_text=facts_text, facts_text=facts_text,
new_claims_text=new_claims_text, new_claims_text=new_claims_text,
articles_text=articles_text, articles_text=articles_text,
) ) + bewertungsregeln()
try: try:
# EU-Modus: gezielte Belegsuche je Faktengruppe plus Nachhak-Runde. # EU-Modus: gezielte Belegsuche je Faktengruppe plus Nachhak-Runde.
@@ -965,9 +1113,11 @@ class FactCheckerAgent:
"""Validiert Fakten und ordnet Quellen-URLs aus den Artikeln zu. """Validiert Fakten und ordnet Quellen-URLs aus den Artikeln zu.
Stellt sicher, dass jeder confirmed/established Fakt URLs in der Stellt sicher, dass jeder confirmed/established Fakt URLs in der
evidence hat, damit das Frontend die Quellen korrekt anzeigen kann. evidence hat, damit das Frontend die Quellen korrekt anzeigen kann,
zaehlt die Belege nach unabhaengigen Medienhaeusern und stuft Fakten
ab, deren Status mehr Belegtiefe behauptet als vorhanden ist.
""" """
url_pattern = re.compile(r'https?://') url_pattern = _URL_RE
# Verfuegbare Artikel-URLs sammeln (dedupliziert nach URL) # Verfuegbare Artikel-URLs sammeln (dedupliziert nach URL)
article_sources = [] article_sources = []
seen_urls = set() seen_urls = set()
@@ -1025,8 +1175,9 @@ class FactCheckerAgent:
) )
# sources_count aktualisieren # sources_count aktualisieren
all_urls = url_pattern.findall(fact["evidence"]) fact["sources_count"] = zaehle_medienhaeuser(
fact["sources_count"] = len(set(all_urls)) url_pattern.findall(fact["evidence"])
)
logger.info( logger.info(
f"Fakt '{fact.get('claim', '')[:50]}...' ergaenzt mit " f"Fakt '{fact.get('claim', '')[:50]}...' ergaenzt mit "
@@ -1040,6 +1191,15 @@ class FactCheckerAgent:
f"Fakt herabgestuft ({old_status} -> {fact['status']}): " f"Fakt herabgestuft ({old_status} -> {fact['status']}): "
f"keine Quellen zuordnebar: '{fact.get('claim', '')[:60]}...'" f"keine Quellen zuordnebar: '{fact.get('claim', '')[:60]}...'"
) )
else:
# Nur Belege aus der Antwort des Modells: die Zahl trotzdem
# selbst bestimmen, statt der Selbstauskunft zu vertrauen.
fact["sources_count"] = zaehle_medienhaeuser(
url_pattern.findall(fact["evidence"] or "")
)
for fact in facts:
pruefe_belegtiefe(fact)
return facts return facts
def _parse_response(self, response: str, articles: list[dict] = None) -> list[dict]: def _parse_response(self, response: str, articles: list[dict] = None) -> list[dict]:

Datei anzeigen

@@ -2073,7 +2073,7 @@ class AgentOrchestrator:
# Status-Statistik sammeln # Status-Statistik sammeln
if new_status in ("confirmed", "established"): if new_status in ("confirmed", "established"):
confirmed_count += 1 confirmed_count += 1
elif new_status in ("contradicted", "disputed"): elif new_status in ("contradicted", "disputed", "false"):
contradicted_count += 1 contradicted_count += 1
await db.commit() await db.commit()
@@ -2161,7 +2161,7 @@ class AgentOrchestrator:
"type": "status_change", "type": "status_change",
"title": title, "title": title,
"text": f"{sc['claim']}: {sc['old_status']} \u2192 {sc['new_status']}", "text": f"{sc['claim']}: {sc['old_status']} \u2192 {sc['new_status']}",
"icon": "error" if sc["new_status"] in ("contradicted", "disputed") else "success", "icon": "error" if sc["new_status"] in ("contradicted", "disputed", "false") else "success",
}) })
if created_by: if created_by:

Datei anzeigen

@@ -31,23 +31,27 @@ FC_STATUS_LABELS_DE = {
# 1:1 vom Monitor-Frontend (components.js) — konsistent zum UI. # 1:1 vom Monitor-Frontend (components.js) — konsistent zum UI.
"confirmed": "Bestätigt", "confirmed": "Bestätigt",
"unconfirmed": "Unbestätigt", "unconfirmed": "Unbestätigt",
"contradicted": "Widerlegt", # "Widerlegt" war hier falsch: Der Status meint laut Auftrag Belege, die
# einander widersprechen. Im Bericht vom 01.08.2026 stand deshalb eine
# sachlich zutreffende Zahlenspanne als widerlegt. Die echte Widerlegung
# hat jetzt den eigenen Status "false".
"contradicted": "Widersprüchlich",
"developing": "Unklar", "developing": "Unklar",
"established": "Gesichert", "established": "Gesichert",
"disputed": "Umstritten", "disputed": "Umstritten",
"unverified": "Ungeprüft", "unverified": "Ungeprüft",
"false": "Falsch", "false": "Widerlegt",
} }
FC_STATUS_LABELS_EN = { FC_STATUS_LABELS_EN = {
"confirmed": "Confirmed", "confirmed": "Confirmed",
"unconfirmed": "Unconfirmed", "unconfirmed": "Unconfirmed",
"contradicted": "Contradicted", "contradicted": "Conflicting",
"developing": "Developing", "developing": "Developing",
"established": "Established", "established": "Established",
"disputed": "Disputed", "disputed": "Disputed",
"unverified": "Unverified", "unverified": "Unverified",
"false": "False", "false": "Disproved",
} }

Datei anzeigen

@@ -16,7 +16,7 @@ logger = logging.getLogger("osint.fact_consolidation")
STATUS_PRIORITY = { STATUS_PRIORITY = {
"confirmed": 5, "established": 5, "confirmed": 5, "established": 5,
"contradicted": 4, "disputed": 4, "contradicted": 4, "disputed": 4, "false": 4,
"unconfirmed": 3, "unverified": 3, "unconfirmed": 3, "unverified": 3,
"developing": 1, "developing": 1,
} }

Datei anzeigen

@@ -20,7 +20,7 @@ logger = logging.getLogger("osint.post_refresh_qc")
STATUS_PRIORITY = { STATUS_PRIORITY = {
"confirmed": 5, "established": 5, "confirmed": 5, "established": 5,
"contradicted": 4, "disputed": 4, "contradicted": 4, "disputed": 4, "false": 4,
"unconfirmed": 3, "unverified": 3, "unconfirmed": 3, "unverified": 3,
"developing": 1, "developing": 1,
} }

Datei anzeigen

@@ -1199,6 +1199,12 @@ a.dev-source-pill:hover {
} }
.factcheck-icon.contradicted { .factcheck-icon.contradicted {
background: var(--tint-warning);
color: var(--warning);
}
/* Widerlegt: eigener Status seit der Trennung von Widerspruch und Widerlegung */
.factcheck-icon.false {
background: var(--tint-error); background: var(--tint-error);
color: var(--error); color: var(--error);
} }
@@ -1914,6 +1920,11 @@ a.dev-source-pill:hover {
} }
.badge-contradicted { .badge-contradicted {
background: var(--tint-warning);
color: var(--warning);
}
.badge-false {
background: var(--tint-error); background: var(--tint-error);
color: var(--error); color: var(--error);
} }

Datei anzeigen

@@ -111,7 +111,8 @@
"card.sources_overview": "Quellenübersicht", "card.sources_overview": "Quellenübersicht",
"fc.label.confirmed": "Bestätigt durch mehrere Quellen", "fc.label.confirmed": "Bestätigt durch mehrere Quellen",
"fc.label.unconfirmed": "Nicht unabhängig bestätigt", "fc.label.unconfirmed": "Nicht unabhängig bestätigt",
"fc.label.contradicted": "Widerlegt", "fc.label.contradicted": "Quellen widersprechen sich",
"fc.label.false": "Widerlegt",
"fc.label.developing": "Faktenlage noch im Fluss", "fc.label.developing": "Faktenlage noch im Fluss",
"fc.label.established": "Gesicherter Fakt (3+ Quellen)", "fc.label.established": "Gesicherter Fakt (3+ Quellen)",
"fc.label.disputed": "Umstrittener Sachverhalt", "fc.label.disputed": "Umstrittener Sachverhalt",
@@ -122,10 +123,12 @@
"fc.tooltip.unconfirmed": "Unbestätigt: Bisher nur aus einer Quelle bekannt. Eine unabhängige Bestätigung steht aus.", "fc.tooltip.unconfirmed": "Unbestätigt: Bisher nur aus einer Quelle bekannt. Eine unabhängige Bestätigung steht aus.",
"fc.tooltip.unverified": "Ungeprüft: Die Aussage konnte bisher nicht anhand verfügbarer Quellen überprüft werden.", "fc.tooltip.unverified": "Ungeprüft: Die Aussage konnte bisher nicht anhand verfügbarer Quellen überprüft werden.",
"fc.tooltip.disputed": "Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.", "fc.tooltip.disputed": "Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.",
"fc.tooltip.contradicted": "Widerlegt: Zuverlässige Quellen widersprechen dieser Aussage. Wahrscheinlich falsch.", "fc.tooltip.contradicted": "Widersprüchlich: Die Belege widersprechen einander, keine Seite ist entschieden. Unterschiedliche Zahlen verschiedener Erhebungsstellen gelten nicht als Widerspruch.",
"fc.tooltip.false": "Widerlegt: Ein Beleg entkräftet diese Aussage nachweislich.",
"fc.chip.confirmed": "Bestätigt", "fc.chip.confirmed": "Bestätigt",
"fc.chip.unconfirmed": "Unbestätigt", "fc.chip.unconfirmed": "Unbestätigt",
"fc.chip.contradicted": "Widerlegt", "fc.chip.contradicted": "Widersprüchlich",
"fc.chip.false": "Widerlegt",
"fc.chip.developing": "Unklar", "fc.chip.developing": "Unklar",
"fc.chip.established": "Gesichert", "fc.chip.established": "Gesichert",
"fc.chip.disputed": "Umstritten", "fc.chip.disputed": "Umstritten",
@@ -133,7 +136,7 @@
"refresh.no_developments": "Keine neuen Entwicklungen", "refresh.no_developments": "Keine neuen Entwicklungen",
"refresh.new_articles_suffix": "neue Artikel", "refresh.new_articles_suffix": "neue Artikel",
"refresh.confirmed_suffix": "Fakten bestätigt", "refresh.confirmed_suffix": "Fakten bestätigt",
"refresh.contradicted_suffix": "widerlegt", "refresh.contradicted_suffix": "widersprüchlich",
"progress.status.queued": "In Warteschlange", "progress.status.queued": "In Warteschlange",
"progress.status.researching": "Recherchiert...", "progress.status.researching": "Recherchiert...",
"progress.status.deep_researching": "Tiefenrecherche...", "progress.status.deep_researching": "Tiefenrecherche...",

Datei anzeigen

@@ -111,7 +111,8 @@
"card.sources_overview": "Sources overview", "card.sources_overview": "Sources overview",
"fc.label.confirmed": "Confirmed by multiple sources", "fc.label.confirmed": "Confirmed by multiple sources",
"fc.label.unconfirmed": "Not independently confirmed", "fc.label.unconfirmed": "Not independently confirmed",
"fc.label.contradicted": "Contradicted", "fc.label.contradicted": "Sources conflict",
"fc.label.false": "Disproved",
"fc.label.developing": "Facts still developing", "fc.label.developing": "Facts still developing",
"fc.label.established": "Established fact (3+ sources)", "fc.label.established": "Established fact (3+ sources)",
"fc.label.disputed": "Disputed matter", "fc.label.disputed": "Disputed matter",
@@ -122,10 +123,12 @@
"fc.tooltip.unconfirmed": "Unconfirmed: known from only one source so far. Independent confirmation is pending.", "fc.tooltip.unconfirmed": "Unconfirmed: known from only one source so far. Independent confirmation is pending.",
"fc.tooltip.unverified": "Unverified: the claim could not yet be checked against available sources.", "fc.tooltip.unverified": "Unverified: the claim could not yet be checked against available sources.",
"fc.tooltip.disputed": "Disputed: sources disagree. There is both supporting and contradicting evidence.", "fc.tooltip.disputed": "Disputed: sources disagree. There is both supporting and contradicting evidence.",
"fc.tooltip.contradicted": "Contradicted: reliable sources contradict this claim. Likely false.", "fc.tooltip.contradicted": "Conflicting: the evidence contradicts itself and no side is settled. Differing figures from different authorities do not count as a conflict.",
"fc.tooltip.false": "Disproved: evidence refutes this claim.",
"fc.chip.confirmed": "Confirmed", "fc.chip.confirmed": "Confirmed",
"fc.chip.unconfirmed": "Unconfirmed", "fc.chip.unconfirmed": "Unconfirmed",
"fc.chip.contradicted": "Contradicted", "fc.chip.contradicted": "Conflicting",
"fc.chip.false": "Disproved",
"fc.chip.developing": "Developing", "fc.chip.developing": "Developing",
"fc.chip.established": "Established", "fc.chip.established": "Established",
"fc.chip.disputed": "Disputed", "fc.chip.disputed": "Disputed",
@@ -133,7 +136,7 @@
"refresh.no_developments": "No new developments", "refresh.no_developments": "No new developments",
"refresh.new_articles_suffix": "new articles", "refresh.new_articles_suffix": "new articles",
"refresh.confirmed_suffix": "facts confirmed", "refresh.confirmed_suffix": "facts confirmed",
"refresh.contradicted_suffix": "contradicted", "refresh.contradicted_suffix": "conflicting",
"progress.status.queued": "Queued", "progress.status.queued": "Queued",
"progress.status.researching": "Researching...", "progress.status.researching": "Researching...",
"progress.status.deep_researching": "Deep research...", "progress.status.deep_researching": "Deep research...",

Datei anzeigen

@@ -2385,7 +2385,8 @@ async handleRefresh() {
confirmed: 'Bestätigt', confirmed: 'Bestätigt',
established: 'Gesichert', established: 'Gesichert',
unconfirmed: 'Unbestätigt', unconfirmed: 'Unbestätigt',
contradicted: 'Widersprochen', contradicted: 'Widersprüchlich',
false: 'Widerlegt',
disputed: 'Umstritten', disputed: 'Umstritten',
developing: 'In Entwicklung', developing: 'In Entwicklung',
unverified: 'Ungeprüft', unverified: 'Ungeprüft',

Datei anzeigen

@@ -82,7 +82,8 @@ const UI = {
_fcLabelDefaultsDE: { _fcLabelDefaultsDE: {
confirmed: 'Bestätigt durch mehrere Quellen', confirmed: 'Bestätigt durch mehrere Quellen',
unconfirmed: 'Nicht unabhängig bestätigt', unconfirmed: 'Nicht unabhängig bestätigt',
contradicted: 'Widerlegt', contradicted: 'Quellen widersprechen sich',
false: 'Widerlegt',
developing: 'Faktenlage noch im Fluss', developing: 'Faktenlage noch im Fluss',
established: 'Gesicherter Fakt (3+ Quellen)', established: 'Gesicherter Fakt (3+ Quellen)',
disputed: 'Umstrittener Sachverhalt', disputed: 'Umstrittener Sachverhalt',
@@ -95,12 +96,14 @@ const UI = {
unconfirmed: 'Unbestätigt: Bisher nur aus einer Quelle bekannt. Eine unabhängige Bestätigung steht aus.', unconfirmed: 'Unbestätigt: Bisher nur aus einer Quelle bekannt. Eine unabhängige Bestätigung steht aus.',
unverified: 'Ungeprüft: Die Aussage konnte bisher nicht anhand verfügbarer Quellen überprüft werden.', unverified: 'Ungeprüft: Die Aussage konnte bisher nicht anhand verfügbarer Quellen überprüft werden.',
disputed: 'Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.', disputed: 'Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.',
contradicted: 'Widerlegt: Zuverlässige Quellen widersprechen dieser Aussage. Wahrscheinlich falsch.', contradicted: 'Widersprüchlich: Die Belege widersprechen einander, keine Seite ist entschieden. Unterschiedliche Zahlen verschiedener Erhebungsstellen gelten nicht als Widerspruch.',
false: 'Widerlegt: Ein Beleg entkräftet diese Aussage nachweislich.',
}, },
_fcChipDefaultsDE: { _fcChipDefaultsDE: {
confirmed: 'Bestätigt', confirmed: 'Bestätigt',
unconfirmed: 'Unbestätigt', unconfirmed: 'Unbestätigt',
contradicted: 'Widerlegt', contradicted: 'Widersprüchlich',
false: 'Widerlegt',
developing: 'Unklar', developing: 'Unklar',
established: 'Gesichert', established: 'Gesichert',
disputed: 'Umstritten', disputed: 'Umstritten',
@@ -138,7 +141,8 @@ const UI = {
factCheckIcons: { factCheckIcons: {
confirmed: '&#10003;', confirmed: '&#10003;',
unconfirmed: '?', unconfirmed: '?',
contradicted: '&#10007;', contradicted: '&#8644;',
false: '&#10007;',
developing: '&#8635;', developing: '&#8635;',
established: '&#10003;', established: '&#10003;',
disputed: '&#9888;', disputed: '&#9888;',
@@ -154,7 +158,7 @@ const UI = {
factchecks.forEach(fc => { factchecks.forEach(fc => {
statusCounts[fc.status] = (statusCounts[fc.status] || 0) + 1; statusCounts[fc.status] = (statusCounts[fc.status] || 0) + 1;
}); });
const statusOrder = ['confirmed', 'established', 'developing', 'unconfirmed', 'unverified', 'disputed', 'contradicted']; const statusOrder = ['confirmed', 'established', 'developing', 'unconfirmed', 'unverified', 'disputed', 'contradicted', 'false'];
const usedStatuses = statusOrder.filter(s => statusCounts[s]); const usedStatuses = statusOrder.filter(s => statusCounts[s]);
if (usedStatuses.length <= 1) return ''; if (usedStatuses.length <= 1) return '';
@@ -558,7 +562,7 @@ const UI = {
const parts = []; const parts = [];
if (data.new_articles > 0) parts.push(data.new_articles + ' neue Artikel'); if (data.new_articles > 0) parts.push(data.new_articles + ' neue Artikel');
if (data.confirmed_count > 0) parts.push(data.confirmed_count + ' Fakten best\u00e4tigt'); if (data.confirmed_count > 0) parts.push(data.confirmed_count + ' Fakten best\u00e4tigt');
if (data.contradicted_count > 0) parts.push(data.contradicted_count + ' widerlegt'); if (data.contradicted_count > 0) parts.push(data.contradicted_count + ' widersprüchlich');
const summaryText = parts.length > 0 ? parts.join(', ') : 'Keine neuen Entwicklungen'; const summaryText = parts.length > 0 ? parts.join(', ') : 'Keine neuen Entwicklungen';
const summaryEl = document.getElementById('progress-complete-summary'); const summaryEl = document.getElementById('progress-complete-summary');

Datei anzeigen

@@ -5,6 +5,11 @@ sind durch Testdoubles ersetzt.
Aufruf aus dem Projektstamm: Aufruf aus dem Projektstamm:
venv/bin/python tests/test_eu_factcheck.py venv/bin/python tests/test_eu_factcheck.py
Seit der Belegtiefe-Pruefung (Block 2) traegt eine einzelne Quelle keine
Bestaetigung mehr. Die Faelle H und J liefern deshalb Belege aus mehreren
unabhaengigen Haeusern, sonst pruefen sie nicht mehr die Zuordnung, sondern
nur noch die Mindestzahl.
""" """
import asyncio import asyncio
import os import os
@@ -219,7 +224,8 @@ antworten.append(json_fakten([
(F_TOTE, "unconfirmed", "unklar"), (F_TOTE, "unconfirmed", "unklar"),
(F_SANCHEZ, "unconfirmed", "unklar"), (F_SANCHEZ, "unconfirmed", "unklar"),
])) ]))
antworten.append(json_fakten([(F_TOTE, "confirmed", "[S1] https://example.org/a belegt")])) antworten.append(json_fakten([(F_TOTE, "confirmed",
"[S1] https://example.org/a und [S2] https://beispiel.de/b belegen")]))
facts_h, _ = asyncio.run(fc._eu_pruefen( facts_h, _ = asyncio.run(fc._eu_pruefen(
"Auftrag", title="Ceuta (Opfer)", articles=None, output_language="Deutsch", "Auftrag", title="Ceuta (Opfer)", articles=None, output_language="Deutsch",
search_items=[F_TOTE, F_SANCHEZ], incident_type="adhoc", search_items=[F_TOTE, F_SANCHEZ], incident_type="adhoc",
@@ -254,7 +260,8 @@ neu()
# Suchquelle. Vor der Korrektur wurde so ein Fakt herabgestuft. # Suchquelle. Vor der Korrektur wurde so ein Fakt herabgestuft.
NUR_SUCHE = "Die Innenministerkonferenz beschliesst einen Fahrplan" NUR_SUCHE = "Die Innenministerkonferenz beschliesst einen Fahrplan"
antworten.append(json_fakten([ antworten.append(json_fakten([
(NUR_SUCHE, "established", "belegt durch die Suche"), (NUR_SUCHE, "established",
"belegt durch die Suche: https://example.org/a, https://beispiel.de/b, https://muster.at/c"),
(F_GRENZE, "confirmed", "belegt"), (F_GRENZE, "confirmed", "belegt"),
])) ]))
facts_j, _ = asyncio.run(fc.check("Ceuta", ARTS, "research", "Deutsch")) facts_j, _ = asyncio.run(fc.check("Ceuta", ARTS, "research", "Deutsch"))

Datei anzeigen

@@ -0,0 +1,250 @@
"""Testet die Belegtiefe des Faktenchecks.
Grundlage sind die drei Fehler, die der Vergleich der Ceuta-Berichte vom
01.08.2026 in der EU-Fassung zeigte:
1. Eine sachlich zutreffende Zahlenspanne stand als WIDERLEGT, weil der Status
"contradicted" im Auftrag Quellendivergenz meint, in der Anzeige aber als
Widerlegung ausgegeben wurde.
2. "Der Ausloeser ist weiterhin unklar" wurde von der Nachhak-Runde auf
BESTAETIGT hochgestuft, weil "developing" die niedrigste Prioritaet hat.
3. Eine Behauptung stand als BESTAETIGT mit einer Quelle, deren Evidenz
woertlich mit "Nur durch Al Jazeera berichtet" begann.
Dazu kommt die Zaehlung: sechs France24-Treffer sind ein Medienhaus, nicht
sechs Belege.
Laeuft ohne Netzzugriff, ohne Kosten und ohne Datenbank.
Aufruf aus dem Projektstamm:
venv/bin/python tests/test_faktencheck_belegtiefe.py
"""
import asyncio
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src"))
import agents.factchecker as fc # noqa: E402
from agents.claude_client import ClaudeUsage # noqa: E402
import report_generator as rg # noqa: E402
ok = 0
fail = 0
def pruefe(name, bedingung, extra=""):
global ok, fail
if bedingung:
ok += 1
print(" OK " + name)
else:
fail += 1
print(" FEHL " + name + " " + str(extra))
# ---------------------------------------------------------------------------
# A. Zaehlung nach Medienhaus
# ---------------------------------------------------------------------------
print("\nA. Belege zaehlen")
france24 = [
"https://www.france24.com/en/ceuta-spain-slams-selfish-eu-partners",
"https://www.france24.com/en/video/20260801-ceuta-enclave-almost-back-to-normal-1",
"https://www.france24.com/en/video/20260801-ceuta-most-migrants-have-returned-1",
"https://www.france24.com/en/ceuta-2026-dwarfs-2021-migration-crisis",
]
pruefe("A1 vier France24-Fundstellen sind ein Haus",
fc.zaehle_medienhaeuser(france24) == 1, fc.zaehle_medienhaeuser(france24))
gemischt = france24 + [
"https://www.tagesschau.de/ausland/europa/ceuta-100.html",
"https://www.theguardian.com/world/2026/aug/01/spain-ceuta",
"https://www.bbc.co.uk/news/articles/abc",
]
pruefe("A2 drei weitere Haeuser ergeben vier",
fc.zaehle_medienhaeuser(gemischt) == 4, fc.zaehle_medienhaeuser(gemischt))
redirects = [
"https://news.google.com/rss/articles/AAA?oc=5",
"https://news.google.com/rss/articles/BBB?oc=5",
]
pruefe("A3 zwei Weiterleitungen bleiben zwei, sie koennen zwei Zeitungen sein",
fc.zaehle_medienhaeuser(redirects) == 2, fc.zaehle_medienhaeuser(redirects))
pruefe("A4 Satzzeichen am Adressende stoeren nicht",
fc.zaehle_medienhaeuser(["https://www.zeit.de/a.html)."]) == 1)
pruefe("A5 leere Liste ergibt null", fc.zaehle_medienhaeuser([]) == 0)
# Regressionsschutz: das alte Muster zaehlte nur das Schema und lieferte
# deshalb fuer jede Evidenz 1 oder 2, egal wie viele Quellen darin standen.
evidenz = ("Bestätigt durch: tagesschau (https://www.tagesschau.de/a.html), "
"Guardian (https://www.theguardian.com/b), BBC (https://www.bbc.co.uk/c)")
pruefe("A6 Adressen werden vollstaendig erkannt, nicht nur das Schema",
fc.zaehle_medienhaeuser(fc._URL_RE.findall(evidenz)) == 3,
fc._URL_RE.findall(evidenz))
# ---------------------------------------------------------------------------
# B. Nachbedingung: Status gegen Beleglage
# ---------------------------------------------------------------------------
print("\nB. Belegtiefe pruefen")
einzelquelle = {
"claim": "Spaniens Außenminister versicherte, die Integrität sei garantiert",
"status": "confirmed",
"sources_count": 1,
"evidence": "Nur durch Al Jazeera berichtet (https://www.aljazeera.com/news/x)",
}
fc.pruefe_belegtiefe(einzelquelle)
pruefe("B1 bestaetigt mit einer Quelle wird herabgestuft",
einzelquelle["status"] == "unconfirmed", einzelquelle["status"])
zwei = {"claim": "x", "status": "confirmed", "sources_count": 2, "evidence": ""}
fc.pruefe_belegtiefe(zwei)
pruefe("B2 zwei Haeuser tragen eine Bestaetigung", zwei["status"] == "confirmed")
research_knapp = {"claim": "x", "status": "established", "sources_count": 2, "evidence": ""}
fc.pruefe_belegtiefe(research_knapp)
pruefe("B3 gesichert verlangt drei Haeuser", research_knapp["status"] == "unverified",
research_knapp["status"])
primaer = {
"claim": "Weber kritisiert Spanien",
"status": "confirmed",
"sources_count": 1,
"evidence_type": "primary",
"evidence": "O-Ton im ZDF-Interview (https://www.zdfheute.de/politik/x.html)",
}
fc.pruefe_belegtiefe(primaer)
pruefe("B4 Primaerbeleg traegt allein", primaer["status"] == "confirmed", primaer["status"])
ohne_zahl = {
"claim": "x", "status": "confirmed",
"evidence": "a (https://www.zeit.de/1), b (https://www.faz.net/2)",
}
fc.pruefe_belegtiefe(ohne_zahl)
pruefe("B5 fehlende Belegzahl wird aus der Evidenz bestimmt",
ohne_zahl["sources_count"] == 2 and ohne_zahl["status"] == "confirmed", ohne_zahl)
widerspruch = {"claim": "x", "status": "contradicted", "sources_count": 1, "evidence": ""}
fc.pruefe_belegtiefe(widerspruch)
pruefe("B6 Widerspruchs-Status wird nicht angetastet", widerspruch["status"] == "contradicted")
# ---------------------------------------------------------------------------
# C. Anzeige: Widerspruch ist keine Widerlegung
# ---------------------------------------------------------------------------
print("\nC. Statusbezeichnungen")
labels = rg._fc_labels("de")
pruefe("C1 contradicted heisst nicht mehr Widerlegt",
labels["contradicted"] == "Widersprüchlich", labels["contradicted"])
pruefe("C2 Widerlegt ist dem eigenen Status vorbehalten",
labels["false"] == "Widerlegt", labels.get("false"))
pruefe("C3 englische Fassung getrennt",
rg._fc_labels("en")["contradicted"] == "Conflicting", rg._fc_labels("en")["contradicted"])
pruefe("C4 false hat eine Prioritaet wie andere belegte Befunde",
fc.STATUS_PRIORITY.get("false") == 4, fc.STATUS_PRIORITY.get("false"))
aufbereitet = rg._prepare_fact_checks([
{"claim": "a", "status": "contradicted"}, {"claim": "b", "status": "false"},
], "de")
pruefe("C5 Bericht beschriftet beide Status getrennt",
[f["status_label"] for f in aufbereitet] == ["Widersprüchlich", "Widerlegt"],
[f["status_label"] for f in aufbereitet])
# ---------------------------------------------------------------------------
# D. Bewertungsregeln liegen jedem Auftrag bei
# ---------------------------------------------------------------------------
print("\nD. Auftragsregeln")
regeln = fc.bewertungsregeln()
for stichwort, name in (
("Medienhäuser", "D1 Unabhaengigkeit ueber Medienhaeuser"),
("news.google.com", "D2 Weiterleitungen ausgeschlossen"),
("Erhebungsstellen", "D3 Zahlenspannen sind kein Widerspruch"),
("primary", "D4 Primaerbeleg definiert"),
("Kenntnisstand", "D5 keine Aussagen ueber den Kenntnisstand"),
):
pruefe(name, stichwort in regeln)
umschreibungen = ["fuer", "muessen", "koennen", "haeuser", "zaehlt", "Widerspruechlich",
"Auftraege", "gehoeren", "unabhaengig", "genuegt"]
gefunden = [u for u in umschreibungen if u.lower() in regeln.lower()]
pruefe("D6 Regeln nutzen echte Umlaute statt Umschreibungen", not gefunden, gefunden)
# ---------------------------------------------------------------------------
# E. Nachhak-Runde
# ---------------------------------------------------------------------------
print("\nE. Nachhak-Runde")
class FakeSuche:
def __init__(self):
self.block = "\n[S1] Beleg | zeit.de | https://www.zeit.de/a"
self.usage = ClaudeUsage()
self.queries = ["q"]
self.quellen = []
async def fake_plan(**kwargs):
return FakeSuche()
antwort = {"text": "[]"}
async def fake_call(prompt, **kwargs):
return antwort["text"], ClaudeUsage()
import agents.eu_researcher as eur # noqa: E402
import agents.claude_client as cc # noqa: E402
eur.plan_verification_searches = fake_plan
cc.call_claude = fake_call
agent = fc.FactCheckerAgent()
# Fall 1: Die Nachrunde will eine Unklarheit bestaetigen.
unklar = {
"claim": "Der Auslöser des massenhaften Grenzübertritts ist weiterhin unklar",
"status": "developing", "sources_count": 4,
"evidence": "Mehrere Quellen berichten über Unklarheit (https://www.bbc.co.uk/a)",
}
antwort["text"] = ('[{"claim": "Der Auslöser des massenhaften Grenzübertritts ist weiterhin unklar",'
' "status": "confirmed", "evidence": "Vier Quellen nennen die Unklarheit"}]')
facts, _ = asyncio.run(agent._eu_nachhaken(
facts=[unklar], offene=[unklar], title="Ceuta", output_language="Deutsch",
incident_type="adhoc", avoid_queries=[],
))
pruefe("E1 eine Unklarheit wird nicht bestaetigt", facts[0]["status"] == "developing", facts[0]["status"])
# Fall 2: echte Hochstufung, aber nur eine Quelle im Beleg
duenn = {
"claim": "Spaniens Außenminister versicherte, die Integrität sei garantiert",
"status": "unconfirmed", "sources_count": 1,
"evidence": "Nur durch Al Jazeera berichtet (https://www.aljazeera.com/a)",
}
antwort["text"] = ('[{"claim": "Spaniens Außenminister versicherte, die Integrität sei garantiert",'
' "status": "confirmed", "evidence": "Auch hier (https://www.aljazeera.com/b)"}]')
facts, _ = asyncio.run(agent._eu_nachhaken(
facts=[duenn], offene=[duenn], title="Ceuta", output_language="Deutsch",
incident_type="adhoc", avoid_queries=[],
))
pruefe("E2 zweiter Treffer desselben Hauses traegt keine Bestaetigung",
facts[0]["status"] == "unconfirmed", facts[0]["status"])
# Fall 3: gedeckte Hochstufung durch ein zweites Haus
tragfaehig = {
"claim": "Italien führt Grenzkontrollen für Flüge und Schiffe aus Spanien ein",
"status": "unconfirmed", "sources_count": 1,
"evidence": "Kurier (https://www.kurier.at/a)",
}
antwort["text"] = ('[{"claim": "Italien führt Grenzkontrollen für Flüge und Schiffe aus Spanien ein",'
' "status": "confirmed", "evidence": "Bestätigt (https://www.ansa.it/b)"}]')
facts, _ = asyncio.run(agent._eu_nachhaken(
facts=[tragfaehig], offene=[tragfaehig], title="Ceuta", output_language="Deutsch",
incident_type="adhoc", avoid_queries=[],
))
pruefe("E3 zweites unabhaengiges Haus stuft hoch",
facts[0]["status"] == "confirmed", facts[0]["status"])
pruefe("E4 Belegzahl folgt der neuen Evidenz",
facts[0]["sources_count"] == 2, facts[0]["sources_count"])
print("\nErgebnis: " + str(ok) + " bestanden, " + str(fail) + " fehlgeschlagen")
sys.exit(1 if fail else 0)