{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://torneo.ai/schemas/palmares.v1.json",
  "title": "TORNEO palmarès d'une catégorie, v1 (accumulation d'épreuves)",
  "description": "Le palmarès accumule les épreuves d'une catégorie: matchs gagnés, nuls, perdus, épreuves gagnées (TORNEO-009 §3). C'est lui le chiffre citable, et il ne se recouvre pas: un compte de victoires est un entier. Il ne compte QUE le significatif; il n'invente aucune séparation et ne convertit aucun nul en demi-point.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "category",
    "epreuves",
    "standings",
    "cadence",
    "publishable",
    "computed_at"
  ],
  "properties": {
    "schema_version": {
      "const": "palmares.v1"
    },
    "category": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    },
    "epreuves": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "epreuve_id",
          "observed_at",
          "status"
        ],
        "properties": {
          "epreuve_id": {
            "type": "string",
            "pattern": "^[A-Z0-9][A-Z0-9-]*$"
          },
          "observed_at": {
            "$ref": "#/$defs/utc_datetime"
          },
          "status": {
            "enum": [
              "OK",
              "LOCAL_VALIDITY",
              "INDETERMINATE"
            ]
          },
          "counted": {
            "type": "boolean",
            "description": "Une épreuve dont le status n'est pas OK ne compte pas au palmarès. Elle reste listée: son exclusion est une donnée, pas un trou."
          },
          "not_counted_reason": {
            "type": "string"
          }
        }
      },
      "description": "Toutes les épreuves de la catégorie, comptées ou non. Retirer une épreuve défavorable du tableau serait exactement la faute que le panel doit pouvoir détecter."
    },
    "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": 1,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "tool",
                "match_wins",
                "match_draws",
                "match_losses",
                "epreuves_won",
                "epreuves_entered"
              ],
              "properties": {
                "tool": {
                  "type": "string",
                  "pattern": "^[a-z0-9][a-z0-9_-]*$"
                },
                "match_wins": {
                  "type": "integer",
                  "minimum": 0
                },
                "match_draws": {
                  "type": "integer",
                  "minimum": 0
                },
                "match_losses": {
                  "type": "integer",
                  "minimum": 0
                },
                "epreuves_won": {
                  "type": "integer",
                  "minimum": 0
                },
                "epreuves_entered": {
                  "type": "integer",
                  "minimum": 0
                },
                "non_dominated": {
                  "type": "boolean",
                  "description": "La colonne « non-dominé » de D3: aucun autre outil ne l'a battu sur au moins une dimension sans en perdre aucune, selon pareto_rule. C'est la seule colonne qui répond à « lequel puis-je écarter », et elle se lit sans poids."
                },
                "first_seen": {
                  "$ref": "#/$defs/utc_datetime"
                }
              }
            }
          }
        }
      },
      "description": "Un palmarès PAR DIMENSION. Un outil peut mener en fiabilité et suivre en coût; l'écrire est plus utile qu'une note unique, et honnête là où une note ne l'est pas."
    },
    "titles": {
      "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 titre de chaque dimension: l'outil SEUL en tête de sa table. Une égalité en tête ne produit pas de titre, elle produit null, et null se publie."
    },
    "pareto_set": {
      "type": "array",
      "items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]*$" },
      "description": "L'ensemble non-dominé de la saison, calculé selon pareto_rule. Peut être vide en cas de cycle, qui est alors nommé."
    },
    "pareto_rule": {
      "enum": ["non_dominated_in_latest_counted_epreuve", "never_dominated_when_present"],
      "description": "Vocabulaire fermé, déclaré au protocole. Les deux règles répondent à deux questions honnêtes: « qui n'est dominé par personne aujourd'hui » et « qui n'a jamais été dominé LORSQU'IL ÉTAIT PRÉSENT » (le second nom dit l'absence: un outil qui n'a disputé qu'une épreuve et fait nul n'a jamais été dominé, ce qui ne prouve rien; la colonne des épreuves disputées est là pour ça). Choisir la règle après lecture des résultats est la façon la plus discrète de fabriquer un vainqueur, et c'est pourquoi le contrôleur exige la règle ET ses épreuves sources."
    },
    "pareto_source_epreuves": {
      "type": "array",
      "items": { "type": "string", "pattern": "^[A-Z0-9][A-Z0-9-]*$" },
      "description": "Les épreuves d'où sort l'ensemble non-dominé. Elles doivent toutes être comptées, et pour la règle « dernière épreuve comptée » il ne peut y en avoir qu'une: la plus récente."
    },
    "dominance_cycles": {
      "type": "array",
      "items": {
        "type": "array",
        "minItems": 2,
        "items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]*$" }
      }
    },
    "champion": {
      "type": ["string", "null"],
      "description": "Nommé uniquement si un même outil tient TOUS les titres de dimension de la saison. Sinon null, et les titres se lisent un par un."
    },
    "denominators": {
      "type": "object",
      "additionalProperties": false,
      "required": ["comparisons_played", "comparisons_concluded"],
      "properties": {
        "comparisons_played": {
          "type": "integer", "minimum": 0,
          "description": "Une comparaison = un match sur une dimension. Le palmarès ne porte pas les matchs eux-mêmes, seulement des tables par dimension: il compte donc des comparaisons, et il le dit. Le décompte des matchs vit dans chaque épreuve."
        },
        "comparisons_concluded": { "type": "integer", "minimum": 0 },
        "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 » à l'échelle de la saison. Un palmarès sans ce rapport laisse croire qu'un outil a gagné souvent alors qu'il a surtout joué peu."
    },
    "lenses": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name", "preregistered", "frozen_at", "constraints", "order", "survivors", "non_dominated"],
        "properties": {
          "name": { "type": "string", "minLength": 1 },
          "preregistered": {
            "const": true,
            "description": "Toujours true. Un profil écrit après lecture des résultats choisit les contraintes qui font gagner l'outil qu'on voulait voir gagner."
          },
          "frozen_at": { "$ref": "#/$defs/utc_datetime" },
          "constraints": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": ["dimension", "rule"],
              "properties": {
                "dimension": { "enum": ["quality", "latency", "cost", "reliability", "energy"] },
                "rule": { "enum": ["must_not_lose"] }
              }
            },
            "description": "Des contraintes ÉLIMINATOIRES, pas des poids. « ne doit perdre aucune confrontation en qualité » est vérifiable; « la qualité compte pour 40 % » ne l'est pas."
          },
          "order": {
            "type": "array",
            "items": { "enum": ["quality", "latency", "cost", "reliability", "energy"] },
            "description": "Les dimensions que ce profil regarde. Restreindre le regard n'est pas pondérer: on montre les non-dominés SUR CES DIMENSIONS, sans les additionner."
          },
          "eliminated": { "type": "object", "additionalProperties": { "type": "string" } },
          "undetermined_constraints": {
            "type": "array",
            "items": { "enum": ["quality", "latency", "cost", "reliability", "energy"] },
            "description": "Les dimensions sur lesquelles la contrainte du profil n'a pu être ni satisfaite ni violée, faute d'une seule comparaison départagée. Trois états et non deux: présenter l'indétermination comme une qualification ferait passer une absence de mesure pour une exigence remplie. Un profil qui en porte n'est pas publiable."
          },
          "survivors": { "type": "array", "items": { "type": "string" } },
          "non_dominated": { "type": "array", "items": { "type": "string" } }
        }
      },
      "description": "Les profils-lentilles préenregistrés (D3). Un profil élimine et montre; il ne classe pas et ne couronne jamais. Aucun champ « titre » n'existe ici, et c'est délibéré."
    },
    "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" }
      },
      "description": "Le protocole où la règle de Pareto et les critères d'inclusion des épreuves étaient écrits AVANT les résultats. Sans lui, un organisateur essaie les deux règles et publie celle qui l'arrange; le contrôleur vérifie l'empreinte du document et qu'il nomme bien la règle publiée. Exigé dès que le palmarès est publiable."
    },
    "cadence": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "minimum_epreuves_before_ranking",
        "target_interval_days"
      ],
      "properties": {
        "minimum_epreuves_before_ranking": {
          "type": "integer",
          "minimum": 1,
          "description": "TORNEO-009 §3: une épreuve unique ne fait pas un palmarès. Ce nombre est fixé au protocole, pas au moment où le résultat plaît."
        },
        "target_interval_days": {
          "type": "integer",
          "minimum": 1
        },
        "last_epreuve_at": {
          "$ref": "#/$defs/utc_datetime"
        }
      }
    },
    "publishable": {
      "type": "boolean",
      "description": "False tant que le nombre d'épreuves comptées est sous minimum_epreuves_before_ranking. Un palmarès non publiable existe et se calcule; il ne se sert pas comme classement."
    },
    "not_publishable_reason": {
      "type": "string"
    },
    "computed_at": {
      "$ref": "#/$defs/utc_datetime"
    },
    "notes": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      }
    }
  },
  "$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$"
    }
  }
}
