feat implement Plan, rotation, and the HD presets

next_plan indexes into plans with a doubly-wrapped modulo, so it is total for any completed_workouts including negative values, not just the non-negative counts the app layer is expected to supply. Ideal Routine: the three-way split from HD1, each workout built around a pre-exhaustion pairing (isolation immediately preceding the compound for the same target) plus 6-8 rep prescriptions with substitutes drawn from the catalog's whitelists. Consolidation Routine: two workouts of two compound lifts each, resting a week rather than four days. Also implements Recovery's time primitives (timestamp, duration, seconds conversions) since Routine.recovery_base needed Recovery.days before Recovery's own turn. Sessions, readiness, and prescribe remain stubbed for that turn.

Commit
598ab5b44299bc3a1254fed6b0f9fbc1ec53ffa8
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/core/recovery.ml
index e59d34ef..b235d07d 100644..100644
@@ -1,21 +1,22 @@
1 Removed: (* Minimal stubs only; implementation deferred until after the review gate. *)
1 Added: (* Time primitives are implemented here since Routine needs them to seed its
2 Added: presets. Sessions, readiness, and prescribe are implemented on Recovery's
3 Added: own turn. *)
2 4
3 5 type timestamp = int
4 6
5 Removed: let timestamp_of_unix_seconds _ = failwith "TODO"
6 Removed: let timestamp_to_unix_seconds _ = failwith "TODO"
7 Added: let timestamp_of_unix_seconds s = s
8 Added: let timestamp_to_unix_seconds t = t
7 9
8 10 type duration = int
9 11
10 Removed: let hours _ = failwith "TODO"
11 Removed: let days _ = failwith "TODO"
12 Removed: let duration_to_seconds _ = failwith "TODO"
12 Added: let hours n = n * 3600
13 Added: let days n = n * 86400
14 Added: let duration_to_seconds d = d
15 Added: let prescribe ~base:_ ~evidence:_ = failwith "TODO"
13 16
14 17 module type CLOCK = sig
15 18 val now : unit -> timestamp
16 19 end
17 Removed:
18 Removed: let prescribe ~base:_ ~evidence:_ = failwith "TODO"
19 20
20 21 type readiness =
21 22 | Ready
lib/core/routine.ml
index 08cfeeaf..5338df78 100644..100644
@@ -1,23 +1,135 @@
1 Removed: (* Minimal stubs only; implementation deferred until after the review gate. *)
2 Removed:
3 1 module Plan = struct
4 Removed: type t = unit
2 Added: type t = { name : string; prescriptions : Prescription.t list }
5 3 type error = Empty_plan
6 4
7 Removed: let make ~name:_ ~prescriptions:_ = failwith "TODO"
8 Removed: let name _ = failwith "TODO"
9 Removed: let prescriptions _ = failwith "TODO"
10 Removed: let pp _ _ = failwith "TODO"
5 Added: let make ~name ~prescriptions =
6 Added: match prescriptions with
7 Added: | [] -> Error Empty_plan
8 Added: | _ -> Ok { name; prescriptions }
9 Added:
10 Added: let name t = t.name
11 Added: let prescriptions t = t.prescriptions
12 Added:
13 Added: let pp fmt t =
14 Added: Format.fprintf fmt "%s: %a" t.name
15 Added: (Format.pp_print_list
16 Added: ~pp_sep:(fun fmt () -> Format.fprintf fmt ", ")
17 Added: Prescription.pp)
18 Added: t.prescriptions
11 19 end
12 20
13 Removed: type t = unit
21 Added: type t = {
22 Added: name : string;
23 Added: plans : Plan.t list;
24 Added: recovery_base : Recovery.duration;
25 Added: }
26 Added:
14 27 type error = Empty_routine
15 28
16 Removed: let make ~name:_ ~plans:_ = failwith "TODO"
17 Removed: let name _ = failwith "TODO"
18 Removed: let plans _ = failwith "TODO"
19 Removed: let next_plan _ ~completed_workouts:_ = failwith "TODO"
20 Removed: let recovery_base _ = failwith "TODO"
21 Removed: let pp _ _ = failwith "TODO"
22 Removed: let ideal_routine = ()
23 Removed: let consolidation_routine = ()
29 Added: let make ~name ~plans =
30 Added: match plans with
31 Added: | [] -> Error Empty_routine
32 Added: | _ -> Ok { name; plans; recovery_base = Recovery.days 4 }
33 Added:
34 Added: let name t = t.name
35 Added: let plans t = t.plans
36 Added:
37 Added: let next_plan t ~completed_workouts =
38 Added: let n = List.length t.plans in
39 Added: List.nth t.plans (((completed_workouts mod n) + n) mod n)
40 Added:
41 Added: let recovery_base t = t.recovery_base
42 Added:
43 Added: let pp fmt t =
44 Added: Format.fprintf fmt "%s (%d workouts)" t.name (List.length t.plans)
45 Added:
46 Added: (* {1 Heavy Duty presets} *)
47 Added:
48 Added: let by_name n =
49 Added: match
50 Added: List.find_opt (fun ex -> String.equal (Exercise.name ex) n) Exercise.catalog
51 Added: with
52 Added: | Some ex -> ex
53 Added: | None -> invalid_arg (Printf.sprintf "no catalog exercise named %s" n)
54 Added:
55 Added: let ok = function
56 Added: | Ok v -> v
57 Added: | Error _ -> invalid_arg "invalid preset definition"
58 Added:
59 Added: let band lo hi =
60 Added: ok
61 Added: (Units.Rep_range.make
62 Added: ~min:(ok (Units.Reps.of_int lo))
63 Added: ~max:(ok (Units.Reps.of_int hi)))
64 Added:
65 Added: let prescribe ?(substitutes = []) exercise_name lo hi =
66 Added: ok
67 Added: (Prescription.make ~exercise:(by_name exercise_name)
68 Added: ~target_reps:(band lo hi)
69 Added: ~allowed_substitutes:(List.map by_name substitutes))
70 Added:
71 Added: let plan name prescriptions = ok (Plan.make ~name ~prescriptions)
72 Added:
73 Added: (* Mentzer's Ideal Routine: a three-way split built around pre-exhaustion
74 Added: supersets — an isolation movement immediately preceding the compound for
75 Added: the same target, so the larger muscle is the limiting factor. *)
76 Added: let ideal_routine =
77 Added: let workout_a =
78 Added: plan "Workout A: Chest, Shoulders, Triceps"
79 Added: [
80 Added: prescribe "Chest Flye" 6 8;
81 Added: prescribe "Barbell Bench Press" 6 8
82 Added: ~substitutes:[ "Dumbbell Bench Press" ];
83 Added: prescribe "Lateral Raise" 6 8;
84 Added: prescribe "Overhead Press" 6 8
85 Added: ~substitutes:[ "Dumbbell Shoulder Press" ];
86 Added: prescribe "Triceps Pushdown" 6 8 ~substitutes:[ "Skullcrusher" ];
87 Added: ]
88 Added: in
89 Added: let workout_b =
90 Added: plan "Workout B: Back, Biceps"
91 Added: [
92 Added: prescribe "Pulldown" 6 8 ~substitutes:[ "Pull-Up" ];
93 Added: prescribe "Barbell Row" 6 8
94 Added: ~substitutes:[ "Dumbbell Row"; "Seated Cable Row" ];
95 Added: prescribe "Barbell Curl" 6 8 ~substitutes:[ "Dumbbell Curl" ];
96 Added: ]
97 Added: in
98 Added: let workout_c =
99 Added: plan "Workout C: Legs"
100 Added: [
101 Added: prescribe "Leg Extension" 6 8;
102 Added: prescribe "Back Squat" 6 8 ~substitutes:[ "Leg Press" ];
103 Added: prescribe "Leg Curl" 6 8;
104 Added: prescribe "Calf Raise" 6 8;
105 Added: ]
106 Added: in
107 Added: {
108 Added: name = "Ideal Routine";
109 Added: plans = [ workout_a; workout_b; workout_c ];
110 Added: recovery_base = Recovery.days 4;
111 Added: }
112 Added:
113 Added: (* The Consolidation Routine: the minimalist variant — a handful of compound
114 Added: movements, one working set each, trained infrequently. *)
115 Added: let consolidation_routine =
116 Added: let workout_a =
117 Added: plan "Workout A"
118 Added: [
119 Added: prescribe "Back Squat" 6 8 ~substitutes:[ "Leg Press" ];
120 Added: prescribe "Pulldown" 6 8;
121 Added: ]
122 Added: in
123 Added: let workout_b =
124 Added: plan "Workout B"
125 Added: [
126 Added: prescribe "Barbell Bench Press" 6 8
127 Added: ~substitutes:[ "Dumbbell Bench Press" ];
128 Added: prescribe "Deadlift" 6 8;
129 Added: ]
130 Added: in
131 Added: {
132 Added: name = "Consolidation Routine";
133 Added: plans = [ workout_a; workout_b ];
134 Added: recovery_base = Recovery.days 7;
135 Added: }
test/test_hito.ml
index 2d3807a8..99d34ffc 100644..100644
@@ -4,4 +4,4 @@
4 4 let () =
5 5 Alcotest.run "hito"
6 6 (Test_units.suite @ Test_exercise.suite @ Test_set.suite
7 Removed: @ Test_set_group.suite @ Test_prescription.suite)
7 Added: @ Test_set_group.suite @ Test_prescription.suite @ Test_routine.suite)