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.

Commit
84a6a7d032c8ff67a7773053bb5af72b1da5378a
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/core/exercise.ml
index 91f8e452..b255071d 100644..100644
@@ -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
index 05e5b5f6..0eb391ec 100644..100644
@@ -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)