{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://www.your-applications.com/schemas/workout-manifest.json",
  "title": "Your Trainer library manifest",
  "description": "Library-level index for the curated .ytw workout library at public/your-trainer/library/. Distinct from the per-workout .ytw schema (public/your-trainer/workout-schema.html) and from FEAT-0081's pack-manifest (public/your-trainer/packs/manifest.json). The per-workout entry below is the canonical superset-target for FEAT-0081 packs — same snake_case fields; FEAT-0081 packs add pack-specific extras like 'sparkline' for the D3 contents grid.",
  "type": "object",
  "required": ["schema_version", "generated_at", "workouts"],
  "additionalProperties": false,
  "properties": {
    "schema_version": {"type": "integer", "const": 1},
    "generated_at": {"type": "string", "format": "date-time"},
    "generator": {"type": "string", "description": "Generator identifier + version for provenance."},
    "packs": {
      "type": "array",
      "description": "Per-category .zip bundles published alongside the library. Each pack contains every .ytw in (set, category), named by slug.",
      "items": {"$ref": "#/definitions/pack_entry"}
    },
    "workouts": {"type": "array", "items": {"$ref": "#/definitions/workout_entry"}}
  },
  "definitions": {
    "pack_entry": {
      "type": "object",
      "required": ["pack_id", "name", "description", "version", "set", "category", "file_path", "content_hash", "workout_count", "total_bytes", "total_ride_time_seconds", "experience_level", "hrm_required", "format"],
      "additionalProperties": false,
      "properties": {
        "pack_id": {"type": "string"},
        "name": {"type": "string"},
        "description": {"type": "string", "description": "One- to two-sentence summary visible in the catalog listing before the rider taps a pack. Mirrors the description carried inside the .ytwpack's per-pack manifest so the catalog renders without a ZIP fetch."},
        "version": {"type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[A-Za-z0-9.-]+)?$", "description": "Pack version in SemVer form (MAJOR.MINOR.PATCH, optional -prerelease). Increment on any content change so the app's version-aware updater (REQ-0173) surfaces it."},
        "set": {"type": "string", "enum": ["power", "hr-zone"]},
        "category": {"type": "string"},
        "file_path": {"type": "string", "description": "Path relative to public/your-trainer/ (e.g. 'packs/sweet-spot/v1.0.0.ytwpack')."},
        "zwo_bundle_file_path": {"type": "string", "description": "Optional. Path relative to public/your-trainer/ for a plain .zip containing every workout in this pack as a .zwo (Zwift format). No embedded manifest — Zwift has no pack format; rider unzips + imports per-workout. Present iff at least one workout in the pack has a .zwo emission (POWER-only — HR_ZONE has no .zwo equivalent)."},
        "zwo_bundle_total_bytes": {"type": "integer", "minimum": 1, "description": "Optional. Size of the .zwo bundle .zip in bytes. Present iff zwo_bundle_file_path is."},
        "content_hash": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"},
        "workout_count": {"type": "integer", "minimum": 1},
        "total_bytes": {"type": "integer", "minimum": 1, "description": "Size of the .ytwpack in bytes."},
        "total_ride_time_seconds": {"type": "integer", "minimum": 1},
        "experience_level": {"type": "string", "enum": ["beginner", "intermediate", "advanced", "mixed"], "description": "Lowercase wire format; UI capitalises for display."},
        "hrm_required": {"type": "boolean"},
        "icon_url": {"type": ["string", "null"], "description": "Absolute URL to a pack icon (PNG, ~256x256). Null when no icon has been authored — the app renders a fallback keyed off set/category."},
        "format": {"type": "string", "enum": ["ytwpack"], "description": "File-format discriminator; .ytwpack = ZIP-with-embedded-manifest per the workout-pack-manifest-schema."}
      }
    },
    "workout_entry": {
      "type": "object",
      "required": ["slug", "name", "set", "category", "duration_seconds", "intensity_summary", "tss", "intensity_factor", "difficulty_score", "discipline_tags", "physiology_focus", "requires_power_meter", "version", "launch_slice", "file_path"],
      "additionalProperties": false,
      "properties": {
        "slug": {"type": "string", "description": "Kebab-case stable identifier. Becomes the .ytw filename."},
        "name": {"type": "string"},
        "named": {"type": "string", "description": "Optional named-workout identifier."},
        "set": {"type": "string", "enum": ["power", "hr-zone"]},
        "category": {"type": "string"},
        "duration_seconds": {"type": "integer", "minimum": 1},
        "intensity_summary": {"type": "string", "description": "Human-readable one-liner (e.g. '4x15min @ 90% FTP')."},
        "tss": {"type": "number", "minimum": 0, "description": "Estimated Training Stress Score, computed from interval power + duration."},
        "intensity_factor": {"type": "number", "minimum": 0, "description": "Normalised intensity (sqrt of avg weighted power^4 / FTP — approximated via interval averages)."},
        "difficulty_score": {
          "type": "object",
          "required": ["overall"],
          "additionalProperties": false,
          "properties": {
            "overall": {"type": "integer", "minimum": 1, "maximum": 10, "description": "TrainerRoad-Workout-Levels-style overall score (1-10)."},
            "z1": {"type": "number", "minimum": 0, "maximum": 1, "description": "Fraction of work-block time in Z1."},
            "z2": {"type": "number", "minimum": 0, "maximum": 1},
            "z3": {"type": "number", "minimum": 0, "maximum": 1},
            "z4": {"type": "number", "minimum": 0, "maximum": 1},
            "z5": {"type": "number", "minimum": 0, "maximum": 1}
          }
        },
        "discipline_tags": {"type": "array", "items": {"type": "string", "enum": ["road", "tt", "tri", "gravel", "mtb", "cx", "indoor"]}},
        "physiology_focus": {"type": "array", "items": {"type": "string"}},
        "cadence_focus": {"type": "string", "enum": ["standard", "high", "low", "pyramid", "mixed", "standing"], "description": "Constrained by REQ-0027. Semantics: data/training-principles/hr-zone-cadence.md."},
        "requires_power_meter": {"type": "boolean"},
        "requires_hrm": {"type": "boolean"},
        "repeatable": {"type": "boolean", "description": "True when the body extends cleanly under the app's runtime repeat-N feature. False indicates the rider should pick a longer pre-built variant instead. Default: true."},
        "plan_recipe_affiliation": {"type": "string"},
        "locale_variants": {"type": "array", "items": {"type": "string"}, "description": "BCP-47 locale codes present in the .ytw's strings block."},
        "version": {"type": "integer", "minimum": 1},
        "launch_slice": {"type": "boolean", "description": "True for the bundled-launch starter set; false for later expansion waves."},
        "file_path": {"type": "string", "description": "Path relative to public/your-trainer/ (e.g. 'library/power/sweet-spot/sweet-spot-4x15min-90pct.ytw')."},
        "zwo_path": {"type": "string", "description": "Optional. Path relative to public/your-trainer/ for the .zwo (Zwift) rendering of this workout. POWER-only — HR_ZONE workouts have no .zwo equivalent. EN-only — Zwift has no localisation story for workout files."},
        "provenance": {
          "type": "object",
          "description": "ADR-0005: records which path produced this entry's tss/IF/difficulty + which validated the .ytw. Lets future regenerations audit MCP vs local divergence and lets operators identify pre-MCP entries.",
          "additionalProperties": false,
          "required": ["difficulty_source", "validated_by"],
          "properties": {
            "difficulty_source": {"type": "string", "enum": ["mcp", "local"], "description": "Where tss / intensity_factor / difficulty_score came from. 'mcp' = the live yourtrainer-mcp's workout_difficulty tool; 'local' = the in-house _ytw_metrics helper (fallback for airgap / unreachable MCP)."},
            "validated_by": {"type": "string", "enum": ["mcp", "local"], "description": "Which validator approved the .ytw. 'mcp' = the live MCP's validate('ytw'); 'local' = the in-house lightweight JSON Schema validator."},
            "mcp_version": {"type": "string", "description": "Optional. yourtrainer-mcp server version string from initialize's serverInfo.version. Present iff at least one of difficulty_source / validated_by is 'mcp'."}
          }
        },
        "sparkline": {
          "type": "array",
          "description": "Compact per-interval shape data for thumbnail rendering (the D3 sparkline in FEAT-0081 + the library-browser per-card diagram). One entry per .ytw interval (repeats already expanded). Lets clients draw the workout shape without fetching the .ytw.",
          "items": {
            "type": "object",
            "required": ["d", "p", "z"],
            "additionalProperties": false,
            "properties": {
              "d": {"type": "integer", "minimum": 1, "description": "Duration in seconds."},
              "p": {"type": "integer", "minimum": 0, "description": "Target power as % of FTP. For HR_ZONE workouts, a synthetic value (target_hr_zone * 20) for visualisation purposes only — do not interpret as power."},
              "e": {"type": "integer", "minimum": 0, "description": "Optional ramp end-power %. Present only on ramps; absent means flat at p."},
              "z": {"type": "string", "enum": ["Z1", "Z2", "Z3", "Z4", "Z5"], "description": "Intensity zone token; drives fill colour."},
              "t": {"type": "string", "enum": ["WARMUP", "COOLDOWN"], "description": "Optional segment role. Absent means body INTERVAL."}
            }
          }
        }
      }
    }
  }
}
