{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://app.shockai.co/developers/schemas/v0/ModelManifest.json",
  "title": "ModelManifest",
  "type": "object",
  "properties": {
    "specVersion": {
      "type": "string",
      "const": "0.1",
      "description": "Version of the ShockAI Model API specification this document follows."
    },
    "id": {
      "$ref": "#/$defs/ModelId"
    },
    "version": {
      "type": "string",
      "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$",
      "description": "A semantic version. A result is always pinned to the exact version that produced it."
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "summary": {
      "type": "string",
      "minLength": 1,
      "maxLength": 500
    },
    "vendor": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120
        },
        "url": {
          "type": "string",
          "format": "uri"
        }
      },
      "required": [
        "name"
      ]
    },
    "method": {
      "$ref": "#/$defs/ModelMethod"
    },
    "capabilities": {
      "minItems": 1,
      "type": "array",
      "items": {
        "$ref": "#/$defs/Capability"
      }
    },
    "execution": {
      "type": "object",
      "properties": {
        "mode": {
          "type": "string",
          "enum": [
            "sync",
            "async"
          ],
          "description": "Sync models answer `POST /runs` within the timeout; async models accept the run and call back."
        },
        "typicalSeconds": {
          "type": "number",
          "exclusiveMinimum": 0
        },
        "maxSeconds": {
          "type": "number",
          "exclusiveMinimum": 0,
          "maximum": 604800,
          "description": "Hard limit. A run still going at this point is cancelled."
        }
      },
      "required": [
        "mode",
        "typicalSeconds",
        "maxSeconds"
      ]
    },
    "pricing": {
      "oneOf": [
        {
          "type": "object",
          "properties": {
            "model": {
              "type": "string",
              "const": "included"
            }
          },
          "required": [
            "model"
          ]
        },
        {
          "type": "object",
          "properties": {
            "model": {
              "type": "string",
              "const": "per_run"
            },
            "currency": {
              "type": "string",
              "minLength": 3,
              "maxLength": 3
            },
            "unitAmount": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991,
              "description": "Minor units, e.g. cents."
            }
          },
          "required": [
            "model",
            "currency",
            "unitAmount"
          ]
        },
        {
          "type": "object",
          "properties": {
            "model": {
              "type": "string",
              "const": "per_hole"
            },
            "currency": {
              "type": "string",
              "minLength": 3,
              "maxLength": 3
            },
            "unitAmount": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            }
          },
          "required": [
            "model",
            "currency",
            "unitAmount"
          ]
        },
        {
          "type": "object",
          "properties": {
            "model": {
              "type": "string",
              "const": "quote"
            }
          },
          "required": [
            "model"
          ]
        },
        {
          "type": "object",
          "properties": {
            "model": {
              "type": "string",
              "const": "licence"
            },
            "note": {
              "description": "The customer brings their own licence; the platform charges nothing on the run.",
              "type": "string",
              "maxLength": 500
            }
          },
          "required": [
            "model"
          ]
        }
      ],
      "description": "How a run is charged."
    },
    "dataHandling": {
      "type": "object",
      "properties": {
        "regions": {
          "minItems": 1,
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 2,
            "maxLength": 40
          },
          "description": "Where the case is processed, for example `au`, `eu`, `us`."
        },
        "retention": {
          "type": "string",
          "enum": [
            "none",
            "30d",
            "contract"
          ]
        },
        "trainsOnCustomerData": {
          "type": "boolean"
        }
      },
      "required": [
        "regions",
        "retention",
        "trainsOnCustomerData"
      ]
    },
    "documentationUrl": {
      "type": "string",
      "format": "uri"
    },
    "signing": {
      "description": "How the provider signs what it sends the platform. Required of an async model, whose results arrive as signed callbacks.",
      "type": "object",
      "properties": {
        "callbackKeys": {
          "minItems": 1,
          "maxItems": 3,
          "type": "array",
          "items": {
            "$ref": "#/$defs/Ed25519Jwk"
          },
          "description": "The public keys your callbacks are signed with, as Ed25519 JWKs: one, or two while you rotate. The platform pins them when the model is connected, so a changed key is a new manifest version."
        }
      },
      "required": [
        "callbackKeys"
      ]
    },
    "build": {
      "description": "What is deployed behind this version.",
      "type": "object",
      "properties": {
        "hash": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128,
          "pattern": "^[A-Za-z0-9._:+/@-]+$",
          "description": "Identifies the code and weights answering, for example a commit or image digest. Every result reports it in `provenance.buildHash`."
        }
      },
      "required": [
        "hash"
      ]
    }
  },
  "required": [
    "specVersion",
    "id",
    "version",
    "name",
    "summary",
    "vendor",
    "method",
    "capabilities",
    "execution",
    "pricing",
    "dataHandling"
  ],
  "description": "What a model says about itself: capabilities, inputs, validity, execution, pricing and data handling.",
  "$defs": {
    "ModelId": {
      "type": "string",
      "minLength": 3,
      "maxLength": 128,
      "pattern": "^[a-z0-9][a-z0-9-]*\\/[a-z0-9][a-z0-9.-]*$",
      "description": "A model identifier in the form `vendor/model`, lower case, for example `shockai/site-law`."
    },
    "ModelMethod": {
      "type": "string",
      "enum": [
        "empirical",
        "kinematic",
        "numerical",
        "statistical",
        "hybrid"
      ],
      "description": "The family of method, so a user can tell a fitted law from a simulation."
    },
    "Capability": {
      "type": "object",
      "properties": {
        "output": {
          "$ref": "#/$defs/OutputKind"
        },
        "inputs": {
          "type": "object",
          "properties": {
            "required": {
              "minItems": 1,
              "type": "array",
              "items": {
                "$ref": "#/$defs/InputPath"
              }
            },
            "requiredAnyOf": {
              "default": [],
              "description": "Inputs that stand in for one another: the model needs at least one from each group, for example the charge, from the stated MIC or from the holes. A case carrying none of a group is declined with `missing_input`, as for a missing required input.",
              "type": "array",
              "items": {
                "minItems": 2,
                "type": "array",
                "items": {
                  "$ref": "#/$defs/InputPath"
                }
              }
            },
            "optional": {
              "default": [],
              "type": "array",
              "items": {
                "$ref": "#/$defs/InputPath"
              }
            }
          },
          "required": [
            "required"
          ]
        },
        "validity": {
          "description": "The domain the model is validated on. Outputs outside it must be marked `outside` or declined.",
          "type": "object",
          "properties": {
            "distanceM": {
              "$ref": "#/$defs/Range"
            },
            "scaledDistance": {
              "$ref": "#/$defs/Range"
            },
            "rockClasses": {
              "type": "array",
              "items": {
                "$ref": "#/$defs/RockClass"
              }
            },
            "notes": {
              "type": "string",
              "maxLength": 1000
            }
          }
        }
      },
      "required": [
        "output",
        "inputs"
      ],
      "description": "One kind of output a model produces, and what it needs to produce it."
    },
    "OutputKind": {
      "type": "string",
      "enum": [
        "receiver_ppv",
        "max_charge",
        "receiver_waveform",
        "receiver_spectrum",
        "ppv_field",
        "airblast",
        "damage_envelope",
        "strain_field",
        "flyrock_zone",
        "fragmentation_curve",
        "movement_field",
        "timing_proposal",
        "hole_diagnosis",
        "strain_amplitude"
      ],
      "description": "A kind of answer: a prediction of what a blast will do, or, for `hole_diagnosis` and `strain_amplitude`, a reading of what a fired blast did."
    },
    "InputPath": {
      "type": "string",
      "enum": [
        "design.holes",
        "design.products",
        "design.initiation",
        "design.maxInstantaneousCharge",
        "design.drillingDeviation",
        "asBuilt.holes",
        "asBuilt.firing",
        "site.domains",
        "site.terrain",
        "receivers",
        "calibration.siteLaw",
        "calibration.airblastLaw",
        "calibration.seedWaveforms",
        "calibration.vod",
        "calibration.outcomes",
        "calibration.strainAmplitude",
        "measured.records",
        "measured.backbreak"
      ],
      "description": "A part of the BlastCase. The case a model receives is built from the parts it declares and nothing else. `design.holes` carries the holes and the layout family they form; `design.drillingDeviation` carries `design.drillingDeviationM`."
    },
    "Range": {
      "type": "object",
      "properties": {
        "min": {
          "type": "number"
        },
        "max": {
          "type": "number"
        }
      },
      "description": "An inclusive interval. Either bound may be omitted. A range whose `min` exceeds its `max` is empty: a model declared valid over one is valid nowhere, and no prediction falls inside a fitted range that is empty."
    },
    "RockClass": {
      "type": "string",
      "enum": [
        "super_soft",
        "soft",
        "medium",
        "hard"
      ],
      "description": "Coarse rock class used when no measured properties are available."
    },
    "Ed25519Jwk": {
      "type": "object",
      "properties": {
        "kty": {
          "type": "string",
          "const": "OKP"
        },
        "crv": {
          "type": "string",
          "const": "Ed25519"
        },
        "x": {
          "type": "string",
          "minLength": 43,
          "maxLength": 43,
          "pattern": "^[A-Za-z0-9_-]+$",
          "description": "The 32-byte public key, base64url without padding."
        },
        "kid": {
          "description": "Ignored: the key id is always the RFC 7638 thumbprint.",
          "type": "string",
          "minLength": 1,
          "maxLength": 128
        },
        "use": {
          "type": "string",
          "const": "sig"
        },
        "alg": {
          "type": "string",
          "const": "EdDSA"
        }
      },
      "required": [
        "kty",
        "crv",
        "x"
      ],
      "description": "An Ed25519 public key as an RFC 8037 JWK. Publish only the public half: the platform refuses a manifest whose key carries its private part (`d`)."
    }
  }
}
