{
  "$ref": "#/definitions/Scenario",
  "definitions": {
    "Scenario": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "minLength": 1,
          "description": "Unique slug. Referenced by ScenarioSnapshot.scenario_id."
        },
        "label": {
          "type": "string",
          "minLength": 1,
          "description": "Human-readable label in the instance's default language."
        },
        "region": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          ],
          "description": "ISO 3166-1 alpha-2 / ISO 3166-2 region code, or array. Matches Poll.region."
        },
        "election_type": {
          "type": "string",
          "description": "Election type, e.g. \"parliamentary\". Matches Poll.type."
        },
        "is_default": {
          "type": "boolean",
          "description": "Whether this is the default scenario shown to end users. At most one scenario per region/election_type should be true."
        },
        "choices": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          },
          "minItems": 1,
          "description": "Ordered list of choice_ids to include. Order controls display order in charts and tables."
        },
        "preferred_tags": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          },
          "minItems": 1,
          "description": "Priority-ordered list of poll output tags to try when looking for a direct value. First matching tag wins."
        },
        "fallback": {
          "type": "string",
          "enum": [
            "members_sum",
            "none"
          ],
          "default": "members_sum",
          "description": "What to do when no raw output has the choice directly. \"members_sum\": sum member choice values. \"none\": leave as absent."
        },
        "composition": {
          "type": "object",
          "additionalProperties": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "description": "Coalition composition map: choice_id → member choice_ids. Authoritative source for member lookups during derivation."
        },
        "others_id": {
          "type": "string",
          "description": "choice_id of the catch-all \"others\" bucket. Unmapped parties flow here automatically."
        },
        "estimation": {
          "type": "object",
          "properties": {
            "allow_partial_members": {
              "type": "boolean",
              "default": true,
              "description": "Include a coalition even if some member values are missing (partial sum). Default: true."
            },
            "active_min_polls": {
              "type": "integer",
              "minimum": 1,
              "default": 2,
              "description": "Minimum number of recent polls in which a choice must appear (at ≥ active_min_value) to be considered \"known active\". Default: 2."
            },
            "active_window_polls": {
              "type": "integer",
              "minimum": 1,
              "default": 10,
              "description": "How many of the most recent polls to search when applying the known-active filter. Default: 10."
            },
            "active_min_value": {
              "type": "number",
              "minimum": 0,
              "default": 0.3,
              "description": "Minimum value_percent a choice must have in a poll to count as an \"active\" appearance. Default: 0.3."
            },
            "cut_fraction": {
              "type": "number",
              "minimum": 0,
              "maximum": 1,
              "default": 0.5,
              "description": "Fraction of lower_cut_percent to assign as the estimated value for a choice under the cut. Default: 0.5 (half the cut)."
            },
            "pool_method": {
              "type": "string",
              "enum": [
                "equal",
                "proportional"
              ],
              "default": "equal",
              "description": "How the unaccounted residual pool is distributed among absent known-active choices. Default: \"equal\"."
            },
            "pool_cap_per_party": {
              "type": "number",
              "minimum": 0,
              "description": "Maximum value_percent any single choice may receive from the pool. No cap if omitted."
            },
            "max_total": {
              "type": "number",
              "minimum": 0,
              "default": 100,
              "description": "Hard ceiling: sum of all derived values in a scenario poll may not exceed this. Default: 100."
            }
          },
          "additionalProperties": false,
          "description": "Policy for filling missing values when deriving a poll's results under a scenario.\n\nLayer: config (embedded in Scenario)"
        },
        "extras": {
          "type": "object",
          "additionalProperties": {},
          "description": "Arbitrary extra fields."
        }
      },
      "required": [
        "id",
        "label",
        "region",
        "choices",
        "preferred_tags",
        "estimation"
      ],
      "additionalProperties": false,
      "description": "Definition of a display/simulation grouping.\n\nLayer: config (input to the compute layer)\n\nSpecifies which choices are included and how to fill missing values from raw poll data. Input to the compute-scenarios script which produces ScenarioSnapshot files."
    }
  },
  "$schema": "http://json-schema.org/draft-07/schema#"
}