{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:cede:schema:model-object:v0",
  "title": "CEDE model object, v0",
  "description": "The registry's model object (SPEC.md section 2A): metadata, declared inputs, logic, outputs — plus the report of the automatic validation pass every model gets on creation and the model card that pass generates. SPEC.md section 2A and section 4 (P8, P9) 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 the perimeter directly: logic is a closed four-primitive composition, so no revision can quietly admit a code model without a section 8 amendment, and model_card.authorship is required, so no model can exist in the registry without carrying its author.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "id",
    "status",
    "metadata",
    "declared_inputs",
    "logic",
    "outputs",
    "validation",
    "model_card"
  ],
  "properties": {
    "schema_version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
      "description": "Version of this schema the model conforms to."
    },
    "id": {
      "type": "string",
      "minLength": 1,
      "description": "Server-assigned identity of the model. Immutable, like a risk object's."
    },
    "status": {
      "enum": ["private"],
      "description": "Lifecycle of the model (SPEC.md section 2A). Models are private by default. This build composes and runs private models only; publication, which makes a version immutable and public in the registry, is not part of this surface, so 'private' is the only value a model can hold today."
    },
    "metadata": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "version", "license", "peril", "region", "author"],
      "description": "SPEC.md section 2A metadata: author, version, license, peril, region. Version and license are mandatory and the model card restates them.",
      "properties": {
        "name": { "type": "string", "minLength": 1, "maxLength": 120 },
        "version": {
          "type": "string",
          "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
          "description": "Semantic version. Mandatory because a published model is immutable and a change is a new version, never an edit."
        },
        "license": { "type": "string", "minLength": 1 },
        "peril": {
          "enum": [
            "earthquake",
            "tropical_cyclone",
            "flood",
            "wildfire",
            "severe_convective_storm",
            "weather_station",
            "cloud_outage",
            "grid_outage"
          ],
          "description": "The published peril taxonomy (SPEC.md section 2), the same enum the risk object uses."
        },
        "region": {
          "type": "object",
          "description": "Where the model applies, as the author describes it."
        },
        "author": {
          "type": "string",
          "minLength": 1,
          "description": "The account that composed the model. Set by the platform, never by the caller: authorship a caller could choose is not authorship (SPEC.md section 4, P9)."
        }
      }
    },
    "declared_inputs": {
      "type": "object",
      "additionalProperties": false,
      "required": ["risk_object_fields", "feeds", "note"],
      "description": "The risk object fields the model reads plus the platform feeds it consumes, declared up front and never discovered at run time (SPEC.md section 2A). A run reads the object through this list and nothing else.",
      "properties": {
        "risk_object_fields": {
          "type": "array",
          "minItems": 1,
          "items": { "type": "string", "minLength": 1 }
        },
        "feeds": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/feed_ref" }
        },
        "note": { "type": "string", "minLength": 1 }
      }
    },
    "logic": { "$ref": "#/$defs/composition" },
    "outputs": {
      "type": "object",
      "additionalProperties": false,
      "required": ["index", "payout_ratio", "technical_price"],
      "description": "What the model produces: indices and technical prices, under exactly the same rules as a platform model's outputs (SPEC.md section 4, P8).",
      "properties": {
        "index": { "$ref": "#/$defs/output" },
        "payout_ratio": { "$ref": "#/$defs/output" },
        "technical_price": { "$ref": "#/$defs/output" }
      }
    },
    "validation": { "$ref": "#/$defs/validation" },
    "model_card": { "$ref": "#/$defs/model_card" }
  },
  "$defs": {
    "feed_ref": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "version"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "version": { "type": "string", "minLength": 1 },
        "measurement": { "type": "string", "minLength": 1 },
        "unit": { "type": "string", "minLength": 1 },
        "period_of_record": { "type": "object" }
      }
    },
    "output": {
      "type": "object",
      "additionalProperties": false,
      "required": ["unit", "description"],
      "properties": {
        "unit": { "type": "string", "minLength": 1 },
        "description": { "type": "string", "minLength": 1 }
      }
    },
    "composition": {
      "type": "object",
      "additionalProperties": false,
      "required": ["feed", "measurement", "transform", "payout"],
      "description": "The v1 logic vocabulary in full (SPEC.md section 2A): a feed, a measurement, a transform and a payout function. additionalProperties is false and these four are required, so this schema cannot admit a code model, an expression, or a script — user-submitted code models are out of scope for v1 and need a SPEC.md section 8 amendment before any work on them begins.",
      "properties": {
        "feed": {
          "type": "object",
          "additionalProperties": false,
          "required": ["id", "version"],
          "properties": {
            "id": { "type": "string", "minLength": 1 },
            "version": {
              "type": "string",
              "minLength": 1,
              "description": "The pinned snapshot vintage. A model reads one vintage and never the network, which is what makes its results replay byte for byte."
            }
          }
        },
        "measurement": {
          "type": "object",
          "additionalProperties": false,
          "required": ["variable", "unit"],
          "properties": {
            "variable": { "type": "string", "minLength": 1 },
            "unit": { "type": "string", "minLength": 1 }
          }
        },
        "transform": {
          "type": "object",
          "additionalProperties": false,
          "required": ["type", "window_days"],
          "properties": {
            "type": { "enum": ["rolling_sum", "rolling_maximum", "threshold_day_count"] },
            "window_days": { "type": "integer", "minimum": 1, "maximum": 366 },
            "day_threshold": {
              "type": "number",
              "description": "Level a single day must reach to be counted. Belongs to threshold_day_count only."
            }
          }
        },
        "payout": {
          "type": "object",
          "additionalProperties": false,
          "required": ["type", "maximum_payout_ratio"],
          "properties": {
            "type": { "enum": ["step", "linear"] },
            "points": {
              "type": "array",
              "minItems": 1,
              "items": {
                "type": "object",
                "additionalProperties": false,
                "required": ["level", "payout_ratio"],
                "properties": {
                  "level": { "type": "number" },
                  "payout_ratio": { "type": "number", "minimum": 0, "maximum": 1 }
                }
              }
            },
            "attachment": { "type": "number" },
            "exhaustion": { "type": "number" },
            "maximum_payout_ratio": { "type": "number", "minimum": 0, "maximum": 1 }
          }
        }
      }
    },
    "validation": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "schema_conformance",
        "determinism",
        "backtest",
        "degenerate_flags",
        "feeds"
      ],
      "description": "The report of the automatic validation pass (SPEC.md section 2A). All five parts are required: a model that exists is a model that passed all of them, so there is no shape here for a model validated in part.",
      "properties": {
        "schema_conformance": {
          "type": "object",
          "additionalProperties": false,
          "required": ["passed", "schema", "note"],
          "properties": {
            "passed": { "const": true },
            "schema": { "type": "string", "minLength": 1 },
            "note": { "type": "string", "minLength": 1 }
          }
        },
        "determinism": {
          "type": "object",
          "additionalProperties": false,
          "required": ["passed", "evaluations", "result_digest", "note"],
          "properties": {
            "passed": { "const": true },
            "evaluations": { "type": "integer", "minimum": 2 },
            "result_digest": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$",
              "description": "sha256 of the canonical bytes of the backtest. SPEC.md section 7 requires published models to re-validate byte for byte in CI; this is the byte string that must reproduce."
            },
            "note": { "type": "string", "minLength": 1 }
          }
        },
        "backtest": { "$ref": "#/$defs/backtest" },
        "degenerate_flags": { "$ref": "#/$defs/degenerate_flags" },
        "feeds": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/feed_ref" }
        }
      }
    },
    "backtest": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "window",
        "burn_rate",
        "standard_deviation",
        "triggering_years",
        "maximum_payout_ratio",
        "maximum_index_level",
        "payout_function",
        "years",
        "data_coverage"
      ],
      "description": "The full backtest of the validation pass: every whole year of the feed's period of record, year by year. Descriptive history, never a projection (SPEC.md section 3.5).",
      "properties": {
        "payout_function": { "$ref": "#/$defs/payout_function" },
        "window": {
          "type": "object",
          "additionalProperties": false,
          "required": ["start", "end", "years"],
          "properties": {
            "start": { "type": "string", "minLength": 1 },
            "end": { "type": "string", "minLength": 1 },
            "years": { "type": "integer", "minimum": 1 }
          }
        },
        "burn_rate": { "type": "number", "minimum": 0 },
        "standard_deviation": { "type": "number", "minimum": 0 },
        "triggering_years": { "type": "integer", "minimum": 0 },
        "maximum_payout_ratio": { "type": "number", "minimum": 0, "maximum": 1 },
        "maximum_index_level": { "type": "number" },
        "years": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "year",
              "index_level",
              "index_unit",
              "payout_ratio",
              "measured_from",
              "measured_to",
              "days_reported",
              "days_in_year"
            ],
            "properties": {
              "year": { "type": "integer" },
              "index_level": { "type": "number" },
              "index_unit": { "type": "string", "minLength": 1 },
              "payout_ratio": { "type": "number", "minimum": 0, "maximum": 1 },
              "measured_from": { "type": ["string", "null"] },
              "measured_to": { "type": ["string", "null"] },
              "days_reported": { "type": "integer", "minimum": 0 },
              "days_in_year": { "type": "integer", "minimum": 1 }
            }
          }
        },
        "data_coverage": {
          "type": "object",
          "additionalProperties": false,
          "required": ["years_with_missing_days", "note"],
          "description": "SPEC.md section 3.5: years where the source data is thin are disclosed, never interpolated silently.",
          "properties": {
            "years_with_missing_days": {
              "type": "array",
              "items": { "type": "integer" }
            },
            "note": { "type": "string", "minLength": 1 }
          }
        }
      }
    },
    "payout_function": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "type",
        "semantics",
        "unit",
        "points",
        "maximum_payout_ratio",
        "attachment_level",
        "evaluation"
      ],
      "description": "The payout function the backtest above evaluated, with the rule for reading it — the same block, from the same helper, that a Backtest artifact carries (backtest-response.v0.schema.json). CEDE-125: a column of levels and ratios does not say which function produced it, and the builder composes either of two that disagree about every level between two points — a step holds the lower point's ratio, a linear ramp interpolates. Naming the type is not enough, so evaluation states the rule in words and semantics states it as a constant a client can branch on. Both functions publish in this one shape: a linear function's points are exactly its two ends, (attachment, 0) and (exhaustion, maximum_payout_ratio).",
      "properties": {
        "type": { "enum": ["step", "linear"] },
        "semantics": {
          "enum": ["flat_steps", "linear_interpolation"],
          "description": "Which reading of the points below 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."
        },
        "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: a threshold_day_count index is in days whatever the feed publishes."
        },
        "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": { "type": "number", "minimum": 0, "maximum": 1 }
            }
          }
        },
        "maximum_payout_ratio": { "type": "number", "minimum": 0, "maximum": 1 },
        "attachment_level": {
          "type": "number",
          "description": "The level of the first point: where the model first pays anything."
        },
        "evaluation": { "type": "string", "minLength": 1 }
      }
    },
    "degenerate_flags": {
      "type": "object",
      "additionalProperties": false,
      "required": ["flags", "triggering_years", "years", "note"],
      "description": "Stress and degenerate-case flags (SPEC.md section 2A), minimally always pays and never pays. Measurements of the model's behaviour, never a score or a ranking (SPEC.md section 4, P9).",
      "properties": {
        "flags": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["flag", "severity", "detail"],
            "properties": {
              "flag": {
                "enum": [
                  "always_pays",
                  "never_pays",
                  "always_pays_in_full",
                  "thin_trigger_history"
                ]
              },
              "severity": { "enum": ["high", "medium"] },
              "detail": { "type": "string", "minLength": 1 }
            }
          }
        },
        "triggering_years": { "type": "integer", "minimum": 0 },
        "years": { "type": "integer", "minimum": 1 },
        "note": { "type": "string", "minLength": 1 }
      }
    },
    "model_card": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "title",
        "version",
        "authorship",
        "licence",
        "peril",
        "region",
        "inputs",
        "method",
        "assumptions",
        "limitations",
        "measurements",
        "disclosure"
      ],
      "description": "The auto-generated model card (SPEC.md section 2A): assumptions, limitations and licence, carrying authorship (SPEC.md section 4, P9). Generated on creation, not written by the author.",
      "properties": {
        "title": { "type": "string", "minLength": 1 },
        "version": { "type": "string", "minLength": 1 },
        "authorship": {
          "type": "object",
          "additionalProperties": false,
          "required": ["author", "note"],
          "properties": {
            "author": { "type": "string", "minLength": 1 },
            "note": { "type": "string", "minLength": 1 }
          }
        },
        "license": {
          "type": "string",
          "minLength": 1,
          "description": "The licence, restated under the same name the creation request gives it (metadata.license, SPEC.md section 2A). Every card generated from schema_version 0.2.0 onward carries it, and it always holds the identical string to licence below; a card composed before 0.2.0 carries licence alone, which is why this member is not required."
        },
        "licence": {
          "type": "string",
          "minLength": 1,
          "description": "The same string as license above, under this repository's house spelling. Retained because published documents already carry it; new readers should prefer license, which matches what the composer sent."
        },
        "peril": { "type": "string", "minLength": 1 },
        "region": { "type": "object" },
        "inputs": { "type": "object" },
        "method": { "type": "string", "minLength": 1 },
        "assumptions": {
          "type": "array",
          "minItems": 1,
          "items": { "type": "string", "minLength": 1 }
        },
        "limitations": {
          "type": "array",
          "minItems": 1,
          "items": { "type": "string", "minLength": 1 }
        },
        "measurements": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "note",
            "years_of_record",
            "triggering_years",
            "burn_rate",
            "maximum_index_level"
          ],
          "description": "Performance metrics as measurements, never as rankings (SPEC.md section 4, P9). There is no score, no ordering and no comparison to another model anywhere in this document.",
          "properties": {
            "note": { "type": "string", "minLength": 1 },
            "years_of_record": { "type": "integer", "minimum": 1 },
            "triggering_years": { "type": "integer", "minimum": 0 },
            "burn_rate": { "type": "number", "minimum": 0 },
            "maximum_index_level": { "type": "number" }
          }
        },
        "disclosure": {
          "const": "This model is analytics software. It computes an index level from public data and a payout ratio from that level. It is not a contract, it determines nothing, and no result it produces is a price at which any party stands ready to transact.",
          "description": "Pinned as a constant for the same reason the price disclosure is: a free-text field can be reworded into an implication of transactability one careless edit at a time, and a constant cannot."
        }
      }
    }
  }
}
