{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://modelos.inverence-llull.com/ficha-0.8.schema.json",
  "title": "Ficha de modelo — contrato v0.8",
  "description": "Contrato formal de la ficha de definición de modelo (v0.8). DOS PRODUCTORES, UN CONTRATO: (a) el generador TOL de Inverence, que emite `metadata` + `model` con las secciones en inglés; (b) el formulario estático index.html, que emite las secciones en la raíz con nombres en español. v0.8 reconoce por fin la forma del generador, que es la que sus fichas han tenido siempre (la v0.7 describía solo la del formulario y nunca habría validado una ficha del generador). CAMBIOS v0.7 → v0.8: (1) `schema_version` en la RAÍZ es el discriminador de versión, y es su ÚNICA ubicación — una `schema_version` dentro de `metadata.identity` se rechaza con 400 pidiendo que se mueva a la raíz, porque dos ubicaciones para el mismo dato es un desync esperando a ocurrir; (2) `_meta` y `template_version` NO existen en el contrato (una ficha antigua que los traiga no falla: viajan como extras no documentados); (3) `sources.repo` solo exige `url` — la `revision` es opcional, así que una url sola es una procedencia legal, documental; (4) la RESOLUBILIDAD de `sources.files[].path` deja de exigirse: el generador declara rutas absolutas de la máquina que estimó y no adjunta ficheros, y eso es legítimo. Lo que SÍ se mantiene: si un fichero declarado VIAJA en el envío, su sha256 recalculado debe casar con el declarado (400 con el diff). Una ficha v0.5/v0.6/v0.7 migra a v0.8 en PASSTHROUGH (se le sube la versión, no se le remapea nada).",
  "type": "object",
  "required": [
    "schema_version"
  ],
  "properties": {
    "schema_version": {
      "description": "Versión canónica del contrato, en la RAÍZ del JSON y solo ahí. El backend lee este campo para decidir la migración. Una ficha v0.5 (sin campo), v0.6 o v0.7 se migra a 0.8 antes de validar; una ficha del generador sin el campo se trata igual (passthrough).",
      "const": "0.8"
    },
    "metadata": {
      "type": "object",
      "description": "FORMA DEL GENERADOR TOL. Todas las secciones son opcionales y ninguna restringe sus propiedades adicionales: el generador añade y retira campos entre versiones del emisor, y el contrato no debe romperse por eso. En 1.0.2 respecto a 1.0.0 desaparecieron diez campos (artifact.input_reconstruction, data.data_format, data.data_origin_refresh, data.drivers_required, data.drivers_supply, environment.non_ascii_alphabet, invocation.entry_point, runtime.dependencies, runtime.needs_network, runtime.network_detail) y dos se mudaron de `environment` a `runtime` (source_encoding, source_locale): TODOS son tolerados-ausentes, ninguno es required en ningún sitio.",
      "additionalProperties": true,
      "properties": {
        "schema_version": {
          "description": "PROHIBIDO AQUÍ. La versión del contrato vive en la raíz y solo en la raíz.",
          "not": {}
        },
        "identity": {
          "type": "object",
          "description": "Identidad del modelo. `creation_time` (v0.8) es el instante de estimación tal y como lo estampa el emisor.",
          "additionalProperties": true,
          "properties": {
            "schema_version": {
              "description": "PROHIBIDO AQUÍ — muévela a la raíz del JSON.",
              "not": {}
            },
            "model_id": {
              "type": "string"
            },
            "model_name": {
              "type": "string"
            },
            "model_version": {
              "type": "string"
            },
            "model_class": {
              "type": "string"
            },
            "client": {
              "type": "string"
            },
            "client_phase": {
              "type": "string"
            },
            "project": {
              "type": "string"
            },
            "submodel": {
              "type": "string"
            },
            "parent_model": {
              "type": "string"
            },
            "purpose": {
              "type": "string"
            },
            "owner": {
              "type": "string"
            },
            "task_type": {
              "type": "string"
            },
            "task_type_other": {
              "type": "string"
            },
            "task_parameters": {
              "type": "string"
            },
            "creation_time": {
              "type": "string",
              "description": "v0.8 — instante de creación/estimación, formato libre tal y como lo emite el generador."
            }
          }
        },
        "runtime": {
          "type": "object",
          "description": "Motor y runtime. `engine` es TEXTO LIBRE del emisor y NO se valida contra un patrón: es la cadena de build de TOL, y su valor exacto es evidencia (dos builds distintos bajo la misma versión explican divergencias numéricas de convergencia entre estimaciones del mismo modelo). `emitter`/`hostname`/`operating_system` (v0.8) identifican el entorno que emitió la ficha, lo que hace auditables esas divergencias.",
          "additionalProperties": true,
          "properties": {
            "engine": {
              "type": "string",
              "description": "Cadena de build del motor, verbatim. Texto libre por diseño."
            },
            "emitter": {
              "type": "string",
              "description": "v0.8 — qué generó la ficha (p. ej. TolLlull.1.4)."
            },
            "hostname": {
              "type": "string",
              "description": "v0.8 — host/contenedor donde se estimó."
            },
            "operating_system": {
              "type": "string",
              "description": "v0.8 — sistema operativo del entorno de estimación."
            },
            "packaging": {
              "type": "string"
            },
            "build_artifacts": {
              "type": "string"
            },
            "source_encoding": {
              "type": "string",
              "description": "v0.8 — encoding del código fuente (estaba en `environment` hasta 1.0.0)."
            },
            "source_locale": {
              "type": "string",
              "description": "v0.8 — locale del entorno (estaba en `environment` hasta 1.0.0)."
            }
          }
        },
        "environment": {
          "type": "object",
          "description": "Sección LEGADA (1.0.0): encoding/locale/alfabeto. Desde 1.0.2 sus campos vivos están en `runtime`. Se sigue tolerando para que una ficha antigua valide.",
          "additionalProperties": true
        },
        "data": {
          "type": "object",
          "additionalProperties": true,
          "description": "Datos: granularidad, unidades/escala, drivers."
        },
        "estimation": {
          "type": "object",
          "additionalProperties": true,
          "description": "Estimación: algoritmo, método, ventana, determinismo."
        },
        "invocation": {
          "type": "object",
          "additionalProperties": true,
          "description": "Invocación: argumentos y objetos resultantes."
        },
        "artifact": {
          "type": "object",
          "description": "Artefacto ejecutable/serializado.",
          "additionalProperties": true,
          "properties": {
            "serialized": {
              "type": "array",
              "description": "v0.8 — artefactos serializados (el .oza y compañía), pineados por contenido. `loader`/`load_path` documentan CÓMO se recarga el objeto; el par sha256+size es lo que lo pinea.",
              "items": {
                "type": "object",
                "additionalProperties": true,
                "properties": {
                  "path": {
                    "type": "string",
                    "description": "Nombre/ruta del artefacto serializado (p. ej. el .oza)."
                  },
                  "loader": {
                    "type": "string",
                    "description": "Llamada de carga, verbatim (p. ej. Ois.Load(\"…oza\"))."
                  },
                  "load_path": {
                    "type": "string"
                  },
                  "sha256": {
                    "type": "string",
                    "pattern": "^[a-fA-F0-9]{64}$"
                  },
                  "size": {
                    "type": "integer",
                    "minimum": 0
                  }
                }
              }
            }
          }
        },
        "outputs": {
          "type": "object",
          "additionalProperties": true,
          "description": "Salidas: punto de entrada de previsión y de banda, escala, unidades."
        },
        "operations": {
          "type": "object",
          "description": "Operativo/gobierno.",
          "additionalProperties": true,
          "properties": {
            "resource_profile": {
              "type": "string",
              "description": "v0.8 — coste de ejecución declarado, texto libre."
            },
            "reestimation_cadence": {
              "type": "string"
            }
          }
        },
        "limits": {
          "type": "object",
          "description": "Límites declarados del modelo.",
          "additionalProperties": true,
          "properties": {
            "band_caveats": {
              "type": "string",
              "description": "v0.8 — qué NO cubre la banda. VACÍO o ESPECÍFICO: la cadena vacía es una declaración legal (no hay salvedades que declarar); lo que no vale es una salvedad genérica que no diga nada. El contrato no puede distinguirlas, así que esto queda dicho aquí y se revisa a ojo."
            },
            "max_horizon": {
              "type": "string"
            },
            "licit_aggregations": {
              "type": "string"
            },
            "invalidation_conditions": {
              "type": "string"
            }
          }
        },
        "sources": {
          "$ref": "#/$defs/sources"
        }
      }
    },
    "identidad": {
      "type": "object",
      "description": "FORMA DEL FORMULARIO — §1 Identidad.",
      "required": [
        "model_id"
      ],
      "properties": {
        "model_id": {
          "type": "string"
        },
        "nombre": {
          "type": "string"
        },
        "version": {
          "type": "string"
        },
        "model_class": {
          "type": "string"
        },
        "dominio": {
          "type": "string",
          "description": "Cliente / dominio. Gemelo de metadata.identity.client: es el PRIMER segmento de la clave de archivo (<cliente>/<proyecto>/fichas/<model_id>/<ts>). Sin barras ni espacios — un segmento con barra partiria la clave y no se normaliza en silencio."
        },
        "proyecto": {
          "type": "string",
          "description": "Gemelo de metadata.identity.project: SEGUNDO segmento de la clave. Un proyecto solo es unico dentro de un cliente. Sin barras ni espacios."
        },
        "fase_comercial": {
          "type": "string",
          "enum": [
            "exploracion",
            "piloto",
            "produccion",
            ""
          ]
        },
        "submodelo": {
          "type": "string"
        },
        "jerarquia": {
          "type": "string"
        },
        "proposito": {
          "type": "string"
        },
        "responsable": {
          "type": "string"
        },
        "task_kind": {
          "type": "string"
        },
        "task_kind_otro": {
          "type": "string"
        },
        "task_params": {
          "type": "string"
        }
      }
    },
    "motor": {
      "type": "object",
      "description": "Formulario §2 Motor y runtime."
    },
    "data": {
      "type": "object",
      "description": "Formulario §3 Datos."
    },
    "entorno": {
      "type": "object",
      "description": "Formulario §4 Entorno: encoding y locale."
    },
    "estimacion": {
      "type": "object",
      "description": "Formulario §5 Estimación / entrenamiento."
    },
    "invocacion": {
      "type": "object",
      "description": "Formulario §6 Invocación / ejecución."
    },
    "artefacto": {
      "type": "object",
      "description": "Formulario §7 Artefacto ejecutable / serializado."
    },
    "salidas": {
      "type": "object",
      "description": "Formulario §8 Salidas."
    },
    "operativo": {
      "type": "object",
      "description": "Formulario §9 Operativo / gobierno."
    },
    "sources": {
      "$ref": "#/$defs/sources"
    },
    "model": {
      "type": "object",
      "description": "El modelo estimado, tal y como lo emite el generador TOL. El formulario NO lo edita; al importarlo lo preserva INTACTO en el re-export. Este esquema DOCUMENTA su estructura pero NO la valida por dentro: un `model` real no debe fallar nunca por sus campos internos.",
      "additionalProperties": true,
      "properties": {
        "stats": {
          "description": "Diagnósticos del ajuste (Sigma, R2, Asymmetry, Kurtosis, DataNumber…). Estos números son el ancla de reproducibilidad: dos estimaciones del mismo modelo en entornos distintos pueden diferir en las últimas cifras."
        },
        "coefficients": {
          "type": "array",
          "description": "Coeficientes estimados. Campos documentados, no enforced: Name, Factor, Order, Value, StDs, TStudent, RefuseProb y `role` (ar | ma | transfer; confirmado por Inverence en encoding-spec-v1 (commit a46b38a9f494d4e523a5b5fc307a253803d0a7c7), el esquema no restringe su valor).",
          "items": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "correlations": {
          "description": "Matriz de correlaciones de los estimadores, triangular inferior ESTRICTA: la fila i lleva i-1 elementos, la primera va vacía y la diagonal (=1) es implícita. v0.7 lo documentaba mal como «i+1»."
        },
        "conditioning": {
          "description": "Condicionamiento numérico de la estimación."
        },
        "series": {
          "description": "Series del modelo: ARRAY DE OBJETOS-FILA con claves = nombres de columna. Diccionario de columnas pendiente de confirmar por Inverence."
        }
      }
    },
    "_source": {
      "type": "array",
      "description": "APÉNDICE NO-CANÓNICO del formulario (fallback offline): los adjuntos con sus bytes en base64, para recargarlos byte-fieles en el navegador. El generador NO lo emite y la validación NO lo exige. El sha256 canónico vive en `sources.files`.",
      "items": {
        "type": "object",
        "properties": {
          "filename": {
            "type": "string"
          },
          "section": {
            "type": "string",
            "enum": [
              "codigo",
              "datos"
            ]
          },
          "role": {
            "type": "string"
          },
          "role_otro": {
            "type": "string"
          },
          "size": {
            "type": "integer",
            "minimum": 0
          },
          "sha256": {
            "type": "string",
            "pattern": "^[a-fA-F0-9]{64}$"
          },
          "base64": {
            "type": "string"
          }
        }
      }
    }
  },
  "allOf": [
    {
      "if": {
        "required": [
          "metadata"
        ]
      },
      "then": {
        "description": "Ficha del generador: la identidad vive en metadata.identity y es lo único que se exige.",
        "properties": {
          "metadata": {
            "required": [
              "identity"
            ],
            "properties": {
              "identity": {
                "required": [
                  "model_id"
                ]
              }
            }
          }
        }
      },
      "else": {
        "description": "Ficha del formulario: identidad y procedencia en la raíz.",
        "required": [
          "identidad",
          "sources"
        ]
      }
    }
  ],
  "$defs": {
    "sources": {
      "type": "object",
      "description": "Procedencia del modelo: el repositorio y el sha256 de cada fichero. NO se exige que cada `path` resuelva (v0.8): el generador declara rutas absolutas de la máquina que estimó y no adjunta los ficheros, y eso es una declaración legítima. Lo que sí se comprueba, en el backend, es que un fichero declarado que VIAJA en el envío tenga el sha256 que dice tener.",
      "required": [
        "files"
      ],
      "properties": {
        "repo": {
          "type": "object",
          "description": "Repositorio de origen. `url` es obligatoria; `revision` es OPCIONAL desde v0.8 — una url sola es procedencia documental legal, aunque no pinea por contenido.",
          "required": [
            "url"
          ],
          "properties": {
            "url": {
              "type": "string",
              "description": "URL del repositorio git."
            },
            "revision": {
              "type": "string",
              "description": "Commit o tag exacto. Opcional; su ausencia significa procedencia documental, no pineada."
            }
          }
        },
        "files": {
          "type": "array",
          "description": "Ficheros del modelo, pineados por contenido. La terna nombre + sha256 + size es lo exigido; `role` y otros campos del emisor se toleran.",
          "minItems": 1,
          "items": {
            "type": "object",
            "additionalProperties": true,
            "required": [
              "path",
              "sha256",
              "size"
            ],
            "properties": {
              "path": {
                "type": "string",
                "description": "Ruta o nombre del fichero. Puede ser absoluta y de otra máquina: es una declaración, no una promesa de resolubilidad."
              },
              "sha256": {
                "type": "string",
                "pattern": "^[a-fA-F0-9]{64}$"
              },
              "size": {
                "type": "integer",
                "minimum": 0
              },
              "role": {
                "type": "string",
                "description": "Papel del fichero según el emisor (p. ej. «modelo (base)»)."
              }
            }
          }
        }
      }
    }
  }
}
