{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://polls.karbach.digital/data/schemas/wahl_btw.schema.json",
  "title": "BTW-Wahl-Snapshot (polls v4)",
  "description": "Schema fuer Bundestagswahl-Snapshots. Pflicht: 17 Felder; Optional: ai_analysis + wahlkreise[299]. Sprint 1 — 2026-05-19.",
  "type": "object",
  "required": [
    "election",
    "parties",
    "party_results",
    "seat_results",
    "coalitions",
    "coalitions_stability",
    "strongest",
    "source",
    "updated_at",
    "methodology",
    "poll_history",
    "input_polls",
    "quantiles_100",
    "days_until_election",
    "view_mode_hint",
    "party_colors"
  ],
  "properties": {
    "election": {
      "type": "object",
      "required": [
        "id",
        "name",
        "state",
        "date",
        "seats",
        "method",
        "threshold"
      ],
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^btw-\\d{4}$"
        },
        "name": {
          "type": "string",
          "minLength": 1
        },
        "state": {
          "type": "string",
          "const": "DE"
        },
        "date": {
          "type": "string",
          "format": "date"
        },
        "seats": {
          "type": "integer",
          "minimum": 100,
          "maximum": 1000,
          "description": "Tatsaechliche Parlamentsgroesse, z.B. 630 fuer BTW 2029"
        },
        "method": {
          "type": "string",
          "enum": [
            "sainte_lague",
            "dhondt",
            "hare_niemeyer"
          ]
        },
        "threshold": {
          "type": "number",
          "minimum": 0,
          "maximum": 10,
          "description": "5%-Klausel = 5"
        }
      }
    },
    "parties": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "minItems": 3,
      "uniqueItems": true
    },
    "party_results": {
      "type": "object",
      "additionalProperties": {
        "type": "object",
        "required": [
          "ci95_pct",
          "entry_prob",
          "median_pct",
          "poll_avg",
          "quantiles_100",
          "sigma"
        ],
        "properties": {
          "poll_avg": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "median_pct": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "sigma": {
            "type": "number",
            "minimum": 0
          },
          "entry_prob": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Wahrscheinlichkeit dass Partei ueber Threshold + Klauseln einzieht"
          },
          "ci95_pct": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "minItems": 2,
            "maxItems": 2
          },
          "quantiles_100": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "minItems": 101,
            "maxItems": 101
          }
        }
      }
    },
    "seat_results": {
      "type": "object",
      "additionalProperties": {
        "type": "object",
        "required": [
          "ci95",
          "mean",
          "median"
        ],
        "properties": {
          "median": {
            "type": "integer",
            "minimum": 0
          },
          "mean": {
            "type": "number",
            "minimum": 0
          },
          "ci95": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "minItems": 2,
            "maxItems": 2
          }
        }
      }
    },
    "coalitions": {
      "type": "object",
      "description": "Koalitions-Wahrscheinlichkeiten in %. Nach D-2-Filter (Sprint 0 Tag 2) nur Koalitionen mit min(entry_prob) >= 30 % aller Mitglieder.",
      "additionalProperties": {
        "type": "number",
        "minimum": 0,
        "maximum": 100
      }
    },
    "coalitions_stability": {
      "type": "object",
      "additionalProperties": {
        "oneOf": [
          {
            "type": "null"
          },
          {
            "type": "object",
            "required": [
              "median_surplus",
              "p10_surplus",
              "p90_surplus",
              "solo_majority_prob"
            ],
            "properties": {
              "median_surplus": {
                "type": "integer"
              },
              "p10_surplus": {
                "type": "integer"
              },
              "p90_surplus": {
                "type": "integer"
              },
              "solo_majority_prob": {
                "type": "number",
                "minimum": 0,
                "maximum": 100
              },
              "member_seats_when_active": {
                "type": "object",
                "description": "S-4.5 (Sprint 4 Tail, 2026-05-19): pro Koalitions-Mitglied mediane Sitzzahl ueber valid-Iterationen. Frontend zeigt bei Phantom-Verdacht 'X Sitze wenn Koalition aktiv'.",
                "additionalProperties": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            }
          }
        ]
      }
    },
    "strongest": {
      "type": "object",
      "additionalProperties": {
        "type": "number",
        "minimum": 0,
        "maximum": 100
      }
    },
    "source": {
      "type": "string",
      "minLength": 1
    },
    "updated_at": {
      "type": "string"
    },
    "methodology": {
      "type": "object",
      "required": [
        "version"
      ],
      "properties": {
        "version": {
          "type": "string"
        }
      }
    },
    "poll_history": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "date",
          "results",
          "institute"
        ],
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "institute": {
            "type": "string"
          },
          "results": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          }
        }
      }
    },
    "input_polls": {
      "type": "integer",
      "minimum": 0
    },
    "quantiles_100": {
      "type": "object",
      "additionalProperties": {
        "type": "array",
        "items": {
          "type": "number"
        },
        "minItems": 101,
        "maxItems": 101
      }
    },
    "days_until_election": {
      "type": "integer"
    },
    "view_mode_hint": {
      "type": "string",
      "enum": [
        "qualitative",
        "quantitative",
        "early"
      ]
    },
    "party_colors": {
      "type": "object",
      "additionalProperties": {
        "type": "string",
        "pattern": "^#[0-9a-fA-F]{3,8}$"
      }
    },
    "ai_analysis": {
      "type": "object",
      "description": "Optional — LLM-generierte Mini-Analyse",
      "properties": {
        "text": {
          "type": "string"
        }
      }
    },
    "wahlkreise": {
      "type": "array",
      "description": "299 Bundestagswahlkreise. Pflicht fuer BTW. Jedes Element mit Direkt-/Zweitstimme-Hochrechnung + Winner.",
      "minItems": 299,
      "maxItems": 299,
      "items": {
        "type": "object",
        "required": [
          "nr",
          "name",
          "state",
          "erst",
          "zweit"
        ],
        "properties": {
          "nr": {
            "type": "integer",
            "minimum": 1,
            "maximum": 299
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "state": {
            "type": "string",
            "pattern": "^[A-Z]{2}$"
          },
          "erst": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          },
          "zweit": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          },
          "erst_winner": {
            "type": [
              "string",
              "null"
            ]
          },
          "zweit_winner": {
            "type": [
              "string",
              "null"
            ]
          },
          "winner": {
            "type": [
              "string",
              "null"
            ],
            "description": "Legacy-Feld, redundant zu erst_winner"
          }
        }
      }
    },
    "confidence_score": {
      "oneOf": [
        {
          "type": "object",
          "description": "v4.4 Tier1-d: Per-Wahl-Konfidenz-Score (1-10, eine Nachkommastelle)",
          "properties": {
            "value": {
              "type": "number",
              "minimum": 1.0,
              "maximum": 10.0
            },
            "scale": {
              "type": "string"
            },
            "label": {
              "type": "string"
            },
            "components": {
              "type": "object",
              "additionalProperties": {
                "type": "number",
                "minimum": 0.0,
                "maximum": 1.0
              }
            },
            "inputs": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "additionalProperties": false
        },
        {
          "type": "null"
        }
      ],
      "description": "v4.4: Per-Wahl-Konfidenz-Score (null bei vergangenen Wahlen)."
    }
  },
  "additionalProperties": true
}
