{
  "openapi": "3.1.0",
  "info": {
    "title": "FormulaMaps Balance API",
    "version": "1.0.0",
    "description": "API pública (sin login) para que agentes de IA y herramientas equilibren fórmulas de heladería, panadería, pastelería, turrones/confitería y chocolate/cacao con el motor de FormulaMaps. POST /api/balance (helado) devuelve PAC, POD, sólidos, materia grasa, SLNG, agua, kcal y coste, con verdicto en rango y sugerencias. El PAC se da en la ESCALA de FormulaMaps (≈ temperatura de servicio en °C, NEGATIVO; rango de trabajo -12,5…-10,6), que es el número que ve el heladero; el campo PAC_indice_clasico añade el PAC clásico sacarosa-equivalente (sacarosa=100) solo como referencia para herramientas externas. POST /api/balance/panaderia analiza masas de pan y pizza en porcentaje de panadero. POST /api/balance/pasteleria equilibra bizcochos y cremas de pastelería. POST /api/balance/turron clasifica turrones y dulces de obrador por % de almendra/miel y categoría IGP. POST /api/balance/cacao analiza chocolate/cobertura/bombón (% cacao, grasa, denominación legal y temperaturas de templado).",
    "contact": { "name": "FormulaMaps", "url": "https://www.formulamaps.com" }
  },
  "servers": [{ "url": "https://www.formulamaps.com" }],
  "paths": {
    "/api/balance": {
      "get": {
        "summary": "Manifiesto del servicio y tipos disponibles",
        "responses": { "200": { "description": "Información del servicio", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BalanceManifest" } } } } }
      },
      "post": {
        "summary": "Equilibra una fórmula de heladería",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/BalanceRequest" },
              "example": {
                "craft": "helado",
                "type": "helado_leche",
                "ingredients": [
                  { "name": "Leche Entera 3,5 MG", "grams": 620 },
                  { "name": "Nata 35 MG", "grams": 110 },
                  { "name": "Sacarosa", "grams": 150 },
                  { "name": "Dextrosa", "grams": 25 },
                  { "name": "Leche Polvo 1% MG INNOVA", "grams": 55 }
                ]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Resultado del balanceo", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BalanceResponse" } } } },
          "400": { "description": "Petición inválida: falta ingredients, la suma de pesos/gramos es 0 o el craft no está soportado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Límite de peticiones superado (60/min por IP)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Error interno de cálculo", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/balance/panaderia": {
      "post": {
        "summary": "Analiza una masa de pan o pizza en porcentaje de panadero",
        "description": "Harina = 100%. Calcula hidratación (contando el agua real de leche/huevo/mantequilla), sal, levadura, grasa, azúcar y prefermento, comparados con los rangos profesionales del tipo de masa.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PanaderiaRequest" },
              "example": {
                "type": "pizza_napolitana",
                "ingredients": [
                  { "name": "Harina de fuerza W300", "grams": 1000 },
                  { "name": "Agua", "grams": 620 },
                  { "name": "Sal", "grams": 28 },
                  { "name": "Levadura fresca", "grams": 2 }
                ]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Resultado del análisis en porcentaje de panadero", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PanaderiaResponse" } } } },
          "400": { "description": "Petición inválida: falta ingredients o no hay harina reconocible", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Límite de peticiones superado (60/min por IP)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Error interno de cálculo", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/balance/pasteleria": {
      "post": {
        "summary": "Equilibra una receta de pastelería (bizcochos y cremas)",
        "description": "Devuelve la composición en base 100 (azúcares, grasa, proteínas, agua, sólidos, POD, kcal y coste) más los parámetros propios del tipo de receta (bizcochos y horneados o cremas/rellenos), comparados con sus rangos objetivo.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PasteleriaRequest" },
              "example": {
                "type": "genoise",
                "ingredients": [
                  { "name": "Huevo entero", "grams": 300 },
                  { "name": "Azúcar", "grams": 200 },
                  { "name": "Harina", "grams": 200 }
                ]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Resultado del balanceo de pastelería", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PasteleriaResponse" } } } },
          "400": { "description": "Petición inválida: falta ingredients o el tipo no reconoce el ingrediente base", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Límite de peticiones superado (60/min por IP)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Error interno de cálculo", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/balance/turron": {
      "post": {
        "summary": "Clasifica un turrón o dulce de obrador por % de almendra/miel y categoría IGP",
        "description": "Calcula el % de almendra, % de miel, % de azúcar/glucosa, ratio miel:azúcar y grasa, los compara con los umbrales IGP/RTS del tipo (Jijona, Alicante, yema tostada, mazapán de Toledo, guirlache, peladilla) y devuelve una puntuación de 1 a 3 almendras (bronce/plata/oro) y la categoría (Suprema/Extra/Fuera de IGP/Básico).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/TurronRequest" },
              "example": {
                "type": "jijona",
                "ingredients": [
                  { "ingrediente": "almendra_tostada", "peso": 640 },
                  { "ingrediente": "miel", "peso": 120 },
                  { "ingrediente": "azucar", "peso": 180 },
                  { "ingrediente": "clara", "peso": 40 },
                  { "ingrediente": "oblea", "peso": 20 }
                ]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Resultado de la clasificación del turrón", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TurronResponse" } } } },
          "400": { "description": "Petición inválida: faltan ingredientes válidos (almendra/miel/azúcar con peso > 0)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Límite de peticiones superado (60/min por IP)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Error interno de cálculo", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/balance/cacao": {
      "post": {
        "summary": "Analiza un chocolate, cobertura o bombón (% cacao, grasa y templado)",
        "description": "Calcula % de cacao, % de manteca de cacao, grasa total, % de azúcar, % de lácteos y coste, valida contra la denominación legal (UE 2000/36/CE, RD 1055/2003) y devuelve una medalla (oro/plata/bronce) y las 3 temperaturas de templado (fundir, enfriar, trabajar en °C). Para bombón/ganache añade la actividad de agua (aw) estimada.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CacaoRequest" },
              "example": {
                "type": "negro",
                "ingredients": [
                  { "ingrediente": "pasta_cacao", "peso": 700 },
                  { "ingrediente": "manteca_cacao", "peso": 100 },
                  { "ingrediente": "azucar", "peso": 200 },
                  { "ingrediente": "lecitina", "peso": 4 }
                ]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Resultado del análisis de chocolate/cacao", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CacaoResponse" } } } },
          "400": { "description": "Petición inválida: faltan ingredientes válidos (pasta de cacao/manteca/azúcar con peso > 0)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Límite de peticiones superado (60/min por IP)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Error interno de cálculo", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Ingredient": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "description": "Nombre del ingrediente (del catálogo de FormulaMaps) o uno propio si pasas composición." },
          "grams": { "type": "number", "description": "Cantidad. Se trabaja en proporción, no hace falta que sumen 1000." },
          "azucares": { "type": "number" }, "materiagrasa": { "type": "number" }, "esml": { "type": "number" },
          "lactosa": { "type": "number" }, "proteinas": { "type": "number" }, "otrossolidos": { "type": "number" },
          "pac": { "type": "number" }, "pod": { "type": "number" }, "kcal": { "type": "number" }, "precio": { "type": "number" }
        },
        "required": ["name", "grams"]
      },
      "BalanceRequest": {
        "type": "object",
        "properties": {
          "craft": { "type": "string", "enum": ["helado"], "default": "helado" },
          "type": { "type": "string", "enum": ["helado_leche","helado_crema","sorbete_agua","sorbete_leche","crema_untable","veteado","granizado","semifrio","sin_azucar","vegano","salado","base"] },
          "ingredients": { "type": "array", "items": { "$ref": "#/components/schemas/Ingredient" } }
        },
        "required": ["ingredients"]
      },
      "Range": {
        "type": "array",
        "description": "Rango numérico objetivo en forma [mínimo, máximo].",
        "items": { "type": "number" },
        "minItems": 2,
        "maxItems": 2
      },
      "BalanceResponse": {
        "type": "object",
        "description": "Resultado del balanceo (HTTP 200). Todos los parámetros se expresan por 100 g de mezcla y van redondeados a 2 decimales.",
        "required": ["status", "verdict", "craft", "type", "typeLabel", "params", "ranges", "inRange", "pac_info", "suggestions", "warnings", "disclaimer", "catalogoIngredientes", "poweredBy"],
        "properties": {
          "status": { "type": "string", "enum": ["ok"], "description": "Siempre \"ok\" cuando el cálculo se completa; los fallos llegan como HTTP 400/429/500 con el schema Error." },
          "verdict": { "type": "string", "enum": ["equilibrada", "revisar"], "description": "\"equilibrada\" si todos los parámetros de inRange están dentro de rango; \"revisar\" si alguno se sale." },
          "craft": { "type": "string", "enum": ["helado"], "description": "Oficio cubierto por la v1 (siempre \"helado\")." },
          "type": { "type": "string", "enum": ["helado_leche", "helado_crema", "sorbete_agua", "sorbete_leche", "crema_untable", "veteado", "granizado", "semifrio", "sin_azucar", "vegano", "salado", "base"], "description": "Tipo de receta aplicado al evaluar los rangos. Si el type enviado no existe, se usa \"helado_crema\" por defecto." },
          "typeLabel": { "type": "string", "description": "Etiqueta legible del tipo, p. ej. \"Helado crema (base amarilla)\"." },
          "params": {
            "type": "object",
            "description": "Parámetros calculados de la fórmula, por 100 g de mezcla, redondeados a 2 decimales.",
            "required": ["PAC", "POD", "azucares", "materiaGrasa", "SLNG", "solidosTotales", "aguaLibre", "kcal_100g", "coste_100g_eur", "PAC_indice_clasico"],
            "properties": {
              "PAC": { "type": "number", "description": "Poder anticongelante en la ESCALA FormulaMaps: descenso crioscópico relativo del mix ≈ temperatura de servicio en °C. Es NEGATIVO; cuanto más negativo, más anticongelante y más blando el helado. Rango de trabajo -12,5 … -10,6; fórmulas equilibradas ≈ -11,5. Interpreta y comunica el PAC SIEMPRE en esta escala." },
              "POD": { "type": "number", "description": "Poder edulcorante relativo de la mezcla (sacarosa = 100): dulzor percibido." },
              "azucares": { "type": "number", "description": "Azúcares totales, en g por 100 g de mezcla (%)." },
              "materiaGrasa": { "type": "number", "description": "Materia grasa total (láctea + otras grasas), en g por 100 g de mezcla (%)." },
              "SLNG": { "type": "number", "description": "Sólidos lácteos no grasos (ESML), en g por 100 g de mezcla (%)." },
              "solidosTotales": { "type": "number", "description": "Sólidos totales (azúcares + materia grasa + SLNG + otros sólidos), en g por 100 g de mezcla (%)." },
              "aguaLibre": { "type": "number", "description": "Agua libre: 100 − sólidos totales, en g por 100 g de mezcla (%)." },
              "kcal_100g": { "type": "number", "description": "Energía de la mezcla, en kcal por 100 g." },
              "coste_100g_eur": { "type": "number", "description": "Coste de la mezcla en euros por 100 g, según los precios del catálogo o los pasados por ingrediente." },
              "PAC_indice_clasico": { "type": "number", "description": "PAC clásico sacarosa-equivalente (sacarosa = 100, dextrosa ≈ 190). SOLO referencia secundaria para mapear a la convención estándar de heladería; el número de trabajo es el campo PAC." }
            }
          },
          "ranges": {
            "type": "object",
            "description": "Rango objetivo [mín, máx] de cada parámetro para el tipo de receta elegido. El de PAC es el rango de trabajo fijo de FormulaMaps (-12,5 … -10,6) en su escala; el de PAC_indice_clasico depende del tipo y está en la escala clásica.",
            "required": ["PAC", "POD", "azucares", "materiaGrasa", "SLNG", "solidosTotales", "PAC_indice_clasico"],
            "properties": {
              "PAC": { "$ref": "#/components/schemas/Range" },
              "POD": { "$ref": "#/components/schemas/Range" },
              "azucares": { "$ref": "#/components/schemas/Range" },
              "materiaGrasa": { "$ref": "#/components/schemas/Range" },
              "SLNG": { "$ref": "#/components/schemas/Range" },
              "solidosTotales": { "$ref": "#/components/schemas/Range" },
              "PAC_indice_clasico": { "$ref": "#/components/schemas/Range" }
            }
          },
          "inRange": {
            "type": "object",
            "description": "Para cada parámetro comprobado: true si está dentro de su rango, false si está fuera, null si el tipo no define límites para él.",
            "required": ["PAC", "POD", "azucares", "materiaGrasa", "SLNG", "solidosTotales"],
            "properties": {
              "PAC": { "type": ["boolean", "null"], "description": "PAC dentro del rango de trabajo -12,5 … -10,6 (escala FormulaMaps)." },
              "POD": { "type": ["boolean", "null"], "description": "POD dentro del rango del tipo." },
              "azucares": { "type": ["boolean", "null"], "description": "Azúcares dentro del rango del tipo." },
              "materiaGrasa": { "type": ["boolean", "null"], "description": "Materia grasa dentro del rango del tipo." },
              "SLNG": { "type": ["boolean", "null"], "description": "SLNG dentro del rango del tipo." },
              "solidosTotales": { "type": ["boolean", "null"], "description": "Sólidos totales dentro del rango del tipo." }
            }
          },
          "pac_info": {
            "type": "object",
            "description": "Bloque explicativo del PAC para que agentes y herramientas lo interpreten en la escala correcta.",
            "required": ["escala", "valor", "rango_trabajo", "en_rango", "indice_clasico"],
            "properties": {
              "escala": { "type": "string", "description": "Explicación de la escala FormulaMaps del PAC (≈ temperatura de servicio en °C, negativo; el número que ve el heladero en la app)." },
              "valor": { "type": "number", "description": "Valor del PAC en la escala FormulaMaps (igual que params.PAC)." },
              "rango_trabajo": { "$ref": "#/components/schemas/Range" },
              "en_rango": { "type": ["boolean", "null"], "description": "Si el PAC está dentro del rango de trabajo (igual que inRange.PAC)." },
              "indice_clasico": {
                "type": "object",
                "description": "Referencia secundaria en la convención clásica sacarosa-equivalente (sacarosa = 100).",
                "required": ["descripcion", "valor", "rango_por_tipo", "en_rango"],
                "properties": {
                  "descripcion": { "type": "string", "description": "Aclaración de que es solo referencia para herramientas externas; el usuario trabaja con el PAC de la escala FormulaMaps." },
                  "valor": { "type": "number", "description": "PAC clásico sacarosa-equivalente (igual que params.PAC_indice_clasico)." },
                  "rango_por_tipo": { "$ref": "#/components/schemas/Range" },
                  "en_rango": { "type": ["boolean", "null"], "description": "Si el índice clásico está dentro del rango del tipo de receta." }
                }
              }
            }
          },
          "suggestions": { "type": "array", "items": { "type": "string" }, "description": "Sugerencias de corrección en español (qué ingrediente o azúcar subir/bajar). Si todo está en rango, contiene un único mensaje de confirmación." },
          "warnings": { "type": "array", "items": { "type": "string" }, "description": "Avisos del cálculo, p. ej. ingredientes no reconocidos en el catálogo (se tratan como inertes salvo que pases su composición)." },
          "disclaimer": { "type": "string", "description": "Aclaración fija sobre la escala del PAC de FormulaMaps y el índice clásico de referencia." },
          "catalogoIngredientes": { "type": "integer", "description": "Número de ingredientes del catálogo real de FormulaMaps cargados en el motor." },
          "poweredBy": { "type": "string", "description": "Atribución del servicio: \"FormulaMaps · https://www.formulamaps.com\"." }
        }
      },
      "Error": {
        "type": "object",
        "description": "Respuesta de error (HTTP 400, 429 o 500).",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Mensaje de error en español." },
          "supported": { "type": "array", "items": { "type": "string" }, "description": "Solo cuando el craft pedido no está soportado: oficios disponibles en la v1 ([\"helado\"])." }
        }
      },
      "BalanceManifest": {
        "type": "object",
        "description": "Manifiesto que devuelve GET /api/balance: descripción del servicio, tipos de receta disponibles y ejemplo de cuerpo.",
        "required": ["service", "version", "description", "endpoint", "types", "bodyExample", "note", "docs", "poweredBy"],
        "properties": {
          "service": { "type": "string", "description": "Nombre del servicio: \"FormulaMaps Balance API\"." },
          "version": { "type": "string", "description": "Versión de la API (\"1\")." },
          "description": { "type": "string", "description": "Qué hace el servicio: equilibra una fórmula de heladería y devuelve PAC, POD, sólidos, materia grasa, SLNG, agua, kcal y coste con verdicto y sugerencias." },
          "endpoint": { "type": "string", "description": "Endpoint de cálculo: \"POST /api/balance\"." },
          "types": { "type": "array", "items": { "type": "string" }, "description": "Tipos de receta admitidos en el campo type de la petición." },
          "bodyExample": { "$ref": "#/components/schemas/BalanceRequest" },
          "note": { "type": "string", "description": "Nota: puedes pasar la composición de cada ingrediente (azucares, materiagrasa, esml, lactosa, pac, pod…) para máxima precisión; si no, se usa el catálogo estándar." },
          "docs": { "type": "string", "format": "uri", "description": "URL de la documentación para humanos (https://www.formulamaps.com/api.html)." },
          "poweredBy": { "type": "string", "description": "Atribución del servicio." }
        }
      },
      "PanaderiaIngredient": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "description": "Nombre del ingrediente (harina, agua, leche, huevo, sal, levadura, masa madre, mantequilla, aceite, azúcar…)." },
          "grams": { "type": "number", "description": "Cantidad en gramos." },
          "role": { "type": "string", "enum": ["harina", "agua", "leche", "huevo", "sal", "levadura", "masa_madre", "mantequilla", "aceite", "grasa", "azucar", "otro"], "description": "Opcional: fuerza la clasificación del ingrediente si el nombre no es reconocible." }
        },
        "required": ["name", "grams"]
      },
      "PanaderiaRequest": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "enum": ["pan_comun", "baguette", "ciabatta", "pizza_napolitana", "pizza_teglia", "pan_molde", "brioche", "pan_rustico_mm"], "default": "pan_comun", "description": "Tipo de masa, define los rangos de referencia." },
          "ingredients": { "type": "array", "items": { "$ref": "#/components/schemas/PanaderiaIngredient" } }
        },
        "required": ["ingredients"]
      },
      "PanaderiaResponse": {
        "type": "object",
        "description": "Resultado del análisis en porcentaje de panadero (harina = 100%). Todos los porcentajes van redondeados.",
        "properties": {
          "status": { "type": "string", "enum": ["ok"] },
          "verdict": { "type": "string", "enum": ["equilibrada", "revisar"] },
          "type": { "type": "string", "description": "Tipo de masa aplicado." },
          "typeLabel": { "type": "string", "description": "Etiqueta legible del tipo, p. ej. \"Pizza napolitana (AVPN)\"." },
          "params": {
            "type": "object",
            "description": "Parámetros en porcentaje de panadero (harina = 100).",
            "properties": {
              "hidratacion": { "type": "number", "description": "Hidratación total (%), contando agua libre + agua real de leche/huevo/mantequilla/miel." },
              "sal": { "type": "number", "description": "Sal (%)." },
              "levadura": { "type": "number", "description": "Levadura (%)." },
              "prefermento": { "type": "number", "description": "Masa madre/prefermento (%), si se usa." },
              "grasa": { "type": "number", "description": "Grasa total (%)." },
              "azucar": { "type": "number", "description": "Azúcar (%)." }
            }
          },
          "ranges": { "type": "object", "description": "Rango objetivo [mín, máx] de cada parámetro para el tipo de masa elegido." },
          "inRange": { "type": "object", "description": "Para cada parámetro: true/false/null según esté o no dentro de rango." },
          "suggestions": { "type": "array", "items": { "type": "string" } },
          "warnings": { "type": "array", "items": { "type": "string" } },
          "poweredBy": { "type": "string" }
        }
      },
      "PasteleriaIngredient": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "description": "Nombre del catálogo de pastelería de FormulaMaps (Harina, Azúcar, Mantequilla, Aceite, Huevo entero, Yema, Clara de huevo, Leche entera, Nata 35%, Almidón/Maicena, Cacao puro, Chocolate, Chocolate blanco, Chocolate con leche, Almendra molida, Gelatina, Impulsor, Miel, Queso crema, Zumo de limón, Zanahoria rallada, Yogur natural, Agua) o uno propio con composición." },
          "grams": { "type": "number", "description": "Cantidad en gramos." }
        },
        "required": ["name", "grams"]
      },
      "PasteleriaRequest": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "description": "Tipo de receta de pastelería (p. ej. genoise, magdalenas, brownie, pound_cake, financier, macaron, masa_tarta, choux, tarta_santiago, crema_pastelera, crema_inglesa, natillas, diplomatica, muselina, bavarois, ganache_negro, ganache_blanco, mousse_chocolate, buttercream, lemon_curd, merengue, cheesecake, frangipane…). Por defecto genoise.", "default": "genoise" },
          "ingredients": { "type": "array", "items": { "$ref": "#/components/schemas/PasteleriaIngredient" } }
        },
        "required": ["ingredients"]
      },
      "PasteleriaResponse": {
        "type": "object",
        "description": "Resultado del balanceo de pastelería: composición en base 100 (por 100 g de receta) más los parámetros específicos del tipo.",
        "properties": {
          "status": { "type": "string", "enum": ["ok"] },
          "verdict": { "type": "string", "enum": ["equilibrada", "revisar"] },
          "type": { "type": "string" },
          "typeLabel": { "type": "string" },
          "params": {
            "type": "object",
            "description": "Composición base 100 y parámetros del tipo.",
            "properties": {
              "azucares": { "type": "number", "description": "Azúcares, g por 100 g de receta." },
              "grasa": { "type": "number", "description": "Grasa, g por 100 g de receta." },
              "proteinas": { "type": "number", "description": "Proteínas, g por 100 g de receta." },
              "agua": { "type": "number", "description": "Agua, g por 100 g de receta." },
              "solidos": { "type": "number", "description": "Sólidos totales, g por 100 g de receta." },
              "pod": { "type": "number", "description": "Poder de dulzor relativo (sacarosa = 100)." },
              "kcal_100g": { "type": "number" },
              "coste_kg_eur": { "type": "number", "description": "Coste en euros por kg de receta." }
            }
          },
          "ranges": { "type": "object" },
          "inRange": { "type": "object" },
          "trophy": { "type": "string", "description": "Trofeo/medalla si aplica (oro/plata/bronce)." },
          "suggestions": { "type": "array", "items": { "type": "string" } },
          "warnings": { "type": "array", "items": { "type": "string" } },
          "poweredBy": { "type": "string" }
        }
      },
      "TurronIngredient": {
        "type": "object",
        "properties": {
          "ingrediente": { "type": "string", "description": "Id del ingrediente del catálogo de turrones (almendra_tostada, almendra_cruda, miel, azucar, glucosa, azucar_invertido, clara, yema, agua, manteca_cerdo, mantequilla, harina, harina_tostada, aceite_oliva, fruta_confitada, pinon, boniato, avellana_tostada, canela, oblea…)." },
          "peso": { "type": "number", "description": "Cantidad en gramos." }
        },
        "required": ["ingrediente", "peso"]
      },
      "TurronRequest": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "enum": ["jijona", "alicante", "yema_tostada", "mazapan", "guirlache", "peladilla", "generico"], "default": "jijona", "description": "Tipo de turrón/dulce, define los umbrales IGP/RTS." },
          "ingredients": { "type": "array", "items": { "$ref": "#/components/schemas/TurronIngredient" } }
        },
        "required": ["ingredients"]
      },
      "TurronResponse": {
        "type": "object",
        "description": "Resultado de la clasificación IGP/RTS del turrón.",
        "properties": {
          "status": { "type": "string", "enum": ["ok"] },
          "craft": { "type": "string", "enum": ["turron"] },
          "type": { "type": "string" },
          "typeLabel": { "type": "string" },
          "params": {
            "type": "object",
            "description": "Parámetros calculados sobre la base acabada (%).",
            "properties": {
              "pctAlmendra": { "type": "number", "description": "% de almendra sobre el producto acabado." },
              "pctMiel": { "type": "number", "description": "% de miel." }
            }
          },
          "score": {
            "type": "object",
            "description": "Puntuación de 1 a 3 almendras (bronce/plata/oro) y categoría (Suprema/Extra/Fuera de IGP/Básico), con los motivos.",
            "properties": {
              "tipo": { "type": "string" },
              "almendras": { "type": "integer", "minimum": 1, "maximum": 3 },
              "categoria": { "type": "string" },
              "motivos": { "type": "array", "items": { "type": "string" } }
            }
          },
          "poweredBy": { "type": "string" }
        }
      },
      "CacaoIngredient": {
        "type": "object",
        "properties": {
          "ingrediente": { "type": "string", "description": "Id del ingrediente del catálogo de cacao (pasta_cacao, manteca_cacao, cacao_polvo, nibs, cob_negra, cob_leche, cob_blanca, azucar, azucar_glas, dextrosa, azucar_invertido, glucosa, sorbitol, leche_entera_polvo, leche_desn_polvo, nata, mantequilla, pasta_avellana, lecitina, agua)." },
          "peso": { "type": "number", "description": "Cantidad en gramos." }
        },
        "required": ["ingrediente", "peso"]
      },
      "CacaoRequest": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "enum": ["negro", "leche", "blanco", "cobertura", "bean_to_bar", "gianduja", "bombon"], "default": "negro", "description": "Tipo de chocolate, define los rangos legales/objetivo." },
          "ingredients": { "type": "array", "items": { "$ref": "#/components/schemas/CacaoIngredient" } }
        },
        "required": ["ingredients"]
      },
      "CacaoResponse": {
        "type": "object",
        "description": "Resultado del análisis de chocolate/cacao: % cacao, grasa, denominación legal y temperaturas de templado.",
        "properties": {
          "status": { "type": "string", "enum": ["ok"] },
          "craft": { "type": "string", "enum": ["cacao"] },
          "type": { "type": "string" },
          "typeLabel": { "type": "string" },
          "params": {
            "type": "object",
            "description": "Parámetros calculados sobre el producto acabado (%).",
            "properties": {
              "cacaoTotal": { "type": "number", "description": "% de cacao total (manteca + sólidos de cacao)." },
              "manteca": { "type": "number", "description": "% de manteca de cacao." },
              "grasaTotal": { "type": "number", "description": "% de grasa total." },
              "azucar": { "type": "number", "description": "% de azúcar." },
              "lacteos": { "type": "number", "description": "% de lácteos." }
            }
          },
          "score": {
            "type": "object",
            "description": "Medalla (oro/plata/bronce), temperaturas de templado (fundir/enfriar/trabajar en °C) y, para bombón/ganache, actividad de agua (aw) estimada.",
            "properties": {
              "tipo": { "type": "string" },
              "medalla": { "type": "string" },
              "temple": {
                "type": "object",
                "properties": {
                  "fundir": { "$ref": "#/components/schemas/Range" },
                  "enfriar": { "$ref": "#/components/schemas/Range" },
                  "trabajar": { "$ref": "#/components/schemas/Range" }
                }
              },
              "aw": { "type": "number", "description": "Actividad de agua estimada (solo bombón/ganache)." }
            }
          },
          "poweredBy": { "type": "string" }
        }
      }
    }
  }
}
