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.

Commit
e70ee401487e5bc4f05ecf6587fb0c7f8a399a3c
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/core/dune
index f919df98..8d852a3e 100644..100644
@@ -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
index d5b25cb2..3da9d59b 100644..100644
@@ -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
index 00000000..ea30a83f 000000..100644
@@ -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
index 00000000..a03323d6 000000..100644
@@ -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
index ea30a83f..00000000 100644..000000
@@ -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
index a03323d6..00000000 100644..000000
@@ -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. *)