{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:cede:schema:event-catalogue:v0",
  "title": "CEDE public event catalogue page, v0",
  "description": "One page of the pinned hazard event catalogue for a peril: the body of GET /events/{peril} (SPEC.md section 3.2). Every event is a solution exactly as the publisher recorded it, read from the same pinned snapshot the price and backtest paths read and selected by the same rules, so the events a result names can be inspected one by one. The response names the feed id, the vintage and the sha256 of the exact bytes it was read from, and states the selection it applied — a filter that is not published is a filter nobody can reproduce. Feeds are aggregate only (SPEC.md section 4, P10): a catalogue is public hazard data — time, hypocentre, magnitude — and never an individual-level exposure record.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "peril",
    "feed",
    "aggregation_level",
    "coverage",
    "selection",
    "counts",
    "page",
    "events",
    "notes"
  ],
  "properties": {
    "schema_version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
    "peril": {
      "type": "string",
      "minLength": 1,
      "description": "The peril asked for, as SPEC.md section 2's peril.code spells it."
    },
    "feed": { "$ref": "#/$defs/data_source" },
    "aggregation_level": { "$ref": "#/$defs/aggregation_level" },
    "coverage": { "$ref": "#/$defs/coverage" },
    "selection": {
      "type": "object",
      "additionalProperties": false,
      "description": "The filter this page applied, stated back in full. Anything the request did not name is defaulted to the snapshot's own coverage and listed in `defaulted`, so an unfiltered read is everything the record holds and never anything outside it.",
      "required": [
        "box",
        "minimum_magnitude",
        "max_focal_depth_km",
        "from",
        "to",
        "period_bounds",
        "defaulted",
        "defaults"
      ],
      "properties": {
        "box": { "$ref": "#/$defs/box" },
        "minimum_magnitude": {
          "type": "number",
          "description": "Events below this magnitude are not on the page. Never below the snapshot's own minimum: the events under that floor are missing from the extract, not from history."
        },
        "max_focal_depth_km": {
          "type": ["number", "null"],
          "minimum": 0,
          "description": "Depth limit applied, or null when the request named none. An event with no published depth is excluded whenever a limit is set, never assumed shallow."
        },
        "from": { "$ref": "#/$defs/timestamp" },
        "to": { "$ref": "#/$defs/timestamp" },
        "period_bounds": { "type": "string", "minLength": 1 },
        "defaulted": {
          "type": "array",
          "description": "Which parts of the selection the request did not name.",
          "items": {
            "type": "string",
            "enum": ["box", "from", "minimum_magnitude", "to"]
          }
        },
        "defaults": { "type": "string", "minLength": 1 }
      }
    },
    "counts": {
      "type": "object",
      "additionalProperties": false,
      "description": "The three numbers that say what was read, what survived the selection and what this page carries. A reader comparing against a backtest's `events_in_snapshot` and `events_in_box` is comparing the same quantities.",
      "required": ["events_in_snapshot", "selected", "returned"],
      "properties": {
        "events_in_snapshot": { "type": "integer", "minimum": 0 },
        "selected": { "type": "integer", "minimum": 0 },
        "returned": { "type": "integer", "minimum": 0 }
      }
    },
    "page": {
      "type": "object",
      "additionalProperties": false,
      "description": "Where this page sits in the selection, and the address of the next one.",
      "required": ["limit", "offset", "next"],
      "properties": {
        "limit": { "type": "integer", "minimum": 1 },
        "offset": { "type": "integer", "minimum": 0 },
        "next": {
          "type": ["string", "null"],
          "description": "A path to follow for the next page, or null on the last one. It pins the feed and the version explicitly even when the request did not, so paging can never mix two vintages."
        }
      }
    },
    "events": {
      "type": "array",
      "description": "The page, in time order, oldest first. Empty is a valid answer: a selection nothing falls into is a fact about the record, not an error.",
      "items": { "$ref": "#/$defs/event" }
    },
    "notes": {
      "type": "array",
      "minItems": 1,
      "items": { "type": "string", "minLength": 1 }
    }
  },
  "$defs": {
    "timestamp": {
      "type": "string",
      "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$",
      "description": "An instant in UTC, as the catalogue publishes it."
    },
    "event": {
      "type": "object",
      "additionalProperties": false,
      "description": "One catalogue solution, as published: nothing reordered, nothing rounded.",
      "required": [
        "event_id",
        "time",
        "latitude",
        "longitude",
        "depth_km",
        "magnitude",
        "magnitude_type",
        "footprint"
      ],
      "properties": {
        "event_id": {
          "type": "string",
          "minLength": 1,
          "description": "The publisher's own identifier for the solution — never one Cede mints."
        },
        "time": { "$ref": "#/$defs/timestamp" },
        "latitude": { "type": "number", "minimum": -90, "maximum": 90 },
        "longitude": { "type": "number", "minimum": -180, "maximum": 180 },
        "depth_km": {
          "type": ["number", "null"],
          "description": "Focal depth as published, or null where the catalogue records none."
        },
        "magnitude": { "type": "number" },
        "magnitude_type": {
          "type": ["string", "null"],
          "description": "The magnitude scale of this solution (mb, mw, mww, …). Mixed within a catalogue, which is why it is published per event rather than declared once."
        },
        "footprint": {
          "type": "string",
          "minLength": 1,
          "description": "Path to this event's footprint document."
        }
      }
    },
    "box": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "min_latitude",
        "max_latitude",
        "min_longitude",
        "max_longitude",
        "bounds"
      ],
      "properties": {
        "min_latitude": { "type": "number", "minimum": -90, "maximum": 90 },
        "max_latitude": { "type": "number", "minimum": -90, "maximum": 90 },
        "min_longitude": { "type": "number", "minimum": -180, "maximum": 180 },
        "max_longitude": { "type": "number", "minimum": -180, "maximum": 180 },
        "bounds": { "type": "string", "minLength": 1 }
      }
    },
    "aggregation_level": {
      "type": "object",
      "description": "The snapshot's declared aggregation level and what it means for SPEC.md section 4, P10. Events are served only from a level that cannot carry an individual-level record by construction.",
      "required": ["level", "p10"],
      "properties": {
        "level": { "type": "string", "minLength": 1 },
        "note": { "type": "string", "minLength": 1 },
        "p10": { "type": "string", "minLength": 1 }
      }
    },
    "coverage": {
      "type": "object",
      "description": "What the snapshot's manifest claims to hold, copied as committed. The catalogue is complete within these bounds and holds nothing outside them.",
      "required": ["period_of_record", "bounding_box", "minimum_magnitude", "statement"],
      "properties": {
        "period_of_record": {
          "type": "object",
          "required": ["start", "end"],
          "properties": {
            "start": { "type": "string", "minLength": 1 },
            "end": { "type": "string", "minLength": 1 },
            "years": { "type": "integer", "minimum": 0 }
          }
        },
        "bounding_box": {
          "type": "object",
          "required": [
            "min_latitude",
            "max_latitude",
            "min_longitude",
            "max_longitude"
          ],
          "properties": {
            "min_latitude": { "type": "number" },
            "max_latitude": { "type": "number" },
            "min_longitude": { "type": "number" },
            "max_longitude": { "type": "number" }
          }
        },
        "minimum_magnitude": { "type": "number" },
        "event_type": { "type": "string", "minLength": 1 },
        "magnitude_basis": { "type": "string", "minLength": 1 },
        "statement": { "type": "string", "minLength": 1 }
      }
    },
    "data_source": {
      "type": "object",
      "additionalProperties": false,
      "description": "The pinned snapshot this document was read from — the same shape a price or backtest assumption set names, so the two can be compared field by field.",
      "required": [
        "id",
        "name",
        "publisher",
        "kind",
        "version",
        "vintage",
        "sha256",
        "licence"
      ],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string", "minLength": 1 },
        "publisher": { "type": "string", "minLength": 1 },
        "kind": {
          "type": "string",
          "enum": [
            "quake_catalogue",
            "storm_track_set",
            "station_network",
            "footprint_provider",
            "outage_feed",
            "other"
          ]
        },
        "version": { "type": "string", "minLength": 1 },
        "vintage": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
        "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
        "query_url": { "type": "string", "minLength": 1 },
        "record_count": { "type": "integer", "minimum": 0 },
        "licence": { "type": "string", "minLength": 1 }
      }
    }
  }
}
