(** Recorded stimuli, workouts, and history. Recording never judges. *) (** One performed drive to failure. *) module Stimulus : sig type extension = Forced_reps | Negatives | Rest_pause | Static_hold type outcome = | Positive_failure | Beyond_failure of extension * extension list (** Nonempty, in application order. *) val extensions_of_outcome : outcome -> extension list val pp_extension : Format.formatter -> extension -> unit module Effort : sig type t val make : exercise:Exercise.t -> load:float -> reps:int -> outcome:outcome -> t val exercise : t -> Exercise.t val load : t -> float val reps : t -> int val outcome : t -> outcome val extensions : t -> extension list val pp : Format.formatter -> t -> unit end type delivery = | Single of Effort.t | Pair of { first : Effort.t; second : Effort.t } type t val make : delivery -> t (** Records the delivery as performed. *) val delivery : t -> delivery val efforts : t -> Effort.t list (** Performance order. *) val exercises : t -> Exercise.t list val extensions : t -> extension list val is_extended : t -> bool val pp : Format.formatter -> t -> unit end (** Typed signals reported at a point in time. *) module Feedback : sig type level = | Very_poor | Poor | Fair | Good | Very_good (** A five-point subjective score. [Very_poor] is 1, [Very_good] is 5. These are wellness and readiness self-reports, never a measure of the working set's intensity, which is categorical (0% at rest, 100% at failure). *) val level_to_score : level -> int (** The score in 1..5. *) val level_of_score : int -> level option (** [Some] for 1..5, [None] otherwise. *) type signal = | Sleep of level | Appetite of level | Readiness of level | Motivation of level | Difficulty of level | Pain | Injury | Preparation_insufficient type t type error = Duplicate_signal of signal exception Invalid of error val make : reported_at:Recovery.timestamp -> signal list -> t (** Raises [Invalid] when feedback repeats a signal category. *) val reported_at : t -> Recovery.timestamp val signals : t -> signal list end (** A performed or in-progress prescribed workout. *) module Workout : sig type t type shape = As_single | As_pair type completeness = Complete | Incomplete type error = | Not_prescribed of Exercise.id | Delivery_mismatch of { exercise : Exercise.id; prescribed : shape; logged : shape; } | No_such_slot of int (** A slot-targeted operation named a slot the prescription does not have. *) val pp_error : Format.formatter -> error -> unit exception Invalid of error val start : Prescription.Workout.t -> clearance:Recovery.clearance -> started_at:Recovery.timestamp -> t val add_stimulus : t -> Stimulus.t -> t (** Records against the next matching unanswered slot, or as extra volume when every matching slot is already answered. Raises [Invalid] when the stimulus does not conform to any prescribed slot. *) val replace_stimulus : t -> slot:int -> Stimulus.t -> t (** Records the stimulus into a named prescription slot, replacing whatever filled that slot before. A correction, not extra work: it targets the slot and never appends volume. The stimulus must conform to that slot's prescription. Raises [Invalid (No_such_slot _)] for an unknown slot, and [Invalid] for a stimulus the slot does not call for. *) val finish : t -> ended_at:Recovery.timestamp -> t (** Sets [ended_at] once; later calls retain the first value. *) val prescription : t -> Prescription.Workout.t val clearance : t -> Recovery.clearance val started_at : t -> Recovery.timestamp val ended_at : t -> Recovery.timestamp option val is_finished : t -> bool val duration : t -> Recovery.duration option val completeness : t -> completeness val stimuli : t -> Stimulus.t list (** Performance order. *) val performed : t -> (int * Stimulus.t) list (** Each recorded stimulus paired with the prescription slot it filled, in performance order. A slot appears more than once only when extra volume was recorded against it. *) val record_at : t -> slot:int -> Stimulus.t -> t (** Append a stimulus at a named slot, preserving any prior fill of that slot. The faithful-replay primitive: it reconstructs {!performed} verbatim, including recorded extra volume, without the slot-clearing that {!replace_stimulus} performs. Validates the stimulus against the slot and raises [Invalid] as {!replace_stimulus} does. *) val filled_slots : t -> int (** How many distinct prescription slots have a record. Never exceeds the prescription length, even when extra volume was recorded. *) val outstanding : t -> (int * Prescription.Stimulus.t) list val unperformed : t -> Prescription.Stimulus.t list val pp : Format.formatter -> t -> unit end (** Workout history. *) module Log : sig type t val empty : t val add : t -> Workout.t -> t val workouts : t -> Workout.t list (** Most recent first. *) val last_prescription : t -> Prescription.Workout.t option type observation = { exercise : Exercise.t; effort : Stimulus.Effort.t; performed_at : Recovery.timestamp; } val observations : t -> Exercise.t -> observation list (** Oldest first. *) val readiness : t -> now:Recovery.timestamp -> recommended:Recovery.duration -> Recovery.readiness (** [Ready] for an empty log or no finished workout. *) val pp : Format.formatter -> t -> unit end