{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:cede:schema:service-info:v0",
  "title": "CEDE service info, v0",
  "description": "The body of GET /health — one of the two routes the API serves without a key (the other is POST /signup, which hands the first key out), existing so a deploy, a load balancer and the post-deploy check (PROD-09) can ask whether the service is serving without holding a key. It publishes no product data and asserts nothing beyond the fact that a process answered: it says which build is running, which verbs and registry surfaces are reachable, and at which addresses this build's own catalogues of feeds and builder primitives can be read. Nothing more: it is not the route table, which is published separately as api-contract.v0.json.",
  "type": "object",
  "additionalProperties": false,
  "required": ["schema_version", "service", "version", "status", "verbs"],
  "properties": {
    "schema_version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
      "description": "Version of this service-info schema the payload conforms to."
    },
    "service": {
      "const": "cede-api",
      "description": "Which service answered. A constant, so a misrouted health check fails loudly instead of passing against something else."
    },
    "version": {
      "type": "string",
      "minLength": 1,
      "description": "Version of the running build. The rollback drill (PROD-09) reads this before and after to prove the old build is the one serving."
    },
    "status": {
      "const": "ok",
      "description": "A constant: the route answers 200 with this body or it does not answer. Degraded states get their own status values by schema version bump, never a silent widening of this one."
    },
    "verbs": {
      "type": "array",
      "description": "The verbs of SPEC.md section 3 this build serves. Measurement, not marketing: it lists what is reachable now, which is how a client discovers that Analyse is not yet here.",
      "items": {
        "type": "string",
        "enum": [
          "ingest",
          "analyse",
          "price",
          "structure",
          "backtest",
          "monitor",
          "package"
        ]
      },
      "uniqueItems": true
    },
    "registry": {
      "type": "array",
      "description": "The registry surfaces of SPEC.md section 2A this build serves. Optional rather than required, because a build that predates the registry published a valid service-info document without it and adding a required member to a published schema would retroactively invalidate it. Separate from verbs because the registry is a second first-class primitive alongside the seven verbs, not one of them. Measurement, not marketing: it lists what is reachable now.",
      "items": {
        "type": "string",
        "enum": ["model_builder", "model_run", "model_registry"]
      },
      "uniqueItems": true
    },
    "discovery": {
      "type": "array",
      "description": "The catalogue routes this build serves: the addresses that answer \"what does this build hold\" rather than \"what has this account done\". Each takes no path parameter, returns the same answer for every caller and needs nothing created first, so a client can walk from this document straight to the pinned data and the model builder's vocabulary without reading a documentation page first. Optional rather than required, for the same reason registry is: a build that predates it published a valid service-info document without it. This is not the route table — the whole surface, route by route, is published as api-contract.v0.json — and the verbs above are named rather than addressed because a verb's entry point takes an object the caller must create first.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["route", "serves"],
        "properties": {
          "route": {
            "type": "string",
            "pattern": "^/[a-z0-9-]+$",
            "description": "The path, relative to the API base URL, answering GET. It needs a key like every route but the two that answer without one, so reading this document is not itself a way past authentication."
          },
          "serves": {
            "type": "string",
            "minLength": 1,
            "description": "What that route lists, in one sentence. Measurement, not marketing: it describes what is there to be read, and asserts nothing about it."
          }
        }
      },
      "uniqueItems": true
    }
  }
}
