# GLADIATOR contest definition schema, v2 (JSON Schema 2020-12, YAML surface).
# v2 = v1 plus `design` (the six frozen pairwise parameters) and `coverage`
# (must_include), both projected into result.v2 `conclusion`. contest.v1 is
# preserved and still valid for the categories already run.
# Decision: docs/decisions/L1-0002-result-v2-conclusion.md
$schema: "https://json-schema.org/draft/2020-12/schema"
$id: "https://github.com/devlollc/gladiator/schemas/contest.v2.yaml"
title: "GLADIATOR contest definition, v2"
type: object
additionalProperties: false
required:
  - contest_version
  - category
  - promise
  - task
  - dataset
  - metrics
  - participants
  - budget
  - window
  - scoring
  - design
  - coverage
  - freshness
  - conflicts
  - limits
properties:
  contest_version:
    const: "contest.v2"
  category:
    type: string
    pattern: "^[a-z0-9][a-z0-9_-]*$"
  promise: { type: string, minLength: 1 }
  task: { type: string, minLength: 1 }
  context: { type: string }
  objective_and_population:
    type: string
    minLength: 1
    description: >-
      One sentence: what is measured, on which frozen population, at which
      date. Projected verbatim as the FIRST field of result.v2 conclusion.
  dataset:
    type: object
    additionalProperties: false
    required: [description, size, frozen_at, sha256, contains_personal_data]
    properties:
      description: { type: string, minLength: 1 }
      size: { type: integer, minimum: 1 }
      frozen_at: { $ref: "#/$defs/utc_datetime" }
      sha256: { $ref: "#/$defs/sha256" }
      contains_personal_data: { type: boolean }
  metrics:
    type: object
    additionalProperties: false
    required: [primary]
    properties:
      primary: { type: string, minLength: 1 }
      secondary:
        type: array
        items: { type: string }
  participants:
    type: array
    minItems: 1
    description: >-
      minItems is 1 in v2: a single-arm case study is a legitimate published
      result (C5), reported with conclusion.single_vendor true and no winner.
    items: { $ref: "#/$defs/participant" }
  budget:
    type: object
    additionalProperties: false
    required: [cap_usd]
    properties:
      cap_usd: { type: number, minimum: 0 }
      per_participant_cap_usd: { type: number, minimum: 0 }
  window:
    type: object
    additionalProperties: false
    required: [start, end]
    properties:
      start: { $ref: "#/$defs/utc_datetime" }
      end: { $ref: "#/$defs/utc_datetime" }
  scoring:
    type: object
    additionalProperties: false
    required: [min_n, max_interval_width, confidence, min_blocks_for_rank]
    properties:
      min_n: { type: integer, minimum: 1 }
      max_interval_width: { type: number, exclusiveMinimum: 0, maximum: 1 }
      confidence: { type: number, exclusiveMinimum: 0, exclusiveMaximum: 1 }
      min_blocks_for_rank: { type: integer, minimum: 1 }
  design:
    type: object
    additionalProperties: false
    description: >-
      The pairwise test, frozen before the run. All six parameters are required
      together; the schema refuses a partial design outright, and pairwise.py
      refuses to emit any per-pair flag if one is missing at runtime (C4).
    required: [test, alpha, correction, cluster_unit, power, mde]
    properties:
      test:
        type: string
        minLength: 1
        description: "Named statistical test, e.g. two-proportion z-test, cluster-robust."
      alpha: { type: number, exclusiveMinimum: 0, exclusiveMaximum: 1 }
      correction:
        type: string
        minLength: 1
        description: "Multiplicity correction across pairs, e.g. holm, bonferroni, none_single_pair."
      cluster_unit:
        type: string
        minLength: 1
        description: "Unit of independence used for clustering, e.g. contact, clip, company."
      power: { type: number, exclusiveMinimum: 0, exclusiveMaximum: 1 }
      mde:
        type: number
        exclusiveMinimum: 0
        description: "Minimal detectable effect, absolute, commercially useful. Set before the run."
  coverage:
    type: object
    additionalProperties: false
    required: [must_include]
    properties:
      must_include:
        type: array
        items: { type: string, minLength: 1 }
        description: >-
          Vendors the category must cover to be honest about the market. Any
          entry absent from participants becomes a conclusion.coverage
          must_include_gap row with a reason from the shared enumeration.
  freshness:
    type: object
    additionalProperties: false
    required: [validity_days]
    properties:
      validity_days: { type: integer, minimum: 1 }
  assignment:
    type: object
    additionalProperties: false
    required: [method, seed]
    properties:
      method: { type: string, minLength: 1 }
      seed: { type: [integer, string] }
  applies_to:
    type: array
    items: { type: string, minLength: 1 }
  does_not_apply_to:
    type: array
    items: { type: string, minLength: 1 }
  conflicts:
    type: array
    items: { type: string }
  limits:
    type: array
    minItems: 1
    items: { type: string, minLength: 1 }
  runner: { type: string }
  funding: { type: string }
$defs:
  utc_datetime:
    type: string
    pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$"
  sha256:
    type: string
    pattern: "^[0-9a-f]{64}$"
  participant:
    type: object
    additionalProperties: false
    required: [name]
    properties:
      name: { type: string, pattern: "^[a-z0-9][a-z0-9_-]*$" }
      vendor: { type: string }
      tool_version: { type: string }
      excluded: { type: boolean }
      devlo_linked: { type: boolean }
      exclusion_reason: { type: string }
      blinded:
        type: boolean
        description: >-
          True when this participant appears in the public journal under an
          alias (e.g. arm_a) instead of its real vendor name. The alias is
          the value of `name` here. The alias→vendor mapping is committed as
          `arm_map_sha256` at lock time and revealed only after
          `result_sha256` is set, via an `unblinding` event at the end of
          the journal. Decision: docs/decisions/L1-0003-unblinding.md
      arm_map_sha256:
        $ref: "#/$defs/sha256"
        description: >-
          sha256 of the alias table held in the audit vault. Committed at
          lock time; must match the `arm_map_sha256` in the `unblinding`
          journal event. Required when `blinded: true`.
      access:
        type: object
        additionalProperties: false
        properties:
          api: { type: boolean }
          cli: { type: boolean }
          mcp: { type: boolean }
          auth: { type: string }
          setup_minutes: { type: number, minimum: 0 }
    allOf:
      - if:
          properties:
            devlo_linked: { const: true }
          required: [devlo_linked]
        then:
          required: [excluded, exclusion_reason]
          properties:
            excluded: { const: true }
      - if:
          properties:
            blinded: { const: true }
          required: [blinded]
        then:
          required: [arm_map_sha256]
