{
  "schema_version": "1.0.0",
  "app": "Strongsplit",
  "task": "Read a Strongsplit workout export, then (when asked) generate a NEW workout routine as a file the user can import back into Strongsplit. This document is both the description of the attached export file and the exact, authoritative specification you MUST follow to produce a valid import file. Follow it precisely — do not invent fields, units, or values outside the rules below.",

  "data_model": {
    "summary": "Every file — export or import — is a flat list of SETS. There is no nested routine/exercise object; structure is expressed through shared values and link fields on each row.",
    "row": "One row (CSV line / JSON object) = one set.",
    "grouping": {
      "routine": "Rows that share the same `routine` name belong to the same routine.",
      "session": "Rows that share the same `start_time` belong to the same logged session (export only).",
      "exercise_order": "Within an exercise, `set_index` (0-based) orders the sets.",
      "links": "Dropset and superset rows attach to a parent working/warmup set via `parent_exercise` + `parent_set_index`."
    }
  },

  "targets": {
    "routines": "Reusable workout templates (planned sets). THIS is what you generate for import. Session-only fields (start_time, end_time, is_completed, complete_time) do not apply and must be omitted.",
    "sessions": "Logged workout history (sets actually performed). Sessions are export-only and cannot be imported."
  },

  "generate_importable_routine": {
    "objective": "Produce a file that passes Strongsplit's import validator on the first try.",
    "output_format": {
      "csv": "First line is a header of column names; each later line is one set. Quote any value containing a comma, quote, or newline, doubling inner quotes. Leave a cell empty to omit that field for that row.",
      "json": " .",
      "choose": "Match the format the user asks for. If unspecified, prefer JSON. Deliver the file as a downloadable artifact, not pasted inline, so the user can save and import it."
    },
    "instructions": [
      "ALWAYS include the three required fields on every row: `exercise`, `set_index`, `set_type`. For routine files, also include `routine` on every row.",
      "Use ONE unit system for the whole file and make it match the attached export: if the export used `weight_kg`/`distance_km` use metric columns; if it used `weight_lb`/`distance_mi` use imperial. NEVER mix systems or include both the kg and lb (or km and mi) column.",
      "Only include columns/keys that carry data for that row. Do not pad rows with empty or zero values just to fill the schema.",
      "Use clear, conventional exercise names (e.g. \"Bench Press\", \"Romanian Deadlift\"). On import, names that match the user's existing exercises are reused; unmatched names create a new exercise, so spelling matters.",
      "Number `set_index` from 0 within each exercise. For dropset/superset rows, `set_index` is the position within that drop/superset chain (also 0-based).",
      "For every `dropset` or `superset` row, set `parent_exercise` and `parent_set_index` to point at an EXISTING working or warmup set in the SAME routine. A dropset's parent must be the SAME exercise; a superset's parent is a DIFFERENT exercise (the superset row's own `exercise` is the paired movement).",
      "Booleans: write `true` to enable a flag; omit it (or write `false`) otherwise. The importer treats empty, `false`, and `0` as false and any other value as true.",
      "Stay within every numeric bound in `fields`. Out-of-range values are rejected, not clamped silently — pick realistic values inside the stated min/max.",
      "Respect the file limits in `limits` (max 5 routines, max 1000 rows, max 500KB)."
    ],
    "guardrails": [
      "If you cannot read the attached export, do not guess its units — ask the user which unit system to use before generating.",
      "Do not include session-only fields in a routine file.",
      "If a requested routine would exceed the limits (e.g. more than 5 routines), split it or tell the user instead of producing an invalid file.",
      "When unsure whether an exercise name will match, keep it conventional — a near-match creates a duplicate new exercise."
    ]
  },

  "units": {
    "rule": "Weight and distance columns carry their unit as a suffix. Pick the system that matches the export and use it exclusively.",
    "metric": ["weight_kg", "distance_km"],
    "imperial": ["weight_lb", "distance_mi"],
    "never": "Do not include a field from the unit system the user is not on — the import rejects the file if it sees the wrong-unit column."
  },

  "set_types": {
    "working": "A normal working set.",
    "warmup": "A warm-up set (lighter; not counted as a working set).",
    "dropset": "A drop performed immediately after a parent working/warmup set of the SAME exercise. Requires parent_exercise + parent_set_index.",
    "superset": "A set of a DIFFERENT exercise performed back-to-back with a parent working/warmup set. The row's `exercise` is the superset movement; requires parent_exercise + parent_set_index pointing at the paired (parent) exercise's set."
  },

  "required_fields": {
    "always": ["exercise", "set_index", "set_type"],
    "routines_also": ["routine"],
    "conditional": {
      "parent_exercise": "Required when set_type is `dropset` or `superset`.",
      "parent_set_index": "Required when set_type is `dropset` or `superset`."
    }
  },

  "field_order": [
    "start_time", "end_time", "routine",
    "exercise", "set_index", "set_type", "parent_exercise", "parent_set_index", "is_completed", "complete_time",
    "weight_kg", "weight_is_per_side", "reps", "reps_is_per_side", "rest_s", "time_s", "time_is_per_side",
    "distance_km", "distance_is_per_side", "rm_percent", "rep_range_lower", "rep_range_upper", "rep_range_is_per_side",
    "rpe", "rir", "failure", "left", "right", "pause_s", "negative_s"
  ],

  "fields": [
    {
      "name": "routine",
      "type": "string",
      "applies_to": ["routines", "sessions"],
      "required_for": ["routines"],
      "constraints": { "min_length": 1, "max_length": 50 },
      "description": "The routine's title. Every row of a routine shares it (this is what groups rows into one routine). For an import, each distinct value becomes a separate routine."
    },
    {
      "name": "exercise",
      "type": "string",
      "applies_to": ["routines", "sessions"],
      "required_for": ["routines", "sessions"],
      "constraints": { "min_length": 1, "max_length": 50 },
      "description": "Name of the exercise for this set. Use clear, conventional names so it matches existing exercises on import."
    },
    {
      "name": "set_index",
      "type": "integer",
      "applies_to": ["routines", "sessions"],
      "required_for": ["routines", "sessions"],
      "constraints": { "min": 0, "integer": true },
      "description": "Zero-based position of this set within its exercise. For dropset/superset rows it is the position within that drop/superset chain."
    },
    {
      "name": "set_type",
      "type": "enum",
      "applies_to": ["routines", "sessions"],
      "required_for": ["routines", "sessions"],
      "constraints": { "allowed_values": ["working", "warmup", "dropset", "superset"] },
      "description": "The kind of set. See set_types."
    },
    {
      "name": "parent_exercise",
      "type": "string",
      "applies_to": ["routines", "sessions"],
      "required_for": ["dropset rows", "superset rows"],
      "constraints": { "min_length": 1, "max_length": 50 },
      "description": "For dropset/superset rows, the exercise name of the parent working/warmup set this row hangs off. Must reference a real parent set in the same routine. Omit for working/warmup rows."
    },
    {
      "name": "parent_set_index",
      "type": "integer",
      "applies_to": ["routines", "sessions"],
      "required_for": ["dropset rows", "superset rows"],
      "constraints": { "min": 0, "integer": true },
      "description": "For dropset/superset rows, the set_index of the parent working/warmup set. Together with parent_exercise it identifies the parent. Omit for working/warmup rows."
    },
    {
      "name": "start_time",
      "type": "string (ISO 8601 datetime with offset)",
      "applies_to": ["sessions"],
      "required_for": [],
      "description": "When the session started; groups a logged session's rows. Export only — omit from routine imports."
    },
    {
      "name": "end_time",
      "type": "string (ISO 8601 datetime with offset)",
      "applies_to": ["sessions"],
      "required_for": [],
      "description": "When the session ended. Export only — omit from routine imports."
    },
    {
      "name": "is_completed",
      "type": "boolean",
      "applies_to": ["sessions"],
      "required_for": [],
      "description": "Whether the set was actually completed. Export only — omit from routine imports."
    },
    {
      "name": "complete_time",
      "type": "string (ISO 8601 datetime with offset)",
      "applies_to": ["sessions"],
      "required_for": [],
      "description": "When the set was completed. Export only — omit from routine imports."
    },
    {
      "name": "weight_kg",
      "type": "number",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 0, "max": 999, "increment": 0.5 },
      "description": "Load in kilograms (metric files). Rounded to the nearest 0.5 on import. Do not also include weight_lb. Pair with weight_is_per_side when the value is per side."
    },
    {
      "name": "weight_lb",
      "type": "number",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 0, "max": 999, "increment": 0.5 },
      "description": "Load in pounds (imperial files). Rounded to the nearest 0.5 on import. Imperial alternative to weight_kg — never include both."
    },
    {
      "name": "weight_is_per_side",
      "type": "boolean",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "description": "True if the weight is per side (e.g. per dumbbell / per side of a barbell) rather than the total."
    },
    {
      "name": "reps",
      "type": "number",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 1, "max": 99, "prefer_integer": true },
      "description": "Number of repetitions. Use whole numbers; the app supports 0.5 increments only when the user has enabled decimal reps, so prefer integers unless asked."
    },
    {
      "name": "reps_is_per_side",
      "type": "boolean",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "description": "True if reps are counted per side for a unilateral exercise."
    },
    {
      "name": "rest_s",
      "type": "integer (seconds)",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 0, "max": 600, "integer": true },
      "description": "Target rest after this set, in seconds (0–600, i.e. up to 10 minutes)."
    },
    {
      "name": "time_s",
      "type": "integer (seconds)",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 1, "max": 3600, "integer": true },
      "description": "Duration of the set in seconds, for timed exercises (planks, carries, etc.). 1–3600 (up to 1 hour)."
    },
    {
      "name": "time_is_per_side",
      "type": "boolean",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "description": "True if the time value is per side for a unilateral exercise."
    },
    {
      "name": "distance_km",
      "type": "number",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 0.05, "max": 99 },
      "description": "Distance in kilometres (metric files). Do not also include distance_mi."
    },
    {
      "name": "distance_mi",
      "type": "number",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 0.05, "max": 99 },
      "description": "Distance in miles (imperial files). Imperial alternative to distance_km — never include both."
    },
    {
      "name": "distance_is_per_side",
      "type": "boolean",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "description": "True if the distance value is per side for a unilateral exercise."
    },
    {
      "name": "rm_percent",
      "type": "number",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 1, "max": 999 },
      "description": "Target intensity as a percentage of one-rep max (1RM), e.g. 75 for 75%."
    },
    {
      "name": "rep_range_lower",
      "type": "number",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 1, "max": 99, "less_than": "rep_range_upper" },
      "description": "Lower bound of a target rep range. MUST be paired with rep_range_upper (both or neither) and must be strictly less than it."
    },
    {
      "name": "rep_range_upper",
      "type": "number",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 1, "max": 99, "greater_than": "rep_range_lower" },
      "description": "Upper bound of a target rep range. MUST be paired with rep_range_lower and must be strictly greater than it."
    },
    {
      "name": "rep_range_is_per_side",
      "type": "boolean",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "description": "True if the rep range is per side for a unilateral exercise."
    },
    {
      "name": "rpe",
      "type": "number",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 5, "max": 10, "increment": 0.5 },
      "description": "Rate of Perceived Exertion, 5 (very easy) to 10 (max effort). Half-point increments allowed."
    },
    {
      "name": "rir",
      "type": "number",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 0, "max": 5 },
      "description": "Reps In Reserve, 0 (none left) to 5 (five or more left)."
    },
    {
      "name": "failure",
      "type": "boolean",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "description": "True if the set is taken to muscular failure."
    },
    {
      "name": "left",
      "type": "boolean",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "description": "Marks the set as the left side of a unilateral movement, when the two sides are logged separately."
    },
    {
      "name": "right",
      "type": "boolean",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "description": "Marks the set as the right side of a unilateral movement, when the two sides are logged separately."
    },
    {
      "name": "pause_s",
      "type": "integer (seconds)",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 1, "max": 60 },
      "description": "Pause duration in seconds for paused reps (e.g. paused bench)."
    },
    {
      "name": "negative_s",
      "type": "integer (seconds)",
      "applies_to": ["routines", "sessions"],
      "required_for": [],
      "constraints": { "min": 1, "max": 60 },
      "description": "Eccentric (negative) tempo in seconds per rep."
    }
  ],

  "validation_rules": [
    "The file must be a non-empty list of set rows.",
    "Every row needs exercise, set_index, set_type — and routine for routine files.",
    "set_type must be exactly one of: working, warmup, dropset, superset.",
    "Every dropset/superset row must include parent_exercise and parent_set_index, and they must resolve to a real working/warmup set in the same routine (matching exercise name case-insensitively and set_index).",
    "A dropset's parent must be the same exercise; a superset's parent must be a different exercise.",
    "Use only one unit system per file; do not include both kg and lb, or both km and mi.",
    "rep_range_lower and rep_range_upper must both be present or both absent, with lower strictly less than upper.",
    "Every numeric value must fall within the field's min/max; out-of-range rows are rejected.",
    "Unknown/extra columns are ignored, but do not rely on them to carry meaning."
  ],

  "limits": {
    "max_routines_per_file": 5,
    "max_rows": 1000,
    "max_file_size_kb": 500
  },

  "examples": {
    "routine_csv": "routine,exercise,set_index,set_type,parent_exercise,parent_set_index,weight_kg,weight_is_per_side,reps,rest_s\nPush Day,Bench Press,0,warmup,,,40,false,12,60\nPush Day,Bench Press,1,working,,,60,false,8,120\nPush Day,Bench Press,2,working,,,60,false,8,120\nPush Day,Incline DB Press,0,working,,,24,true,10,90\nPush Day,Cable Fly,0,superset,Incline DB Press,0,15,false,12,90",
    "routine_json": [
      { "routine": "Push Day", "exercise": "Bench Press", "set_index": 0, "set_type": "warmup", "weight_kg": 40, "reps": 12, "rest_s": 60 },
      { "routine": "Push Day", "exercise": "Bench Press", "set_index": 1, "set_type": "working", "weight_kg": 60, "reps": 8, "rest_s": 120 },
      { "routine": "Push Day", "exercise": "Bench Press", "set_index": 2, "set_type": "working", "weight_kg": 60, "reps": 8, "rest_s": 120 },
      { "routine": "Push Day", "exercise": "Bench Press", "set_index": 0, "set_type": "dropset", "parent_exercise": "Bench Press", "parent_set_index": 2, "weight_kg": 45, "reps": 8, "rest_s": 0 },
      { "routine": "Push Day", "exercise": "Incline DB Press", "set_index": 0, "set_type": "working", "weight_kg": 24, "weight_is_per_side": true, "reps": 10, "rest_s": 90 },
      { "routine": "Push Day", "exercise": "Cable Fly", "set_index": 0, "set_type": "superset", "parent_exercise": "Incline DB Press", "parent_set_index": 0, "weight_kg": 15, "reps": 12, "rest_s": 90 }
    ],
    "session_json": [
      { "start_time": "2026-06-10T08:00:00Z", "end_time": "2026-06-10T09:05:00Z", "routine": "Push Day", "exercise": "Bench Press", "set_index": 0, "set_type": "working", "is_completed": true, "complete_time": "2026-06-10T08:12:00Z", "weight_kg": 60, "reps": 8, "rpe": 8 }
    ]
  }
}
