{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:cede:schema:hazard:v0",
  "title": "CEDE point hazard lookup, v0",
  "description": "The body of GET /hazard — what one pinned public snapshot says about one coordinate. SPEC.md section 3.2 makes hazard lookups part of Analyse and requires assumptions and data vintages to be exposed on every artifact, so the feed id, the vintage, the digest of the exact bytes and the limitations of the record are first-class members here rather than prose. The answer is a measurement over a fixed record: it carries no loading, no premium and no statement about risk appetite.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "peril",
    "point",
    "feed",
    "metrics",
    "assumptions",
    "disclosure"
  ],
  "properties": {
    "schema_version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
    "peril": {
      "type": "string",
      "minLength": 1,
      "description": "The peril asked about, from the risk object schema's peril_code vocabulary. Only the perils this build pins a feed for can be asked; the refusal for any other names them."
    },
    "point": {
      "type": "object",
      "additionalProperties": false,
      "required": ["latitude", "longitude"],
      "description": "The coordinate asked about, echoed as it was read.",
      "properties": {
        "latitude": { "type": "number", "minimum": -90, "maximum": 90 },
        "longitude": { "type": "number", "minimum": -180, "maximum": 180 }
      }
    },
    "feed": { "$ref": "#/$defs/feed" },
    "metrics": {
      "description": "What the snapshot holds for this point. The shape follows the snapshot: a catalogue extract answers with exceedance rates, a gridded daily series with return levels off its annual maxima. `basis` says which.",
      "oneOf": [
        { "$ref": "#/$defs/catalogue_metrics" },
        { "$ref": "#/$defs/series_metrics" }
      ]
    },
    "assumptions": { "$ref": "#/$defs/assumptions" },
    "disclosure": {
      "type": "string",
      "minLength": 1,
      "description": "The fixed sentence stating what this answer is and is not (SPEC.md section 4). A measurement over a public record, never a view of risk and never a price."
    }
  },
  "$defs": {
    "feed": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "version", "vintage", "kind", "sha256", "links"],
      "description": "The one pinned snapshot this answer was computed from. A lookup never merges two extracts: a number a reader cannot reproduce from a single pinned file is a number nobody can check.",
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "version": { "type": "string", "minLength": 1 },
        "vintage": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
        "kind": { "type": "string", "minLength": 1 },
        "sha256": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$",
          "description": "Digest of the exact bytes read, in full. The same value appears on the feed object and is re-derivable from the data document at links.data."
        },
        "links": {
          "type": "object",
          "additionalProperties": false,
          "required": ["self", "data"],
          "description": "API paths, relative to the deployment's base URL: the snapshot's manifest, and its bytes.",
          "properties": {
            "self": { "type": "string", "minLength": 1 },
            "data": { "type": "string", "minLength": 1 }
          }
        }
      }
    },
    "catalogue_metrics": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "basis",
        "cell",
        "minimum_magnitude",
        "events_in_cell",
        "exceedance"
      ],
      "properties": {
        "basis": { "const": "catalogue_exceedance" },
        "cell": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "min_latitude",
            "max_latitude",
            "min_longitude",
            "max_longitude",
            "half_width_degrees",
            "bounds"
          ],
          "description": "The area counted, axis-aligned in degrees around the point. Whole cell inside the snapshot's extract, or the lookup refuses.",
          "properties": {
            "min_latitude": { "type": "number" },
            "max_latitude": { "type": "number" },
            "min_longitude": { "type": "number" },
            "max_longitude": { "type": "number" },
            "half_width_degrees": { "type": "number", "exclusiveMinimum": 0 },
            "bounds": { "type": "string", "minLength": 1 }
          }
        },
        "minimum_magnitude": {
          "type": "number",
          "description": "The smallest magnitude the extract holds. Thresholds below it are absent rather than reported as zero: those events are missing from the snapshot, not from history."
        },
        "events_in_cell": { "type": "integer", "minimum": 0 },
        "largest_event": { "$ref": "#/$defs/event" },
        "exceedance": {
          "type": "array",
          "description": "One row per magnitude threshold at or above the extract's minimum, ascending.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "magnitude",
              "events",
              "occurrences",
              "annual_rate",
              "annual_occurrence_rate"
            ],
            "properties": {
              "magnitude": { "type": "number" },
              "events": {
                "type": "integer",
                "minimum": 0,
                "description": "Catalogue solutions at or above this magnitude in the cell, aftershocks included."
              },
              "occurrences": {
                "type": "integer",
                "minimum": 0,
                "description": "Those solutions grouped by the occurrence window in the assumptions, so a mainshock and its aftershocks read as one."
              },
              "annual_rate": { "type": "number", "minimum": 0 },
              "annual_occurrence_rate": { "type": "number", "minimum": 0 },
              "mean_return_period_years": {
                "type": "number",
                "exclusiveMinimum": 0,
                "description": "Period of record divided by occurrences. Absent when the cell holds none at this threshold — an unobserved event has no observed frequency, and reporting one would be inventing it."
              }
            }
          }
        }
      }
    },
    "series_metrics": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "basis",
        "measurement",
        "grid_point",
        "days_observed",
        "days_missing",
        "maximum_daily",
        "annual_maxima",
        "return_levels"
      ],
      "properties": {
        "basis": { "const": "daily_series_return_levels" },
        "measurement": {
          "type": "object",
          "additionalProperties": false,
          "required": ["variable", "unit", "resolution"],
          "properties": {
            "variable": { "type": "string", "minLength": 1 },
            "unit": { "type": "string", "minLength": 1 },
            "resolution": { "type": "string", "minLength": 1 }
          }
        },
        "grid_point": {
          "type": "object",
          "additionalProperties": false,
          "required": ["latitude", "longitude", "offset_degrees"],
          "description": "Where the snapshot actually measures. The value is the model cell's, not the requested address's, and the offset is how far apart the two are.",
          "properties": {
            "latitude": { "type": "number", "minimum": -90, "maximum": 90 },
            "longitude": { "type": "number", "minimum": -180, "maximum": 180 },
            "offset_degrees": {
              "type": "object",
              "additionalProperties": false,
              "required": ["latitude", "longitude"],
              "properties": {
                "latitude": { "type": "number" },
                "longitude": { "type": "number" }
              }
            }
          }
        },
        "days_observed": { "type": "integer", "minimum": 0 },
        "days_missing": {
          "type": "integer",
          "minimum": 0,
          "description": "Days inside the record the publisher did not report. A gap is counted and disclosed, never filled: a missing day read as zero is a day the index says was dry."
        },
        "maximum_daily": {
          "type": "object",
          "additionalProperties": false,
          "required": ["value", "date"],
          "properties": {
            "value": { "type": "number" },
            "date": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }
          }
        },
        "annual_maxima": {
          "type": "object",
          "additionalProperties": false,
          "required": ["years", "mean", "minimum", "maximum"],
          "properties": {
            "years": { "type": "integer", "minimum": 0 },
            "mean": { "type": "number" },
            "minimum": { "type": "number" },
            "maximum": { "type": "number" }
          }
        },
        "return_levels": {
          "type": "array",
          "description": "Levels read off the ranked annual maxima. A return period the record cannot reach is absent rather than extrapolated.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["return_period_years", "exceedance_probability", "level"],
            "properties": {
              "return_period_years": { "type": "integer", "exclusiveMinimum": 0 },
              "exceedance_probability": {
                "type": "number",
                "exclusiveMinimum": 0,
                "maximum": 1
              },
              "level": { "type": "number" }
            }
          }
        }
      }
    },
    "event": {
      "type": "object",
      "additionalProperties": false,
      "required": ["event_id", "time", "magnitude", "latitude", "longitude"],
      "description": "One catalogue solution, as published.",
      "properties": {
        "event_id": { "type": "string", "minLength": 1 },
        "time": { "type": "string", "minLength": 1 },
        "magnitude": { "type": "number" },
        "magnitude_type": { "type": "string", "minLength": 1 },
        "latitude": { "type": "number", "minimum": -90, "maximum": 90 },
        "longitude": { "type": "number", "minimum": -180, "maximum": 180 },
        "depth_km": { "type": "number" }
      }
    },
    "assumptions": {
      "type": "object",
      "additionalProperties": false,
      "required": ["data_sources", "period_of_record", "method", "limitations"],
      "description": "Everything the number rests on, stated in the response rather than in a footnote (SPEC.md section 3.2).",
      "properties": {
        "data_sources": {
          "type": "array",
          "minItems": 1,
          "maxItems": 1,
          "items": { "$ref": "#/$defs/data_source" }
        },
        "period_of_record": {
          "type": "object",
          "additionalProperties": false,
          "required": ["start", "end", "years"],
          "properties": {
            "start": { "type": "string", "minLength": 1 },
            "end": { "type": "string", "minLength": 1 },
            "years": { "type": "number", "exclusiveMinimum": 0 }
          }
        },
        "method": {
          "type": "object",
          "additionalProperties": false,
          "required": ["name", "description"],
          "properties": {
            "name": { "type": "string", "minLength": 1 },
            "description": { "type": "string", "minLength": 1 },
            "occurrence_window": {
              "type": "string",
              "minLength": 1,
              "description": "ISO 8601 duration inside which a mainshock and its aftershocks count as one occurrence. Present for a catalogue lookup, which is the only basis that clusters."
            }
          }
        },
        "limitations": {
          "type": "array",
          "minItems": 1,
          "description": "What this answer cannot tell you, in the response itself. A hazard number whose limitations live in the documentation is a number that will be read without them.",
          "items": { "type": "string", "minLength": 1 }
        }
      }
    },
    "data_source": {
      "type": "object",
      "required": ["id", "version", "vintage", "sha256"],
      "description": "The pinned snapshot, in the same shape a technical price and a backtest name it in their assumption sets, so one reader's lineage check works on all three.",
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string", "minLength": 1 },
        "publisher": { "type": "string", "minLength": 1 },
        "kind": { "type": "string", "minLength": 1 },
        "version": { "type": "string", "minLength": 1 },
        "vintage": { "type": "string", "minLength": 1 },
        "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
        "licence": { "type": "string", "minLength": 1 },
        "query_url": { "type": "string", "minLength": 1 },
        "record_count": { "type": "integer", "minimum": 0 }
      }
    }
  }
}
