{
  "openapi": "3.1.0",
  "info": {
    "title": "API de lectura del Banco de ítems CERP",
    "version": "1.0.0-beta",
    "summary": "Ítems de construcción con precios de referencia, procedencia y descomposición (España y Argentina).",
    "description": "API pública de solo lectura del banco de ítems de construcción de CERP, en beta. Sin clave y con cuota limitada:\nla búsqueda devuelve como mucho 25 resultados y no pagina. Para el catálogo completo, el bundle\npúblico (https://github.com/cerptech/cerp-items-data); para agentes de IA, el servidor MCP en https://items.cerp.es/mcp.\n\nToda respuesta con datos incluye `license` y `attribution`: la atribución es obligatoria al reutilizar los datos.\nLos precios van en la moneda de la fuente (EUR en España, ARS en Argentina), sin conversión, con fecha y procedencia.\n\nClaves con más cuota y export masivo: lista de espera en https://items.cerp.es/api-datos.",
    "contact": {
      "name": "CERP",
      "email": "admin@cerp.es",
      "url": "https://items.cerp.es/api-datos"
    },
    "license": {
      "name": "Reutilización con atribución (Ley 37/2007, RISP)",
      "url": "https://github.com/cerptech/cerp-items-data#licencia-y-atribución"
    }
  },
  "servers": [
    {
      "url": "https://items.cerp.es",
      "description": "Producción"
    }
  ],
  "externalDocs": {
    "description": "API de datos y MCP",
    "url": "https://items.cerp.es/api-datos"
  },
  "tags": [
    {
      "name": "Banco de ítems",
      "description": "Lectura del catálogo"
    }
  ],
  "paths": {
    "/api/v1/meta": {
      "get": {
        "operationId": "getMeta",
        "tags": [
          "Banco de ítems"
        ],
        "summary": "Versión del bundle, países, conteos, licencia y atribución",
        "responses": {
          "200": {
            "description": "Metadatos del banco.",
            "headers": {
              "Cache-Control": {
                "description": "public, s-maxage=3600, stale-while-revalidate=86400",
                "schema": {
                  "type": "string"
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "CORS abierto (solo GET y OPTIONS).",
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MetaResponse"
                }
              }
            }
          },
          "502": {
            "description": "No se pudo leer el bundle de datos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "operationId": "searchItems",
        "tags": [
          "Banco de ítems"
        ],
        "summary": "Buscar ítems por texto",
        "description": "Resultados ordenados por relevancia, con sinónimos del sector de España y Argentina, tolerancia a erratas y filtro por unidad («m2 tabique»); no distingue tildes ni mayúsculas. Un código de ítem pegado tal cual devuelve esa ficha. Sin offset: si `hasMore` es true, afina la consulta.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Texto a buscar.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            },
            "example": "placa de anclaje hormigón"
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Catálogo: es (España, por defecto) o ar (Argentina).",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "ar"
              ],
              "default": "es"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Filtra por tipo de ítem.",
            "schema": {
              "type": "string",
              "enum": [
                "work_item",
                "auxiliary",
                "material",
                "labor",
                "machinery",
                "other"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultados (1-25).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resultados de la búsqueda.",
            "headers": {
              "Cache-Control": {
                "description": "public, s-maxage=3600, stale-while-revalidate=86400",
                "schema": {
                  "type": "string"
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "CORS abierto (solo GET y OPTIONS).",
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "Parámetro ausente o inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "No se pudo leer el bundle de datos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}": {
      "get": {
        "operationId": "getItem",
        "tags": [
          "Banco de ítems"
        ],
        "summary": "Ficha de un ítem",
        "description": "Ficha con la descomposición resuelta y costeada, todos los precios con su procedencia (fuente, código, edición, ámbito, fecha y antigüedad) y la verificación automática.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Código del ítem (wi = partida, mt = material, mo = mano de obra, mq = maquinaria, au = auxiliar, ot = otro).",
            "schema": {
              "type": "string",
              "pattern": "^(wi|mt|mo|mq|au|ch|ot)-[a-z0-9-]+$"
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Catálogo: es (España, por defecto) o ar (Argentina).",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "ar"
              ],
              "default": "es"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ficha del ítem.",
            "headers": {
              "Cache-Control": {
                "description": "public, s-maxage=3600, stale-while-revalidate=86400",
                "schema": {
                  "type": "string"
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "CORS abierto (solo GET y OPTIONS).",
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemResponse"
                }
              }
            }
          },
          "400": {
            "description": "Código con formato inválido o país desconocido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El ítem no existe en ese catálogo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "No se pudo leer el bundle de datos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/chapters": {
      "get": {
        "operationId": "listChapters",
        "tags": [
          "Banco de ítems"
        ],
        "summary": "Árbol de capítulos",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Catálogo: es (España, por defecto) o ar (Argentina).",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "ar"
              ],
              "default": "es"
            }
          },
          {
            "name": "depth",
            "in": "query",
            "required": false,
            "description": "Niveles a expandir (1-6). Sin valor, el árbol completo.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 6
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Árbol de capítulos con conteos y URL de cada nodo.",
            "headers": {
              "Cache-Control": {
                "description": "public, s-maxage=3600, stale-while-revalidate=86400",
                "schema": {
                  "type": "string"
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "CORS abierto (solo GET y OPTIONS).",
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChaptersResponse"
                }
              }
            }
          },
          "400": {
            "description": "Parámetro inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "No se pudo leer el bundle de datos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Banco de ítems"
        ],
        "summary": "Salud del dataset de cada país",
        "description": "Los mismos números que la página de salud del dataset: ítems por tipo, estados de verificación, antigüedad de los precios (distribución por año y % por encima del umbral de la aduana), cobertura por capítulo con la completitud de la descomposición, observaciones abiertas de la aduana y fuentes con su licencia y atribución exacta. Se calcula sobre el bundle publicado y se renueva cada día o al publicarse una versión nueva. `notPublished` dice qué datos no están en el bundle y dónde viven.",
        "externalDocs": {
          "description": "Salud del dataset",
          "url": "https://items.cerp.es/salud"
        },
        "responses": {
          "200": {
            "description": "Salud del dataset de España y Argentina.",
            "headers": {
              "Cache-Control": {
                "description": "public, s-maxage=3600, stale-while-revalidate=86400",
                "schema": {
                  "type": "string"
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "CORS abierto (solo GET y OPTIONS).",
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          },
          "502": {
            "description": "No se pudo leer el bundle de datos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_parameter",
                  "missing_parameter",
                  "invalid_id",
                  "item_not_found",
                  "not_found",
                  "upstream_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "Mensaje legible, en español."
              }
            }
          }
        }
      },
      "License": {
        "type": "object",
        "description": "Licencia del bundle de datos (README de cerp-items-data).",
        "required": [
          "name",
          "text",
          "url"
        ],
        "properties": {
          "name": {
            "type": "string",
            "examples": [
              "Reutilización con atribución (Ley 37/2007, RISP)"
            ]
          },
          "text": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Bundle": {
        "type": "object",
        "description": "Versión del bundle de datos que respondió.",
        "required": [
          "version",
          "generatedAt"
        ],
        "properties": {
          "version": {
            "type": "string",
            "examples": [
              "0.5.0"
            ]
          },
          "generatedAt": {
            "type": "string",
            "description": "Fecha de generación del bundle (ISO 8601)."
          }
        }
      },
      "Attribution": {
        "type": "array",
        "description": "Textos de atribución exactos del bundle. Obligatorio citarlos al reutilizar los datos.",
        "items": {
          "type": "string"
        },
        "examples": [
          [
            "Información obtenida del Portal de la Junta de Andalucía"
          ]
        ]
      },
      "Country": {
        "type": "string",
        "enum": [
          "es",
          "ar"
        ]
      },
      "Currency": {
        "type": "string",
        "enum": [
          "EUR",
          "ARS"
        ],
        "description": "EUR en España, ARS en Argentina. Sin conversión."
      },
      "ItemKind": {
        "type": "string",
        "enum": [
          "chapter",
          "work_item",
          "auxiliary",
          "material",
          "labor",
          "machinery",
          "other"
        ]
      },
      "GeoScope": {
        "type": "object",
        "required": [
          "country"
        ],
        "properties": {
          "country": {
            "type": "string",
            "enum": [
              "ES",
              "AR"
            ]
          },
          "region": {
            "type": "string"
          },
          "province": {
            "type": "string"
          },
          "zone": {
            "type": "string"
          }
        }
      },
      "MetaCountry": {
        "type": "object",
        "required": [
          "code",
          "label",
          "currency",
          "version",
          "generatedAt",
          "url",
          "counts",
          "attribution"
        ],
        "properties": {
          "code": {
            "$ref": "#/components/schemas/Country"
          },
          "label": {
            "type": "string",
            "examples": [
              "España"
            ]
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "version": {
            "type": "string"
          },
          "generatedAt": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "counts": {
            "type": "object",
            "required": [
              "total",
              "chapters",
              "byKind",
              "byVerification",
              "quarantined"
            ],
            "properties": {
              "total": {
                "type": "integer"
              },
              "chapters": {
                "type": "integer"
              },
              "byKind": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "byVerification": {
                "type": "object",
                "description": "Ítems por estado de verificación de la aduana del banco.",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "quarantined": {
                "type": "integer"
              }
            }
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          }
        }
      },
      "Meta": {
        "type": "object",
        "required": [
          "name",
          "apiVersion",
          "countries",
          "limits",
          "links"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "apiVersion": {
            "type": "string",
            "examples": [
              "1.0.0-beta"
            ]
          },
          "countries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MetaCountry"
            }
          },
          "limits": {
            "type": "object",
            "required": [
              "searchMaxLimit",
              "mcpSearchMaxLimit",
              "offset"
            ],
            "properties": {
              "searchMaxLimit": {
                "type": "integer",
                "const": 25
              },
              "mcpSearchMaxLimit": {
                "type": "integer"
              },
              "offset": {
                "type": "boolean",
                "const": false,
                "description": "La búsqueda no pagina: afina la consulta."
              }
            }
          },
          "links": {
            "type": "object",
            "required": [
              "site",
              "openapi",
              "mcp",
              "llms",
              "docs",
              "health",
              "data",
              "contact"
            ],
            "properties": {
              "site": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "mcp": {
                "type": "string",
                "format": "uri"
              },
              "llms": {
                "type": "string",
                "format": "uri"
              },
              "docs": {
                "type": "string",
                "format": "uri"
              },
              "health": {
                "type": "string",
                "format": "uri",
                "description": "Página de salud del dataset."
              },
              "data": {
                "type": "string",
                "format": "uri"
              },
              "contact": {
                "type": "string"
              }
            }
          }
        }
      },
      "SearchResult": {
        "type": "object",
        "required": [
          "id",
          "summary",
          "unit",
          "kind",
          "price",
          "currency",
          "chapter",
          "url",
          "apiUrl"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "wi-acero-s275jr-en-placa-de-anclaje-a-muro-hormigon-o-fabrica"
            ]
          },
          "summary": {
            "type": "string"
          },
          "unit": {
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "$ref": "#/components/schemas/ItemKind"
          },
          "price": {
            "type": [
              "number",
              "null"
            ],
            "description": "Precio de referencia del índice, en la moneda del país."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "chapter": {
            "type": [
              "string",
              "null"
            ],
            "description": "Clave del capítulo; null en conceptos básicos."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Ficha en items.cerp.es."
          },
          "apiUrl": {
            "type": "string",
            "format": "uri",
            "description": "Ficha completa en la API."
          }
        }
      },
      "Search": {
        "type": "object",
        "required": [
          "query",
          "country",
          "kind",
          "limit",
          "count",
          "hasMore",
          "results"
        ],
        "properties": {
          "query": {
            "type": "string"
          },
          "country": {
            "$ref": "#/components/schemas/Country"
          },
          "kind": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ItemKind"
              },
              {
                "type": "null"
              }
            ]
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 25
          },
          "count": {
            "type": "integer"
          },
          "hasMore": {
            "type": "boolean",
            "description": "Hay más coincidencias que `limit`. No hay offset: afina la consulta."
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchResult"
            }
          }
        }
      },
      "Price": {
        "type": "object",
        "required": [
          "value",
          "currency",
          "kind",
          "date",
          "source",
          "sourceLabel",
          "sourceCode",
          "edition",
          "scope",
          "ageMonths",
          "stale",
          "staleAfterMonths"
        ],
        "properties": {
          "value": {
            "type": "number"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "kind": {
            "type": "string",
            "enum": [
              "list",
              "computed_from_bom",
              "curated"
            ],
            "description": "list: publicado por la fuente; computed_from_bom: calculado por CERP desde la descomposición; curated: fijado por CERP en la curación (por ejemplo, el coste empresa de la mano de obra)."
          },
          "date": {
            "type": "string",
            "description": "Fecha del precio (YYYY-MM)."
          },
          "source": {
            "type": "string",
            "examples": [
              "bcca"
            ]
          },
          "sourceLabel": {
            "type": "string",
            "examples": [
              "BCCA"
            ]
          },
          "sourceCode": {
            "type": "string",
            "description": "Código del concepto en la fuente de origen."
          },
          "edition": {
            "type": "string"
          },
          "scope": {
            "$ref": "#/components/schemas/GeoScope"
          },
          "ageMonths": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Meses desde la fecha del precio."
          },
          "stale": {
            "type": "boolean",
            "description": "Supera la antigüedad que el banco da por vigente en el país."
          },
          "staleAfterMonths": {
            "type": "integer",
            "description": "24 en España, 3 en Argentina."
          }
        }
      },
      "BomLine": {
        "type": "object",
        "required": [
          "ref",
          "summary",
          "role",
          "isPercentage",
          "qty",
          "unit",
          "unitPrice",
          "amount",
          "url"
        ],
        "properties": {
          "ref": {
            "type": "string",
            "description": "Id del componente."
          },
          "summary": {
            "type": "string"
          },
          "role": {
            "$ref": "#/components/schemas/ItemKind"
          },
          "isPercentage": {
            "type": "boolean"
          },
          "qty": {
            "type": "number",
            "description": "Cantidad efectiva (factor × rendimiento); en porcentuales, tanto por uno."
          },
          "unit": {
            "type": [
              "string",
              "null"
            ]
          },
          "unitPrice": {
            "type": [
              "number",
              "null"
            ]
          },
          "amount": {
            "type": [
              "number",
              "null"
            ]
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        }
      },
      "Bom": {
        "type": "object",
        "description": "Descomposición costeada con la regla BC3 (los porcentuales se aplican sobre el subtotal previo).",
        "required": [
          "zone",
          "currency",
          "lines",
          "total",
          "complete"
        ],
        "properties": {
          "zone": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ámbito del precio de referencia; null = sin zona."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BomLine"
            }
          },
          "total": {
            "type": [
              "number",
              "null"
            ],
            "description": "Coste directo computado; null si alguna línea no tiene precio."
          },
          "complete": {
            "type": "boolean"
          }
        }
      },
      "Finding": {
        "type": "object",
        "required": [
          "rule",
          "label",
          "message",
          "count"
        ],
        "properties": {
          "rule": {
            "type": "string",
            "examples": [
              "duplicate_candidate"
            ]
          },
          "label": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "Item": {
        "type": "object",
        "required": [
          "id",
          "country",
          "kind",
          "summary",
          "text",
          "unit",
          "url",
          "chapter",
          "classification",
          "price",
          "prices",
          "bom",
          "verification",
          "sources",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "country": {
            "$ref": "#/components/schemas/Country"
          },
          "kind": {
            "$ref": "#/components/schemas/ItemKind"
          },
          "summary": {
            "type": "string"
          },
          "text": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pliego / descripción larga."
          },
          "unit": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "chapter": {
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "key",
                  "title",
                  "url"
                ],
                "properties": {
                  "key": {
                    "type": "string"
                  },
                  "title": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "classification": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "price": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Price"
              },
              {
                "type": "null"
              }
            ],
            "description": "Precio de referencia (el que destaca la ficha)."
          },
          "prices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Price"
            },
            "description": "Todos los precios, del más reciente al más antiguo."
          },
          "bom": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Bom"
              },
              {
                "type": "null"
              }
            ]
          },
          "verification": {
            "type": "object",
            "required": [
              "status",
              "findings"
            ],
            "properties": {
              "status": {
                "type": "string",
                "examples": [
                  "auto_ok",
                  "conflicted"
                ]
              },
              "findings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Finding"
                }
              }
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "source",
                "code",
                "edition",
                "matchedBy"
              ],
              "properties": {
                "source": {
                  "type": "string"
                },
                "code": {
                  "type": "string"
                },
                "edition": {
                  "type": "string"
                },
                "matchedBy": {
                  "type": "string"
                }
              }
            }
          },
          "createdAt": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          }
        }
      },
      "ChapterNode": {
        "type": "object",
        "required": [
          "label",
          "slug",
          "items",
          "key",
          "url",
          "subgroups"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "items": {
            "type": "integer",
            "description": "Ítems descendientes."
          },
          "key": {
            "type": [
              "string",
              "null"
            ],
            "description": "Clave del capítulo en el bundle (solo en el nivel con archivo propio)."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "subgroups": {
            "type": "integer",
            "description": "Subgrupos directos, aunque no se expandan por depth."
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChapterNode"
            }
          }
        }
      },
      "Chapters": {
        "type": "object",
        "required": [
          "country",
          "total",
          "depth",
          "chapters"
        ],
        "properties": {
          "country": {
            "$ref": "#/components/schemas/Country"
          },
          "total": {
            "type": "integer"
          },
          "depth": {
            "type": [
              "integer",
              "null"
            ]
          },
          "chapters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChapterNode"
            }
          }
        }
      },
      "Percent": {
        "type": [
          "number",
          "null"
        ],
        "minimum": 0,
        "maximum": 100,
        "description": "Porcentaje con un decimal (0-100); null si la base es cero."
      },
      "HealthCountry": {
        "type": "object",
        "description": "Salud del dataset de un país, calculada sobre el bundle publicado (capítulos + básicos). Los mismos números que la página /salud (/ar/salud).",
        "required": [
          "country",
          "label",
          "currency",
          "computedAt",
          "bundle",
          "items",
          "verification",
          "prices",
          "bom",
          "chapters",
          "chaptersTotal",
          "findings",
          "crossCheck",
          "sources",
          "attribution",
          "notPublished"
        ],
        "properties": {
          "country": {
            "$ref": "#/components/schemas/Country"
          },
          "label": {
            "type": "string",
            "examples": [
              "España"
            ]
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "computedAt": {
            "type": "string",
            "description": "Instante del cálculo (ISO 8601): la antigüedad se mide a esta fecha."
          },
          "bundle": {
            "$ref": "#/components/schemas/Bundle"
          },
          "items": {
            "type": "object",
            "required": [
              "total",
              "analyzed",
              "byKind"
            ],
            "properties": {
              "total": {
                "type": "integer",
                "description": "Ítems publicados según el index.json del país."
              },
              "analyzed": {
                "type": "integer",
                "description": "Ítems leídos para el cálculo; coincide con total."
              },
              "byKind": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "kind",
                    "label",
                    "count"
                  ],
                  "properties": {
                    "kind": {
                      "$ref": "#/components/schemas/ItemKind"
                    },
                    "label": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "verification": {
            "type": "object",
            "required": [
              "statuses",
              "lastChangeAt"
            ],
            "properties": {
              "statuses": {
                "type": "array",
                "description": "Los cinco estados de la aduana. quarantined y unverified no se publican: de quarantined el bundle trae solo el conteo; de unverified no trae ni el conteo (count: null).",
                "items": {
                  "type": "object",
                  "required": [
                    "status",
                    "label",
                    "meaning",
                    "published",
                    "count"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "verified",
                        "auto_ok",
                        "conflicted",
                        "quarantined",
                        "unverified"
                      ]
                    },
                    "label": {
                      "type": "string"
                    },
                    "meaning": {
                      "type": "string"
                    },
                    "published": {
                      "type": "boolean"
                    },
                    "count": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Ítems en ese estado según el index.json; null si el bundle no trae el conteo (unverified). Nunca se completa con 0."
                    }
                  }
                }
              },
              "lastChangeAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Última vez que la aduana cambió el estado o las observaciones de algún ítem publicado (no la fecha de la última corrida completa, que no está en el bundle)."
              }
            }
          },
          "prices": {
            "type": "object",
            "required": [
              "staleAfterMonths",
              "itemsWithPrice",
              "itemsWithoutPrice",
              "entries",
              "entriesByKind",
              "byYear",
              "stale",
              "stalePct",
              "medianAgeMonths",
              "dateRange"
            ],
            "properties": {
              "staleAfterMonths": {
                "type": "integer",
                "description": "Umbral de la aduana: 24 meses en España, 3 en Argentina."
              },
              "itemsWithPrice": {
                "type": "integer"
              },
              "itemsWithoutPrice": {
                "type": "integer"
              },
              "entries": {
                "type": "integer",
                "description": "Entradas de precio (un ítem puede tener varias: zonas, ediciones)."
              },
              "entriesByKind": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "kind",
                    "label",
                    "entries"
                  ],
                  "properties": {
                    "kind": {
                      "type": "string",
                      "enum": [
                        "list",
                        "computed_from_bom",
                        "curated"
                      ]
                    },
                    "label": {
                      "type": "string"
                    },
                    "entries": {
                      "type": "integer"
                    }
                  }
                }
              },
              "byYear": {
                "type": "array",
                "description": "Ítems según el año de su precio más reciente.",
                "items": {
                  "type": "object",
                  "required": [
                    "year",
                    "items"
                  ],
                  "properties": {
                    "year": {
                      "type": "integer"
                    },
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              },
              "stale": {
                "type": "integer",
                "description": "Ítems cuyo precio más reciente supera el umbral (criterio de stale_edition)."
              },
              "stalePct": {
                "$ref": "#/components/schemas/Percent"
              },
              "medianAgeMonths": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Mediana de la antigüedad del precio más reciente, en meses."
              },
              "dateRange": {
                "oneOf": [
                  {
                    "type": "object",
                    "required": [
                      "from",
                      "to"
                    ],
                    "properties": {
                      "from": {
                        "type": "string"
                      },
                      "to": {
                        "type": "string"
                      }
                    },
                    "description": "Fechas (YYYY-MM) más antigua y más reciente entre todas las entradas de precio."
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "bom": {
            "type": "object",
            "required": [
              "workItems",
              "withBom",
              "completeBom",
              "completeBomPct"
            ],
            "properties": {
              "workItems": {
                "type": "integer"
              },
              "withBom": {
                "type": "integer"
              },
              "completeBom": {
                "type": "integer",
                "description": "Partidas con al menos una línea directa de mano de obra, una de material y una de maquinaria."
              },
              "completeBomPct": {
                "$ref": "#/components/schemas/Percent"
              }
            }
          },
          "chapters": {
            "type": "array",
            "description": "Cobertura por capítulo (partidas de obra).",
            "items": {
              "type": "object",
              "required": [
                "key",
                "title",
                "workItems",
                "withBom",
                "completeBom",
                "completeBomPct",
                "withPrice",
                "datedPrice",
                "currentPrice",
                "currentPricePct"
              ],
              "properties": {
                "key": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "workItems": {
                  "type": "integer"
                },
                "withBom": {
                  "type": "integer"
                },
                "completeBom": {
                  "type": "integer"
                },
                "completeBomPct": {
                  "$ref": "#/components/schemas/Percent"
                },
                "withPrice": {
                  "type": "integer"
                },
                "datedPrice": {
                  "type": "integer",
                  "description": "Partidas con precio de fecha legible: la base de currentPricePct."
                },
                "currentPrice": {
                  "type": "integer",
                  "description": "Partidas cuyo precio más reciente no supera el umbral del país."
                },
                "currentPricePct": {
                  "$ref": "#/components/schemas/Percent"
                }
              }
            }
          },
          "chaptersTotal": {
            "type": "object",
            "description": "Totales de la tabla de capítulos: la suma de sus filas, con los mismos denominadores.",
            "required": [
              "workItems",
              "withBom",
              "completeBom",
              "completeBomPct",
              "withPrice",
              "datedPrice",
              "currentPrice",
              "currentPricePct"
            ],
            "properties": {
              "workItems": {
                "type": "integer"
              },
              "withBom": {
                "type": "integer"
              },
              "completeBom": {
                "type": "integer"
              },
              "completeBomPct": {
                "$ref": "#/components/schemas/Percent"
              },
              "withPrice": {
                "type": "integer"
              },
              "datedPrice": {
                "type": "integer"
              },
              "currentPrice": {
                "type": "integer"
              },
              "currentPricePct": {
                "$ref": "#/components/schemas/Percent"
              }
            }
          },
          "findings": {
            "type": "array",
            "description": "Observaciones abiertas de la aduana sobre los ítems publicados, por regla (las nueve, también las que no emiten).",
            "items": {
              "type": "object",
              "required": [
                "rule",
                "label",
                "severity",
                "findings",
                "items",
                "applies",
                "applicableItems",
                "notApplicableReason"
              ],
              "properties": {
                "rule": {
                  "type": "string",
                  "examples": [
                    "duplicate_candidate"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "severity": {
                  "type": "string",
                  "enum": [
                    "critical",
                    "warning",
                    "info",
                    "warning_or_critical"
                  ]
                },
                "findings": {
                  "type": "integer"
                },
                "items": {
                  "type": "integer"
                },
                "applies": {
                  "type": "boolean",
                  "description": "Si la regla puede emitir observaciones sobre los ítems publicados del país. false si no cubre el país (chapter_mismatch solo audita la taxonomía orden-ejecucion con códigos BCCA: solo España; unit_mismatch no emite en ningún país) o si ningún ítem cumple su condición de entrada. Con false, sus 0 observaciones no son una comprobación."
                },
                "applicableItems": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Ítems publicados que cumplen la condición de entrada de la regla, calculado sobre el bundle (bom_sum_mismatch: con descomposición y algún precio de lista > 0; price_delta: precios de lista > 0 de dos fuentes; missing_bom: partidas de obra). Es un máximo. 0 si la regla no cubre el país; null si el bundle no alcanza para calcularlo (duplicate_candidate, y chapter_mismatch donde se aplica)."
                },
                "notApplicableReason": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Por qué la regla no se aplica en el país; null si se aplica."
                }
              }
            }
          },
          "crossCheck": {
            "type": "object",
            "required": [
              "listSources",
              "itemsWithListPricesFromTwoSources",
              "itemsWithPricesFromTwoSources"
            ],
            "properties": {
              "listSources": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Fuentes con precios de lista en el país."
              },
              "itemsWithListPricesFromTwoSources": {
                "type": "integer",
                "description": "Ítems con precios de lista (> 0) de dos fuentes distintas: la condición para que la aduana cruce precios."
              },
              "itemsWithPricesFromTwoSources": {
                "type": "integer"
              }
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "label",
                "name",
                "items",
                "priceEntries",
                "listPrices",
                "editions",
                "license",
                "attribution"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "examples": [
                    "bcca"
                  ]
                },
                "label": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "items": {
                  "type": "integer",
                  "description": "Ítems que citan la fuente en su procedencia."
                },
                "priceEntries": {
                  "type": "integer"
                },
                "listPrices": {
                  "type": "integer"
                },
                "editions": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "license": {
                  "type": "string"
                },
                "attribution": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Texto de atribución exacto del index.json; null si no se exige."
                }
              }
            }
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          },
          "notPublished": {
            "type": "array",
            "description": "Datos que el bundle público no trae y dónde viven.",
            "items": {
              "type": "object",
              "required": [
                "what",
                "where"
              ],
              "properties": {
                "what": {
                  "type": "string"
                },
                "where": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "countries"
        ],
        "properties": {
          "countries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HealthCountry"
            }
          }
        }
      },
      "MetaResponse": {
        "type": "object",
        "required": [
          "data",
          "bundle",
          "license",
          "attribution"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Meta"
          },
          "bundle": {
            "$ref": "#/components/schemas/Bundle"
          },
          "license": {
            "$ref": "#/components/schemas/License"
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "required": [
          "data",
          "bundle",
          "license",
          "attribution"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Search"
          },
          "bundle": {
            "$ref": "#/components/schemas/Bundle"
          },
          "license": {
            "$ref": "#/components/schemas/License"
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          }
        }
      },
      "ItemResponse": {
        "type": "object",
        "required": [
          "data",
          "bundle",
          "license",
          "attribution"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Item"
          },
          "bundle": {
            "$ref": "#/components/schemas/Bundle"
          },
          "license": {
            "$ref": "#/components/schemas/License"
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          }
        }
      },
      "ChaptersResponse": {
        "type": "object",
        "required": [
          "data",
          "bundle",
          "license",
          "attribution"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Chapters"
          },
          "bundle": {
            "$ref": "#/components/schemas/Bundle"
          },
          "license": {
            "$ref": "#/components/schemas/License"
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "required": [
          "data",
          "bundle",
          "license",
          "attribution"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Health"
          },
          "bundle": {
            "$ref": "#/components/schemas/Bundle"
          },
          "license": {
            "$ref": "#/components/schemas/License"
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          }
        }
      }
    }
  }
}