(** Parses a native Hevy CSV export into a {!Workout_import.batch}. This is a web-layer adapter: it turns untrusted export text into the application's import use case. It does not construct canonical evidence or apply Heavy Duty judgment. It never depends on [hito.persistence]. The whole export text is retained verbatim through {!Hito_app.Workout_import.make_batch}, so provenance survives the parse. The adapter enforces the strict native schema. The header must carry all 14 verified columns, in any order. Rows the adapter cannot promote — warmups, non-strength data, unknown set types, malformed fields — become {!Workout_import.warning} values, not sets. A row is dropped, never guessed at. Rows group into workouts by the exact [(title, start_time, end_time)] triple, in first-appearance order. *) type error = | Malformed_csv of { record : int; field : int; detail : string } (** The CSV text does not conform to RFC4180. [record] and [field] are 1-based, as {!Csv.Failure} reports them. *) | Missing_column of string (** A required column is absent from the header. The first missing column in canonical order is reported. *) | Empty_file (** The export has no rows at all, not even a header. *) val pp_error : Format.formatter -> error -> unit val parse : batch_id:Hito_app.Workout_import.batch_id -> fingerprint:string -> imported_at:Recovery.timestamp -> utc_offset_seconds:int -> string -> (Hito_app.Workout_import.batch, error) result (** [parse ~batch_id ~fingerprint ~imported_at ~utc_offset_seconds source] parses [source] as a native Hevy export. [utc_offset_seconds] is the offset of the wall-clock times in the export. Hevy datetimes are local wall time, so UTC seconds = civil seconds - offset. Returns [Error Empty_file] for empty input, [Error (Missing_column _)] when the header lacks a required column, and [Error (Malformed_csv _)] when the CSV text is not well formed. On success the batch retains [source] exactly. Rows the adapter cannot use appear in {!Workout_import.batch_warnings} in source order. *)