{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://pactem.io/schemas/grid_registry.v1.json",
  "title": "Pactem — Registre de grille",
  "description": "Source de vérité unique des seuils de classification. Chargé en lecture seule, vérifié par condensat. Aucune valeur de ce registre ne peut être surchargée au runtime (principe de non-substitution).",
  "type": "object",
  "required": ["registry_version", "published_at", "content_hash", "grids"],
  "additionalProperties": false,
  "properties": {
    "registry_version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
    "published_at": { "type": "string", "format": "date" },
    "content_hash": {
      "type": "string",
      "pattern": "^[a-f0-9]{64}$",
      "description": "SHA-256 du registre canonicalisé (JSON trié, sans espaces), ce champ exclu."
    },
    "notes": { "type": "string" },
    "grids": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Grid" } },
    "lexicon": { "$ref": "#/$defs/Lexicon" }
  },

  "$defs": {

    "Lexicon": {
      "type": "object",
      "description": "Tout ce qui influence une classification vit dans le registre, donc sous le sceau.",
      "additionalProperties": false,
      "properties": {
        "duration_units": { "type": "object" },
        "duration_words": {
          "type": "object",
          "description": "Table fermée des nombres en toutes lettres. Les baux réels écrivent « deux mois », « quinze jours ».",
          "additionalProperties": { "type": "integer", "minimum": 1 }
        },
        "negation_markers": { "type": "array", "items": { "type": "string" } },
        "renvoi_markers": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Précédence 1 — règle K0, « À savoir », jamais au-dessus. Entrées multi-mots uniquement."
        },
        "absence_markers": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Précédence 3 — le critère n'apparaît nulle part dans le document."
        },
        "derogation_markers": {
          "type": "array",
          "minItems": 12,
          "items": { "type": "string" },
          "description": "Liste fermée publiée. Règle 4 : abaissement d'un cran, plancher « À savoir ». Un marqueur de dérogation seul est sans effet — E-A16 exige une contrepartie cumulativement."
        },
        "contrepartie_markers": {
          "type": "array",
          "minItems": 1,
          "items": { "type": "string" },
          "description": "E-A16 — la charte §8.6 subordonne l'abaissement à « une contrepartie explicitement favorable au signataire ». « Par dérogation » signale un écart, il n'établit aucune contrepartie."
        },
        "remplacement_markers": {
          "type": "array",
          "minItems": 1,
          "items": { "type": "string" },
          "description": "Mécanisme de remplacement d'un colocataire sortant, condition structurelle de 4C.clause_solidarite.R1."
        },
        "resiliation_sans_formalite_markers": {
          "type": "array",
          "minItems": 1,
          "items": { "type": "string" },
          "description": "Marqueurs décomposés — une formule figée ne survit pas à la négation distribuée sur deux compléments coordonnés."
        },
        "regulatory_floors": { "type": "array" }
      }
    },

    "Level": {
      "type": "string",
      "enum": ["conforme", "a_savoir", "a_noter", "a_verifier"],
      "description": "Quatre niveaux fixes. Aucun ajout, retrait ou renommage possible."
    },

    "Grid": {
      "type": "object",
      "required": ["code", "label", "criteria"],
      "additionalProperties": false,
      "properties": {
        "code": { "type": "string", "pattern": "^[0-9][A-Z]$" },
        "label": { "type": "string" },
        "context_enum": {
          "type": "object",
          "description": "Valeurs admises par clé de contexte. Validé à la frontière d'API (E-08b) : une valeur hors énumération est une erreur bruyante, jamais une dégradation silencieuse.",
          "additionalProperties": { "type": "array", "items": { "type": "string" } }
        },
        "criteria": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Criterion" } }
      }
    },

    "Criterion": {
      "type": "object",
      "required": ["key", "label", "theme", "order", "silence_regime", "silence_statement", "scope", "branches"],
      "additionalProperties": false,
      "properties": {
        "key": {
          "type": "string",
          "pattern": "^[a-z0-9_]+$",
          "description": "Identifiant stable. Ne doit jamais être renommé : il est cité par les jeux de test et par les analyses archivées."
        },
        "label": { "type": "string", "description": "Libellé affiché au signataire." },
        "theme": {
          "type": "string",
          "description": "Clé de regroupement pour la règle 2 (promotion par accumulation). Deux orange sur le même thème promeuvent en rouge ; sur des thèmes distincts, ils ne se cumulent pas."
        },
        "order": { "type": "integer", "minimum": 0, "description": "Ordre canonique. Sert aussi à la sélection déterministe des trois verts affichés." },
        "silence_regime": {
          "type": "string",
          "enum": ["conforme", "a_savoir", "a_noter", "a_verifier"],
          "description": "Règle 5. 'a_verifier' réservé aux mentions légalement obligatoires ou présentes dans plus de 95 % du marché observé ; 'conforme' aux clauses dont l'absence est favorable au signataire, déclaré critère par critère et jamais déduit."
        },
        "silence_statement": { "type": "string" },
        "silence_justification": { "type": "string", "description": "Motif du régime de silence retenu. Non affiché au signataire." },
        "empty_field_regime": {
          "type": "string",
          "enum": ["conforme", "a_savoir", "a_noter", "a_verifier"],
          "default": "a_noter",
          "description": "Traitement de la rubrique présente mais non renseignée, distinct du silence complet."
        },
        "empty_field_markers": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Marqueurs lexicaux indiquant une rubrique présente non renseignée."
        },
        "scope": { "type": "string", "enum": ["document", "per_guarantee"] },
        "aggregation": { "type": "string", "enum": ["max_amount", "min_duration", "none"], "default": "none" },
        "context_required": {
          "type": "array",
          "items": { "type": "string", "enum": ["bail_type", "signed_or_renewed_on", "profession", "insured_activity"] },
          "default": [],
          "description": "Si une clé listée est absente du contexte, le critère est servi en cyan. Jamais deviné."
        },
        "comment": { "type": "string", "description": "Commentaire de calibration, non affiché au signataire." },
        "branches": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Branch" } }
      }
    },

    "Branch": {
      "type": "object",
      "required": ["id", "level", "predicate", "statement"],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^[0-9][A-Z]\\.[a-z0-9_]+\\.[A-Z0-9]+$",
          "description": "Identifiant stable de branche, cité par les tests. Ex. 4C.depot_garantie.R1"
        },
        "level": { "$ref": "#/$defs/Level" },
        "predicate": { "$ref": "#/$defs/Predicate" },
        "statement": {
          "type": "string",
          "description": "Énoncé descriptif affiché. Décrit ce que la clause dit et, le cas échéant, ce que le texte applicable prévoit. Ne qualifie pas, ne conseille pas."
        },
        "legal_basis": { "type": ["string", "null"], "default": null },
        "effective_from": { "type": ["string", "null"], "format": "date", "default": null },
        "effective_until": { "type": ["string", "null"], "format": "date", "default": null },
        "applies_to_context": {
          "type": ["object", "null"],
          "default": null,
          "description": "Restriction contextuelle. Ex. {\"bail_type\": [\"nu\"]}",
          "additionalProperties": { "type": "array", "items": { "type": "string" } }
        },
        "evidence_required": { "type": "boolean", "default": false },
        "enabled": { "type": "boolean", "default": true, "description": "Branche désactivée = source primaire non vérifiée. La raison est portée par 'comment'." },
        "comment": { "type": "string" }
      }
    },

    "Anchor": {
      "description": "Ancre lexicale (E-06, forme inline). Chaîne ou tableau de chaînes, non vide.",
      "oneOf": [
        { "type": "string", "minLength": 1 },
        { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } }
      ]
    },

    "AnchorFallback": {
      "type": "string",
      "enum": ["whole_releve", "none"],
      "deprecated": true,
      "description": "DÉPRÉCIÉ — retiré en tête de G-1 (E-06c). Un repli sur le relevé entier annule l'ancrage exactement là où il servait : sur « dépôt de garantie : un mois de loyer, bail de trois ans », l'ancre introuvable fait lire « 3 ans » et un bail banal ressort en « À vérifier ». Toléré ici uniquement parce que le retrait doit être mesuré sur corpus avant scellement."
    },

    "Predicate": {
      "description": "Arbre d'opérateurs fermé. Aucun code arbitraire : c'est ce qui rend le registre auditable et non surchargeable.",
      "oneOf": [
        { "type": "object", "required": ["op"], "additionalProperties": false,
          "properties": { "op": { "const": "absent" } } },

        { "type": "object", "required": ["op"], "additionalProperties": false,
          "properties": { "op": { "const": "below_regulatory_floor" } } },

        { "type": "object", "required": ["op"], "additionalProperties": false,
          "description": "E-A15 — porte soit `values` (littéraux pré-normalisés), soit `values_from_lexicon` (clé du lexique racine), jamais les deux. Dupliquer une liste dans une branche la fait cesser d'être unique sous sceau.",
          "oneOf": [
            { "required": ["values"], "not": { "required": ["values_from_lexicon"] } },
            { "required": ["values_from_lexicon"], "not": { "required": ["values"] } }
          ],
          "properties": { "op": { "enum": ["contains_any", "contains_all"] },
                          "values": { "type": "array", "minItems": 1, "items": { "type": "string" } },
                          "values_from_lexicon": { "type": "string", "minLength": 1 } } },

        { "type": "object", "required": ["op", "anchor", "pattern", "window"], "additionalProperties": false,
          "description": "Forme opérateur de l'ancrage (E-06). Une durée n'est un préavis que si le mot 'préavis' lui est adjacent.",
          "properties": { "op": { "const": "near" },
                          "anchor": { "type": "string" },
                          "pattern": { "type": "string" },
                          "window": { "type": "integer", "minimum": 1, "maximum": 80 },
                          "capture_cmp": { "type": "string", "enum": ["<", "<=", ">", ">=", "=="] },
                          "capture_value": { "type": "number" },
                          "capture_value_from_context": { "type": "string" },
                          "anchor_reject": { "type": "array", "items": { "type": "string" } },
                          "anchor_fallback": { "$ref": "#/$defs/AnchorFallback" } } },

        { "type": "object", "required": ["op", "cmp", "anchor", "window"], "additionalProperties": false,
          "description": "Forme inline de l'ancrage (E-06) : anchor + window portés par le prédicat lui-même.",
          "properties": { "op": { "enum": ["amount_max", "amount_min"] },
                          "cmp": { "type": "string", "enum": ["<", "<=", ">", ">=", "=="] },
                          "value": { "type": "number" },
                          "value_from_context": { "type": "string" },
                          "anchor": { "$ref": "#/$defs/Anchor" },
                          "window": { "type": "integer", "minimum": 1, "maximum": 80 },
                          "anchor_reject": { "type": "array", "items": { "type": "string" } },
                          "anchor_fallback": { "$ref": "#/$defs/AnchorFallback" } } },

        { "type": "object", "required": ["op", "unit", "cmp", "anchor", "window"], "additionalProperties": false,
          "description": "Forme inline de l'ancrage (E-06) : anchor + window portés par le prédicat lui-même.",
          "properties": { "op": { "const": "duration" },
                          "unit": { "type": "string", "enum": ["day", "week", "month", "year"] },
                          "cmp": { "type": "string", "enum": ["<", "<=", ">", ">=", "=="] },
                          "value": { "type": "number" },
                          "value_from_context": { "type": "string" },
                          "anchor": { "$ref": "#/$defs/Anchor" },
                          "window": { "type": "integer", "minimum": 1, "maximum": 80 },
                          "anchor_reject": { "type": "array", "items": { "type": "string" } },
                          "anchor_fallback": { "$ref": "#/$defs/AnchorFallback" } } },

        { "type": "object", "required": ["op", "of"], "additionalProperties": false,
          "properties": { "op": { "const": "not" }, "of": { "$ref": "#/$defs/Predicate" } } },

        { "type": "object", "required": ["op", "of"], "additionalProperties": false,
          "properties": { "op": { "enum": ["and", "or"] },
                          "of": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/Predicate" } } } }
      ]
    }
  }
}
