{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://pactem.io/schemas/analysis.v1.1.json",
  "title": "Pactem — Analyse standardisée",
  "x-pactem-version": "1.1",
  "x-pactem-content-hash": "fdcf83f8dc4c037e61c0c15f504e1e48a313515f7a69e536c77721c582cc558c",
  "x-pactem-history": "1.0 : première version, archivée sous registry/archive/. 1.1 (11 septembre 2026, arbitrage du 7 septembre 2026 §1 et §2) : locator et silence_reason sur Finding ; registry_hash et pages_read requis à la racine ; attestation décrite comme une chaîne de hachage publique sans secret ; cache_status accepte 'none' ; branch_id 'silence' est la seule marque publique de non-localisation.",
  "description": "Sortie du moteur. Ne contient AUCUN champ agrégé : pas de score, pas de niveau global, pas de compteur d'alertes, pas d'indice de confiance global. L'unité de restitution est la clause.",
  "type": "object",
  "required": [
    "analysis_id",
    "grid_code",
    "registry_version",
    "registry_hash",
    "document_hash",
    "pages_read",
    "context",
    "findings",
    "generated_at"
  ],
  "additionalProperties": false,
  "properties": {
    "analysis_id": {
      "type": "string"
    },
    "grid_code": {
      "type": "string",
      "pattern": "^[0-9][A-Z]$"
    },
    "registry_version": {
      "type": "string",
      "pattern": "^\\d+\\.\\d+\\.\\d+$"
    },
    "registry_hash": {
      "type": "string",
      "pattern": "^[a-f0-9]{64}$",
      "description": "content_hash de la version du registre qui a produit l'analyse — le sceau publié sur pactem.io/registre. C'est le rattachement de chaque analyse à une méthode identifiée."
    },
    "document_hash": {
      "type": "string",
      "pattern": "^[a-f0-9]{64}$",
      "description": "SHA-256 du texte normalisé (NFKC, minuscules, espaces réduits, en-têtes/pieds et numéros de page retirés)."
    },
    "pages_read": {
      "type": "integer",
      "minimum": 0,
      "description": "Nombre de pages du document effectivement lues par le moteur. Sert au bloc de couverture des surfaces ; il n'est pas un indicateur de qualité."
    },
    "cache_status": {
      "type": "string",
      "enum": [
        "served_from_cache",
        "computed_live",
        "cache_refused_hash_mismatch",
        "none"
      ],
      "description": "'cache_refused_hash_mismatch' : le document présenté diverge du modèle en cache, l'analyse en cache n'a pas été servie. 'none' : le moteur ne connaît pas l'état du cache."
    },
    "context": {
      "$ref": "#/$defs/AnalysisContext"
    },
    "degraded_criteria": {
      "type": "array",
      "description": "Critères servis au niveau « À savoir » faute de contexte (marqueur interne context_missing). Repris tels quels dans le bloc de couverture.",
      "items": {
        "type": "object",
        "required": [
          "criterion_key",
          "missing_context"
        ],
        "additionalProperties": false,
        "properties": {
          "criterion_key": {
            "type": "string"
          },
          "missing_context": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      }
    },
    "findings": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Finding"
      }
    },
    "generated_at": {
      "type": "string",
      "format": "date-time"
    },
    "attestation": {
      "type": "object",
      "description": "Chaîne de hachage publique, sans secret. chain_hash = SHA-256 du JSON canonique de l'analyse (sans le champ attestation) concaténé à previous_hash ; previous_hash = chain_hash de l'analyse précédente du même déploiement, ou la valeur d'amorce fixée et publiée. Vérifiable par quiconque tient deux analyses, sans rien demander à Pactem. Ce n'est ni un horodatage qualifié au sens eIDAS ni une preuve d'authenticité (celle-ci relève d'une signature, non prévue ici) ; l'attestation ne doit jamais être présentée comme l'un ou l'autre.",
      "required": [
        "chain_hash",
        "previous_hash"
      ],
      "additionalProperties": false,
      "properties": {
        "chain_hash": {
          "type": "string",
          "pattern": "^[a-f0-9]{64}$"
        },
        "previous_hash": {
          "type": "string",
          "pattern": "^[a-f0-9]{64}$"
        }
      }
    }
  },
  "$defs": {
    "Level": {
      "type": "string",
      "enum": [
        "conforme",
        "a_savoir",
        "a_noter",
        "a_verifier"
      ]
    },
    "Locator": {
      "type": [
        "object",
        "null"
      ],
      "default": null,
      "additionalProperties": false,
      "properties": {
        "page": {
          "type": "integer"
        },
        "offset": {
          "type": "integer"
        }
      }
    },
    "AnalysisContext": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "grid_code"
      ],
      "properties": {
        "grid_code": {
          "type": "string"
        },
        "signed_or_renewed_on": {
          "type": [
            "string",
            "null"
          ],
          "format": "date",
          "default": null
        },
        "bail_type": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "nu",
            "meuble",
            "mobilite",
            null
          ],
          "default": null
        },
        "profession": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "insured_activity": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "provided_by": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "distributor",
            "signer",
            "inferred_from_document",
            null
          ],
          "default": null,
          "description": "'inferred_from_document' plafonne au niveau « À savoir » les critères qui en dépendent."
        }
      }
    },
    "Releve": {
      "type": "object",
      "required": [
        "criterion_key",
        "value"
      ],
      "additionalProperties": false,
      "properties": {
        "criterion_key": {
          "type": "string"
        },
        "scope_label": {
          "type": [
            "string",
            "null"
          ],
          "default": null,
          "description": "Nom de la garantie, pour les critères de portée per_guarantee."
        },
        "value": {
          "type": "string",
          "description": "Texte du relevé, ou la chaîne exacte 'non identifié'. Jamais une valeur inventée."
        },
        "quote": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "locator": {
          "$ref": "#/$defs/Locator"
        },
        "source": {
          "type": "string",
          "enum": [
            "cg",
            "cp"
          ],
          "default": "cg"
        }
      }
    },
    "Finding": {
      "type": "object",
      "required": [
        "criterion_key",
        "level",
        "branch_id",
        "statement",
        "registry_version"
      ],
      "additionalProperties": false,
      "properties": {
        "criterion_key": {
          "type": "string"
        },
        "scope_label": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "level": {
          "$ref": "#/$defs/Level"
        },
        "branch_id": {
          "type": "string",
          "description": "Branche du registre qui a produit le niveau, ou la chaîne exacte 'silence' quand le critère n'est pas localisé dans le document — absent, renvoyé à des conditions particulières, ou illisible. 'silence' est la seule marque publique de non-localisation : le niveau servi est alors le silence_regime du critère, et statement porte le silence_statement du registre. Les marqueurs internes du moteur (no_branch, empty_field, context_missing) ne sortent pas de l'API."
        },
        "silence_reason": {
          "type": "string",
          "enum": [
            "absent",
            "renvoi_cp",
            "illisible"
          ],
          "description": "Motif de non-localisation. Présent si et seulement si branch_id vaut 'silence'."
        },
        "statement": {
          "type": "string"
        },
        "quote": {
          "type": [
            "string",
            "null"
          ],
          "default": null,
          "description": "Extrait littéral de la clause. Un niveau « À vérifier » exige une clause localisée : quote et locator non nuls."
        },
        "locator": {
          "$ref": "#/$defs/Locator"
        },
        "legal_basis": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "promoted_by": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "rule_2",
            "rule_3",
            null
          ],
          "default": null
        },
        "demoted_by": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "rule_4",
            null
          ],
          "default": null
        },
        "review_trigger": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "SEMANTIC_L2",
            null
          ],
          "default": null
        },
        "registry_version": {
          "type": "string"
        }
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "branch_id": {
                "const": "silence"
              }
            },
            "required": [
              "branch_id"
            ]
          },
          "then": {
            "required": [
              "silence_reason"
            ]
          },
          "else": {
            "not": {
              "required": [
                "silence_reason"
              ]
            }
          }
        },
        {
          "if": {
            "properties": {
              "level": {
                "const": "a_verifier"
              }
            },
            "required": [
              "level"
            ]
          },
          "then": {
            "properties": {
              "branch_id": {
                "not": {
                  "const": "silence"
                }
              },
              "quote": {
                "type": "string",
                "minLength": 1
              },
              "locator": {
                "type": "object"
              }
            },
            "required": [
              "quote",
              "locator"
            ]
          }
        }
      ]
    }
  }
}
