refactor separate prescription, logbook, and assessment concerns

The three were tangled: Progression both read the log and did the planning, Workout married the prescribed and logged states in one module (forcing a declaration order and an `item` hoist to work around it), and Prescription.evaluate put a relationship function on a plan type. Concerns are now one per module group, with dependencies flowing one way (Logbook -> Workout -> Routine -> Prescription): - prescription: Prescription (per-exercise atom, extracted from Routine), Routine (the split, cycling through Plans), Workout (today's plan made concrete with targets). None of these know what was performed. - logbook: Set, Set_group, and a new Logbook module. An entry holds the Workout it was logged against, and the logbook answers the history questions that previously had no home — evidence, completed_workouts for rotation, last_workout_at. - assessment: Progression is now the sole judge; classify and guidance moved here off the plan type. Recovery consumes it. Consequence, reversing an earlier decision: Workout.create takes evidence as performance samples rather than a lookup returning log entries. Since an entry holds its Workout, an entry-returning lookup would close a dependency cycle. Samples are also the honest input — a target depends on past performances, not on timestamps or set grouping. Also pared down per YAGNI: Set_group no longer models extra volume at all (strict adherence, so the acknowledgement escape hatch and the compliance predicate are both gone), superset is exactly two exercises, and the Version module is removed since it only ever proved the test harness worked. Repository.record loses its finished_at field, which duplicated what the entry already knows.

Commit
23d1566505f2a56ab4b2be8cfb83b1c2ec367f2a
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
bin/main.ml
index f961aad8..f1be9eab 100644..100644
@@ -1,1 +1,3 @@
1 Removed: let () = Printf.printf "hito %s\n" Hito_core.Version.version
1 Added: (* Placeholder entry point. This becomes the Ocsigen server launcher once the
2 Added: web tier is implemented. *)
3 Added: let () = print_endline "hito"
lib/app/memory_repo.ml
index ed009d8a..3f83b8eb 100644..100644
@@ -5,6 +5,6 @@
5 5 let create () = failwith "TODO"
6 6 let list_routines _ = failwith "TODO"
7 7 let find_routine _ _ = failwith "TODO"
8 Removed: let save_workout _ _ = failwith "TODO"
8 Added: let save _ _ = failwith "TODO"
9 Added: let logbook _ = failwith "TODO"
9 10 let history _ = failwith "TODO"
10 Removed: let last_workout_at _ = failwith "TODO"
lib/app/repository.ml
index 6cc93926..fbdd95cd 100644..100644
@@ -6,11 +6,7 @@
6 6 let routine_id s = s
7 7 let workout_id s = s
8 8
9 Removed: type record = {
10 Removed: id : workout_id;
11 Removed: workout : Hito_core.Workout.Logged.t;
12 Removed: finished_at : Hito_core.Recovery.timestamp;
13 Removed: }
9 Added: type record = { id : workout_id; entry : Hito_core.Logbook.Entry.t }
14 10 [@@warning "-69"]
15 11
16 12 module type S = sig
@@ -18,7 +14,7 @@
18 14
19 15 val list_routines : t -> (routine_id * Hito_core.Routine.t) list
20 16 val find_routine : t -> routine_id -> Hito_core.Routine.t option
21 Removed: val save_workout : t -> record -> unit
17 Added: val save : t -> record -> unit
18 Added: val logbook : t -> Hito_core.Logbook.t
22 19 val history : t -> record list
23 Removed: val last_workout_at : t -> Hito_core.Recovery.timestamp option
24 20 end
lib/app/repository.mli
index 8821776f..2158d769 100644..100644
@@ -1,6 +1,5 @@
1 1 (** Persistence port for the application layer. Pure module type: no database,
2 Removed: no Eliom. Identity for routines and finished workouts is assigned here,
3 Removed: since the pure core does not carry persistence ids. *)
2 Added: no Eliom. Identity is assigned here, since the pure core carries none. *)
4 3
5 4 type routine_id = private string
6 5 type workout_id = private string
@@ -8,18 +7,20 @@
8 7 val routine_id : string -> routine_id
9 8 val workout_id : string -> workout_id
10 9
11 Removed: type record = {
12 Removed: id : workout_id;
13 Removed: workout : Hito_core.Workout.Logged.t;
14 Removed: finished_at : Hito_core.Recovery.timestamp;
15 Removed: }
10 Added: type record = { id : workout_id; entry : Hito_core.Logbook.Entry.t }
11 Added: (** A stored logbook entry. The entry already knows when it finished and which
12 Added: prescription it was performed against. *)
16 13
17 14 module type S = sig
18 15 type t
19 16
20 17 val list_routines : t -> (routine_id * Hito_core.Routine.t) list
21 18 val find_routine : t -> routine_id -> Hito_core.Routine.t option
22 Removed: val save_workout : t -> record -> unit
19 Added: val save : t -> record -> unit
20 Added:
21 Added: val logbook : t -> Hito_core.Logbook.t
22 Added: (** The stored log, from which evidence and rotation position are derived. *)
23 Added:
23 24 val history : t -> record list
24 Removed: val last_workout_at : t -> Hito_core.Recovery.timestamp option
25 Added: (** Most recent first. *)
25 26 end
lib/app/service.ml
index 46a356ae..cbc8f800 100644..100644
@@ -13,7 +13,7 @@
13 13 let prescribe _ ~routine:_ ~substitutions:_ = failwith "TODO"
14 14
15 15 type start_outcome =
16 Removed: | Started of Workout.Logged.t
16 Added: | Started of Logbook.Entry.t
17 17 | Not_recovered of Recovery.error
18 18
19 19 let start _ _ = failwith "TODO"
lib/app/service.mli
index 4e41361f..7a504241 100644..100644
@@ -16,28 +16,28 @@
16 16 t ->
17 17 routine:Repository.routine_id ->
18 18 substitutions:(Exercise.id * Exercise.t) list ->
19 Removed: (Workout.Prescribed.t * Recovery.readiness, error) result
20 Removed: (** This session's plan, with targets drawn from stored history, alongside
21 Removed: current readiness so the caller can warn before starting. *)
19 Added: (Workout.t * Recovery.readiness, error) result
20 Added: (** Today's workout, with targets and rotation position drawn from the stored
21 Added: logbook, alongside current readiness so the caller can warn first. *)
22 22
23 Removed: (** Whether a workout could be started under the recovery window. *)
23 Added: (** Whether logging could begin under the recovery window. *)
24 24 type start_outcome =
25 Removed: | Started of Workout.Logged.t
25 Added: | Started of Logbook.Entry.t
26 26 | Not_recovered of Recovery.error
27 27 (** Retry via {!start_overriding} if the trainee insists. *)
28 28
29 Removed: val start : t -> Workout.Prescribed.t -> start_outcome
29 Added: val start : t -> Workout.t -> start_outcome
30 30
31 31 val start_overriding :
32 Removed: t -> Workout.Prescribed.t -> acknowledged:string -> Workout.Logged.t
33 Removed: (** Start despite an incomplete window, recording the acknowledgement. *)
32 Added: t -> Workout.t -> acknowledged:string -> Logbook.Entry.t
33 Added: (** Begin despite an incomplete window, recording the acknowledgement. *)
34 34
35 35 val log_group :
36 Removed: Workout.Logged.t ->
36 Added: Logbook.Entry.t ->
37 37 Set_group.t ->
38 Removed: (Workout.Logged.t, Workout.Logged.error) result
38 Added: (Logbook.Entry.t, Logbook.Entry.error) result
39 39
40 Removed: val finish : t -> Workout.Logged.t -> Repository.record
40 Added: val finish : t -> Logbook.Entry.t -> Repository.record
41 41 (** Complete, persist, and return the stored record. *)
42 42
43 43 val history : t -> Repository.record list
lib/core/logbook.ml
index 00000000..4d7b80a8 000000..100644
@@ -0,0 +1,31 @@
1 Added: (* Minimal stubs only; implementation deferred until after the review gate. *)
2 Added:
3 Added: module Entry = struct
4 Added: type t = unit
5 Added: type error = Set_exercise_not_prescribed of Exercise.id | Already_finished
6 Added:
7 Added: let start _ _ = failwith "TODO"
8 Added: let add_group _ _ = failwith "TODO"
9 Added: let finish _ _ = failwith "TODO"
10 Added: let is_finished _ = failwith "TODO"
11 Added: let workout _ = failwith "TODO"
12 Added: let session _ = failwith "TODO"
13 Added: let groups _ = failwith "TODO"
14 Added: let working_sets _ = failwith "TODO"
15 Added: let performances _ = failwith "TODO"
16 Added: let guidance _ = failwith "TODO"
17 Added: let unperformed _ = failwith "TODO"
18 Added: let volume _ = failwith "TODO"
19 Added: let duration _ = failwith "TODO"
20 Added: let pp _ _ = failwith "TODO"
21 Added: end
22 Added:
23 Added: type t = unit
24 Added:
25 Added: let empty = ()
26 Added: let add _ _ = failwith "TODO"
27 Added: let entries _ = failwith "TODO"
28 Added: let evidence _ _ = failwith "TODO"
29 Added: let completed_workouts _ ~routine:_ = failwith "TODO"
30 Added: let last_workout_at _ = failwith "TODO"
31 Added: let pp _ _ = failwith "TODO"
lib/core/logbook.mli
index 00000000..e154b1fe 000000..100644
@@ -0,0 +1,68 @@
1 Added: (** The training log: what was actually performed.
2 Added:
3 Added: Each entry records the {!Hito_core.Workout.t} it was performed against — a
4 Added: log without its prescription is meaningless. The logbook is also the source
5 Added: of the evidence future prescriptions are calculated from. *)
6 Added:
7 Added: (** One performed workout. *)
8 Added: module Entry : sig
9 Added: type t
10 Added: type error = Set_exercise_not_prescribed of Exercise.id | Already_finished
11 Added:
12 Added: val start : Workout.t -> Recovery.session -> t
13 Added:
14 Added: val add_group : t -> Set_group.t -> (t, error) result
15 Added: (** Append a performed group. Each working set's exercise is validated
16 Added: individually, since a group may span exercises. *)
17 Added:
18 Added: val finish : (module Recovery.CLOCK) -> t -> t
19 Added: (** Complete the entry and stamp the session's end. *)
20 Added:
21 Added: val is_finished : t -> bool
22 Added: val workout : t -> Workout.t
23 Added: val session : t -> Recovery.session
24 Added: val groups : t -> Set_group.t list
25 Added:
26 Added: val working_sets : t -> Set.Working.t list
27 Added: (** Every working set performed, in order. *)
28 Added:
29 Added: val performances : t -> (Exercise.id * Progression.sample) list
30 Added: (** Each working set's performance, keyed by its own exercise. *)
31 Added:
32 Added: val guidance : t -> (Exercise.id * Progression.guidance) list
33 Added: (** What was performed, judged against the prescription it was logged against.
34 Added: *)
35 Added:
36 Added: val unperformed : t -> Exercise.t list
37 Added: (** Prescribed exercises with no logged working set. *)
38 Added:
39 Added: val volume : t -> float
40 Added: (** Σ (load × reps). Diagnostic, not a target. *)
41 Added:
42 Added: val duration : t -> Recovery.duration option
43 Added: (** Wall-clock time, once finished. Heavy Duty prescribes brief workouts. *)
44 Added:
45 Added: val pp : Format.formatter -> t -> unit
46 Added: end
47 Added:
48 Added: type t
49 Added: (** A chronological log of finished entries. *)
50 Added:
51 Added: val empty : t
52 Added: val add : t -> Entry.t -> t
53 Added:
54 Added: val entries : t -> Entry.t list
55 Added: (** Most recent first. *)
56 Added:
57 Added: val evidence : t -> Exercise.id -> Progression.sample list
58 Added: (** An exercise's past performances, oldest-first — the input to
59 Added: {!Hito_core.Progression.prescribe}. *)
60 Added:
61 Added: val completed_workouts : t -> routine:string -> int
62 Added: (** How many workouts of a named routine have been logged; drives rotation via
63 Added: {!Hito_core.Routine.next_plan}. *)
64 Added:
65 Added: val last_workout_at : t -> Recovery.timestamp option
66 Added: (** When the most recent entry finished; feeds recovery gating. *)
67 Added:
68 Added: val pp : Format.formatter -> t -> unit
lib/core/prescription.ml
index 00000000..2e09916b 000000..100644
@@ -0,0 +1,9 @@
1 Added: (* Minimal stubs only; implementation deferred until after the review gate. *)
2 Added:
3 Added: type t = unit
4 Added:
5 Added: let make ~exercise:_ ~target_reps:_ ~allowed_substitutes:_ = failwith "TODO"
6 Added: let exercise _ = failwith "TODO"
7 Added: let target_reps _ = failwith "TODO"
8 Added: let allowed_substitutes _ = failwith "TODO"
9 Added: let pp _ _ = failwith "TODO"
lib/core/prescription.mli
index 00000000..3d88c911 000000..100644
@@ -0,0 +1,21 @@
1 Added: (** One exercise's plan: the movement, the target rep band, and which
2 Added: substitutes are permitted. Heavy Duty prescribes a single working set, so
3 Added: that is implicit and not configurable.
4 Added:
5 Added: Purely a plan — it knows nothing of what was performed. Judging performance
6 Added: against a prescription is {!Hito_core.Progression}'s job. *)
7 Added:
8 Added: type t
9 Added:
10 Added: val make :
11 Added: exercise:Exercise.t ->
12 Added: target_reps:Units.Rep_range.t ->
13 Added: allowed_substitutes:Exercise.t list ->
14 Added: (t, Exercise.error) result
15 Added: (** Substitutes must be on the exercise's catalog whitelist, so a prescription
16 Added: may only narrow the curated set. *)
17 Added:
18 Added: val exercise : t -> Exercise.t
19 Added: val target_reps : t -> Units.Rep_range.t
20 Added: val allowed_substitutes : t -> Exercise.t list
21 Added: val pp : Format.formatter -> t -> unit
lib/core/progression.ml
index fdbbc993..45357bc4 100644..100644
@@ -11,9 +11,6 @@
11 11 let evaluate ~history:_ = failwith "TODO"
12 12 let beats ~previous:_ ~current:_ = failwith "TODO"
13 13
14 Removed: type 'a prescribed = { value : 'a; evidence : sample list; status : t }
15 Removed: [@@warning "-69"]
16 Removed:
17 14 type band = Below_range | In_range | Above_range
18 15
19 16 let classify ~target_reps:_ _ = failwith "TODO"
@@ -21,6 +18,13 @@
21 18 type target =
22 19 | Add_reps of { load : Units.Weight.t; min_reps : Units.Reps.t }
23 20 | Add_load of { min_load : Units.Weight.t; reps : Units.Reps.t }
21 Added:
22 Added: type guidance = { band : band; next : target } [@@warning "-69"]
23 Added:
24 Added: let guide ~target_reps:_ _ = failwith "TODO"
25 Added:
26 Added: type 'a prescribed = { value : 'a; evidence : sample list; status : t }
27 Added: [@@warning "-69"]
24 28
25 29 let prescribe ~target_reps:_ ~evidence:_ = failwith "TODO"
26 30 let volume _ = failwith "TODO"
lib/core/progression.mli
index eac60c53..e46e82a7 100644..100644
@@ -1,8 +1,7 @@
1 Removed: (** Progression: what the evidence says about past performance, and what it
2 Removed: prescribes next. Distinct from {!Hito_core.Routine.Prescription} (the plan).
1 Added: (** Assessment: what the logged evidence says, and what it prescribes next.
3 2
4 Removed: Encodes the HD imperative — beat the last session; add reps within the
5 Removed: prescribed band, then add load. *)
3 Added: This is the only module that relates performance to plan. Prescriptions do
4 Added: not judge, and the logbook does not interpret. *)
6 5
7 6 type t = Progressing | Stalled
8 7
@@ -10,6 +9,8 @@
10 9 val pp : Format.formatter -> t -> unit
11 10
12 11 type sample = Set.Working.performance
12 Added: (** One working set's performance — the unit of evidence. *)
13 Added:
13 14 type error = Insufficient_data
14 15
15 16 val evaluate : history:sample list -> (t, error) result
@@ -18,18 +19,8 @@
18 19 val beats : previous:sample -> current:sample -> bool
19 20 (** Progressive overload: heavier, or equal load for more reps. *)
20 21
21 Removed: (** {1 Prescription from evidence}
22 Added: (** {1 Judging a performance against a band} *)
22 23
23 Removed: Anything prescribed is derived from logged evidence and carries both the
24 Removed: evidence and the status computed from it. *)
25 Removed:
26 Removed: type 'a prescribed = {
27 Removed: value : 'a;
28 Removed: evidence : sample list; (** Oldest-first history the value came from. *)
29 Removed: status : t; (** What that evidence showed. *)
30 Removed: }
31 Removed:
32 Removed: (** Where a performance fell relative to the prescribed band. *)
33 24 type band =
34 25 | Below_range (** Load too heavy. *)
35 26 | In_range
@@ -43,13 +34,23 @@
43 34 | Add_load of { min_load : Units.Weight.t; reps : Units.Reps.t }
44 35 (** Band exceeded: raise load, reset to the band's bottom. *)
45 36
37 Added: type guidance = { band : band; next : target }
38 Added: (** How one performance landed, and what follows from it. *)
39 Added:
40 Added: val guide : target_reps:Units.Rep_range.t -> sample -> guidance
41 Added:
42 Added: (** {1 Prescribing from evidence}
43 Added:
44 Added: Anything prescribed carries the evidence it came from and the status that
45 Added: evidence showed. *)
46 Added:
47 Added: type 'a prescribed = { value : 'a; evidence : sample list; status : t }
48 Added:
46 49 val prescribe :
47 50 target_reps:Units.Rep_range.t -> evidence:sample list -> target prescribed
48 Removed: (** The next target, justified by the evidence it was computed from. *)
49 51
50 52 (** {1 Metrics} *)
51 53
52 54 val volume : sample list -> float
53 55 (** Σ (load × reps), in kilogram-reps. A diagnostic, never a target: under Heavy
54 Removed: Duty rising volume is a warning, not an achievement. Progress is overload
55 Removed: ({!beats}); volume merely describes how much work it took. *)
56 Added: Duty rising volume is a warning, not an achievement. *)
lib/core/routine.ml
index dee8ae54..08cfeeaf 100644..100644
@@ -1,26 +1,22 @@
1 1 (* Minimal stubs only; implementation deferred until after the review gate. *)
2 2
3 Removed: module Prescription = struct
3 Added: module Plan = struct
4 4 type t = unit
5 Added: type error = Empty_plan
5 6
6 Removed: let make ~exercise:_ ~target_reps:_ ~allowed_substitutes:_ = failwith "TODO"
7 Removed: let exercise _ = failwith "TODO"
8 Removed: let target_reps _ = failwith "TODO"
9 Removed: let allowed_substitutes _ = failwith "TODO"
10 Removed:
11 Removed: type guidance = { band : Progression.band; next : Progression.target }
12 Removed: [@@warning "-69"]
13 Removed:
14 Removed: let evaluate _ ~performance:_ = failwith "TODO"
7 Added: let make ~name:_ ~prescriptions:_ = failwith "TODO"
8 Added: let name _ = failwith "TODO"
9 Added: let prescriptions _ = failwith "TODO"
15 10 let pp _ _ = failwith "TODO"
16 11 end
17 12
18 13 type t = unit
19 14 type error = Empty_routine
20 15
21 Removed: let make ~name:_ ~prescriptions:_ = failwith "TODO"
16 Added: let make ~name:_ ~plans:_ = failwith "TODO"
22 17 let name _ = failwith "TODO"
23 Removed: let prescriptions _ = failwith "TODO"
18 Added: let plans _ = failwith "TODO"
19 Added: let next_plan _ ~completed_workouts:_ = failwith "TODO"
24 20 let recovery_base _ = failwith "TODO"
25 21 let pp _ _ = failwith "TODO"
26 22 let ideal_routine = ()
lib/core/routine.mli
index a892c93f..bb2d0ea3 100644..100644
@@ -1,42 +1,39 @@
1 Removed: (** Routines and prescriptions.
1 Added: (** A routine is a {e sequence of workouts} cycled through over time — the Ideal
2 Added: Routine is a three-way split, the Consolidation Routine a two-way one.
2 3
3 Removed: A prescription is the plan for one exercise; a routine is an ordered list of
4 Removed: them. Heavy Duty prescribes exactly one working set per exercise — that is
5 Removed: nominal and not configurable. Deviation exists only at log time, as
6 Removed: acknowledged extra volume. *)
4 Added: Purely a template: it carries no targets and no history. *)
7 5
8 Removed: module Prescription : sig
6 Added: (** One workout within a routine, e.g. Mentzer's "Workout A". *)
7 Added: module Plan : sig
9 8 type t
9 Added: type error = Empty_plan
10 10
11 11 val make :
12 Removed: exercise:Exercise.t ->
13 Removed: target_reps:Units.Rep_range.t ->
14 Removed: allowed_substitutes:Exercise.t list ->
15 Removed: (t, Exercise.error) result
12 Added: name:string -> prescriptions:Prescription.t list -> (t, error) result
16 13
17 Removed: val exercise : t -> Exercise.t
18 Removed: val target_reps : t -> Units.Rep_range.t
19 Removed: val allowed_substitutes : t -> Exercise.t list
14 Added: val name : t -> string
20 15
21 Removed: type guidance = { band : Progression.band; next : Progression.target }
22 Removed: (** How a performance landed against the plan, and what follows from it. *)
16 Added: val prescriptions : t -> Prescription.t list
17 Added: (** In order; sequence matters for pre-exhaust pairings. *)
23 18
24 Removed: val evaluate : t -> performance:Progression.sample -> guidance
25 19 val pp : Format.formatter -> t -> unit
26 20 end
27 21
28 22 type t
29 Removed: (** Abstract routine: a named, non-empty, ordered list of prescriptions. *)
23 Added: (** A named, non-empty sequence of workouts. *)
30 24
31 25 type error = Empty_routine
32 26
33 Removed: val make : name:string -> prescriptions:Prescription.t list -> (t, error) result
27 Added: val make : name:string -> plans:Plan.t list -> (t, error) result
34 28 val name : t -> string
35 Removed: val prescriptions : t -> Prescription.t list
29 Added: val plans : t -> Plan.t list
36 30
31 Added: val next_plan : t -> completed_workouts:int -> Plan.t
32 Added: (** The workout due next, cycling through {!plans}. *)
33 Added:
37 34 val recovery_base : t -> Recovery.duration
38 Removed: (** The routine's nominal rest between sessions; Consolidation rests longer than
39 Removed: Ideal. Evidence adjusts it (see {!Hito_core.Recovery.prescribe}). *)
35 Added: (** Nominal rest between workouts; Consolidation rests longer than Ideal.
36 Added: Evidence adjusts it (see {!Hito_core.Recovery.prescribe}). *)
40 37
41 38 val pp : Format.formatter -> t -> unit
42 39
lib/core/set_group.ml
index ae5c3737..94ec4a1d 100644..100644
@@ -1,19 +1,18 @@
1 1 (* Minimal stubs only; implementation deferred until after the review gate. *)
2 2
3 3 type t = unit
4 Removed: type extra_volume = { sets : Set.Working.t list; acknowledged : string }
5 4
6 Removed: let straight ?warm_ups:_ ?extra:_ ~working:_ () = failwith "TODO"
7 Removed: let superset ?warm_ups:_ ~first:_ ~second:_ ?rest:_ () = failwith "TODO"
5 Added: let straight ?warm_ups:_ ~working:_ () = failwith "TODO"
6 Added: let superset ?warm_ups:_ ~first:_ ~second:_ () = failwith "TODO"
8 7 let pre_exhaust ?warm_ups:_ ~isolation:_ ~compound:_ () = failwith "TODO"
9 8
10 9 type view =
11 Removed: | Straight of {
10 Added: | Straight of { warm_ups : Set.Warm_up.t list; working : Set.Working.t }
11 Added: | Superset of {
12 12 warm_ups : Set.Warm_up.t list;
13 Removed: working : Set.Working.t;
14 Removed: extra : extra_volume option;
13 Added: first : Set.Working.t;
14 Added: second : Set.Working.t;
15 15 }
16 Removed: | Superset of { warm_ups : Set.Warm_up.t list; sets : Set.Working.t list }
17 16 | Pre_exhaust of {
18 17 warm_ups : Set.Warm_up.t list;
19 18 isolation : Set.Working.t;
@@ -23,6 +22,4 @@
23 22 let view _ = failwith "TODO"
24 23 let working_sets _ = failwith "TODO"
25 24 let warm_ups _ = failwith "TODO"
26 Removed: let extra_volume _ = failwith "TODO"
27 Removed: let is_high_intensity_compliant _ = failwith "TODO"
28 25 let pp _ _ = failwith "TODO"
lib/core/set_group.mli
index affb2f98..7613d119 100644..100644
@@ -1,31 +1,23 @@
1 1 (** How working sets are grouped and sequenced within a workout.
2 2
3 Removed: A group may span several exercises (superset, pre-exhaust), so the exercise
4 Removed: is a property of each individual set, never of the group. Each shape is
5 Removed: enforced by construction, so every constructor returns a plain group. *)
3 Added: A group may span exercises (superset, pre-exhaust), so the exercise belongs
4 Added: to each individual set, never to the group. Heavy Duty prescribes exactly
5 Added: one working set per exercise and this model admits no exceptions — extra
6 Added: volume is unrepresentable. *)
6 7
7 8 type t
8 9
9 Removed: type extra_volume = { sets : Set.Working.t list; acknowledged : string }
10 Removed: (** Heavy Duty prescribes a single working set. Extra sets are possible but
11 Removed: never silent — they must be acknowledged, as an early workout must be. *)
12 Removed:
13 10 val straight :
14 Removed: ?warm_ups:Set.Warm_up.t list ->
15 Removed: ?extra:extra_volume ->
16 Removed: working:Set.Working.t ->
17 Removed: unit ->
18 Removed: t
11 Added: ?warm_ups:Set.Warm_up.t list -> working:Set.Working.t -> unit -> t
19 12 (** One exercise: the working set, optionally preceded by a warm-up ramp. *)
20 13
21 14 val superset :
22 15 ?warm_ups:Set.Warm_up.t list ->
23 16 first:Set.Working.t ->
24 17 second:Set.Working.t ->
25 Removed: ?rest:Set.Working.t list ->
26 18 unit ->
27 19 t
28 Removed: (** Working sets across exercises, back-to-back. *)
20 Added: (** Two exercises' working sets, back-to-back. *)
29 21
30 22 val pre_exhaust :
31 23 ?warm_ups:Set.Warm_up.t list ->
@@ -39,12 +31,12 @@
39 31 (** {1 Inspection} *)
40 32
41 33 type view =
42 Removed: | Straight of {
34 Added: | Straight of { warm_ups : Set.Warm_up.t list; working : Set.Working.t }
35 Added: | Superset of {
43 36 warm_ups : Set.Warm_up.t list;
44 Removed: working : Set.Working.t;
45 Removed: extra : extra_volume option;
37 Added: first : Set.Working.t;
38 Added: second : Set.Working.t;
46 39 }
47 Removed: | Superset of { warm_ups : Set.Warm_up.t list; sets : Set.Working.t list }
48 40 | Pre_exhaust of {
49 41 warm_ups : Set.Warm_up.t list;
50 42 isolation : Set.Working.t;
@@ -57,9 +49,4 @@
57 49 (** Every working set, in order. Each carries its own exercise. *)
58 50
59 51 val warm_ups : t -> Set.Warm_up.t list
60 Removed: val extra_volume : t -> extra_volume option
61 Removed:
62 Removed: val is_high_intensity_compliant : t -> bool
63 Removed: (** [true] iff the group carries no extra volume. *)
64 Removed:
65 52 val pp : Format.formatter -> t -> unit
lib/core/version.ml
index 40403c67..00000000 100644..000000
@@ -1,1 +0,0 @@
1 Removed: let version = "0.1.0-dev"
lib/core/version.mli
index 75ff731e..00000000 100644..000000
@@ -1,3 +0,0 @@
1 Removed: (** Library metadata. *)
2 Removed:
3 Removed: val version : string
lib/core/workout.ml
index 269e434c..ec7c7eef 100644..100644
@@ -1,39 +1,21 @@
1 1 (* Minimal stubs only; implementation deferred until after the review gate. *)
2 2
3 3 type item = {
4 Removed: prescription : Routine.Prescription.t;
4 Added: prescription : Prescription.t;
5 5 exercise : Exercise.t;
6 6 target : Progression.target Progression.prescribed option;
7 7 }
8 8 [@@warning "-69"]
9 9
10 Removed: module Logged = struct
11 Removed: type t = unit
12 Removed: type error = Set_exercise_not_prescribed of Exercise.id | Already_finished
10 Added: type t = unit
11 Added: type error = Not_substitutable of Exercise.error
13 12
14 Removed: let add_group _ _ = failwith "TODO"
15 Removed: let finish _ _ = failwith "TODO"
16 Removed: let is_finished _ = failwith "TODO"
17 Removed: let session _ = failwith "TODO"
18 Removed: let items _ = failwith "TODO"
19 Removed: let groups _ = failwith "TODO"
20 Removed: let working_sets _ = failwith "TODO"
21 Removed: let performances _ = failwith "TODO"
22 Removed: let guidance _ = failwith "TODO"
23 Removed: let unperformed _ = failwith "TODO"
24 Removed: let volume _ = failwith "TODO"
25 Removed: let duration _ = failwith "TODO"
26 Removed: let pp _ _ = failwith "TODO"
27 Removed: end
13 Added: let create ~routine:_ ~completed_workouts:_ ~substitutions:_ ~evidence:_ =
14 Added: failwith "TODO"
28 15
29 Removed: module Prescribed = struct
30 Removed: type t = unit
31 Removed: type error = Not_substitutable of Exercise.error
32 Removed:
33 Removed: let create ~routine:_ ~substitutions:_ ~logs:_ = failwith "TODO"
34 Removed: let routine _ = failwith "TODO"
35 Removed: let items _ = failwith "TODO"
36 Removed: let recovery _ = failwith "TODO"
37 Removed: let start _ _ = failwith "TODO"
38 Removed: let pp _ _ = failwith "TODO"
39 Removed: end
16 Added: let routine _ = failwith "TODO"
17 Added: let plan _ = failwith "TODO"
18 Added: let items _ = failwith "TODO"
19 Added: let prescribes _ _ = failwith "TODO"
20 Added: let recovery _ = failwith "TODO"
21 Added: let pp _ _ = failwith "TODO"
lib/core/workout.mli
index 50aebff3..fe7ba091 100644..100644
@@ -1,80 +1,39 @@
1 Removed: (** A workout in its two states: {!Prescribed} (this session's plan) and
2 Removed: {!Logged} (what happened). Distinct types with a one-way transition, so a
3 Removed: plan can never be mistaken for a record. *)
1 Added: (** Today's workout: the routine's next plan made concrete, with a target per
2 Added: exercise derived from past evidence.
4 3
4 Added: Still a plan, not a record — performing it produces a
5 Added: {!Hito_core.Logbook.Entry}. Targets come from evidence rather than log
6 Added: entries, which keeps this module independent of the logbook. *)
7 Added:
5 8 type item = {
6 Removed: prescription : Routine.Prescription.t;
9 Added: prescription : Prescription.t;
7 10 exercise : Exercise.t;
8 11 (** The prescribed movement, or a chosen substitute. *)
9 12 target : Progression.target Progression.prescribed option;
10 13 (** [None] with no prior evidence. *)
11 14 }
12 Removed: (** One exercise's plan for this session. Shared by both states: {!Prescribed}
13 Removed: produces items, {!Logged} retains them to judge what was performed. *)
14 15
15 Removed: (** The record of a performed session. Defined before {!Prescribed} because a
16 Removed: prescription is calculated from past logs. *)
17 Removed: module Logged : sig
18 Removed: type t
19 Removed: type error = Set_exercise_not_prescribed of Exercise.id | Already_finished
16 Added: type t
17 Added: type error = Not_substitutable of Exercise.error
20 18
21 Removed: val add_group : t -> Set_group.t -> (t, error) result
22 Removed: (** Append a performed group. Each working set's exercise is validated
23 Removed: individually, since a group may span exercises. *)
19 Added: val create :
20 Added: routine:Routine.t ->
21 Added: completed_workouts:int ->
22 Added: substitutions:(Exercise.id * Exercise.t) list ->
23 Added: evidence:(Exercise.id -> Progression.sample list) ->
24 Added: (t, error) result
25 Added: (** Instantiate the routine's next workout, selected by rotation from
26 Added: [completed_workouts]. [evidence] yields an exercise's past performances,
27 Added: from which its target is calculated. *)
24 28
25 Removed: val finish : (module Recovery.CLOCK) -> t -> t
26 Removed: (** Complete the workout and stamp the session's end. *)
29 Added: val routine : t -> Routine.t
30 Added: val plan : t -> Routine.Plan.t
31 Added: val items : t -> item list
27 32
28 Removed: val is_finished : t -> bool
29 Removed: val session : t -> Recovery.session
30 Removed: val items : t -> item list
31 Removed: val groups : t -> Set_group.t list
33 Added: val prescribes : t -> Exercise.id -> bool
34 Added: (** Whether [id] is the movement of one of this workout's items. *)
32 35
33 Removed: val working_sets : t -> Set.Working.t list
34 Removed: (** Every working set performed, in order. *)
36 Added: val recovery : t -> Recovery.duration Progression.prescribed
37 Added: (** The rest window prescribed before the next workout. *)
35 38
36 Removed: val performances : t -> (Exercise.id * Progression.sample) list
37 Removed: (** Each working set's performance, keyed by its own exercise. *)
38 Removed:
39 Removed: val guidance : t -> (Exercise.id * Routine.Prescription.guidance) list
40 Removed: (** What was performed, judged against the plan. *)
41 Removed:
42 Removed: val unperformed : t -> Exercise.t list
43 Removed: (** Prescribed exercises with no logged working set. *)
44 Removed:
45 Removed: (** {1 Metrics} *)
46 Removed:
47 Removed: val volume : t -> float
48 Removed: (** Σ (load × reps) across all working sets. Diagnostic, not a target. *)
49 Removed:
50 Removed: val duration : t -> Recovery.duration option
51 Removed: (** Wall-clock time, once finished. Heavy Duty prescribes brief workouts, so a
52 Removed: shorter session at equal or better overload is favourable. *)
53 Removed:
54 Removed: val pp : Format.formatter -> t -> unit
55 Removed: end
56 Removed:
57 Removed: (** This session's plan: a routine instantiated with targets drawn from logs. *)
58 Removed: module Prescribed : sig
59 Removed: type t
60 Removed: type error = Not_substitutable of Exercise.error
61 Removed:
62 Removed: val create :
63 Removed: routine:Routine.t ->
64 Removed: substitutions:(Exercise.id * Exercise.t) list ->
65 Removed: logs:(Exercise.id -> Logged.t list) ->
66 Removed: (t, error) result
67 Removed: (** Instantiate [routine] for this session. [logs] yields an exercise's past
68 Removed: workouts, the evidence its target is calculated from. *)
69 Removed:
70 Removed: val routine : t -> Routine.t
71 Removed: val items : t -> item list
72 Removed:
73 Removed: val recovery : t -> Recovery.duration Progression.prescribed
74 Removed: (** The rest window prescribed before the next session. *)
75 Removed:
76 Removed: val start : t -> Recovery.session -> Logged.t
77 Removed: (** Begin logging against this plan. *)
78 Removed:
79 Removed: val pp : Format.formatter -> t -> unit
80 Removed: end
39 Added: val pp : Format.formatter -> t -> unit
lib/web/pages.ml
index 239ca2b2..d46010f5 100644..100644
@@ -1,6 +1,6 @@
1 1 (* Minimal stubs only; implementation deferred until after the review gate. *)
2 2
3 3 let choose_routine ~routines:_ = failwith "TODO"
4 Removed: let log_workout ~workout:_ = failwith "TODO"
4 Added: let log_workout ~entry:_ = failwith "TODO"
5 5 let history ~records:_ = failwith "TODO"
6 6 let recovery_warning ~error:_ = failwith "TODO"
lib/web/pages.mli
index 7a8661f1..93cc7dd5 100644..100644
@@ -8,7 +8,8 @@
8 8 Html_types.html Eliom_content.Html.elt
9 9
10 10 val log_workout :
11 Removed: workout:Hito_core.Workout.Logged.t -> Html_types.html Eliom_content.Html.elt
11 Added: entry:Hito_core.Logbook.Entry.t -> Html_types.html Eliom_content.Html.elt
12 Added: (** The active-logging page for the entry in progress. *)
12 13
13 14 val history :
14 15 records:Repository.record list -> Html_types.html Eliom_content.Html.elt
test/test_hito.ml
index 682b382e..a076cdba 100644..100644
@@ -1,13 +1,8 @@
1 Removed: (** Test harness entry point for hito.
1 Added: (** Test harness entry point.
2 2
3 Removed: As the project grows, per-module test suites are registered here. For Task 1
4 Removed: this contains a single trivial test proving the Alcotest harness is wired
5 Removed: correctly. *)
3 Added: Per-module suites are registered here as their implementations land
4 Added: ([Test_units.suite], [Test_exercise.suite], and so on). Until then this
5 Added: smoke test confirms the harness links against the library. *)
6 6
7 Removed: let test_version_is_nonempty () =
8 Removed: Alcotest.(check bool)
9 Removed: "version string is non-empty" true
10 Removed: (String.length Hito_core.Version.version > 0)
11 Removed:
12 Removed: let harness_suite = [ ("version", `Quick, test_version_is_nonempty) ]
13 Removed: let () = Alcotest.run "hito" [ ("harness", harness_suite) ]
7 Added: let smoke = [ ("links", `Quick, fun () -> ()) ]
8 Added: let () = Alcotest.run "hito" [ ("harness", smoke) ]
test/test_routine.ml
index 15ab9fa6..3809ec56 100644..100644
@@ -1,29 +1,46 @@
1 1 (** Unit tests for {!Hito_core.Routine}, authored against routine.mli.
2 2
3 Removed: NOTE: Not yet registered in the main runner; wired in during implementation
4 Removed: (Task 8). routine.mli is the source of truth over these assertions.
3 Added: NOTE: Not yet registered in the main runner; wired in during implementation.
4 Added: routine.mli is the source of truth over these assertions.
5 5
6 Removed: Under test: the HD presets build and are non-empty, a routine derives its
7 Removed: trained muscle groups, and a prescription cannot widen the catalog's
8 Removed: substitution whitelist. *)
6 Added: A routine is a sequence of workouts rotated through, not a flat list of
7 Added: exercises. *)
9 8
10 9 open Hito_core
11 10
12 11 let preset_tests =
13 12 [
14 Removed: ( "ideal routine has prescriptions",
13 Added: ( "ideal routine cycles several workouts",
15 14 `Quick,
16 15 fun () ->
17 16 Alcotest.(check bool)
18 Removed: "non-empty" true
19 Removed: (List.length (Routine.prescriptions Routine.ideal_routine) > 0) );
20 Removed: ( "consolidation routine has prescriptions",
17 Added: "more than one workout" true
18 Added: (List.length (Routine.plans Routine.ideal_routine) > 1) );
19 Added: ( "consolidation routine has workouts",
21 20 `Quick,
22 21 fun () ->
23 22 Alcotest.(check bool)
24 23 "non-empty" true
25 Removed: (List.length (Routine.prescriptions Routine.consolidation_routine) > 0)
26 Removed: );
24 Added: (List.length (Routine.plans Routine.consolidation_routine) > 0) );
25 Added: ( "every workout prescribes at least one exercise",
26 Added: `Quick,
27 Added: fun () ->
28 Added: List.iter
29 Added: (fun plan ->
30 Added: Alcotest.(check bool)
31 Added: "plan non-empty" true
32 Added: (List.length (Routine.Plan.prescriptions plan) > 0))
33 Added: (Routine.plans Routine.ideal_routine) );
34 Added: ( "rotation wraps around the cycle",
35 Added: `Quick,
36 Added: fun () ->
37 Added: let r = Routine.ideal_routine in
38 Added: let n = List.length (Routine.plans r) in
39 Added: let first = Routine.next_plan r ~completed_workouts:0 in
40 Added: let wrapped = Routine.next_plan r ~completed_workouts:n in
41 Added: Alcotest.(check string)
42 Added: "same workout after a full cycle" (Routine.Plan.name first)
43 Added: (Routine.Plan.name wrapped) );
27 44 ]
28 45
29 46 let make_tests =
@@ -33,7 +50,14 @@
33 50 fun () ->
34 51 Alcotest.(check bool)
35 52 "empty rejected" true
36 Removed: (Result.is_error (Routine.make ~name:"Empty" ~prescriptions:[])) );
53 Added: (Result.is_error (Routine.make ~name:"Empty" ~plans:[])) );
54 Added: ( "empty workout rejected",
55 Added: `Quick,
56 Added: fun () ->
57 Added: Alcotest.(check bool)
58 Added: "empty rejected" true
59 Added: (Result.is_error
60 Added: (Routine.Plan.make ~name:"Workout A" ~prescriptions:[])) );
37 61 ]
38 62
39 63 let suite = [ ("routine.presets", preset_tests); ("routine.make", make_tests) ]