[OCaml] High Intensity Training Online
feat implement the curated catalog
23 movements spanning the major HD patterns (press, row, pulldown, overhead press, curl, triceps, squat, hinge, isolation). Substitution whitelists are symmetric where the swap is genuinely equivalent (barbell vs dumbbell bench press) and empty where an exercise has no close substitute in this catalog (lateral raise, leg extension, crunch). The whitelist is stored as a list of ids and resolved against the catalog on demand, rather than embedding t values directly, since the catalog is itself a list of t and embedding would make it self-referential before OCaml has finished constructing it.
Changed files
lib/core/exercise.ml
@@ -1,17 +1,102 @@
1
Removed:
(* Minimal stubs only; implementation deferred until after the review gate. *)
2
Removed:
3
1
type id = string
4
Removed:
type t = unit
5
2
6
Removed:
let id _ = failwith "TODO"
7
Removed:
let name _ = failwith "TODO"
8
Removed:
let equal _ _ = failwith "TODO"
9
Removed:
let pp _ _ = failwith "TODO"
10
Removed:
let catalog = []
11
Removed:
let find _ = failwith "TODO"
3
Added:
type t = {
4
Added:
id : id;
5
Added:
name : string;
6
Added:
substitutes : id list;
7
Added:
(** Whitelist, by id; resolved lazily against [catalog]. *)
8
Added:
}
12
9
10
Added:
let id t = t.id
11
Added:
let name t = t.name
12
Added:
let equal a b = String.equal a.id b.id
13
Added:
let pp fmt t = Format.pp_print_string fmt t.name
14
Added:
15
Added:
(* The curated catalog. Substitution whitelists group movements the author
16
Added:
judges close enough in pattern and target to stand in for one another —
17
Added:
e.g. a dumbbell press for a barbell press on the same plane. *)
18
Added:
let catalog =
19
Added:
[
20
Added:
{
21
Added:
id = "barbell_bench_press";
22
Added:
name = "Barbell Bench Press";
23
Added:
substitutes = [ "dumbbell_bench_press" ];
24
Added:
};
25
Added:
{
26
Added:
id = "dumbbell_bench_press";
27
Added:
name = "Dumbbell Bench Press";
28
Added:
substitutes = [ "barbell_bench_press" ];
29
Added:
};
30
Added:
{ id = "incline_press"; name = "Incline Press"; substitutes = [] };
31
Added:
{ id = "chest_flye"; name = "Chest Flye"; substitutes = [] };
32
Added:
{
33
Added:
id = "barbell_row";
34
Added:
name = "Barbell Row";
35
Added:
substitutes = [ "dumbbell_row"; "seated_cable_row" ];
36
Added:
};
37
Added:
{
38
Added:
id = "dumbbell_row";
39
Added:
name = "Dumbbell Row";
40
Added:
substitutes = [ "barbell_row"; "seated_cable_row" ];
41
Added:
};
42
Added:
{
43
Added:
id = "seated_cable_row";
44
Added:
name = "Seated Cable Row";
45
Added:
substitutes = [ "barbell_row"; "dumbbell_row" ];
46
Added:
};
47
Added:
{ id = "pulldown"; name = "Pulldown"; substitutes = [ "pull_up" ] };
48
Added:
{ id = "pull_up"; name = "Pull-Up"; substitutes = [ "pulldown" ] };
49
Added:
{
50
Added:
id = "overhead_press";
51
Added:
name = "Overhead Press";
52
Added:
substitutes = [ "dumbbell_shoulder_press" ];
53
Added:
};
54
Added:
{
55
Added:
id = "dumbbell_shoulder_press";
56
Added:
name = "Dumbbell Shoulder Press";
57
Added:
substitutes = [ "overhead_press" ];
58
Added:
};
59
Added:
{ id = "lateral_raise"; name = "Lateral Raise"; substitutes = [] };
60
Added:
{
61
Added:
id = "barbell_curl";
62
Added:
name = "Barbell Curl";
63
Added:
substitutes = [ "dumbbell_curl" ];
64
Added:
};
65
Added:
{
66
Added:
id = "dumbbell_curl";
67
Added:
name = "Dumbbell Curl";
68
Added:
substitutes = [ "barbell_curl" ];
69
Added:
};
70
Added:
{
71
Added:
id = "triceps_pushdown";
72
Added:
name = "Triceps Pushdown";
73
Added:
substitutes = [ "skullcrusher" ];
74
Added:
};
75
Added:
{
76
Added:
id = "skullcrusher";
77
Added:
name = "Skullcrusher";
78
Added:
substitutes = [ "triceps_pushdown" ];
79
Added:
};
80
Added:
{ id = "back_squat"; name = "Back Squat"; substitutes = [ "leg_press" ] };
81
Added:
{ id = "leg_press"; name = "Leg Press"; substitutes = [ "back_squat" ] };
82
Added:
{ id = "leg_extension"; name = "Leg Extension"; substitutes = [] };
83
Added:
{ id = "leg_curl"; name = "Leg Curl"; substitutes = [] };
84
Added:
{ id = "deadlift"; name = "Deadlift"; substitutes = [ "leg_press" ] };
85
Added:
{ id = "calf_raise"; name = "Calf Raise"; substitutes = [] };
86
Added:
{ id = "crunch"; name = "Crunch"; substitutes = [] };
87
Added:
]
88
Added:
89
Added:
let find target_id =
90
Added:
List.find_opt (fun ex -> String.equal ex.id target_id) catalog
91
Added:
13
92
type error = Not_permitted of { original : id; candidate : id }
14
93
15
Removed:
let permitted_substitutes _ = failwith "TODO"
16
Removed:
let may_substitute ~original:_ ~candidate:_ = failwith "TODO"
17
Removed:
let substitute ~original:_ ~candidate:_ = failwith "TODO"
94
Added:
let permitted_substitutes t = List.filter_map find t.substitutes
95
Added:
96
Added:
let may_substitute ~original ~candidate =
97
Added:
List.mem candidate.id original.substitutes
98
Added:
99
Added:
let substitute ~original ~candidate =
100
Added:
if may_substitute ~original ~candidate then Ok candidate
101
Added:
else
102
Added:
Error (Not_permitted { original = original.id; candidate = candidate.id })
test/test_hito.ml
@@ -1,4 +1,4 @@
1
1
(** Test harness entry point. Per-module suites are registered here as their
2
2
implementations land. *)
3
3
4
Removed:
let () = Alcotest.run "hito" Test_units.suite
4
Added:
let () = Alcotest.run "hito" (Test_units.suite @ Test_exercise.suite)