{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://torneo.ai/schemas/epreuve.v1.json",
  "title": "TORNEO épreuve, v1 (un tour complet de matchs deux à deux)",
  "description": "Une épreuve est un tour complet: tous les outils inscrits d'une catégorie s'affrontent deux à deux sur un corpus gelé et daté, propre à cette épreuve (TORNEO-009 §3). Elle produit un tableau de matchs, pas une note. C'est l'épreuve qui porte la correction de multiplicité, parce que c'est elle la famille de tests: k(k-1)/2 matchs.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "epreuve_id",
    "category",
    "tools",
    "corpus",
    "preregistration",
    "multiplicity",
    "alpha",
    "dimensions",
    "matches",
    "standings",
    "observed_at",
    "validators",
    "status"
  ],
  "properties": {
    "tool_vendors": {
      "type": "object",
      "additionalProperties": {"type": "string", "minLength": 1},
      "description": "Tool to vendor identity. A champion requires two distinct non-withdrawn vendors; missing identity does not establish diversity."
    },
    "schema_version": {
      "const": "epreuve.v1"
    },
    "epreuve_id": {
      "type": "string",
      "pattern": "^[A-Z0-9][A-Z0-9-]*$"
    },
    "category": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    },
    "tools": {
      "type": "array",
      "minItems": 2,
      "uniqueItems": true,
      "items": {
        "type": "string",
        "pattern": "^[a-z0-9][a-z0-9_-]*$"
      },
      "description": "Les outils INSCRITS. Un outil retiré après la collecte est une faute de conduite: le champ withdrawn existe pour que ce cas soit déclaré et non caché."
    },
    "withdrawn": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "tool",
          "reason",
          "withdrawn_at"
        ],
        "properties": {
          "tool": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "minLength": 1
          },
          "withdrawn_at": {
            "$ref": "#/$defs/utc_datetime"
          }
        }
      },
      "description": "Un outil qui n'a pas couru et pourquoi. Bloqué n'est pas battu: un outil absent ne perd aucun match, il n'en dispute aucun."
    },
    "corpus": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "unit_count",
        "sha256",
        "frozen_at"
      ],
      "properties": {
        "unit_count": {
          "type": "integer",
          "minimum": 1
        },
        "sha256": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$"
        },
        "frozen_at": {
          "$ref": "#/$defs/utc_datetime"
        },
        "description": {
          "type": "string"
        }
      },
      "description": "Le corpus est propre à l'épreuve et gelé AVANT la collecte. Son empreinte est ce qui permet à un tiers de rejouer l'analyse."
    },
    "preregistration": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "document",
        "sha256",
        "frozen_at"
      ],
      "properties": {
        "document": {
          "type": "string",
          "minLength": 1
        },
        "sha256": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$"
        },
        "frozen_at": {
          "$ref": "#/$defs/utc_datetime"
        }
      }
    },
    "multiplicity": {
      "enum": [
        "holm",
        "bonferroni",
        "none"
      ]
    },
    "alpha": {
      "type": "number",
      "exclusiveMinimum": 0,
      "exclusiveMaximum": 1
    },
    "matches": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": [
          "schema_version",
          "match_id",
          "tools",
          "dimensions"
        ],
        "properties": {
          "schema_version": {
            "const": "match.v1"
          }
        },
        "description": "Chaque élément est validé contre match.v1 par tools/check_tournament_schema.py. Le $ref inter-fichiers est évité à dessein: les schémas du dépôt sont autoportants et se valident sans résolveur."
      },
      "description": "Le tour est COMPLET: exactement k(k-1)/2 matchs pour k outils inscrits et non retirés. Un tour incomplet n'est pas une épreuve, et le contrôleur le refuse."
    },
    "standings": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "dimension",
          "table"
        ],
        "properties": {
          "dimension": {
            "enum": [
              "quality",
              "latency",
              "cost",
              "reliability",
              "energy"
            ]
          },
          "table": {
            "type": "array",
            "minItems": 2,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "tool",
                "wins",
                "draws",
                "losses"
              ],
              "properties": {
                "tool": {
                  "type": "string"
                },
                "wins": {
                  "type": "integer",
                  "minimum": 0
                },
                "draws": {
                  "type": "integer",
                  "minimum": 0
                },
                "losses": {
                  "type": "integer",
                  "minimum": 0
                },
                "not_measured": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            }
          }
        }
      },
      "description": "Un classement PAR DIMENSION, dérivé des matchs et jamais saisi. Il n'existe pas de classement agrégé: additionner qualité, coût et latence exigerait des poids que personne ne peut justifier, et le lecteur choisirait mal sans le savoir."
    },
    "observed_at": {
      "$ref": "#/$defs/utc_datetime"
    },
    "status": {
      "enum": [
        "OK",
        "LOCAL_VALIDITY",
        "INDETERMINATE"
      ],
      "description": "Repris tel quel du format actuel: seul OK est servi comme preuve. Une épreuve à un seul bloc reste LOCAL_VALIDITY, le format torneo ne change pas cette règle."
    },
    "validators": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/validator"
      },
      "description": "Les validateurs qui ont validé LA CONDUITE de cette épreuve, pas le chiffre (TORNEO-009 §5). Le tableau peut être vide: une épreuve non encore validée se publie comme non validée, elle ne se publie pas comme validée."
    },
    "evidence_uri": {
      "type": "string",
      "minLength": 1
    },
    "limits": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "dimensions": {
      "type": "array",
      "minItems": 1,
      "items": {
        "enum": [
          "quality",
          "latency",
          "cost",
          "reliability",
          "energy"
        ]
      },
      "description": "Les dimensions que cette épreuve mesure ou déclare non mesurables. Tout match du tour porte exactement ces dimensions: un tour où un match mesure le coût et un autre non ne se compare pas."
    },
    "epreuve_winners": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "quality": {
          "type": [
            "string",
            "null"
          ]
        },
        "latency": {
          "type": [
            "string",
            "null"
          ]
        },
        "cost": {
          "type": [
            "string",
            "null"
          ]
        },
        "reliability": {
          "type": [
            "string",
            "null"
          ]
        },
        "energy": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "description": "Le vainqueur PAR DIMENSION, ou null. Aucun vainqueur d'épreuve toutes dimensions confondues: TORNEO n'agrège pas des grandeurs sans commune mesure."
    },
    "pareto_set": {
      "type": "array",
      "items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]*$" },
      "description": "Les outils qu'aucun autre ne domine EN CONFRONTATION DIRECTE: A domine B si, dans leur match, A gagne au moins une dimension et n'en perd aucune (D3). Aucune pondération n'entre ici, donc aucune ne s'y cache. Cet ensemble peut être VIDE, et alors dominance_cycles doit nommer le cycle: la domination directe n'est pas transitive, trois outils peuvent se battre en rond, et ce résultat se publie."
    },
    "dominance_basis": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["dominant", "dominated", "won"],
        "properties": {
          "dominant": { "type": "string" },
          "dominated": { "type": "string" },
          "won": { "type": "array", "items": { "type": "string" } },
          "undecided": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "dimension": { "type": "string" },
                "draw_kind": { "type": ["string", "null"] }
              }
            }
          },
          "not_measured": { "type": "array", "items": { "type": "string" } }
        }
      },
      "description": "Sur quoi repose chaque domination. « A domine B » se lit vite comme « B est inférieur en tout »; c'est faux, la domination ne dit rien des dimensions où le test n'a pas séparé ni de celles qui ne sont pas mesurées. Publier la base laisse voir la différence entre « A gagne le coût et la qualité » et « A gagne la qualité, le reste est indéterminé »."
    },
    "dominance_cycles": {
      "type": "array",
      "items": {
        "type": "array",
        "minItems": 2,
        "items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]*$" }
      },
      "description": "Les GROUPES d'outils pris dans un cycle de domination, chacun étant une composante fortement connexe du graphe: à l'intérieur, chaque outil en domine un autre de proche en proche, donc aucun ordre n'existe entre eux. Obligatoire dès que pareto_set est vide: un ensemble vide sans cycle nommé passerait pour une erreur de calcul, alors que c'est un fait sur les outils. On publie le groupe et non la liste des circuits: sur un tour complet sans nul, les circuits simples se comptent par centaines de milliers et le groupe dit la même chose."
    },
    "champion": {
      "type": ["string", "null"],
      "description": "D3: nommé UNIQUEMENT si un outil tient le titre de TOUTES les dimensions, et à condition qu'aucune dimension de l'épreuve ne soit restée non mesurée. Gagner le coût et perdre la fiabilité ne fait pas un champion; cela fait un titre de dimension. La couronne est rare par construction, et c'est ce qui la rend informative."
    },
    "denominators": {
      "type": "object",
      "additionalProperties": false,
      "required": ["comparisons_played", "comparisons_concluded", "matches_played",
                   "matches_concluded"],
      "properties": {
        "comparisons_played": {
          "type": "integer", "minimum": 0,
          "description": "Une comparaison = un match SUR UNE dimension. Un match en porte cinq: confondre les deux multiplierait le tableau par cinq sans que personne ne s'en aperçoive."
        },
        "comparisons_concluded": { "type": "integer", "minimum": 0 },
        "matches_played": { "type": "integer", "minimum": 0 },
        "matches_concluded": {
          "type": "integer", "minimum": 0,
          "description": "Un match est conclu dès qu'UNE dimension a départagé les deux outils. C'est le dénominateur littéral de D3, « matchs conclus sur matchs joués »."
        },
        "per_dimension": {
          "type": "object",
          "additionalProperties": {
            "type": "object",
            "additionalProperties": false,
            "required": ["played", "concluded"],
            "properties": {
              "played": { "type": "integer", "minimum": 0 },
              "concluded": { "type": "integer", "minimum": 0 },
              "draws_insufficient_precision": { "type": "integer", "minimum": 0 },
              "draws_equivalent_within_margin": { "type": "integer", "minimum": 0 },
              "draws_unspecified": { "type": "integer", "minimum": 0 },
              "not_measured": { "type": "integer", "minimum": 0 }
            }
          }
        }
      },
      "description": "« Matchs conclus sur matchs joués » (D3), globalement et par dimension. Sans ce dénominateur, trois victoires se lisent comme une domination alors qu'elles peuvent venir de trois comparaisons sur trente."
    },
    "epreuve_winner_rule": {
      "type": "string",
      "description": "La règle qui produit un vainqueur de dimension, écrite et préenregistrée."
    },
    "arbitration_rule": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "document",
        "sha256"
      ],
      "properties": {
        "document": {
          "type": "string",
          "minLength": 1
        },
        "sha256": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$"
        }
      },
      "description": "La règle écrite selon laquelle les désaccords ont été arbitrés (TORNEO-010 §1). Sans elle, la case correspondante de la charte n'est pas cochable, et c'est un défaut de protocole, pas de validateur."
    },
    "replay": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "command",
        "artefacts",
        "corpus_available_to_third_party"
      ],
      "properties": {
        "command": {
          "type": "string",
          "minLength": 1
        },
        "artefacts": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "string",
            "minLength": 1
          }
        },
        "corpus_available_to_third_party": {
          "type": "boolean"
        },
        "unavailability_reason": {
          "type": "string"
        }
      },
      "description": "De quoi un tiers a besoin pour refaire l'analyse depuis le corpus gelé. TORNEO-010 §2: la charge de la preuve est sur TORNEO, jamais sur le validateur, donc cela doit être vérifiable par commande."
    }
  },
  "$defs": {
    "utc_datetime": {
      "type": "string",
      "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$"
    },
    "validator": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "validator_id",
        "signed_at",
        "charter_version",
        "checks"
      ],
      "properties": {
        "validator_id": {
          "type": "string",
          "minLength": 1,
          "description": "Identifiant du validateur. Aucun nom n'est inscrit dans le schéma tant que Charles n'a désigné personne: le champ existe, la valeur attend."
        },
        "signed_at": {
          "$ref": "#/$defs/utc_datetime"
        },
        "charter_version": {
          "type": "string",
          "minLength": 1
        },
        "checks": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "corpus_frozen_before_collection",
            "test_and_threshold_preregistered",
            "no_tool_withdrawn_after_the_fact",
            "disagreements_arbitrated_by_the_written_rule",
            "third_party_can_replay_from_the_frozen_corpus"
          ],
          "properties": {
            "corpus_frozen_before_collection": {
              "type": "boolean"
            },
            "test_and_threshold_preregistered": {
              "type": "boolean"
            },
            "no_tool_withdrawn_after_the_fact": {
              "type": "boolean"
            },
            "disagreements_arbitrated_by_the_written_rule": {
              "type": "boolean"
            },
            "third_party_can_replay_from_the_frozen_corpus": {
              "type": "boolean"
            }
          },
          "description": "Les cases de la charte. Une case qu'un validateur ne peut pas cocher est un défaut de protocole, pas un défaut de validateur: chaque case correspond à un fait vérifiable par commande, et le contrôleur refuse une case cochée que les données contredisent."
        },
        "reservations": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          },
          "description": "Ce que le validateur signe malgré tout, en le disant. Une réserve se publie avec l'épreuve."
        }
      }
    }
  }
}
