{
  "openapi": "3.1.0",
  "info": {
    "title": "VisionSetil public API",
    "version": "2.0.0",
    "summary": "Educational Iberian mycology catalog and gated Identify trial.",
    "description": "VisionSetil public REST API for educational mushroom study on the Iberian Peninsula.\n\nThere is **no GraphQL** API.\n\n`identify_available` is false until the quality gate says otherwise. Classify may return `mode: blocked`.\n\nOrientation only: never forage permission, never consumption permission, never “safe to eat”.\n\nRate limits: RFC 9237 `RateLimit` / `RateLimit-Policy` plus `Retry-After` on 429. Defaults: 60 req/min general, 20 req/min classify, 10 req/min auth.\n\nHuman + agent docs: https://visionsetil.com/docs · machine index: https://visionsetil.com/llms.txt",
    "contact": {
      "name": "VisionSetil editorial",
      "email": "privacidad@visionsetil.com",
      "url": "https://visionsetil.com/contact"
    },
    "license": {
      "name": "Catalog data CC BY 4.0; product orientation-only",
      "url": "https://visionsetil.com/datos/LICENCIA.txt"
    }
  },
  "servers": [
    {
      "url": "https://visionsetil.com",
      "description": "Production origin (Caddy strips /api for the backend)"
    }
  ],
  "tags": [
    {
      "name": "health",
      "description": "Liveness and Identify readiness (public)."
    },
    {
      "name": "species",
      "description": "Educational catalog. Risk labels are study metadata, not edibility permission."
    },
    {
      "name": "classify",
      "description": "Multi-view Identify trial. First-party app via /api/classify (proxy key). External agents should prefer GET /api/species. May be blocked."
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "health"
        ],
        "summary": "Liveness probe",
        "description": "Process is up. Rate-limit exempt. Use this to see if the API origin is reachable.",
        "responses": {
          "200": {
            "description": "Service is alive.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/healthz": {
      "get": {
        "operationId": "getHealthz",
        "tags": [
          "health"
        ],
        "summary": "Alias of liveness",
        "description": "Same as /api/health. Rate-limit exempt.",
        "responses": {
          "200": {
            "description": "Service is alive.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/api/readyz": {
      "get": {
        "operationId": "getReadyz",
        "tags": [
          "health"
        ],
        "summary": "Readiness + Identify gate (honest)",
        "description": "Database/models readiness plus nested `quality_gate` and `identify_available`. A 200 does not mean Identify is a production classifier. Agents must read `identify_available` (currently false) and never treat output as permission to eat.",
        "responses": {
          "200": {
            "description": "Readiness payload. Identify may still be blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Readyz"
                }
              }
            }
          },
          "503": {
            "description": "Not ready (DB or models).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Readyz"
                }
              }
            }
          }
        }
      }
    },
    "/api/species": {
      "get": {
        "operationId": "listSpecies",
        "tags": [
          "species"
        ],
        "summary": "Search the educational catalog",
        "description": "Public list of Iberian taxa with educational risk labels. Never interpret labels as forage or consumption permission. Default locale es.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Scientific or common-name search string."
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en",
                "ca",
                "eu"
              ]
            },
            "description": "UI locale. Invalid values return 400."
          },
          {
            "name": "risk_level",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Educational risk filter (study metadata only)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 2000,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of catalog rows.",
            "headers": {
              "RateLimit": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 dictionary, e.g. limit=60, remaining=41, reset=12. Always present on non-exempt API responses."
                }
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 policy, e.g. 60;w=60 (quota per window seconds)."
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpeciesList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid locale or query.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Agents should honour Retry-After and RateLimit.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "description": "Seconds to wait after HTTP 429. Omitted on success."
                }
              },
              "RateLimit": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 dictionary, e.g. limit=60, remaining=41, reset=12. Always present on non-exempt API responses."
                }
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 policy, e.g. 60;w=60 (quota per window seconds)."
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "status",
                    "retry_after_seconds"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "rate_limit_exceeded"
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer",
                      "example": 429
                    },
                    "retry_after_seconds": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "bucket": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/species/poisonous": {
      "get": {
        "operationId": "listPoisonousSpecies",
        "tags": [
          "species"
        ],
        "summary": "Educational high-risk list",
        "description": "Taxa labeled poisonous or deadly for study. Presence on this list is not a forage guide and never a consumption verdict.",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en",
                "ca",
                "eu"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Array of educational risk summaries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Agents should honour Retry-After and RateLimit.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "description": "Seconds to wait after HTTP 429. Omitted on success."
                }
              },
              "RateLimit": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 dictionary, e.g. limit=60, remaining=41, reset=12. Always present on non-exempt API responses."
                }
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 policy, e.g. 60;w=60 (quota per window seconds)."
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "status",
                    "retry_after_seconds"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "rate_limit_exceeded"
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer",
                      "example": 429
                    },
                    "retry_after_seconds": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "bucket": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/species/lookup": {
      "get": {
        "operationId": "lookupSpeciesByScientificName",
        "tags": [
          "species"
        ],
        "summary": "Lookup one taxon by scientific name",
        "description": "Public educational detail for one Latin binomial. Risk labels are study metadata, never consumption permission.",
        "parameters": [
          {
            "name": "scientific_name",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Latin binomial, e.g. Amanita phalloides."
          },
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en",
                "ca",
                "eu"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Localized educational detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpeciesDetail"
                }
              }
            }
          },
          "404": {
            "description": "Unknown scientific name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Agents should honour Retry-After and RateLimit.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "description": "Seconds to wait after HTTP 429. Omitted on success."
                }
              },
              "RateLimit": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 dictionary, e.g. limit=60, remaining=41, reset=12. Always present on non-exempt API responses."
                }
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 policy, e.g. 60;w=60 (quota per window seconds)."
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "status",
                    "retry_after_seconds"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "rate_limit_exceeded"
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer",
                      "example": 429
                    },
                    "retry_after_seconds": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "bucket": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/species/{slug}": {
      "get": {
        "operationId": "getSpeciesBySlug",
        "tags": [
          "species"
        ],
        "summary": "Get one catalog card by slug",
        "description": "Same facts as /enciclopedia/{slug}. Educational only.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            },
            "description": "URL slug, e.g. amanita-phalloides."
          },
          {
            "name": "locale",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en",
                "ca",
                "eu"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Localized educational detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpeciesDetail"
                }
              }
            }
          },
          "400": {
            "description": "Invalid slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Agents should honour Retry-After and RateLimit.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "description": "Seconds to wait after HTTP 429. Omitted on success."
                }
              },
              "RateLimit": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 dictionary, e.g. limit=60, remaining=41, reset=12. Always present on non-exempt API responses."
                }
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 policy, e.g. 60;w=60 (quota per window seconds)."
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "status",
                    "retry_after_seconds"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "rate_limit_exceeded"
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer",
                      "example": 429
                    },
                    "retry_after_seconds": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "bucket": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/classify": {
      "post": {
        "operationId": "classifyImages",
        "tags": [
          "classify"
        ],
        "summary": "Identify trial — orientation only, may abstain",
        "description": "Upload 1–4 field photos (gills/pores, profile, base/volva, habitat). The product quality gate currently keeps Identify in trial: responses may use `mode=blocked` and `identify_available` on /api/readyz is false. Never interpret predictions as species certainty or permission to collect or eat. First-party web traffic is proxied with a server-side key. External callers without a key receive 401. Prefer GET /api/species for agent facts.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "images"
                ],
                "properties": {
                  "images": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "minItems": 1,
                    "maxItems": 4,
                    "description": "Field photos. Prefer gills, profile, base/volva."
                  },
                  "view_types": {
                    "type": "string",
                    "description": "Optional comma-separated views, e.g. 'gills,front,habitat,detail'."
                  },
                  "locale": {
                    "type": "string",
                    "enum": [
                      "es",
                      "en",
                      "ca",
                      "eu"
                    ]
                  },
                  "country": {
                    "type": "string"
                  },
                  "habitat": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Honesty contract: always includes mode, quality_gate, locale. decision=rejected means abstain. Never consumption permission.",
            "headers": {
              "RateLimit": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 dictionary, e.g. limit=60, remaining=41, reset=12. Always present on non-exempt API responses."
                }
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 policy, e.g. 60;w=60 (quota per window seconds)."
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassifyResult"
                }
              }
            }
          },
          "400": {
            "description": "Missing images, invalid locale, or bad view_types.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "API key required for non-proxied callers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Agents should honour Retry-After and RateLimit.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "description": "Seconds to wait after HTTP 429. Omitted on success."
                }
              },
              "RateLimit": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 dictionary, e.g. limit=60, remaining=41, reset=12. Always present on non-exempt API responses."
                }
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 policy, e.g. 60;w=60 (quota per window seconds)."
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "status",
                    "retry_after_seconds"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "rate_limit_exceeded"
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer",
                      "example": 429
                    },
                    "retry_after_seconds": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "bucket": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/classify": {
      "post": {
        "operationId": "classifyImagesV1",
        "tags": [
          "classify"
        ],
        "summary": "Versioned compatibility alias for the Identify trial",
        "description": "The stable v1 alias of POST /api/classify. It preserves the existing accepted/rejected response and honesty fields.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/ClassifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Compatible v1 response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassifyResult"
                }
              }
            }
          },
          "400": {
            "description": "Missing images, invalid locale, or bad view_types.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "API key required for non-proxied callers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Agents should honour Retry-After and RateLimit.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "description": "Seconds to wait after HTTP 429. Omitted on success."
                }
              },
              "RateLimit": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 dictionary, e.g. limit=60, remaining=41, reset=12. Always present on non-exempt API responses."
                }
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 policy, e.g. 60;w=60 (quota per window seconds)."
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "status",
                    "retry_after_seconds"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "rate_limit_exceeded"
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer",
                      "example": 429
                    },
                    "retry_after_seconds": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "bucket": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v2/classify": {
      "post": {
        "operationId": "classifyImagesV2",
        "tags": [
          "classify"
        ],
        "summary": "Safety-first hierarchical Identify contract",
        "description": "Returns an explicit accepted, abstained, or blocked decision and the safest supported resolution. It never grants consumption permission.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/ClassifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Explicit v2 decision contract.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassifyV2Result"
                }
              }
            }
          },
          "400": {
            "description": "Missing images, invalid locale, or bad view_types.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "API key required for non-proxied callers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Agents should honour Retry-After and RateLimit.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "description": "Seconds to wait after HTTP 429. Omitted on success."
                }
              },
              "RateLimit": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 dictionary, e.g. limit=60, remaining=41, reset=12. Always present on non-exempt API responses."
                }
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string",
                  "description": "RFC 9237 policy, e.g. 60;w=60 (quota per window seconds)."
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "status",
                    "retry_after_seconds"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "rate_limit_exceeded"
                    },
                    "message": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer",
                      "example": 429
                    },
                    "retry_after_seconds": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "bucket": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Required for POST /api/classify from outside the first-party Caddy proxy. Public GET catalog and health do not need a key."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          },
          "supported": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Readyz": {
        "type": "object",
        "properties": {
          "ready": {
            "type": "boolean"
          },
          "identify_available": {
            "type": "boolean",
            "description": "Product unlock for Identify. Currently false."
          },
          "quality_gate": {
            "$ref": "#/components/schemas/QualityGate"
          }
        }
      },
      "QualityGate": {
        "type": "object",
        "properties": {
          "species_id_allowed": {
            "type": "boolean"
          },
          "metrics_acceptable": {
            "type": "boolean"
          },
          "block_enabled": {
            "type": "boolean"
          },
          "verdict": {
            "type": "string"
          },
          "reason_code": {
            "type": "string"
          },
          "test_map_at_3": {
            "type": [
              "number",
              "null"
            ]
          },
          "safety_recall_deadly": {
            "type": [
              "number",
              "null"
            ]
          },
          "min_map_at_3": {
            "type": "number",
            "example": 0.2
          },
          "min_deadly_recall": {
            "type": "number",
            "example": 0.9
          },
          "calibration_ece": {
            "type": [
              "number",
              "null"
            ]
          },
          "max_calibration_ece": {
            "type": "number",
            "example": 0.05
          },
          "contract_version": {
            "type": "string",
            "example": "2026-08-23"
          }
        }
      },
      "ClassifyRequest": {
        "type": "object",
        "required": [
          "images"
        ],
        "properties": {
          "images": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "binary"
            },
            "minItems": 1,
            "maxItems": 10
          },
          "view_types": {
            "type": "string"
          },
          "locale": {
            "type": "string",
            "enum": [
              "es",
              "en",
              "ca",
              "eu"
            ]
          },
          "country": {
            "type": "string"
          },
          "habitat": {
            "type": "string"
          },
          "persist": {
            "type": "boolean",
            "default": true
          },
          "train_opt_in": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "SpeciesList": {
        "type": "object",
        "required": [
          "items",
          "total",
          "limit",
          "offset"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "total": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "catalog_version": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          }
        }
      },
      "SpeciesDetail": {
        "type": "object",
        "additionalProperties": true,
        "description": "Educational card: scientific name, common names, family, risk label, lookalikes. Risk is study metadata, not permission to eat."
      },
      "ClassifyResult": {
        "type": "object",
        "required": [
          "request_id",
          "decision",
          "mode",
          "quality_gate",
          "locale"
        ],
        "properties": {
          "request_id": {
            "type": "string"
          },
          "decision": {
            "type": "string",
            "enum": [
              "accepted",
              "rejected"
            ]
          },
          "mode": {
            "type": "string",
            "enum": [
              "real",
              "mock",
              "blocked"
            ]
          },
          "locale": {
            "type": "string"
          },
          "quality_gate": {
            "$ref": "#/components/schemas/QualityGate"
          },
          "predictions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "orientative_predictions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "rejection_reason": {
            "type": "string",
            "nullable": true
          },
          "safety_level": {
            "type": "string"
          },
          "recommend_human_review": {
            "type": "boolean"
          },
          "missing_evidence": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "dangerous_lookalikes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "final_warning": {
            "type": "string"
          }
        }
      },
      "ClassifyV2Result": {
        "type": "object",
        "required": [
          "request_id",
          "decision",
          "resolution",
          "candidates",
          "evidence",
          "missing_views",
          "warnings",
          "quality_gate",
          "calibration",
          "locale"
        ],
        "properties": {
          "request_id": {
            "type": "string"
          },
          "decision": {
            "type": "string",
            "enum": [
              "accepted",
              "abstained",
              "blocked"
            ]
          },
          "resolution": {
            "type": "string",
            "enum": [
              "species",
              "genus",
              "family",
              "none"
            ]
          },
          "candidates": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "evidence": {
            "type": "object",
            "required": [
              "views_available",
              "dangerous_lookalikes",
              "human_review_recommended"
            ],
            "properties": {
              "views_available": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "dangerous_lookalikes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "human_review_recommended": {
                "type": "boolean"
              }
            }
          },
          "missing_views": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "model_version": {
            "type": [
              "string",
              "null"
            ]
          },
          "quality_gate": {
            "$ref": "#/components/schemas/QualityGate"
          },
          "calibration": {
            "type": "object",
            "required": [
              "ece",
              "max_ece",
              "status"
            ],
            "properties": {
              "ece": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "max_ece": {
                "type": "number",
                "example": 0.05
              },
              "status": {
                "type": "string",
                "enum": [
                  "passed",
                  "failed",
                  "unavailable"
                ]
              }
            }
          },
          "locale": {
            "type": "string"
          }
        }
      }
    }
  }
}
