[OCaml] High Intensity Training Online
refactor Rename Progression to Progress
HD1 speaks of "progress" throughout ("witnessing progress", "progress comes to a halt") and never of "progression" — the nominalization is conventional-bodybuilding vocabulary, the orthodoxy HD1 argues against. Rename the module to match the doctrine's own word. Behaviour is unchanged; this is a vocabulary alignment. Update the core dune modules list and header comment, and fix the odoc cross-reference in evidence.mli.
Changed files
lib/core/dune
@@ -1,8 +1,8 @@
1
1
; Two namespaces mirror the plan/record split: Prescription states what to do,
2
Removed:
; Evidence records what was done, and Progression is the only judge.
2
Added:
; Evidence records what was done, and Progress is the only judge.
3
3
4
4
(library
5
5
(name hito_core)
6
6
(public_name hito.core)
7
Removed:
(modules exercise recovery prescription evidence progression)
7
Added:
(modules exercise recovery prescription evidence progress)
8
8
(wrapped false))
lib/core/evidence.mli
@@ -172,7 +172,7 @@
172
172
end
173
173
174
174
(** One effort for one exercise, observed at a point in time. The canonical unit
175
Removed:
that {!Progression} judges. *)
175
Added:
that {!Progress} judges. *)
176
176
module Observation : sig
177
177
type t
178
178
lib/core/progress.ml
@@ -0,0 +1,183 @@
1
Added:
module Effort = Evidence.Stimulus.Effort
2
Added:
3
Added:
let beats ~previous ~current =
4
Added:
let load = Float.compare (Effort.load current) (Effort.load previous) in
5
Added:
let reps = Int.compare (Effort.reps current) (Effort.reps previous) in
6
Added:
load > 0 || (load = 0 && reps > 0)
7
Added:
8
Added:
type assessment = Progressing | Stalled
9
Added:
type error = Insufficient_data
10
Added:
11
Added:
exception Invalid of error
12
Added:
13
Added:
let pp_assessment ppf = function
14
Added:
| Progressing -> Format.pp_print_string ppf "progressing"
15
Added:
| Stalled -> Format.pp_print_string ppf "stalled"
16
Added:
17
Added:
let pp_error ppf Insufficient_data =
18
Added:
Format.pp_print_string ppf "not enough record to judge"
19
Added:
20
Added:
let stall_window = Recovery.days 14
21
Added:
let rec last = function [] -> None | [ x ] -> Some x | _ :: rest -> last rest
22
Added:
23
Added:
(* The most recent observation that improved on the one before it. *)
24
Added:
let last_advance observations =
25
Added:
let rec scan found = function
26
Added:
| (previous : Evidence.Observation.t) :: (current :: _ as rest) ->
27
Added:
let found =
28
Added:
if
29
Added:
beats
30
Added:
~previous:(Evidence.Observation.effort previous)
31
Added:
~current:(Evidence.Observation.effort current)
32
Added:
then Some current
33
Added:
else found
34
Added:
in
35
Added:
scan found rest
36
Added:
| _ -> found
37
Added:
in
38
Added:
scan None observations
39
Added:
40
Added:
let elapsed_between (a : Evidence.Observation.t) (b : Evidence.Observation.t) =
41
Added:
Recovery.duration_to_seconds
42
Added:
(Recovery.elapsed
43
Added:
~since:(Evidence.Observation.performed_at a)
44
Added:
~now:(Evidence.Observation.performed_at b))
45
Added:
46
Added:
let assess observations =
47
Added:
let window = Recovery.duration_to_seconds stall_window in
48
Added:
match (observations, last observations) with
49
Added:
| ([] | [ _ ]), _ | _, None -> raise (Invalid Insufficient_data)
50
Added:
| first :: _, Some latest -> (
51
Added:
match last_advance observations with
52
Added:
| Some advance ->
53
Added:
if elapsed_between advance latest >= window then Stalled
54
Added:
else Progressing
55
Added:
| None ->
56
Added:
(* Never advanced. Only a stall once the record is long enough to say
57
Added:
so. Otherwise two sessions a day apart would condemn the routine. *)
58
Added:
if elapsed_between first latest >= window then Stalled
59
Added:
else raise (Invalid Insufficient_data))
60
Added:
61
Added:
type remedy =
62
Added:
| Lay_off_then_reduce of {
63
Added:
lay_off : Recovery.duration;
64
Added:
drop_stimuli_per_workout : int;
65
Added:
extra_rest : Recovery.duration;
66
Added:
}
67
Added:
68
Added:
let remedy = function
69
Added:
| Progressing -> None
70
Added:
| Stalled ->
71
Added:
Some
72
Added:
(Lay_off_then_reduce
73
Added:
{
74
Added:
lay_off = Recovery.days 7;
75
Added:
drop_stimuli_per_workout = 1;
76
Added:
extra_rest = Recovery.days 1;
77
Added:
})
78
Added:
79
Added:
let pp_remedy ppf
80
Added:
(Lay_off_then_reduce { lay_off; drop_stimuli_per_workout; extra_rest }) =
81
Added:
Format.fprintf ppf
82
Added:
"take %a off, then drop %d stimulus per workout and add %a between workouts"
83
Added:
Recovery.pp_duration lay_off drop_stimuli_per_workout Recovery.pp_duration
84
Added:
extra_rest
85
Added:
86
Added:
let load_increase_trigger = 12
87
Added:
let load_increase ~current = (current *. 1.10, current *. 1.20)
88
Added:
89
Added:
type load_verdict = Hold | Increase of float * float | Too_heavy
90
Added:
91
Added:
(* The trigger is absolute, so the window's ceiling never gates the verdict —
92
Added:
only its floor does. *)
93
Added:
let judge_load ~rep_range effort =
94
Added:
let reps = Effort.reps effort in
95
Added:
let min_reps, _ = Prescription.Rep_range.bounds rep_range in
96
Added:
if reps >= load_increase_trigger then
97
Added:
let low, high = load_increase ~current:(Effort.load effort) in
98
Added:
Increase (low, high)
99
Added:
else if reps < min_reps then Too_heavy
100
Added:
else Hold
101
Added:
102
Added:
let pp_load_verdict ppf = function
103
Added:
| Hold -> Format.pp_print_string ppf "hold the load"
104
Added:
| Increase (low, high) ->
105
Added:
Format.fprintf ppf "raise the load to %g-%g kg" low high
106
Added:
| Too_heavy -> Format.pp_print_string ppf "load is too heavy for the window"
107
Added:
108
Added:
type diagnostic =
109
Added:
| Excess_volume of int
110
Added:
| Extensions_on_every_stimulus of int
111
Added:
| Trained_under_recovered of int
112
Added:
113
Added:
let excess_volume workout =
114
Added:
List.length (Evidence.Workout.stimuli workout)
115
Added:
> Evidence.Workout.filled_slots workout
116
Added:
117
Added:
let all_extended workout =
118
Added:
match Evidence.Workout.stimuli workout with
119
Added:
| [] -> false
120
Added:
| stimuli -> List.for_all Evidence.Stimulus.is_extended stimuli
121
Added:
122
Added:
let under_recovered workout =
123
Added:
match Recovery.basis (Evidence.Workout.clearance workout) with
124
Added:
| Recovery.Recovered -> false
125
Added:
| Recovery.Overridden _ -> true
126
Added:
127
Added:
let diagnose log =
128
Added:
let workouts = Evidence.Log.workouts log in
129
Added:
let count predicate = List.length (List.filter predicate workouts) in
130
Added:
let excessive = count excess_volume in
131
Added:
let extended = count all_extended in
132
Added:
let overridden = count under_recovered in
133
Added:
let when_present n d = if n > 0 then [ d n ] else [] in
134
Added:
when_present excessive (fun n -> Excess_volume n)
135
Added:
@ when_present extended (fun n -> Extensions_on_every_stimulus n)
136
Added:
@ when_present overridden (fun n -> Trained_under_recovered n)
137
Added:
138
Added:
let pp_diagnostic ppf = function
139
Added:
| Excess_volume n ->
140
Added:
Format.fprintf ppf "%d workout(s) recorded more stimuli than prescribed" n
141
Added:
| Extensions_on_every_stimulus n ->
142
Added:
Format.fprintf ppf "%d workout(s) extended every stimulus beyond failure"
143
Added:
n
144
Added:
| Trained_under_recovered n ->
145
Added:
Format.fprintf ppf "%d workout(s) begun before recovery was complete" n
146
Added:
147
Added:
(* How many distinct exercises in [workout] beat their most recent earlier
148
Added:
observation in [log]. Progress is the Heavy Duty signal — an increase in reps
149
Added:
or load — judged here, in the one module that judges, so a caller receives a
150
Added:
count and never re-derives the rule. An exercise with no earlier observation
151
Added:
does not count: a first performance has nothing to beat. The comparison
152
Added:
baseline is the latest observation strictly before this workout started, so
153
Added:
re-opening a saved workout to correct it does not compare an exercise against
154
Added:
itself. *)
155
Added:
let workout_progress log workout =
156
Added:
let started = Evidence.Workout.started_at workout in
157
Added:
let progressed exercise current =
158
Added:
let prior =
159
Added:
Evidence.Log.observations log exercise
160
Added:
|> List.filter (fun o ->
161
Added:
Recovery.duration_to_seconds
162
Added:
(Recovery.elapsed
163
Added:
~since:(Evidence.Observation.performed_at o)
164
Added:
~now:started)
165
Added:
> 0)
166
Added:
in
167
Added:
match last prior with
168
Added:
| None -> false
169
Added:
| Some previous ->
170
Added:
beats ~previous:(Evidence.Observation.effort previous) ~current
171
Added:
in
172
Added:
Evidence.Workout.stimuli workout
173
Added:
|> List.concat_map Evidence.Stimulus.efforts
174
Added:
|> List.fold_left
175
Added:
(fun (seen, count) effort ->
176
Added:
let exercise = Evidence.Stimulus.Effort.exercise effort in
177
Added:
if
178
Added:
List.exists (Exercise.equal exercise) seen
179
Added:
|| not (progressed exercise effort)
180
Added:
then (exercise :: seen, count)
181
Added:
else (exercise :: seen, count + 1))
182
Added:
([], 0)
183
Added:
|> snd
lib/core/progress.mli
@@ -0,0 +1,61 @@
1
Added:
(** Interprets recorded training against Heavy Duty rules. *)
2
Added:
3
Added:
type assessment = Progressing | Stalled
4
Added:
type error = Insufficient_data
5
Added:
6
Added:
exception Invalid of error
7
Added:
8
Added:
val pp_assessment : Format.formatter -> assessment -> unit
9
Added:
val pp_error : Format.formatter -> error -> unit
10
Added:
11
Added:
val stall_window : Recovery.duration
12
Added:
(** 14 days. *)
13
Added:
14
Added:
val assess : Evidence.Observation.t list -> assessment
15
Added:
(** Raises [Invalid Insufficient_data] when the record cannot support a
16
Added:
judgment. *)
17
Added:
18
Added:
type remedy =
19
Added:
| Lay_off_then_reduce of {
20
Added:
lay_off : Recovery.duration;
21
Added:
drop_stimuli_per_workout : int;
22
Added:
extra_rest : Recovery.duration;
23
Added:
}
24
Added:
25
Added:
val remedy : assessment -> remedy option
26
Added:
(** [Some] only for [Stalled]. It never adds work. *)
27
Added:
28
Added:
val pp_remedy : Format.formatter -> remedy -> unit
29
Added:
30
Added:
val load_increase_trigger : int
31
Added:
(** 12 reps, regardless of the prescribed range. *)
32
Added:
33
Added:
val load_increase : current:float -> float * float
34
Added:
(** 10-20% increase window. *)
35
Added:
36
Added:
type load_verdict = Hold | Increase of float * float | Too_heavy
37
Added:
38
Added:
val judge_load :
39
Added:
rep_range:Prescription.Rep_range.t ->
40
Added:
Evidence.Stimulus.Effort.t ->
41
Added:
load_verdict
42
Added:
(** Only the range floor matters: below it is [Too_heavy]. At twelve reps the
43
Added:
verdict is [Increase]. *)
44
Added:
45
Added:
val pp_load_verdict : Format.formatter -> load_verdict -> unit
46
Added:
47
Added:
(** Counts recorded workouts that show each overtraining risk. *)
48
Added:
type diagnostic =
49
Added:
| Excess_volume of int
50
Added:
| Extensions_on_every_stimulus of int
51
Added:
| Trained_under_recovered of int
52
Added:
53
Added:
val diagnose : Evidence.Log.t -> diagnostic list
54
Added:
val pp_diagnostic : Format.formatter -> diagnostic -> unit
55
Added:
56
Added:
val workout_progress : Evidence.Log.t -> Evidence.Workout.t -> int
57
Added:
(** How many distinct exercises in the workout beat their most recent earlier
58
Added:
observation in the log. Progress is more reps or load; one extra rep counts.
59
Added:
An exercise with no earlier observation does not count, and the baseline is
60
Added:
the latest observation strictly before the workout started, so correcting a
61
Added:
saved workout never compares an exercise against itself. *)
lib/core/progression.ml
@@ -1,183 +0,0 @@
1
Removed:
module Effort = Evidence.Stimulus.Effort
2
Removed:
3
Removed:
let beats ~previous ~current =
4
Removed:
let load = Float.compare (Effort.load current) (Effort.load previous) in
5
Removed:
let reps = Int.compare (Effort.reps current) (Effort.reps previous) in
6
Removed:
load > 0 || (load = 0 && reps > 0)
7
Removed:
8
Removed:
type assessment = Progressing | Stalled
9
Removed:
type error = Insufficient_data
10
Removed:
11
Removed:
exception Invalid of error
12
Removed:
13
Removed:
let pp_assessment ppf = function
14
Removed:
| Progressing -> Format.pp_print_string ppf "progressing"
15
Removed:
| Stalled -> Format.pp_print_string ppf "stalled"
16
Removed:
17
Removed:
let pp_error ppf Insufficient_data =
18
Removed:
Format.pp_print_string ppf "not enough record to judge"
19
Removed:
20
Removed:
let stall_window = Recovery.days 14
21
Removed:
let rec last = function [] -> None | [ x ] -> Some x | _ :: rest -> last rest
22
Removed:
23
Removed:
(* The most recent observation that improved on the one before it. *)
24
Removed:
let last_advance observations =
25
Removed:
let rec scan found = function
26
Removed:
| (previous : Evidence.Observation.t) :: (current :: _ as rest) ->
27
Removed:
let found =
28
Removed:
if
29
Removed:
beats
30
Removed:
~previous:(Evidence.Observation.effort previous)
31
Removed:
~current:(Evidence.Observation.effort current)
32
Removed:
then Some current
33
Removed:
else found
34
Removed:
in
35
Removed:
scan found rest
36
Removed:
| _ -> found
37
Removed:
in
38
Removed:
scan None observations
39
Removed:
40
Removed:
let elapsed_between (a : Evidence.Observation.t) (b : Evidence.Observation.t) =
41
Removed:
Recovery.duration_to_seconds
42
Removed:
(Recovery.elapsed
43
Removed:
~since:(Evidence.Observation.performed_at a)
44
Removed:
~now:(Evidence.Observation.performed_at b))
45
Removed:
46
Removed:
let assess observations =
47
Removed:
let window = Recovery.duration_to_seconds stall_window in
48
Removed:
match (observations, last observations) with
49
Removed:
| ([] | [ _ ]), _ | _, None -> raise (Invalid Insufficient_data)
50
Removed:
| first :: _, Some latest -> (
51
Removed:
match last_advance observations with
52
Removed:
| Some advance ->
53
Removed:
if elapsed_between advance latest >= window then Stalled
54
Removed:
else Progressing
55
Removed:
| None ->
56
Removed:
(* Never advanced. Only a stall once the record is long enough to say
57
Removed:
so. Otherwise two sessions a day apart would condemn the routine. *)
58
Removed:
if elapsed_between first latest >= window then Stalled
59
Removed:
else raise (Invalid Insufficient_data))
60
Removed:
61
Removed:
type remedy =
62
Removed:
| Lay_off_then_reduce of {
63
Removed:
lay_off : Recovery.duration;
64
Removed:
drop_stimuli_per_workout : int;
65
Removed:
extra_rest : Recovery.duration;
66
Removed:
}
67
Removed:
68
Removed:
let remedy = function
69
Removed:
| Progressing -> None
70
Removed:
| Stalled ->
71
Removed:
Some
72
Removed:
(Lay_off_then_reduce
73
Removed:
{
74
Removed:
lay_off = Recovery.days 7;
75
Removed:
drop_stimuli_per_workout = 1;
76
Removed:
extra_rest = Recovery.days 1;
77
Removed:
})
78
Removed:
79
Removed:
let pp_remedy ppf
80
Removed:
(Lay_off_then_reduce { lay_off; drop_stimuli_per_workout; extra_rest }) =
81
Removed:
Format.fprintf ppf
82
Removed:
"take %a off, then drop %d stimulus per workout and add %a between workouts"
83
Removed:
Recovery.pp_duration lay_off drop_stimuli_per_workout Recovery.pp_duration
84
Removed:
extra_rest
85
Removed:
86
Removed:
let load_increase_trigger = 12
87
Removed:
let load_increase ~current = (current *. 1.10, current *. 1.20)
88
Removed:
89
Removed:
type load_verdict = Hold | Increase of float * float | Too_heavy
90
Removed:
91
Removed:
(* The trigger is absolute, so the window's ceiling never gates the verdict —
92
Removed:
only its floor does. *)
93
Removed:
let judge_load ~rep_range effort =
94
Removed:
let reps = Effort.reps effort in
95
Removed:
let min_reps, _ = Prescription.Rep_range.bounds rep_range in
96
Removed:
if reps >= load_increase_trigger then
97
Removed:
let low, high = load_increase ~current:(Effort.load effort) in
98
Removed:
Increase (low, high)
99
Removed:
else if reps < min_reps then Too_heavy
100
Removed:
else Hold
101
Removed:
102
Removed:
let pp_load_verdict ppf = function
103
Removed:
| Hold -> Format.pp_print_string ppf "hold the load"
104
Removed:
| Increase (low, high) ->
105
Removed:
Format.fprintf ppf "raise the load to %g-%g kg" low high
106
Removed:
| Too_heavy -> Format.pp_print_string ppf "load is too heavy for the window"
107
Removed:
108
Removed:
type diagnostic =
109
Removed:
| Excess_volume of int
110
Removed:
| Extensions_on_every_stimulus of int
111
Removed:
| Trained_under_recovered of int
112
Removed:
113
Removed:
let excess_volume workout =
114
Removed:
List.length (Evidence.Workout.stimuli workout)
115
Removed:
> Evidence.Workout.filled_slots workout
116
Removed:
117
Removed:
let all_extended workout =
118
Removed:
match Evidence.Workout.stimuli workout with
119
Removed:
| [] -> false
120
Removed:
| stimuli -> List.for_all Evidence.Stimulus.is_extended stimuli
121
Removed:
122
Removed:
let under_recovered workout =
123
Removed:
match Recovery.basis (Evidence.Workout.clearance workout) with
124
Removed:
| Recovery.Recovered -> false
125
Removed:
| Recovery.Overridden _ -> true
126
Removed:
127
Removed:
let diagnose log =
128
Removed:
let workouts = Evidence.Log.workouts log in
129
Removed:
let count predicate = List.length (List.filter predicate workouts) in
130
Removed:
let excessive = count excess_volume in
131
Removed:
let extended = count all_extended in
132
Removed:
let overridden = count under_recovered in
133
Removed:
let when_present n d = if n > 0 then [ d n ] else [] in
134
Removed:
when_present excessive (fun n -> Excess_volume n)
135
Removed:
@ when_present extended (fun n -> Extensions_on_every_stimulus n)
136
Removed:
@ when_present overridden (fun n -> Trained_under_recovered n)
137
Removed:
138
Removed:
let pp_diagnostic ppf = function
139
Removed:
| Excess_volume n ->
140
Removed:
Format.fprintf ppf "%d workout(s) recorded more stimuli than prescribed" n
141
Removed:
| Extensions_on_every_stimulus n ->
142
Removed:
Format.fprintf ppf "%d workout(s) extended every stimulus beyond failure"
143
Removed:
n
144
Removed:
| Trained_under_recovered n ->
145
Removed:
Format.fprintf ppf "%d workout(s) begun before recovery was complete" n
146
Removed:
147
Removed:
(* How many distinct exercises in [workout] beat their most recent earlier
148
Removed:
observation in [log]. Progress is the Heavy Duty signal — an increase in reps
149
Removed:
or load — judged here, in the one module that judges, so a caller receives a
150
Removed:
count and never re-derives the rule. An exercise with no earlier observation
151
Removed:
does not count: a first performance has nothing to beat. The comparison
152
Removed:
baseline is the latest observation strictly before this workout started, so
153
Removed:
re-opening a saved workout to correct it does not compare an exercise against
154
Removed:
itself. *)
155
Removed:
let workout_progress log workout =
156
Removed:
let started = Evidence.Workout.started_at workout in
157
Removed:
let progressed exercise current =
158
Removed:
let prior =
159
Removed:
Evidence.Log.observations log exercise
160
Removed:
|> List.filter (fun o ->
161
Removed:
Recovery.duration_to_seconds
162
Removed:
(Recovery.elapsed
163
Removed:
~since:(Evidence.Observation.performed_at o)
164
Removed:
~now:started)
165
Removed:
> 0)
166
Removed:
in
167
Removed:
match last prior with
168
Removed:
| None -> false
169
Removed:
| Some previous ->
170
Removed:
beats ~previous:(Evidence.Observation.effort previous) ~current
171
Removed:
in
172
Removed:
Evidence.Workout.stimuli workout
173
Removed:
|> List.concat_map Evidence.Stimulus.efforts
174
Removed:
|> List.fold_left
175
Removed:
(fun (seen, count) effort ->
176
Removed:
let exercise = Evidence.Stimulus.Effort.exercise effort in
177
Removed:
if
178
Removed:
List.exists (Exercise.equal exercise) seen
179
Removed:
|| not (progressed exercise effort)
180
Removed:
then (exercise :: seen, count)
181
Removed:
else (exercise :: seen, count + 1))
182
Removed:
([], 0)
183
Removed:
|> snd
lib/core/progression.mli
@@ -1,61 +0,0 @@
1
Removed:
(** Interprets recorded training against Heavy Duty rules. *)
2
Removed:
3
Removed:
type assessment = Progressing | Stalled
4
Removed:
type error = Insufficient_data
5
Removed:
6
Removed:
exception Invalid of error
7
Removed:
8
Removed:
val pp_assessment : Format.formatter -> assessment -> unit
9
Removed:
val pp_error : Format.formatter -> error -> unit
10
Removed:
11
Removed:
val stall_window : Recovery.duration
12
Removed:
(** 14 days. *)
13
Removed:
14
Removed:
val assess : Evidence.Observation.t list -> assessment
15
Removed:
(** Raises [Invalid Insufficient_data] when the record cannot support a
16
Removed:
judgment. *)
17
Removed:
18
Removed:
type remedy =
19
Removed:
| Lay_off_then_reduce of {
20
Removed:
lay_off : Recovery.duration;
21
Removed:
drop_stimuli_per_workout : int;
22
Removed:
extra_rest : Recovery.duration;
23
Removed:
}
24
Removed:
25
Removed:
val remedy : assessment -> remedy option
26
Removed:
(** [Some] only for [Stalled]. It never adds work. *)
27
Removed:
28
Removed:
val pp_remedy : Format.formatter -> remedy -> unit
29
Removed:
30
Removed:
val load_increase_trigger : int
31
Removed:
(** 12 reps, regardless of the prescribed range. *)
32
Removed:
33
Removed:
val load_increase : current:float -> float * float
34
Removed:
(** 10-20% increase window. *)
35
Removed:
36
Removed:
type load_verdict = Hold | Increase of float * float | Too_heavy
37
Removed:
38
Removed:
val judge_load :
39
Removed:
rep_range:Prescription.Rep_range.t ->
40
Removed:
Evidence.Stimulus.Effort.t ->
41
Removed:
load_verdict
42
Removed:
(** Only the range floor matters: below it is [Too_heavy]. At twelve reps the
43
Removed:
verdict is [Increase]. *)
44
Removed:
45
Removed:
val pp_load_verdict : Format.formatter -> load_verdict -> unit
46
Removed:
47
Removed:
(** Counts recorded workouts that show each overtraining risk. *)
48
Removed:
type diagnostic =
49
Removed:
| Excess_volume of int
50
Removed:
| Extensions_on_every_stimulus of int
51
Removed:
| Trained_under_recovered of int
52
Removed:
53
Removed:
val diagnose : Evidence.Log.t -> diagnostic list
54
Removed:
val pp_diagnostic : Format.formatter -> diagnostic -> unit
55
Removed:
56
Removed:
val workout_progress : Evidence.Log.t -> Evidence.Workout.t -> int
57
Removed:
(** How many distinct exercises in the workout beat their most recent earlier
58
Removed:
observation in the log. Progress is more reps or load; one extra rep counts.
59
Removed:
An exercise with no earlier observation does not count, and the baseline is
60
Removed:
the latest observation strictly before the workout started, so correcting a
61
Removed:
saved workout never compares an exercise against itself. *)