{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:cede:schema:backtest-response:v0",
  "title": "CEDE backtest artifact, v0",
  "description": "The result of the Backtest verb (SPEC.md section 3.5): year-by-year triggered payouts, loss statistics, a burning-cost view, and a data-coverage disclosure for years where the source data is thin — disclosed, never interpolated silently. SPEC.md section 3.5 and section 4 are normative for the semantics; this file encodes them as constraints, and where the two disagree SPEC.md wins and this file has a bug. Two constraints carry SPEC.md section 3.5's out-of-scope clause directly: verb is the constant 'backtest' and disclosure is a fixed sentence stating that the document is descriptive history and not a projection of future returns, so no revision of this schema can quietly turn a backtest into a performance promise.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "run_id",
    "verb",
    "model",
    "subject",
    "window",
    "years",
    "statistics",
    "data_coverage",
    "assumptions",
    "disclosure"
  ],
  "properties": {
    "schema_version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
      "description": "Version of this backtest schema the payload conforms to."
    },
    "id": {
      "$ref": "#/$defs/uuid",
      "description": "Server-assigned identity of a persisted backtest, the id in GET /backtests/{id}. Absent from an artifact computed without being stored — the run_id, which is content-addressed, identifies the computation itself."
    },
    "created_at": {
      "$ref": "#/$defs/timestamp",
      "description": "When this artifact was persisted. Absent from an unpersisted one; never an input to the run, which carries no clock (SPEC.md section 2A)."
    },
    "links": {
      "type": "object",
      "additionalProperties": false,
      "description": "Where this artifact and the structure it describes are read.",
      "properties": {
        "self": { "type": "string", "minLength": 1 },
        "structure": { "type": "string", "minLength": 1 }
      }
    },
    "run_id": {
      "type": "string",
      "pattern": "^backtest-v0:[0-9a-f]{16}$",
      "description": "Content-addressed identity of the run: the same structure, window, model version and pinned archive produce the same id. SPEC.md section 2A's byte-for-byte replay includes the identifier a result is filed under."
    },
    "verb": {
      "const": "backtest",
      "description": "Which of the seven verbs (SPEC.md section 3) produced this document."
    },
    "model": { "$ref": "#/$defs/model_label" },
    "subject": {
      "type": "object",
      "additionalProperties": false,
      "required": ["peril", "trigger_type", "index", "limit", "attachment"],
      "description": "What was backtested. structure_id and object_id are absent for a structure that has not been persisted.",
      "properties": {
        "structure_id": { "type": "string", "minLength": 1 },
        "object_id": { "type": "string", "minLength": 1 },
        "peril": { "type": "string", "minLength": 1 },
        "trigger_type": { "type": "string", "minLength": 1 },
        "index": { "$ref": "#/$defs/index_label" },
        "limit": { "$ref": "#/$defs/money" },
        "attachment": {
          "type": "object",
          "additionalProperties": false,
          "required": ["value"],
          "description": "Where the index first pays, in the index's own unit — read from the payout function, not from prose.",
          "properties": {
            "value": { "type": "number" },
            "unit": { "type": "string" }
          }
        }
      }
    },
    "window": {
      "type": "object",
      "additionalProperties": false,
      "required": ["from_year", "to_year", "years", "basis"],
      "description": "The window replayed. The default is the pinned archive's full period of record (SPEC.md section 3.5: the full forty-year archive); a narrower window is the caller's, stated here so a burning cost is never read over a period other than the one it was computed on.",
      "properties": {
        "from_year": { "$ref": "#/$defs/year" },
        "to_year": { "$ref": "#/$defs/year" },
        "years": { "type": "integer", "minimum": 1 },
        "basis": { "type": "string", "minLength": 1 }
      }
    },
    "years": {
      "type": "array",
      "minItems": 1,
      "description": "One entry per calendar year in the window, zero-payout years included and never omitted: a burning cost read over the triggering years alone is a different and much larger number. A year is shaped like the thing that was replayed: an event catalogue gives year_row (occurrences, magnitudes), a station-measured daily series gives index_year_row (a level, the window it was measured in, the days the publisher reported). One or the other, never a blend — see ADR-0028.",
      "items": {
        "oneOf": [
          { "$ref": "#/$defs/year_row" },
          { "$ref": "#/$defs/index_year_row" }
        ]
      }
    },
    "statistics": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "years",
        "triggering_years",
        "trigger_frequency",
        "return_period_years",
        "total_payout",
        "largest_annual_payout",
        "burning_cost",
        "payout_ratio",
        "limit"
      ],
      "description": "Loss statistics and the burning-cost view SPEC.md section 3.5 names. Every one of them is a count or an average over the window above — a description of what happened, never a rate to extrapolate.",
      "properties": {
        "years": { "type": "integer", "minimum": 1 },
        "triggering_years": { "type": "integer", "minimum": 0 },
        "trigger_frequency": { "$ref": "#/$defs/ratio" },
        "return_period_years": {
          "type": ["number", "null"],
          "minimum": 0,
          "description": "Years in the window divided by the number of triggering years. Null when no year in the window triggered: there is no observed return period, and stating one would be a fitted number this model does not fit."
        },
        "total_payout": { "$ref": "#/$defs/money" },
        "largest_annual_payout": { "$ref": "#/$defs/money" },
        "burning_cost": {
          "type": "object",
          "additionalProperties": false,
          "required": ["annual_payout", "burn_rate", "basis"],
          "properties": {
            "annual_payout": { "$ref": "#/$defs/money" },
            "burn_rate": { "$ref": "#/$defs/ratio" },
            "basis": { "type": "string", "minLength": 1 }
          }
        },
        "payout_ratio": {
          "type": "object",
          "additionalProperties": false,
          "required": ["mean", "standard_deviation", "maximum"],
          "properties": {
            "mean": { "$ref": "#/$defs/ratio" },
            "standard_deviation": { "type": "number", "minimum": 0 },
            "maximum": { "$ref": "#/$defs/ratio" }
          }
        },
        "limit": { "$ref": "#/$defs/money" }
      }
    },
    "data_coverage": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "archive",
        "years_requested",
        "years_with_records",
        "thin_years",
        "interpolation"
      ],
      "description": "SPEC.md section 3.5's disclosure for years where the source data is thin. thin_years is a list of facts about the pinned snapshot, and it is always present — empty means every year in the window carried records, which is itself the disclosure.",
      "properties": {
        "archive": {
          "type": "object",
          "additionalProperties": false,
          "required": ["feed", "version", "period_of_record"],
          "properties": {
            "feed": { "type": "string", "minLength": 1 },
            "version": { "type": "string", "minLength": 1 },
            "period_of_record": {
              "type": "object",
              "additionalProperties": false,
              "required": ["start", "end"],
              "properties": {
                "start": { "$ref": "#/$defs/timestamp" },
                "end": { "$ref": "#/$defs/timestamp" }
              }
            }
          }
        },
        "years_requested": { "type": "integer", "minimum": 1 },
        "years_with_records": { "type": "integer", "minimum": 0 },
        "thin_years": {
          "type": "array",
          "description": "Thinness is a fact about the pinned archive, stated in the archive's own terms: a catalogue year with no record inside the box, or a series year the publisher did not report in full.",
          "items": {
            "oneOf": [
              {
                "type": "object",
                "additionalProperties": false,
                "required": ["year", "catalogue_records_in_box", "note"],
                "properties": {
                  "year": { "$ref": "#/$defs/year" },
                  "catalogue_records_in_box": { "type": "integer", "minimum": 0 },
                  "note": { "type": "string", "minLength": 1 }
                }
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": ["year", "days_reported", "days_in_year", "note"],
                "properties": {
                  "year": { "$ref": "#/$defs/year" },
                  "days_reported": { "type": "integer", "minimum": 0 },
                  "days_in_year": { "type": "integer", "minimum": 1 },
                  "note": { "type": "string", "minLength": 1 }
                }
              }
            ]
          }
        },
        "interpolation": {
          "type": "string",
          "minLength": 1,
          "description": "What was filled in. SPEC.md section 3.5 allows exactly one answer, and this build states it: nothing."
        }
      }
    },
    "assumptions": {
      "type": "object",
      "additionalProperties": false,
      "required": ["data_sources", "method", "event_selection", "limitations"],
      "description": "The assumptions and data vintages SPEC.md section 3.5 requires to be exposed, as on a price. A backtest without them is a number with no way to check it.",
      "properties": {
        "data_sources": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/data_source" }
        },
        "method": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "name",
            "family",
            "description",
            "occurrence_basis",
            "clustering",
            "reinstatements_considered",
            "ratio_precision"
          ],
          "properties": {
            "name": { "type": "string", "minLength": 1 },
            "family": { "type": "string", "minLength": 1 },
            "description": { "type": "string", "minLength": 1 },
            "occurrence_basis": { "type": "string", "minLength": 1 },
            "clustering": {
              "type": "object",
              "additionalProperties": false,
              "required": ["rule", "window"],
              "properties": {
                "rule": { "type": "string", "minLength": 1 },
                "window": { "type": "string", "minLength": 1 }
              }
            },
            "reinstatements_considered": { "type": "boolean" },
            "ratio_precision": { "type": "string", "minLength": 1 }
          }
        },
        "payout_function": { "$ref": "#/$defs/payout_function" },
        "event_selection": {
          "description": "What the replay selected, in the shape of the archive it read: an event catalogue exposes the event set, a station-measured daily series exposes the measurement, the transform and the window each year was measured in (ADR-0028).",
          "oneOf": [
            { "$ref": "#/$defs/event_selection" },
            { "$ref": "#/$defs/index_selection" }
          ]
        },
        "limitations": {
          "type": "array",
          "minItems": 1,
          "items": { "type": "string", "minLength": 1 }
        }
      }
    },
    "reproduction": { "$ref": "#/$defs/reproduction" },
    "disclosure": {
      "const": "Descriptive history: what this structure would have paid against a pinned public catalogue over the window stated in this response, on the assumptions stated with it. It is not a projection, a forecast or an expectation of future payouts, and no part of it may be presented as one.",
      "description": "SPEC.md section 3.5, out of scope: a backtest is descriptive history, not a performance promise. A constant, so it cannot be omitted, softened or varied per response."
    }
  },
  "allOf": [
    {
      "description": "The reproduction surface of 0.2.0 (CEDE-126): from that version on, an artifact carries the payout function it evaluated and the ordered steps that produced every number in it. It is required of a document that declares 0.2.0 or any additive minor after it, and optional of the 0.1.0 documents a consumer already holds, because this schema file is a line and not a version — an additive minor must not retroactively invalidate what was published under it. The enum grows by one entry with each such minor; a version that dropped out of it would be a version that had dropped the surface.",
      "if": {
        "properties": { "schema_version": { "enum": ["0.2.0", "0.3.0"] } },
        "required": ["schema_version"]
      },
      "then": {
        "required": ["reproduction"],
        "properties": {
          "assumptions": { "required": ["payout_function"] }
        }
      }
    },
    {
      "description": "The selection breakdown of 0.3.0 (CEDE-252): from that version on, a catalogue-shaped artifact states what every clause of the selection rule removed and lists the records the depth clause removed, so a reader can tell a depth threshold that never bit from a depth threshold that was never applied. Conditioned on the catalogue shape — events_in_box is required of event_selection and unknown to index_selection — because a station-measured index has no focal depth to disclose. Optional of the 0.1.0 and 0.2.0 documents a consumer already holds, on the same terms as the clause above.",
      "if": {
        "required": ["schema_version", "assumptions"],
        "properties": {
          "schema_version": { "enum": ["0.3.0"] },
          "assumptions": {
            "required": ["event_selection"],
            "properties": {
              "event_selection": { "required": ["events_in_box"] }
            }
          }
        }
      },
      "then": {
        "properties": {
          "assumptions": {
            "properties": {
              "event_selection": {
                "required": ["clauses", "depth_exclusions"]
              }
            }
          }
        }
      }
    }
  ],
  "$defs": {
    "payout_function": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "type",
        "semantics",
        "unit",
        "points",
        "maximum_payout_ratio",
        "attachment_level",
        "evaluation"
      ],
      "description": "The payout function that was evaluated, with the rule for reading it. CEDE-126: an artifact that states the points and the answer but not the rule cannot be checked — a level of 6.7 against points at 6.5 and 7.0 is 0.5 read as a flat step and 0.7 read as an interpolation, and only one of them is what ran. evaluation is that rule in words, and it is the constant the evaluator implements rather than a description written beside it. CEDE-125: semantics is the same statement for a client, which cannot branch on a paragraph of English.",
      "properties": {
        "type": { "type": "string", "minLength": 1 },
        "semantics": {
          "enum": ["flat_steps", "linear_interpolation"],
          "description": "Which reading of the points above is the one that ran, as a constant rather than as prose: flat_steps holds the lower point's ratio all the way to the next point and never produces a ratio between two points' ratios; linear_interpolation rises straight-line from the first point to the last and produces every ratio between them. One value per payout function type, so a client tells a staircase from a ramp with an equality check."
        },
        "unit": {
          "type": "string",
          "description": "The unit the points and the attachment are stated in — the index's own unit, which is not always the measurement's."
        },
        "points": {
          "type": "array",
          "minItems": 1,
          "description": "Every point of the function, ascending by level. The full set, never a summary: a point omitted here is a payout a reader cannot reproduce.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["level", "payout_ratio"],
            "properties": {
              "level": { "type": "number" },
              "payout_ratio": { "$ref": "#/$defs/ratio" }
            }
          }
        },
        "maximum_payout_ratio": { "$ref": "#/$defs/ratio" },
        "attachment_level": {
          "type": "number",
          "description": "The level of the first point: where the structure first pays anything."
        },
        "evaluation": { "type": "string", "minLength": 1 }
      }
    },
    "payout_point": {
      "type": "object",
      "additionalProperties": false,
      "required": ["level", "payout_ratio"],
      "description": "The point of the payout function the index level landed on. Stated rather than left to be re-derived from the ratio, which is impossible whenever two points share one.",
      "properties": {
        "level": { "type": "number" },
        "payout_ratio": { "$ref": "#/$defs/ratio" }
      }
    },
    "reproduction": {
      "type": "object",
      "additionalProperties": false,
      "required": ["statement", "steps"],
      "description": "How to recompute every number in this artifact (CEDE-126). The steps are the model's own, in the order it runs them, each naming the fields it reads and the fields it wrote — so a reader holding the artifact and the pinned snapshot can walk from the publisher's records to the burning cost without asking anyone what the engine did in between.",
      "properties": {
        "statement": { "type": "string", "minLength": 1 },
        "steps": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["step", "name", "rule", "reads", "writes"],
            "properties": {
              "step": { "type": "integer", "minimum": 1 },
              "name": { "type": "string", "minLength": 1 },
              "rule": { "type": "string", "minLength": 1 },
              "reads": {
                "type": "array",
                "items": { "type": "string", "minLength": 1 }
              },
              "writes": {
                "type": "array",
                "minItems": 1,
                "items": { "type": "string", "minLength": 1 }
              }
            }
          }
        },
        "worked_example": {
          "type": "object",
          "additionalProperties": false,
          "required": ["year", "index_level", "payout_ratio", "payout", "narrative"],
          "description": "One payout of this run, done out loud: the first triggering year of the window, chosen by that fixed rule so the example replays byte for byte with everything else. Absent when no year in the window triggered.",
          "properties": {
            "year": { "$ref": "#/$defs/year" },
            "event_id": { "type": "string", "minLength": 1 },
            "index_level": { "type": "number" },
            "payout_point": { "$ref": "#/$defs/payout_point" },
            "payout_ratio": { "$ref": "#/$defs/ratio" },
            "payout": { "$ref": "#/$defs/money" },
            "narrative": { "type": "string", "minLength": 1 }
          }
        }
      }
    },
    "uuid": {
      "type": "string",
      "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
    },
    "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|[+-][0-9]{2}:[0-9]{2})$"
    },
    "year": { "type": "integer", "minimum": 1800, "maximum": 2999 },
    "ratio": { "type": "number", "minimum": 0, "maximum": 1 },
    "money": {
      "type": "object",
      "additionalProperties": false,
      "required": ["amount", "currency"],
      "description": "A currency-tagged amount, as on the risk object (risk-object.v0 money).",
      "properties": {
        "amount": { "type": "number", "minimum": 0 },
        "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }
      }
    },
    "model_label": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "version"],
      "description": "SPEC.md section 4 P9: every result is labelled with the model and version that produced it.",
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "version": { "type": "string", "minLength": 1 }
      }
    },
    "index_label": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "version"],
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "version": { "type": "string", "minLength": 1 }
      }
    },
    "event_selection": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "box",
        "minimum_magnitude",
        "events_in_snapshot",
        "events_in_box",
        "qualifying_occurrences"
      ],
      "properties": {
        "box": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "min_latitude",
            "max_latitude",
            "min_longitude",
            "max_longitude",
            "bounds"
          ],
          "properties": {
            "min_latitude": { "type": "number" },
            "max_latitude": { "type": "number" },
            "min_longitude": { "type": "number" },
            "max_longitude": { "type": "number" },
            "bounds": { "type": "string", "minLength": 1 }
          }
        },
        "max_focal_depth_km": { "type": ["number", "null"] },
        "minimum_magnitude": { "type": "number" },
        "rule": {
          "type": "string",
          "minLength": 1,
          "description": "What makes a catalogue record a qualifying one, in full: the period, the box, the magnitude threshold and what happens to a record the publisher gave no depth for. The counts below are unreproducible without it."
        },
        "events_in_snapshot": { "type": "integer", "minimum": 0 },
        "events_in_box": { "type": "integer", "minimum": 0 },
        "qualifying_events": {
          "type": "integer",
          "minimum": 0,
          "description": "Records that passed every clause of the selection rule, before clustering. The step between events_in_box and qualifying_occurrences, without which neither the thresholds nor the clustering can be checked."
        },
        "qualifying_occurrences": { "type": "integer", "minimum": 0 },
        "clauses": {
          "type": "object",
          "additionalProperties": false,
          "required": ["basis", "steps"],
          "description": "The selection rule as a running count, clause by clause. events_in_box and qualifying_events state the ends of the selection; without this, the thresholds between them are invisible and a reader cannot tell whether the depth threshold excluded one record, a hundred, or was never applied at all.",
          "properties": {
            "basis": { "type": "string", "minLength": 1 },
            "steps": {
              "type": "array",
              "minItems": 1,
              "items": {
                "type": "object",
                "additionalProperties": false,
                "required": ["step", "clause", "excluded", "remaining"],
                "properties": {
                  "step": { "type": "integer", "minimum": 1 },
                  "clause": { "type": "string", "minLength": 1 },
                  "excluded": { "type": "integer", "minimum": 0 },
                  "remaining": { "type": "integer", "minimum": 0 }
                }
              }
            }
          }
        },
        "depth_exclusions": {
          "type": "object",
          "additionalProperties": false,
          "required": ["basis", "max_focal_depth_km", "applied", "count", "events"],
          "description": "The catalogue records the depth clause removed, one by one, each naming which half of the clause removed it: a record the publisher gave no depth for, or a record deeper than the threshold. applied is false when no threshold is set, which is a different fact from a threshold that was set and removed nothing — and one an empty list alone would not distinguish.",
          "properties": {
            "basis": { "type": "string", "minLength": 1 },
            "max_focal_depth_km": { "type": ["number", "null"] },
            "applied": { "type": "boolean" },
            "count": { "type": "integer", "minimum": 0 },
            "events": {
              "type": "array",
              "items": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "event_id",
                  "time",
                  "magnitude",
                  "latitude",
                  "longitude",
                  "reason"
                ],
                "properties": {
                  "event_id": { "type": "string", "minLength": 1 },
                  "time": { "$ref": "#/$defs/timestamp" },
                  "magnitude": { "type": "number" },
                  "latitude": { "type": "number" },
                  "longitude": { "type": "number" },
                  "depth_km": { "type": "number" },
                  "reason": {
                    "enum": ["no published depth", "deeper than max_focal_depth_km"],
                    "description": "Which half of the depth clause removed this record. 'no published depth' is the decision the rule states — a record with no depth is excluded by a threshold rather than assumed shallow — and depth_km is absent from such a record because the publisher gave none."
                  }
                }
              }
            }
          }
        }
      }
    },
    "index_selection": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "measurement",
        "transform",
        "index_unit",
        "attachment_level",
        "days_in_snapshot",
        "days_reported",
        "days_missing",
        "measured_years",
        "unmeasured_years",
        "windows"
      ],
      "description": "The measurement set of an index model: which days the index saw, what it did with them, and what each year measured. The counterpart of event_selection for a station-measured index, and exposed to the same standard — every count is here so the burn can be re-computed by hand from the pinned snapshot.",
      "properties": {
        "measurement": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "variable",
            "unit",
            "statistic",
            "feed_kind"
          ],
          "description": "What was measured, and off which kind of feed. feed_kind is the pinned snapshot's own file kind — the thing that decides which reader parses the publisher's bytes — and is stated separately from the data-source kind so the two alphabets cannot be confused.",
          "properties": {
            "variable": { "type": "string", "minLength": 1 },
            "unit": { "type": "string", "minLength": 1 },
            "statistic": { "type": "string", "minLength": 1 },
            "feed_kind": { "type": "string", "minLength": 1 }
          }
        },
        "transform": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "type",
            "window_days",
            "alignment"
          ],
          "description": "How daily values became one index level per year.",
          "properties": {
            "type": { "type": "string", "minLength": 1 },
            "window_days": { "type": "integer", "minimum": 1 },
            "alignment": { "type": "string", "minLength": 1 }
          }
        },
        "index_unit": { "type": "string", "minLength": 1, "description": "The unit of the index level, which is the unit the payout function's levels and the attachment are read in. Not always the measurement's unit: a day-count transform measures a rainfall series in days." },
        "attachment_level": { "type": "number", "description": "The index level at which the structure first pays, in index_unit." },
        "days_in_snapshot": { "type": "integer", "minimum": 0 },
        "days_reported": { "type": "integer", "minimum": 0, "description": "Days inside the period of record the publisher actually reported. Compared against days_missing rather than assumed equal to the calendar." },
        "days_missing": { "type": "integer", "minimum": 0, "description": "Days the publisher did not report. A gap is disclosed, never interpolated (SPEC.md section 3.5)." },
        "measured_years": { "type": "integer", "minimum": 0 },
        "unmeasured_years": {
          "type": "array",
          "description": "Years with no complete window to measure. They still count as zero-payout years in the burn, which is what the burn's denominator says they are.",
          "items": { "type": "integer" }
        },
        "windows": {
          "type": "array",
          "minItems": 1,
          "description": "Every year's measurement, the zero years included: not a sample, the full set behind the burn.",
          "items": { "$ref": "#/$defs/index_window" }
        }
      }
    },
    "index_window": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "year",
        "unit",
        "payout_ratio",
        "days_reported",
        "days_in_year"
      ],
      "description": "One year as an index model measured it. level and the measured dates are absent from a year the feed could not be measured in, and unmeasured_reason says why in that case.",
      "properties": {
        "year": { "type": "integer" },
        "level": { "type": "number" },
        "unit": { "type": "string", "minLength": 1 },
        "payout_ratio": { "type": "number", "minimum": 0, "maximum": 1 },
        "measured_from": { "type": "string", "minLength": 1 },
        "measured_to": { "type": "string", "minLength": 1 },
        "days_reported": { "type": "integer", "minimum": 0 },
        "days_in_year": { "type": "integer", "minimum": 1 },
        "unmeasured_reason": { "type": "string", "minLength": 1 }
      }
    },
    "index_year_row": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "year",
        "triggered",
        "payout_ratio",
        "payout",
        "index_unit",
        "days_reported",
        "days_in_year"
      ],
      "description": "One year of history as an index model measured it: what the publisher reported, what the index read, and what the structure would have paid. index_level and the measured dates are absent from a year with no complete window to measure, and unmeasured_reason says why — a year that could not be measured still counts as a zero-payout year in the burning cost (SPEC.md section 3.5).",
      "properties": {
        "year": { "$ref": "#/$defs/year" },
        "triggered": { "type": "boolean" },
        "payout_ratio": { "$ref": "#/$defs/ratio" },
        "payout": { "$ref": "#/$defs/money" },
        "index_level": { "type": "number" },
        "index_unit": { "type": "string", "minLength": 1 },
        "measured_from": { "type": "string", "minLength": 1 },
        "measured_to": { "type": "string", "minLength": 1 },
        "days_reported": { "type": "integer", "minimum": 0 },
        "days_in_year": { "type": "integer", "minimum": 1 },
        "unmeasured_reason": { "type": "string", "minLength": 1 },
        "payout_point": { "$ref": "#/$defs/payout_point" },
        "capped_at_maximum": { "type": "boolean" }
      }
    },
    "year_row": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "year",
        "triggered",
        "payout_ratio",
        "payout",
        "occurrences",
        "catalogue_records_in_box"
      ],
      "description": "One year of history: what the archive held, what qualified, and what the structure would have paid.",
      "properties": {
        "year": { "$ref": "#/$defs/year" },
        "triggered": { "type": "boolean" },
        "payout_ratio": { "$ref": "#/$defs/ratio" },
        "payout": { "$ref": "#/$defs/money" },
        "occurrences": {
          "type": "array",
          "description": "The qualifying occurrences attributed to this year, in time order. Every one of them is named, so a reader can go to the publisher's catalogue and check the year against it.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "event_id",
              "time",
              "magnitude",
              "latitude",
              "longitude",
              "payout_ratio",
              "clustered_event_count"
            ],
            "properties": {
              "event_id": { "type": "string", "minLength": 1 },
              "time": { "$ref": "#/$defs/timestamp" },
              "magnitude": { "type": "number" },
              "latitude": { "type": "number", "minimum": -90, "maximum": 90 },
              "longitude": { "type": "number", "minimum": -180, "maximum": 180 },
              "payout_ratio": { "$ref": "#/$defs/ratio" },
              "clustered_event_count": { "type": "integer", "minimum": 1 },
              "index_level": {
                "type": "number",
                "description": "The level the payout function was evaluated at: the largest magnitude among this occurrence's events, which is the magnitude above."
              },
              "payout_point": { "$ref": "#/$defs/payout_point" },
              "capped_at_maximum": {
                "type": "boolean",
                "description": "Whether maximum_payout_ratio bit — that is, whether the point's ratio was above the ceiling and the payout is the ceiling rather than the point."
              },
              "window": {
                "type": "object",
                "additionalProperties": false,
                "required": ["from", "to", "duration", "basis"],
                "description": "The aggregation window this cluster was formed in, measured from its first qualifying event. Stated per occurrence so the clustering can be checked against the publisher's catalogue rather than trusted.",
                "properties": {
                  "from": { "$ref": "#/$defs/timestamp" },
                  "to": { "$ref": "#/$defs/timestamp" },
                  "duration": { "type": "string", "minLength": 1 },
                  "basis": { "type": "string", "minLength": 1 }
                }
              },
              "events": {
                "type": "array",
                "minItems": 1,
                "description": "Every catalogue record the aggregation window swept into this occurrence, in time order — not the peak alone. CEDE-126: clustered_event_count says how many were merged, and only this says which, which is what a reader needs to reproduce the occurrence from the snapshot.",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "event_id",
                    "time",
                    "magnitude",
                    "latitude",
                    "longitude",
                    "is_peak"
                  ],
                  "properties": {
                    "event_id": { "type": "string", "minLength": 1 },
                    "time": { "$ref": "#/$defs/timestamp" },
                    "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" },
                    "is_peak": {
                      "type": "boolean",
                      "description": "True for the record whose magnitude the index read. Exactly one record of an occurrence carries it."
                    }
                  }
                }
              }
            }
          }
        },
        "catalogue_records_in_box": {
          "type": "integer",
          "minimum": 0,
          "description": "How many records the pinned snapshot holds inside the box for this year, at any magnitude. Zero is what makes a year thin (see data_coverage): it separates a year that paid nothing because nothing happened from one that paid nothing because nothing was recorded."
        }
      }
    },
    "data_source": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "name",
        "publisher",
        "kind",
        "version",
        "vintage",
        "sha256",
        "licence"
      ],
      "description": "One pinned snapshot, identified by content. The digest is what makes the run replayable (SPEC.md section 2A).",
      "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 }
      }
    }
  }
}
