(** Interprets recorded training against Heavy Duty rules. *) type assessment = | Progressing | Stalled | Decompensating (** [Stalled] is a halt: progress has stopped but strength holds. [Decompensating] is worse: the trainee loses strength (HD1, "The Economics of Growth and Recovery": "before decompensating, i.e., lose strength and mass"). HD1 names both and applies the same corrective. *) type error = Insufficient_data val pp_assessment : Format.formatter -> assessment -> unit val pp_error : Format.formatter -> error -> unit val stall_window : Recovery.duration (** 14 days. *) val assess : Evidence.Observation.t list -> (assessment, error) result (** [Error Insufficient_data] when the record cannot yet support a judgment: too few observations, or a record that never advanced but is younger than the stall window. This is an ordinary state of a young record, not a fault. *) type remedy = | Lay_off_then_reduce of { lay_off : Recovery.duration; drop_stimuli_per_workout : int; extra_rest : Recovery.duration; } val remedy : assessment -> remedy option (** [Some] for [Stalled] and [Decompensating]. HD1 applies one corrective to a halt and to decompensation alike ("when progress comes to a halt or you start decompensating ... you should decrease them"). The corrective never adds work. *) val pp_remedy : Format.formatter -> remedy -> unit val load_increase_trigger : int (** 12 reps, regardless of the prescribed range. *) val load_increase : current:float -> float * float (** 10-20% increase window. *) type load_verdict = Hold | Increase of float * float | Too_heavy val judge_load : rep_range:Prescription.Rep_range.t -> Evidence.Stimulus.Effort.t -> load_verdict (** Only the range floor matters: below it is [Too_heavy]. At twelve reps the verdict is [Increase]. *) val pp_load_verdict : Format.formatter -> load_verdict -> unit type workout_comparison = { exercise : Exercise.t; previous : Evidence.Stimulus.Effort.t; current : Evidence.Stimulus.Effort.t; } (** One exercise in a workout with a most-recent earlier observation. The baseline is strictly before the workout started, so correcting a saved workout never compares an exercise against itself. *) val workout_comparisons : Evidence.Log.t -> Evidence.Workout.t -> workout_comparison list (** Distinct exercises with a prior baseline, in workout performance order. *) val workout_changes : Evidence.Log.t -> Evidence.Workout.t -> workout_comparison list (** The comparisons that show progressive overload: more load, or equal load and more reps. One extra rep counts. *) (** Counts recorded workouts that show each overtraining risk. *) type diagnostic = | Excess_volume of int | Extensions_on_every_stimulus of int | Trained_under_recovered of int val diagnose : Evidence.Log.t -> diagnostic list val pp_diagnostic : Format.formatter -> diagnostic -> unit val workout_progress : Evidence.Log.t -> Evidence.Workout.t -> int (** How many distinct exercises in the workout beat their most recent earlier observation in the log. Progress is more reps or load. One extra rep counts. An exercise with no earlier observation does not count. The baseline is the latest observation strictly before the workout started, so correcting a saved workout never compares an exercise against itself. *) val previous_observation : Evidence.Observation.t list -> Evidence.Observation.t -> Evidence.Observation.t option (** The most recent observation of the target's exercise strictly before the target was performed, from the given series. This applies the baseline rule of {!workout_comparisons} over a bare observation list. A caller can then compare imported evidence against the observations before it. *)