{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://openworkout.dev/schema/v0/program.json",
  "title": "Open Workout Schema v0: program",
  "description": "What a workout or multi-week program prescribes: weeks, then days, then slots. A single workout has one week with one day. A prescription, never performed work: loads are rules, not weights. Draft; may change until v1.",
  "type": "object",
  "required": ["$schema", "progression", "weeks"],
  "additionalProperties": false,
  "properties": {
    "$schema": { "const": "https://openworkout.dev/schema/v0/program.json" },
    "schedule": { "type": "string", "description": "How the days fall across a week, in words." },
    "progression": { "type": "string", "description": "How the targets change over time, in words." },
    "weeks": {
      "type": "array",
      "minItems": 1,
      "description": "The weeks of one cycle, repeating.",
      "items": { "$ref": "#/$defs/week" }
    }
  },
  "$defs": {
    "week": {
      "type": "object",
      "required": ["days"],
      "additionalProperties": false,
      "properties": {
        "days": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/day" } }
      }
    },
    "day": {
      "type": "object",
      "required": ["key", "name", "slots"],
      "additionalProperties": false,
      "properties": {
        "key": { "type": "string", "description": "Stable within a version; what a link to this day names." },
        "name": { "type": "string" },
        "slots": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/slot" } }
      }
    },
    "slot": {
      "type": "object",
      "required": ["exercise", "sets", "load"],
      "additionalProperties": false,
      "properties": {
        "exercise": { "$ref": "#/$defs/exerciseRef" },
        "sets": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/setGroup" } },
        "load": { "$ref": "#/$defs/loadPrescription" },
        "rest_seconds": { "type": "integer", "minimum": 0 },
        "substitutes": { "type": "array", "items": { "$ref": "#/$defs/exerciseRef" } },
        "notes": { "type": "string" }
      }
    },
    "exerciseRef": {
      "type": "object",
      "required": ["id"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string", "description": "An Open Exercise Catalogue ID (oec:…)." },
        "name": { "type": "string" }
      }
    },
    "setGroup": {
      "type": "object",
      "description": "A run of identical sets. Reps or a duration, not both.",
      "required": ["count"],
      "additionalProperties": false,
      "properties": {
        "count": { "type": "integer", "minimum": 1 },
        "reps_min": { "type": "integer", "minimum": 0 },
        "reps_max": { "type": "integer", "minimum": 0 },
        "duration_s": { "type": "integer", "minimum": 1 },
        "amrap": { "type": "boolean", "description": "As many reps as possible, reps_min at least." }
      }
    },
    "loadPrescription": {
      "type": "object",
      "description": "How to pick the load. Value and unit depend on the type: percent_training_max and rpe take a value, last_plus a value and unit, choice neither.",
      "required": ["type"],
      "additionalProperties": false,
      "properties": {
        "type": { "enum": ["percent_training_max", "rpe", "last_plus", "choice"] },
        "value": { "type": "number" },
        "unit": { "enum": ["kg", "lb"] },
        "after_misses": { "type": "integer", "minimum": 1, "description": "A failure rule on last_plus: after this many sessions short at one weight." },
        "drop_percent": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 100, "description": "Take this much off when the failure rule applies." }
      }
    }
  }
}
