[OCaml] High Intensity Training Online
refactor raise invalid plan construction
Authored prescriptions are built after the app validates input, so invalid pairings, ranges, substitutions, and empty plans now raise scoped exceptions rather than burdening trusted callers with result plumbing. Raw unit conversion and recoverable domain feedback remain results. Evidence records a factual Pair rather than asserting every recorded pair is a pre-exhaustion. This keeps history honest while Prescription remains strict. Align steering with those boundaries, the local-only HD1 source, and the concrete validation command.
Changed files
.kiro/steering/domain.md
@@ -4,6 +4,7 @@
4
4
5
5
- Source of doctrine: `doc/Mike_Mentzer_Heavy_Duty_I.md` (HD1). Every domain
6
6
decision must be justified against it.
7
Added:
- It is copyrighted local reference material, gitignored, and never committed.
7
8
- On conflict between a design choice and the doctrine:
8
9
1. The doctrine wins, or
9
10
2. The conflict is raised explicitly.
.kiro/steering/ocaml.md
@@ -15,11 +15,15 @@
15
15
substantial noise — typically chained `result` plumbing.
16
16
- A single `match` is clearer than a monad; don't reach for one reflexively.
17
17
- Abstract types with smart constructors for anything carrying an invariant.
18
Removed:
- Return `result` with a module-specific error type.
19
Removed:
- Each module defines its own errors.
20
Removed:
- No shared catch-all error type, no `string` errors.
18
Added:
- Use `result` for expected, recoverable failure.
19
Added:
- This includes converting untrusted external data and domain feedback a caller
20
Added:
can present or recover from.
21
Added:
- Each module defines its own errors; no shared catch-all error type or
22
Added:
`string` errors.
23
Added:
- Raise a module-specific exception for a violated authored-plan precondition
24
Added:
after validation.
25
Added:
- Do not use exceptions for normal domain outcomes or recorded deviations.
21
26
- Prefer closed variants — they keep `match` exhaustiveness working for you.
22
Removed:
- Reserve exceptions for genuine programmer error, never for control flow.
23
27
- Total functions where practical; make partiality visible in the type.
24
28
25
29
## Layering
.kiro/steering/workflow.md
@@ -14,7 +14,7 @@
14
14
15
15
- Commit — but only when asked.
16
16
- Never commit unformatted or failing code.
17
Removed:
- Use `direnv exec <repo> dune ...` if the local opam switch is not active.
17
Added:
- Use `direnv exec . dune ...` if the local opam switch is not active.
18
18
19
19
## Commits
20
20
@@ -43,4 +43,5 @@
43
43
- Interface docs state **intention**, not implementation. One line where one
44
44
line will do.
45
45
- A module header says what the module is for and which invariant it upholds.
46
Added:
- No Markdown files unless asked.
46
47
- Don't restate the signature in prose. Document the *why* and the edge cases.
lib/core/evidence.ml
@@ -64,40 +64,18 @@
64
64
65
65
type delivery =
66
66
| Single of Movement.t
67
Removed:
| Pre_exhaust of { isolation : Movement.t; compound : Movement.t }
67
Added:
| Pair of { first : Movement.t; second : Movement.t }
68
68
69
69
type t = { delivery : delivery; warm_ups : Warm_up.t list }
70
70
71
Removed:
type error =
72
Removed:
| Not_a_pre_exhaust of { isolation : Exercise.id; compound : Exercise.id }
73
Removed:
74
Removed:
let pp_error ppf (Not_a_pre_exhaust { isolation; compound }) =
75
Removed:
Format.fprintf ppf "%s cannot pre-exhaust for %s"
76
Removed:
(isolation :> string)
77
Removed:
(compound :> string)
78
Removed:
79
Removed:
let make ?(warm_ups = []) delivery =
80
Removed:
match delivery with
81
Removed:
| Pre_exhaust { isolation; compound }
82
Removed:
when not
83
Removed:
(Exercise.may_pre_exhaust
84
Removed:
~isolation:(Movement.exercise isolation)
85
Removed:
~compound:(Movement.exercise compound)) ->
86
Removed:
Error
87
Removed:
(Not_a_pre_exhaust
88
Removed:
{
89
Removed:
isolation = Exercise.id (Movement.exercise isolation);
90
Removed:
compound = Exercise.id (Movement.exercise compound);
91
Removed:
})
92
Removed:
| _ -> Ok { delivery; warm_ups }
93
Removed:
71
Added:
let make ?(warm_ups = []) delivery = { delivery; warm_ups }
94
72
let delivery t = t.delivery
95
73
let warm_ups t = t.warm_ups
96
74
97
75
let movements t =
98
76
match t.delivery with
99
77
| Single m -> [ m ]
100
Removed:
| Pre_exhaust { isolation; compound } -> [ isolation; compound ]
78
Added:
| Pair { first; second } -> [ first; second ]
101
79
102
80
let exercises t = List.map Movement.exercise (movements t)
103
81
let extensions t = List.concat_map Movement.extensions (movements t)
@@ -106,13 +84,12 @@
106
84
let pp ppf t =
107
85
match t.delivery with
108
86
| Single m -> Movement.pp ppf m
109
Removed:
| Pre_exhaust { isolation; compound } ->
110
Removed:
Format.fprintf ppf "%a into %a" Movement.pp isolation Movement.pp
111
Removed:
compound
87
Added:
| Pair { first; second } ->
88
Added:
Format.fprintf ppf "%a then %a" Movement.pp first Movement.pp second
112
89
end
113
90
114
91
module Workout = struct
115
Removed:
type shape = As_single | As_pre_exhaust
92
Added:
type shape = As_single | As_pair
116
93
117
94
type error =
118
95
| Not_prescribed of Exercise.id
@@ -135,7 +112,7 @@
135
112
136
113
let pp_shape ppf = function
137
114
| As_single -> Format.pp_print_string ppf "a single set"
138
Removed:
| As_pre_exhaust -> Format.pp_print_string ppf "a pre-exhaust pair"
115
Added:
| As_pair -> Format.pp_print_string ppf "a pair"
139
116
140
117
let pp_error ppf = function
141
118
| Not_prescribed id ->
@@ -165,12 +142,12 @@
165
142
let prescribed_shape p =
166
143
match Prescription.Stimulus.delivery p with
167
144
| Prescription.Stimulus.Single _ -> As_single
168
Removed:
| Prescription.Stimulus.Pre_exhaust _ -> As_pre_exhaust
145
Added:
| Prescription.Stimulus.Pre_exhaust _ -> As_pair
169
146
170
147
let logged_shape s =
171
148
match Stimulus.delivery s with
172
149
| Stimulus.Single _ -> As_single
173
Removed:
| Stimulus.Pre_exhaust _ -> As_pre_exhaust
150
Added:
| Stimulus.Pair _ -> As_pair
174
151
175
152
(* A logged movement answers a prescribed one when it is that movement, or a
176
153
substitute the prescription allows *for that movement*. *)
lib/core/evidence.mli
@@ -45,18 +45,13 @@
45
45
46
46
type delivery =
47
47
| Single of Movement.t
48
Removed:
| Pre_exhaust of { isolation : Movement.t; compound : Movement.t }
48
Added:
| Pair of { first : Movement.t; second : Movement.t }
49
49
50
50
type t
51
51
52
Removed:
type error =
53
Removed:
| Not_a_pre_exhaust of { isolation : Exercise.id; compound : Exercise.id }
52
Added:
val make : ?warm_ups:Warm_up.t list -> delivery -> t
53
Added:
(** Records the delivery as performed. *)
54
54
55
Removed:
val pp_error : Format.formatter -> error -> unit
56
Removed:
57
Removed:
val make : ?warm_ups:Warm_up.t list -> delivery -> (t, error) result
58
Removed:
(** Rejects a mislabelled pre-exhaust pairing. *)
59
Removed:
60
55
val delivery : t -> delivery
61
56
62
57
val movements : t -> Movement.t list
@@ -72,7 +67,7 @@
72
67
(** A performed or in-progress prescribed workout. *)
73
68
module Workout : sig
74
69
type t
75
Removed:
type shape = As_single | As_pre_exhaust
70
Added:
type shape = As_single | As_pair
76
71
77
72
type error =
78
73
| Not_prescribed of Exercise.id
lib/core/prescription.ml
@@ -48,6 +48,8 @@
48
48
Units.Rep_range.contains rep_limits (Units.Rep_range.min range)
49
49
&& Units.Rep_range.contains rep_limits (Units.Rep_range.max range)
50
50
51
Added:
exception Invalid of error
52
Added:
51
53
let make ~delivery ~rep_range ~allowed_substitutes =
52
54
let movements = delivery_exercises delivery in
53
55
let unpermitted =
@@ -62,16 +64,17 @@
62
64
match (delivery, within_limits rep_range, unpermitted) with
63
65
| Pre_exhaust { isolation; compound }, _, _
64
66
when not (Exercise.may_pre_exhaust ~isolation ~compound) ->
65
Removed:
Error
66
Removed:
(Not_a_pre_exhaust
67
Removed:
{
68
Removed:
isolation = Exercise.id isolation;
69
Removed:
compound = Exercise.id compound;
70
Removed:
})
71
Removed:
| _, false, _ -> Error Reps_outside_limits
67
Added:
raise
68
Added:
(Invalid
69
Added:
(Not_a_pre_exhaust
70
Added:
{
71
Added:
isolation = Exercise.id isolation;
72
Added:
compound = Exercise.id compound;
73
Added:
}))
74
Added:
| _, false, _ -> raise (Invalid Reps_outside_limits)
72
75
| _, _, Some candidate ->
73
Removed:
Error (Substitute_not_permitted (Exercise.id candidate))
74
Removed:
| _ -> Ok { delivery; rep_range; allowed_substitutes }
76
Added:
raise (Invalid (Substitute_not_permitted (Exercise.id candidate)))
77
Added:
| _ -> { delivery; rep_range; allowed_substitutes }
75
78
76
79
let permits t movement =
77
80
List.exists (Exercise.equal movement) (exercises t)
@@ -93,10 +96,12 @@
93
96
type error = Empty_workout
94
97
type t = { id : id; name : string; stimuli : Stimulus.t list }
95
98
99
Added:
exception Invalid of error
100
Added:
96
101
let make ~id ~name ~stimuli =
97
102
match stimuli with
98
Removed:
| [] -> Error Empty_workout
99
Removed:
| _ -> Ok { id; name; stimuli }
103
Added:
| [] -> raise (Invalid Empty_workout)
104
Added:
| _ -> { id; name; stimuli }
100
105
101
106
let id t = t.id
102
107
let name t = t.name
@@ -109,8 +114,12 @@
109
114
type error = Empty_routine
110
115
type t = { name : string; workouts : Workout.t list }
111
116
117
Added:
exception Invalid of error
118
Added:
112
119
let make ~name ~workouts =
113
Removed:
match workouts with [] -> Error Empty_routine | _ -> Ok { name; workouts }
120
Added:
match workouts with
121
Added:
| [] -> raise (Invalid Empty_routine)
122
Added:
| _ -> { name; workouts }
114
123
115
124
let name t = t.name
116
125
let workouts t = t.workouts
@@ -134,10 +143,6 @@
134
143
135
144
(* {1 The Ideal Routine} *)
136
145
137
Removed:
let preset_exn pp = function
138
Removed:
| Ok v -> v
139
Removed:
| Error e -> invalid_arg (Format.asprintf "Routine preset: %a" pp e)
140
Removed:
141
146
let movement id =
142
147
match Exercise.find id with
143
148
| Some e -> e
@@ -153,9 +158,8 @@
153
158
| Error _ -> assert false
154
159
155
160
let prescribe ?(substitutes = []) delivery =
156
Removed:
preset_exn Stimulus.pp_error
157
Removed:
(Stimulus.make ~delivery ~rep_range:six_to_ten
158
Removed:
~allowed_substitutes:(List.map movement substitutes))
161
Added:
Stimulus.make ~delivery ~rep_range:six_to_ten
162
Added:
~allowed_substitutes:(List.map movement substitutes)
159
163
160
164
let single ?substitutes id =
161
165
prescribe ?substitutes (Stimulus.Single (movement id))
@@ -165,11 +169,7 @@
165
169
(Stimulus.Pre_exhaust
166
170
{ isolation = movement isolation; compound = movement compound })
167
171
168
Removed:
let day ~id ~name stimuli =
169
Removed:
preset_exn
170
Removed:
(fun ppf Workout.Empty_workout ->
171
Removed:
Format.pp_print_string ppf "empty workout")
172
Removed:
(Workout.make ~id ~name ~stimuli)
172
Added:
let day ~id ~name stimuli = Workout.make ~id ~name ~stimuli
173
173
174
174
let ideal_day_one =
175
175
day ~id:"ideal-day-1" ~name:"Day 1"
@@ -207,8 +207,6 @@
207
207
]
208
208
209
209
let ideal =
210
Removed:
preset_exn
211
Removed:
(fun ppf Empty_routine -> Format.pp_print_string ppf "empty routine")
212
Removed:
(make ~name:"Ideal Routine"
213
Removed:
~workouts:[ ideal_day_one; ideal_day_two; ideal_day_three ])
210
Added:
make ~name:"Ideal Routine"
211
Added:
~workouts:[ ideal_day_one; ideal_day_two; ideal_day_three ]
214
212
end
lib/core/prescription.mli
@@ -19,12 +19,14 @@
19
19
val rep_limits : Units.Rep_range.t
20
20
(** 6-12; every prescribed range must lie within it. *)
21
21
22
Added:
exception Invalid of error
23
Added:
22
24
val make :
23
25
delivery:delivery ->
24
26
rep_range:Units.Rep_range.t ->
25
27
allowed_substitutes:Exercise.t list ->
26
Removed:
(t, error) result
27
Removed:
(** Substitutions may only narrow catalog whitelists. *)
28
Added:
t
29
Added:
(** Raises [Invalid] for an invalid pairing, range, or substitution. *)
28
30
29
31
val delivery : t -> delivery
30
32
val rep_range : t -> Units.Rep_range.t
@@ -43,10 +45,11 @@
43
45
type id = private string
44
46
type error = Empty_workout
45
47
46
Removed:
val make :
47
Removed:
id:string -> name:string -> stimuli:Stimulus.t list -> (t, error) result
48
Removed:
(** Rejects an empty workout. *)
48
Added:
exception Invalid of error
49
49
50
Added:
val make : id:string -> name:string -> stimuli:Stimulus.t list -> t
51
Added:
(** Raises [Invalid Empty_workout] for an empty workout. *)
52
Added:
50
53
val id : t -> id
51
54
val name : t -> string
52
55
val stimuli : t -> Stimulus.t list
@@ -62,8 +65,10 @@
62
65
type t
63
66
type error = Empty_routine
64
67
65
Removed:
val make : name:string -> workouts:Workout.t list -> (t, error) result
66
Removed:
(** Rejects an empty routine. *)
68
Added:
exception Invalid of error
69
Added:
70
Added:
val make : name:string -> workouts:Workout.t list -> t
71
Added:
(** Raises [Invalid Empty_routine] for an empty routine. *)
67
72
68
73
val name : t -> string
69
74
lib/web/services.ml
@@ -107,16 +107,11 @@
107
107
match build_movement ~exercise ~load ~reps ~extension with
108
108
| Error detail ->
109
109
Lwt.return (Pages.problem ~title:"Unusable figures" ~detail)
110
Removed:
| Ok movement -> (
111
Removed:
match
112
Removed:
Evidence.Stimulus.make (Evidence.Stimulus.Single movement)
113
Removed:
with
114
Removed:
| Error e ->
115
Removed:
Lwt.return
116
Removed:
(Pages.problem ~title:"Could not record"
117
Removed:
~detail:
118
Removed:
(Format.asprintf "%a" Evidence.Stimulus.pp_error e))
119
Removed:
| Ok stimulus -> logged (Service.log service stimulus)))));
110
Added:
| Ok movement ->
111
Added:
logged
112
Added:
(Service.log service
113
Added:
(Evidence.Stimulus.make
114
Added:
(Evidence.Stimulus.Single movement))))));
120
115
121
116
Eliom_registration.Html.register ~service:Routes.log_pair
122
117
(fun
@@ -148,17 +143,11 @@
148
143
with
149
144
| Error detail, _ | _, Error detail ->
150
145
Lwt.return (Pages.problem ~title:"Unusable figures" ~detail)
151
Removed:
| Ok isolation, Ok compound -> (
152
Removed:
match
153
Removed:
Evidence.Stimulus.make
154
Removed:
(Evidence.Stimulus.Pre_exhaust { isolation; compound })
155
Removed:
with
156
Removed:
| Error e ->
157
Removed:
Lwt.return
158
Removed:
(Pages.problem ~title:"Could not record"
159
Removed:
~detail:
160
Removed:
(Format.asprintf "%a" Evidence.Stimulus.pp_error e))
161
Removed:
| Ok stimulus -> logged (Service.log service stimulus)))));
146
Added:
| Ok first, Ok second ->
147
Added:
logged
148
Added:
(Service.log service
149
Added:
(Evidence.Stimulus.make
150
Added:
(Evidence.Stimulus.Pair { first; second }))))));
162
151
163
152
Eliom_registration.Html.register ~service:Routes.finish (fun () () ->
164
153
match Service.finish service ~ended_at:(now ()) with
test/test_evidence.ml
@@ -20,13 +20,9 @@
20
20
Stimulus.Movement.make ~exercise:(get id) ~load:(kg load) ~reps:(reps r)
21
21
~outcome
22
22
23
Removed:
let single id load r = ok (Stimulus.make (Stimulus.Single (move id load r)))
23
Added:
let single id load r = Stimulus.make (Stimulus.Single (move id load r))
24
Added:
let pair first second = Stimulus.make (Stimulus.Pair { first; second })
24
25
25
Removed:
let pre_exhaust iso_m comp_m =
26
Removed:
ok
27
Removed:
(Stimulus.make
28
Removed:
(Stimulus.Pre_exhaust { isolation = iso_m; compound = comp_m }))
29
Removed:
30
26
(* {1 One stimulus} *)
31
27
32
28
let outcome_tests =
@@ -67,7 +63,7 @@
67
63
`Quick,
68
64
fun () ->
69
65
let s =
70
Removed:
pre_exhaust (move "dumbbell-flyes" 20. 9) (move "incline-press" 60. 7)
66
Added:
pair (move "dumbbell-flyes" 20. 9) (move "incline-press" 60. 7)
71
67
in
72
68
Alcotest.(check int)
73
69
"two movements" 2
@@ -76,28 +72,26 @@
76
72
"isolation leads"
77
73
[ "Dumbbell Flyes"; "Incline Presses" ]
78
74
(List.map Exercise.name (Stimulus.exercises s)) );
79
Removed:
( "a mislabelled pairing is refused",
75
Added:
( "an unrelated pair is recorded as performed",
80
76
`Quick,
81
77
fun () ->
82
Removed:
match
78
Added:
let s =
83
79
Stimulus.make
84
Removed:
(Stimulus.Pre_exhaust
80
Added:
(Stimulus.Pair
85
81
{
86
Removed:
isolation = move "dumbbell-flyes" 20. 9;
87
Removed:
compound = move "squats" 100. 8;
82
Added:
first = move "dumbbell-flyes" 20. 9;
83
Added:
second = move "squats" 100. 8;
88
84
})
89
Removed:
with
90
Removed:
| Error (Stimulus.Not_a_pre_exhaust { isolation; compound }) ->
91
Removed:
Alcotest.(check string)
92
Removed:
"isolation" "dumbbell-flyes"
93
Removed:
(isolation :> string);
94
Removed:
Alcotest.(check string) "compound" "squats" (compound :> string)
95
Removed:
| Ok _ -> Alcotest.fail "expected Not_a_pre_exhaust" );
85
Added:
in
86
Added:
Alcotest.(check (list string))
87
Added:
"both movements"
88
Added:
[ "Dumbbell Flyes"; "Squats" ]
89
Added:
(List.map Exercise.name (Stimulus.exercises s)) );
96
90
( "extensions are gathered across a pre-exhaust's movements",
97
91
`Quick,
98
92
fun () ->
99
93
let s =
100
Removed:
pre_exhaust
94
Added:
pair
101
95
(move "lying-french-press" 30. 9)
102
96
(move
103
97
~outcome:
@@ -121,9 +115,7 @@
121
115
~reps:(reps 10)
122
116
in
123
117
let s =
124
Removed:
ok
125
Removed:
(Stimulus.make ~warm_ups:[ w ]
126
Removed:
(Stimulus.Single (move "squats" 100. 8)))
118
Added:
Stimulus.make ~warm_ups:[ w ] (Stimulus.Single (move "squats" 100. 8))
127
119
in
128
120
Alcotest.(check int) "one warm-up" 1 (List.length (Stimulus.warm_ups s));
129
121
Alcotest.(check int)
@@ -148,10 +140,10 @@
148
140
(* HD1's Day 1, in the order it lists. *)
149
141
let day_one_stimuli =
150
142
[
151
Removed:
pre_exhaust (move "dumbbell-flyes" 20. 9) (move "incline-press" 60. 7);
143
Added:
pair (move "dumbbell-flyes" 20. 9) (move "incline-press" 60. 7);
152
144
single "laterals" 12. 8;
153
145
single "bent-over-laterals" 10. 9;
154
Removed:
pre_exhaust (move "lying-french-press" 30. 8) (move "dips" 0. 6);
146
Added:
pair (move "lying-french-press" 30. 8) (move "dips" 0. 6);
155
147
]
156
148
157
149
let perform stimuli =
@@ -229,11 +221,8 @@
229
221
with
230
222
| Error
231
223
(Workout.Delivery_mismatch
232
Removed:
{
233
Removed:
prescribed = Workout.As_pre_exhaust;
234
Removed:
logged = Workout.As_single;
235
Removed:
_;
236
Removed:
}) ->
224
Added:
{ prescribed = Workout.As_pair; logged = Workout.As_single; _ })
225
Added:
->
237
226
()
238
227
| _ -> Alcotest.fail "expected Delivery_mismatch" );
239
228
( "the prescribed pair, delivered as prescribed, is accepted",
@@ -243,7 +232,7 @@
243
232
"pec pair conforms" true
244
233
(Result.is_ok
245
234
(Workout.add_stimulus (fresh ())
246
Removed:
(pre_exhaust
235
Added:
(pair
247
236
(move "dumbbell-flyes" 20. 9)
248
237
(move "incline-press" 60. 7)))) );
249
238
( "an allowed substitute is accepted in its own role",
@@ -252,8 +241,7 @@
252
241
let w =
253
242
ok
254
243
(Workout.add_stimulus (fresh ())
255
Removed:
(pre_exhaust (move "pec-deck" 45. 9)
256
Removed:
(move "incline-press" 60. 7)))
244
Added:
(pair (move "pec-deck" 45. 9) (move "incline-press" 60. 7)))
257
245
in
258
246
Alcotest.(check int) "recorded" 1 (List.length (Workout.stimuli w));
259
247
Alcotest.(check int)
@@ -267,7 +255,7 @@
267
255
compound. *)
268
256
match
269
257
Workout.add_stimulus (fresh ())
270
Removed:
(pre_exhaust (move "dumbbell-flyes" 20. 9) (move "dips" 0. 7))
258
Added:
(pair (move "dumbbell-flyes" 20. 9) (move "dips" 0. 7))
271
259
with
272
260
| Error _ -> ()
273
261
| Ok _ -> Alcotest.fail "dips is not the prescribed pec compound" );
@@ -428,7 +416,7 @@
428
416
`Quick,
429
417
fun () ->
430
418
let pair =
431
Removed:
pre_exhaust (move "dumbbell-flyes" 20. 9) (move "incline-press" 60. 7)
419
Added:
pair (move "dumbbell-flyes" 20. 9) (move "incline-press" 60. 7)
432
420
in
433
421
let book =
434
422
Log.add Log.empty
test/test_prescription.ml
@@ -32,7 +32,7 @@
32
32
( "a single movement is prescribable",
33
33
`Quick,
34
34
fun () ->
35
Removed:
let p = ok (single "curls") in
35
Added:
let p = single "curls" in
36
36
match Prescription.Stimulus.delivery p with
37
37
| Prescription.Stimulus.Single e ->
38
38
Alcotest.(check string) "curls" "Curls" (Exercise.name e)
@@ -41,22 +41,24 @@
41
41
( "HD1's pec pre-exhaust is prescribable",
42
42
`Quick,
43
43
fun () ->
44
Removed:
let p =
45
Removed:
ok (pair ~isolation:"dumbbell-flyes" ~compound:"incline-press")
46
Removed:
in
44
Added:
let p = pair ~isolation:"dumbbell-flyes" ~compound:"incline-press" in
47
45
Alcotest.(check int)
48
46
"two movements, isolation first" 2
49
47
(List.length (Prescription.Stimulus.exercises p));
50
48
Alcotest.(check string)
51
49
"isolation leads" "Dumbbell Flyes"
52
50
(Exercise.name (List.hd (Prescription.Stimulus.exercises p))) );
53
Removed:
( "an invalid pairing is refused",
51
Added:
( "an invalid pairing raises Invalid",
54
52
`Quick,
55
53
fun () ->
56
Removed:
match pair ~isolation:"dumbbell-flyes" ~compound:"squats" with
57
Removed:
| Ok _ -> Alcotest.fail "expected Error"
58
Removed:
| Error (Prescription.Stimulus.Not_a_pre_exhaust _) -> ()
59
Removed:
| Error _ -> Alcotest.fail "expected Not_a_pre_exhaust" );
54
Added:
match
55
Added:
try
56
Added:
ignore (pair ~isolation:"dumbbell-flyes" ~compound:"squats");
57
Added:
None
58
Added:
with Prescription.Stimulus.Invalid error -> Some error
59
Added:
with
60
Added:
| Some (Prescription.Stimulus.Not_a_pre_exhaust _) -> ()
61
Added:
| _ -> Alcotest.fail "expected Not_a_pre_exhaust" );
60
62
]
61
63
62
64
let rep_window_tests =
@@ -77,27 +79,27 @@
77
79
fun () ->
78
80
List.iter
79
81
(fun (lo, hi) ->
80
Removed:
Alcotest.(check bool)
81
Removed:
(Printf.sprintf "%d-%d accepted" lo hi)
82
Removed:
true
83
Removed:
(Result.is_ok
84
Removed:
(Prescription.Stimulus.make
85
Removed:
~delivery:(Prescription.Stimulus.Single (get "curls"))
86
Removed:
~rep_range:(range lo hi) ~allowed_substitutes:[])))
82
Added:
ignore
83
Added:
(Prescription.Stimulus.make
84
Added:
~delivery:(Prescription.Stimulus.Single (get "curls"))
85
Added:
~rep_range:(range lo hi) ~allowed_substitutes:[]))
87
86
[ (6, 10); (6, 12); (8, 12); (8, 8) ] );
88
Removed:
( "a window escaping the limits is refused",
87
Added:
( "a window escaping the limits raises Invalid",
89
88
`Quick,
90
89
fun () ->
91
90
List.iter
92
91
(fun (lo, hi) ->
93
92
match
94
Removed:
Prescription.Stimulus.make
95
Removed:
~delivery:(Prescription.Stimulus.Single (get "curls"))
96
Removed:
~rep_range:(range lo hi) ~allowed_substitutes:[]
93
Added:
try
94
Added:
ignore
95
Added:
(Prescription.Stimulus.make
96
Added:
~delivery:(Prescription.Stimulus.Single (get "curls"))
97
Added:
~rep_range:(range lo hi) ~allowed_substitutes:[]);
98
Added:
None
99
Added:
with Prescription.Stimulus.Invalid error -> Some error
97
100
with
98
Removed:
| Error Prescription.Stimulus.Reps_outside_limits -> ()
99
Removed:
| Error _ -> Alcotest.fail "expected Reps_outside_limits"
100
Removed:
| Ok _ -> Alcotest.failf "%d-%d should be refused" lo hi)
101
Added:
| Some Prescription.Stimulus.Reps_outside_limits -> ()
102
Added:
| _ -> Alcotest.failf "%d-%d should be refused" lo hi)
101
103
[ (3, 5); (1, 3); (15, 20); (6, 20) ] );
102
104
]
103
105
@@ -107,10 +109,9 @@
107
109
`Quick,
108
110
fun () ->
109
111
let p =
110
Removed:
ok
111
Removed:
(prescribe
112
Removed:
~substitutes:[ get "pec-deck" ]
113
Removed:
(Prescription.Stimulus.Single (get "dumbbell-flyes")))
112
Added:
prescribe
113
Added:
~substitutes:[ get "pec-deck" ]
114
Added:
(Prescription.Stimulus.Single (get "dumbbell-flyes"))
114
115
in
115
116
Alcotest.(check bool)
116
117
"pec deck permitted" true
@@ -121,31 +122,32 @@
121
122
Alcotest.(check bool)
122
123
"squats not permitted" false
123
124
(Prescription.Stimulus.permits p (get "squats")) );
124
Removed:
( "a substitute off the catalog whitelist is refused",
125
Added:
( "a substitute off the catalog whitelist raises Invalid",
125
126
`Quick,
126
127
fun () ->
127
128
match
128
Removed:
prescribe
129
Removed:
~substitutes:[ get "squats" ]
130
Removed:
(Prescription.Stimulus.Single (get "dumbbell-flyes"))
129
Added:
try
130
Added:
ignore
131
Added:
(prescribe
132
Added:
~substitutes:[ get "squats" ]
133
Added:
(Prescription.Stimulus.Single (get "dumbbell-flyes")));
134
Added:
None
135
Added:
with Prescription.Stimulus.Invalid error -> Some error
131
136
with
132
Removed:
| Error (Prescription.Stimulus.Substitute_not_permitted id) ->
137
Added:
| Some (Prescription.Stimulus.Substitute_not_permitted id) ->
133
138
Alcotest.(check string) "squats" "squats" (id :> string)
134
Removed:
| Error _ -> Alcotest.fail "expected Substitute_not_permitted"
135
Removed:
| Ok _ -> Alcotest.fail "expected Error" );
139
Added:
| _ -> Alcotest.fail "expected Substitute_not_permitted" );
136
140
( "a substitute for either half of a pre-exhaust is allowed",
137
141
`Quick,
138
142
fun () ->
139
Removed:
Alcotest.(check bool)
140
Removed:
"pec deck substitutes the isolation" true
141
Removed:
(Result.is_ok
142
Removed:
(prescribe
143
Removed:
~substitutes:[ get "cable-crossovers" ]
144
Removed:
(Prescription.Stimulus.Pre_exhaust
145
Removed:
{
146
Removed:
isolation = get "dumbbell-flyes";
147
Removed:
compound = get "incline-press";
148
Removed:
}))) );
143
Added:
ignore
144
Added:
(prescribe
145
Added:
~substitutes:[ get "cable-crossovers" ]
146
Added:
(Prescription.Stimulus.Pre_exhaust
147
Added:
{
148
Added:
isolation = get "dumbbell-flyes";
149
Added:
compound = get "incline-press";
150
Added:
})) );
149
151
]
150
152
151
153
(* {1 One prescribed workout} *)
@@ -153,10 +155,10 @@
153
155
(* HD1's Day 1: pecs pre-exhaust, two delt isolations, triceps pre-exhaust. *)
154
156
let day_one_stimuli =
155
157
[
156
Removed:
ok (pair ~isolation:"dumbbell-flyes" ~compound:"incline-press");
157
Removed:
ok (single "laterals");
158
Removed:
ok (single "bent-over-laterals");
159
Removed:
ok (pair ~isolation:"lying-french-press" ~compound:"dips");
158
Added:
pair ~isolation:"dumbbell-flyes" ~compound:"incline-press";
159
Added:
single "laterals";
160
Added:
single "bent-over-laterals";
161
Added:
pair ~isolation:"lying-french-press" ~compound:"dips";
160
162
]
161
163
162
164
let workout_tests =
@@ -165,9 +167,8 @@
165
167
`Quick,
166
168
fun () ->
167
169
let w =
168
Removed:
ok
169
Removed:
(Prescription.Workout.make ~id:"ideal-day-1" ~name:"Day 1"
170
Removed:
~stimuli:day_one_stimuli)
170
Added:
Prescription.Workout.make ~id:"ideal-day-1" ~name:"Day 1"
171
Added:
~stimuli:day_one_stimuli
171
172
in
172
173
Alcotest.(check string) "name" "Day 1" (Prescription.Workout.name w);
173
174
Alcotest.(check string)
@@ -176,31 +177,34 @@
176
177
Alcotest.(check int)
177
178
"four stimuli" 4
178
179
(List.length (Prescription.Workout.stimuli w)) );
179
Removed:
( "an empty workout is refused",
180
Added:
( "an empty workout raises Invalid",
180
181
`Quick,
181
182
fun () ->
182
183
match
183
Removed:
Prescription.Workout.make ~id:"empty" ~name:"Nothing" ~stimuli:[]
184
Added:
try
185
Added:
ignore
186
Added:
(Prescription.Workout.make ~id:"empty" ~name:"Nothing" ~stimuli:[]);
187
Added:
false
188
Added:
with
189
Added:
| Prescription.Workout.Invalid Prescription.Workout.Empty_workout ->
190
Added:
true
184
191
with
185
Removed:
| Error Prescription.Workout.Empty_workout -> ()
186
Removed:
| Ok _ -> Alcotest.fail "expected Empty_workout" );
192
Added:
| true -> ()
193
Added:
| false -> Alcotest.fail "expected Empty_workout" );
187
194
( "equality is by id, not by content",
188
195
`Quick,
189
196
fun () ->
190
197
let a =
191
Removed:
ok
192
Removed:
(Prescription.Workout.make ~id:"day-1" ~name:"Day 1"
193
Removed:
~stimuli:day_one_stimuli)
198
Added:
Prescription.Workout.make ~id:"day-1" ~name:"Day 1"
199
Added:
~stimuli:day_one_stimuli
194
200
in
195
201
let renamed =
196
Removed:
ok
197
Removed:
(Prescription.Workout.make ~id:"day-1" ~name:"Renamed"
198
Removed:
~stimuli:[ ok (single "curls") ])
202
Added:
Prescription.Workout.make ~id:"day-1" ~name:"Renamed"
203
Added:
~stimuli:[ single "curls" ]
199
204
in
200
205
let other =
201
Removed:
ok
202
Removed:
(Prescription.Workout.make ~id:"day-2" ~name:"Day 1"
203
Removed:
~stimuli:day_one_stimuli)
206
Added:
Prescription.Workout.make ~id:"day-2" ~name:"Day 1"
207
Added:
~stimuli:day_one_stimuli
204
208
in
205
209
Alcotest.(check bool)
206
210
"same id" true
@@ -223,12 +227,19 @@
223
227
224
228
let routine_tests =
225
229
[
226
Removed:
( "an empty routine is refused",
230
Added:
( "an empty routine raises Invalid",
227
231
`Quick,
228
232
fun () ->
229
Removed:
match Prescription.Routine.make ~name:"Nothing" ~workouts:[] with
230
Removed:
| Error Prescription.Routine.Empty_routine -> ()
231
Removed:
| Ok _ -> Alcotest.fail "expected Empty_routine" );
233
Added:
match
234
Added:
try
235
Added:
ignore (Prescription.Routine.make ~name:"Nothing" ~workouts:[]);
236
Added:
false
237
Added:
with
238
Added:
| Prescription.Routine.Invalid Prescription.Routine.Empty_routine ->
239
Added:
true
240
Added:
with
241
Added:
| true -> ()
242
Added:
| false -> Alcotest.fail "expected Empty_routine" );
232
243
( "the Ideal Routine is HD1's three days",
233
244
`Quick,
234
245
fun () ->
@@ -308,9 +319,8 @@
308
319
`Quick,
309
320
fun () ->
310
321
let stranger =
311
Removed:
ok
312
Removed:
(Prescription.Workout.make ~id:"elsewhere" ~name:"Elsewhere"
313
Removed:
~stimuli:(Prescription.Workout.stimuli (nth 0)))
322
Added:
Prescription.Workout.make ~id:"elsewhere" ~name:"Elsewhere"
323
Added:
~stimuli:(Prescription.Workout.stimuli (nth 0))
314
324
in
315
325
Alcotest.(check string)
316
326
"falls back" "Day 1"
test/test_progression.ml
@@ -237,7 +237,7 @@
237
237
stimuli
238
238
239
239
let laterals ?outcome load r =
240
Removed:
ok (Stimulus.make (Stimulus.Single (move ?outcome ~id:"laterals" load r)))
240
Added:
Stimulus.make (Stimulus.Single (move ?outcome ~id:"laterals" load r))
241
241
242
242
let extended =
243
243
Stimulus.Beyond_failure (Stimulus.Forced_reps, [ Stimulus.Negatives ])
test/test_service.ml
@@ -23,22 +23,18 @@
23
23
Stimulus.Movement.make ~exercise:(get id) ~load:(kg load) ~reps:(reps r)
24
24
~outcome:Stimulus.Positive_failure
25
25
26
Removed:
let single id load r = ok (Stimulus.make (Stimulus.Single (move id load r)))
26
Added:
let single id load r = Stimulus.make (Stimulus.Single (move id load r))
27
Added:
let pair ~first ~second = Stimulus.make (Stimulus.Pair { first; second })
27
28
28
Removed:
let pair ~isolation ~compound =
29
Removed:
ok (Stimulus.make (Stimulus.Pre_exhaust { isolation; compound }))
30
Removed:
31
29
(* HD1's Day 1 as performed. *)
32
30
let day_one_stimuli =
33
31
[
34
32
pair
35
Removed:
~isolation:(move "dumbbell-flyes" 20. 9)
36
Removed:
~compound:(move "incline-press" 60. 7);
33
Added:
~first:(move "dumbbell-flyes" 20. 9)
34
Added:
~second:(move "incline-press" 60. 7);
37
35
single "laterals" 12. 8;
38
36
single "bent-over-laterals" 10. 9;
39
Removed:
pair
40
Removed:
~isolation:(move "lying-french-press" 30. 8)
41
Removed:
~compound:(move "dips" 0. 6);
37
Added:
pair ~first:(move "lying-french-press" 30. 8) ~second:(move "dips" 0. 6);
42
38
]
43
39
44
40
let routine_tests =