refactor fix interface coherency issues found in final review

Structural: Workout.Logged must know its plan to validate exercises and report unperformed ones, but Logged is defined before Prescribed (which takes a lookup returning Logged.t). Its implementation therefore could not have referenced Prescribed.t — the stubs compiled but the real code could not. Hoist `item` to the module's top level so Logged retains the plan without depending on Prescribed. Regression: restore Routine.Prescription.evaluate and its guidance type, dropped by accident when set_scheme was removed. Compliance is no longer part of guidance — that is a Set_group property, queried separately. app/service: the interface skipped the Prescribed state entirely, returning a Logged workout straight from start_workout, which the two-state model makes impossible. Split into prescribe (returning the plan plus current readiness) then start / start_overriding, and give the module a proper error type instead of a bare polymorphic variant. Naming: Recovery.elapsed meant workout length while Recovering's `elapsed` field meant time rested. Renamed to session_duration and `rested`. Cleanup: Units errors carry no string payloads (steering forbids string errors, and the constructor already says what is wrong); trimmed its docs, which predated the lean-documentation convention; dropped stale references to build tasks and to the rejected intensity metric.

Commit
ff6c84c735a73eef0590182fd190787a75794e8a
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
dune-project
index 7e01d86d..b991e218 100644..100644
@@ -7,9 +7,9 @@
7 7 (source
8 8 (github username/reponame))
9 9
10 Removed: (authors "Author Name <author@example.com>")
10 Added: (authors "Marius Peter <hito@marius-peter.com>")
11 11
12 Removed: (maintainers "Maintainer Name <maintainer@example.com>")
12 Added: (maintainers "Marius Peter <hito@marius-peter.com>")
13 13
14 14 (license LICENSE)
15 15
@@ -17,7 +17,7 @@
17 17
18 18 (package
19 19 (name hito)
20 Removed: (synopsis "A short synopsis")
20 Added: (synopsis "High Intensity Trainer Online")
21 21 (description "A longer description")
22 22 (depends ocaml)
23 23 (tags
hito.opam
index a43bbcb1..5dac731c 100644..100644
@@ -1,9 +1,9 @@
1 1 # This file is generated by dune, edit dune-project instead
2 2 opam-version: "2.0"
3 Removed: synopsis: "A short synopsis"
3 Added: synopsis: "High Intensity Trainer Online"
4 4 description: "A longer description"
5 Removed: maintainer: ["Maintainer Name <maintainer@example.com>"]
6 Removed: authors: ["Author Name <author@example.com>"]
5 Added: maintainer: ["Marius Peter <hito@marius-peter.com>"]
6 Added: authors: ["Marius Peter <hito@marius-peter.com>"]
7 7 license: "LICENSE"
8 8 tags: ["add topics" "to describe" "your" "project"]
9 9 homepage: "https://github.com/username/reponame"
lib/app/service.ml
index 99464e04..46a356ae 100644..100644
@@ -1,22 +1,24 @@
1 1 (* Minimal stubs only; implementation deferred until after the review gate. *)
2 2
3 Added: open Hito_core
4 Added:
3 5 module Make (R : Repository.S) = struct
4 Removed: type t = { repo : R.t; clock : (module Hito_core.Recovery.CLOCK) }
5 Removed: [@@warning "-69"]
6 Added: type t = { repo : R.t; clock : (module Recovery.CLOCK) } [@@warning "-69"]
6 7
7 8 let make ~repo ~clock = { repo; clock }
8 9 let list_routines _ = failwith "TODO"
9 10
10 Removed: type start_outcome =
11 Removed: | Started of Hito_core.Workout.Logged.t
12 Removed: | Recovery_warning of Hito_core.Recovery.error
11 Added: type error = Unknown_routine | Not_substitutable of Exercise.error
13 12
14 Removed: let start_workout _ ~routine:_ ~recommended:_ = failwith "TODO"
13 Added: let prescribe _ ~routine:_ ~substitutions:_ = failwith "TODO"
15 14
16 Removed: let start_overriding _ ~routine:_ ~recommended:_ ~acknowledged:_ =
17 Removed: failwith "TODO"
15 Added: type start_outcome =
16 Added: | Started of Workout.Logged.t
17 Added: | Not_recovered of Recovery.error
18 18
19 Added: let start _ _ = failwith "TODO"
20 Added: let start_overriding _ _ ~acknowledged:_ = failwith "TODO"
19 21 let log_group _ _ = failwith "TODO"
20 Removed: let finish_workout _ _ = failwith "TODO"
22 Added: let finish _ _ = failwith "TODO"
21 23 let history _ = failwith "TODO"
22 24 end
lib/app/service.mli
index 6a51adc7..4e41361f 100644..100644
@@ -2,34 +2,43 @@
2 2 clock. The API the web (and future native) layer calls; no Eliom or
3 3 serialization concerns. *)
4 4
5 Added: open Hito_core
6 Added:
5 7 module Make (R : Repository.S) : sig
6 8 type t
7 9
8 Removed: val make : repo:R.t -> clock:(module Hito_core.Recovery.CLOCK) -> t
9 Removed: val list_routines : t -> (Repository.routine_id * Hito_core.Routine.t) list
10 Added: val make : repo:R.t -> clock:(module Recovery.CLOCK) -> t
11 Added: val list_routines : t -> (Repository.routine_id * Routine.t) list
10 12
11 Removed: type start_outcome =
12 Removed: | Started of Hito_core.Workout.Logged.t
13 Removed: | Recovery_warning of Hito_core.Recovery.error
13 Added: type error = Unknown_routine | Not_substitutable of Exercise.error
14 14
15 Removed: val start_workout :
15 Added: val prescribe :
16 16 t ->
17 17 routine:Repository.routine_id ->
18 Removed: recommended:Hito_core.Recovery.duration ->
19 Removed: (start_outcome, [ `Unknown_routine ]) result
18 Added: substitutions:(Exercise.id * Exercise.t) list ->
19 Added: (Workout.Prescribed.t * Recovery.readiness, error) result
20 Added: (** This session's plan, with targets drawn from stored history, alongside
21 Added: current readiness so the caller can warn before starting. *)
20 22
23 Added: (** Whether a workout could be started under the recovery window. *)
24 Added: type start_outcome =
25 Added: | Started of Workout.Logged.t
26 Added: | Not_recovered of Recovery.error
27 Added: (** Retry via {!start_overriding} if the trainee insists. *)
28 Added:
29 Added: val start : t -> Workout.Prescribed.t -> start_outcome
30 Added:
21 31 val start_overriding :
22 Removed: t ->
23 Removed: routine:Repository.routine_id ->
24 Removed: recommended:Hito_core.Recovery.duration ->
25 Removed: acknowledged:string ->
26 Removed: (Hito_core.Workout.Logged.t, [ `Unknown_routine ]) result
32 Added: t -> Workout.Prescribed.t -> acknowledged:string -> Workout.Logged.t
33 Added: (** Start despite an incomplete window, recording the acknowledgement. *)
27 34
28 35 val log_group :
29 Removed: Hito_core.Workout.Logged.t ->
30 Removed: Hito_core.Set_group.t ->
31 Removed: (Hito_core.Workout.Logged.t, Hito_core.Workout.Logged.error) result
36 Added: Workout.Logged.t ->
37 Added: Set_group.t ->
38 Added: (Workout.Logged.t, Workout.Logged.error) result
32 39
33 Removed: val finish_workout : t -> Hito_core.Workout.Logged.t -> Repository.record
40 Added: val finish : t -> Workout.Logged.t -> Repository.record
41 Added: (** Complete, persist, and return the stored record. *)
42 Added:
34 43 val history : t -> Repository.record list
35 44 end
lib/core/recovery.ml
index c19bcbef..e59d34ef 100644..100644
@@ -19,7 +19,7 @@
19 19
20 20 type readiness =
21 21 | Ready
22 Removed: | Recovering of { elapsed : duration; recommended : duration }
22 Added: | Recovering of { rested : duration; recommended : duration }
23 23
24 24 let evaluate_readiness ~now:_ ~last_workout:_ ~recommended:_ = failwith "TODO"
25 25 let is_ready _ = failwith "TODO"
@@ -37,6 +37,6 @@
37 37 let end_session _ _ = failwith "TODO"
38 38 let started_at _ = failwith "TODO"
39 39 let ended_at _ = failwith "TODO"
40 Removed: let elapsed _ = failwith "TODO"
40 Added: let session_duration _ = failwith "TODO"
41 41 let overridden_recovery _ = failwith "TODO"
42 42 let pp_session _ _ = failwith "TODO"
lib/core/recovery.mli
index fe79b16b..33ffb903 100644..100644
@@ -38,7 +38,8 @@
38 38
39 39 type readiness =
40 40 | Ready
41 Removed: | Recovering of { elapsed : duration; recommended : duration }
41 Added: | Recovering of { rested : duration; recommended : duration }
42 Added: (** [rested] of [recommended] has passed. *)
42 43
43 44 val evaluate_readiness :
44 45 now:timestamp -> last_workout:timestamp -> recommended:duration -> readiness
@@ -72,8 +73,8 @@
72 73 val started_at : session -> timestamp
73 74 val ended_at : session -> timestamp option
74 75
75 Removed: val elapsed : session -> duration option
76 Removed: (** Wall-clock duration, once ended. Feeds the intensity metric. *)
76 Added: val session_duration : session -> duration option
77 Added: (** Wall-clock length of the workout, once ended. *)
77 78
78 79 val overridden_recovery : session -> override_reason option
79 80 (** [Some _] iff started early. *)
lib/core/routine.ml
index f3622e5c..dee8ae54 100644..100644
@@ -7,6 +7,11 @@
7 7 let exercise _ = failwith "TODO"
8 8 let target_reps _ = failwith "TODO"
9 9 let allowed_substitutes _ = failwith "TODO"
10 Added:
11 Added: type guidance = { band : Progression.band; next : Progression.target }
12 Added: [@@warning "-69"]
13 Added:
14 Added: let evaluate _ ~performance:_ = failwith "TODO"
10 15 let pp _ _ = failwith "TODO"
11 16 end
12 17
lib/core/routine.mli
index 40f99b1b..a892c93f 100644..100644
@@ -17,6 +17,11 @@
17 17 val exercise : t -> Exercise.t
18 18 val target_reps : t -> Units.Rep_range.t
19 19 val allowed_substitutes : t -> Exercise.t list
20 Added:
21 Added: type guidance = { band : Progression.band; next : Progression.target }
22 Added: (** How a performance landed against the plan, and what follows from it. *)
23 Added:
24 Added: val evaluate : t -> performance:Progression.sample -> guidance
20 25 val pp : Format.formatter -> t -> unit
21 26 end
22 27
lib/core/units.ml
index d5d8abda..6e43a753 100644..100644
@@ -1,11 +1,6 @@
1 Removed: (* Minimal stubs only. Real implementation is deferred until the .mli review
2 Removed: gate (Task 7) is passed. The types are given concrete-but-placeholder
3 Removed: definitions so the module type-checks against units.mli. *)
1 Added: (* Minimal stubs only; implementation deferred until after the review gate. *)
4 2
5 Removed: type error =
6 Removed: | Negative of string
7 Removed: | Not_positive of string
8 Removed: | Inverted_range of string
3 Added: type error = Negative | Not_positive | Inverted_range
9 4
10 5 let pp_error _ _ = failwith "TODO"
11 6
lib/core/units.mli
index 9089302d..ac26c979 100644..100644
@@ -1,105 +1,46 @@
1 Removed: (** Core measurement types for hito.
1 Added: (** Physical quantities recorded during training.
2 2
3 Removed: These types model the physical quantities recorded during training: the load
4 Removed: on the bar ({!Weight}), the number of repetitions performed ({!Reps}), and a
5 Removed: prescribed target repetition band ({!Rep_range}, e.g. Mentzer's 6-8).
3 Added: Every type here is abstract and constructible only through a smart
4 Added: constructor, so an invalid measurement cannot exist. *)
6 5
7 Removed: Every type in this module is [abstract]: values can only be created through
8 Removed: smart constructors that reject nonsensical inputs (negative loads, zero or
9 Removed: negative reps, inverted ranges). Once you hold a value of one of these
10 Removed: types, it is guaranteed to be valid — illegal measurements are
11 Removed: unrepresentable.
12 Removed:
13 Removed: This module is part of the pure domain core: it has no dependency on any web
14 Removed: framework, database, or serialization library. *)
15 Removed:
16 Removed: (** A validation error explaining why a smart constructor rejected its input.
17 Removed: Rendered for developers and, where appropriate, surfaced to users. *)
18 6 type error =
19 Removed: | Negative of string (** A quantity that must be >= 0 was negative. *)
20 Removed: | Not_positive of string (** A quantity that must be > 0 was <= 0. *)
21 Removed: | Inverted_range of string
22 Removed: (** A range whose lower bound exceeded its upper bound. *)
7 Added: | Negative (** A quantity that must be >= 0 was negative, or not finite. *)
8 Added: | Not_positive (** A quantity that must be > 0 was <= 0. *)
9 Added: | Inverted_range (** A range's lower bound exceeded its upper bound. *)
23 10
24 11 val pp_error : Format.formatter -> error -> unit
25 Removed: (** [pp_error fmt e] pretty-prints a human-readable description of [e]. *)
26 12
27 Removed: (** Barbell / dumbbell load, stored internally in kilograms.
28 Removed:
29 Removed: Weight is a nonnegative quantity: a body-weight movement may legitimately
30 Removed: carry a load of [0.0], but a negative load is never valid. *)
13 Added: (** Load in kilograms. Nonnegative: body-weight movements are [zero]. *)
31 14 module Weight : sig
32 15 type t
33 Removed: (** An abstract, always-valid weight. *)
34 16
35 17 val of_kg : float -> (t, error) result
36 Removed: (** [of_kg kg] is [Ok w] when [kg] is finite and [>= 0.0], otherwise
37 Removed: [Error (Negative _)]. *)
38 Removed:
39 18 val to_kg : t -> float
40 Removed: (** [to_kg w] is the load of [w] expressed in kilograms. *)
41 Removed:
42 19 val zero : t
43 Removed: (** [zero] is a load of [0.0] kg, for body-weight movements. *)
44 Removed:
45 20 val compare : t -> t -> int
46 Removed: (** Total ordering by kilograms. *)
47 Removed:
48 21 val equal : t -> t -> bool
49 Removed: (** [equal a b] is [true] when [a] and [b] denote the same load. *)
50 Removed:
51 22 val pp : Format.formatter -> t -> unit
52 Removed: (** Pretty-prints a weight, e.g. ["60.0 kg"]. *)
53 23 end
54 24
55 Removed: (** A count of completed repetitions.
56 Removed:
57 Removed: Reps are a strictly positive whole number: a logged set has at least one
58 Removed: rep. (An abandoned attempt is modelled elsewhere, not as zero reps.) *)
25 Added: (** A completed repetition count. Strictly positive. *)
59 26 module Reps : sig
60 27 type t
61 Removed: (** An abstract, always-valid repetition count. *)
62 28
63 29 val of_int : int -> (t, error) result
64 Removed: (** [of_int n] is [Ok r] when [n > 0], otherwise [Error (Not_positive _)]. *)
65 Removed:
66 30 val to_int : t -> int
67 Removed: (** [to_int r] is the underlying repetition count. *)
68 Removed:
69 31 val compare : t -> t -> int
70 Removed: (** Total ordering by count. *)
71 Removed:
72 32 val equal : t -> t -> bool
73 Removed: (** [equal a b] is [true] when [a] and [b] denote the same count. *)
74 Removed:
75 33 val pp : Format.formatter -> t -> unit
76 Removed: (** Pretty-prints a rep count, e.g. ["8 reps"]. *)
77 34 end
78 35
79 Removed: (** An inclusive target repetition band, such as Mentzer's canonical 6-8.
80 Removed:
81 Removed: A range is valid when its lower bound is [<=] its upper bound; both bounds
82 Removed: are themselves valid {!Reps.t} values (hence strictly positive). *)
36 Added: (** An inclusive target rep band, such as Mentzer's canonical 6-8. *)
83 37 module Rep_range : sig
84 38 type t
85 Removed: (** An abstract, always-valid inclusive rep range. *)
86 39
87 40 val make : min:Reps.t -> max:Reps.t -> (t, error) result
88 Removed: (** [make ~min ~max] is [Ok range] when [min <= max], otherwise
89 Removed: [Error (Inverted_range _)]. *)
90 Removed:
91 41 val min : t -> Reps.t
92 Removed: (** The inclusive lower bound. *)
93 Removed:
94 42 val max : t -> Reps.t
95 Removed: (** The inclusive upper bound. *)
96 Removed:
97 43 val contains : t -> Reps.t -> bool
98 Removed: (** [contains range r] is [true] when [r] falls within [range] inclusive. *)
99 Removed:
100 44 val equal : t -> t -> bool
101 Removed: (** [equal a b] is [true] when [a] and [b] have equal bounds. *)
102 Removed:
103 45 val pp : Format.formatter -> t -> unit
104 Removed: (** Pretty-prints a range, e.g. ["6-8 reps"]. *)
105 46 end
lib/core/version.mli
index 31b9b94a..75ff731e 100644..100644
@@ -1,5 +1,3 @@
1 Removed: (** Library metadata. A trivial module used to validate the build and test
2 Removed: harness during Task 1. *)
1 Added: (** Library metadata. *)
3 2
4 3 val version : string
5 Removed: (** The current version string of the hito core library. *)
lib/core/workout.ml
index ba9875fa..269e434c 100644..100644
@@ -1,5 +1,12 @@
1 1 (* Minimal stubs only; implementation deferred until after the review gate. *)
2 2
3 Added: type item = {
4 Added: prescription : Routine.Prescription.t;
5 Added: exercise : Exercise.t;
6 Added: target : Progression.target Progression.prescribed option;
7 Added: }
8 Added: [@@warning "-69"]
9 Added:
3 10 module Logged = struct
4 11 type t = unit
5 12 type error = Set_exercise_not_prescribed of Exercise.id | Already_finished
@@ -8,9 +15,11 @@
8 15 let finish _ _ = failwith "TODO"
9 16 let is_finished _ = failwith "TODO"
10 17 let session _ = failwith "TODO"
18 Added: let items _ = failwith "TODO"
11 19 let groups _ = failwith "TODO"
12 20 let working_sets _ = failwith "TODO"
13 21 let performances _ = failwith "TODO"
22 Added: let guidance _ = failwith "TODO"
14 23 let unperformed _ = failwith "TODO"
15 24 let volume _ = failwith "TODO"
16 25 let duration _ = failwith "TODO"
@@ -19,20 +28,12 @@
19 28
20 29 module Prescribed = struct
21 30 type t = unit
22 Removed:
23 Removed: type item = {
24 Removed: prescription : Routine.Prescription.t;
25 Removed: exercise : Exercise.t;
26 Removed: target : Progression.target Progression.prescribed option;
27 Removed: }
28 Removed: [@@warning "-69"]
29 Removed:
30 31 type error = Not_substitutable of Exercise.error
31 32
32 33 let create ~routine:_ ~substitutions:_ ~logs:_ = failwith "TODO"
33 34 let routine _ = failwith "TODO"
34 35 let items _ = failwith "TODO"
35 36 let recovery _ = failwith "TODO"
36 Removed: let start _ _ _ = failwith "TODO"
37 Added: let start _ _ = failwith "TODO"
37 38 let pp _ _ = failwith "TODO"
38 39 end
lib/core/workout.mli
index afe41ffd..50aebff3 100644..100644
@@ -2,19 +2,24 @@
2 2 {!Logged} (what happened). Distinct types with a one-way transition, so a
3 3 plan can never be mistaken for a record. *)
4 4
5 Removed: (** The record of a performed session. Defined first because a prescription is
6 Removed: derived from past logs. *)
5 Added: type item = {
6 Added: prescription : Routine.Prescription.t;
7 Added: exercise : Exercise.t;
8 Added: (** The prescribed movement, or a chosen substitute. *)
9 Added: target : Progression.target Progression.prescribed option;
10 Added: (** [None] with no prior evidence. *)
11 Added: }
12 Added: (** One exercise's plan for this session. Shared by both states: {!Prescribed}
13 Added: produces items, {!Logged} retains them to judge what was performed. *)
14 Added:
15 Added: (** The record of a performed session. Defined before {!Prescribed} because a
16 Added: prescription is calculated from past logs. *)
7 17 module Logged : sig
8 18 type t
19 Added: type error = Set_exercise_not_prescribed of Exercise.id | Already_finished
9 20
10 Removed: type error =
11 Removed: | Set_exercise_not_prescribed of Exercise.id
12 Removed: (** A set's exercise is neither prescribed nor a permitted substitute.
13 Removed: *)
14 Removed: | Already_finished
15 Removed:
16 21 val add_group : t -> Set_group.t -> (t, error) result
17 Removed: (** Append a performed group. Every working set's exercise is validated
22 Added: (** Append a performed group. Each working set's exercise is validated
18 23 individually, since a group may span exercises. *)
19 24
20 25 val finish : (module Recovery.CLOCK) -> t -> t
@@ -22,6 +27,7 @@
22 27
23 28 val is_finished : t -> bool
24 29 val session : t -> Recovery.session
30 Added: val items : t -> item list
25 31 val groups : t -> Set_group.t list
26 32
27 33 val working_sets : t -> Set.Working.t list
@@ -30,10 +36,13 @@
30 36 val performances : t -> (Exercise.id * Progression.sample) list
31 37 (** Each working set's performance, keyed by its own exercise. *)
32 38
39 Added: val guidance : t -> (Exercise.id * Routine.Prescription.guidance) list
40 Added: (** What was performed, judged against the plan. *)
41 Added:
33 42 val unperformed : t -> Exercise.t list
34 43 (** Prescribed exercises with no logged working set. *)
35 44
36 Removed: (** {1 Workout-level metrics} *)
45 Added: (** {1 Metrics} *)
37 46
38 47 val volume : t -> float
39 48 (** Σ (load × reps) across all working sets. Diagnostic, not a target. *)
@@ -45,20 +54,9 @@
45 54 val pp : Format.formatter -> t -> unit
46 55 end
47 56
48 Removed: (** This session's plan: a routine instantiated with targets derived from logs.
49 Removed: *)
57 Added: (** This session's plan: a routine instantiated with targets drawn from logs. *)
50 58 module Prescribed : sig
51 59 type t
52 Removed:
53 Removed: type item = {
54 Removed: prescription : Routine.Prescription.t;
55 Removed: exercise : Exercise.t;
56 Removed: (** The prescribed movement, or a chosen substitute. *)
57 Removed: target : Progression.target Progression.prescribed option;
58 Removed: (** [None] with no prior evidence. *)
59 Removed: }
60 Removed: (** One exercise's plan for this session. *)
61 Removed:
62 60 type error = Not_substitutable of Exercise.error
63 61
64 62 val create :
@@ -66,8 +64,8 @@
66 64 substitutions:(Exercise.id * Exercise.t) list ->
67 65 logs:(Exercise.id -> Logged.t list) ->
68 66 (t, error) result
69 Removed: (** Instantiate [routine] for this session. [logs] yields the past logged
70 Removed: workouts for an exercise, from which its target is calculated. *)
67 Added: (** Instantiate [routine] for this session. [logs] yields an exercise's past
68 Added: workouts, the evidence its target is calculated from. *)
71 69
72 70 val routine : t -> Routine.t
73 71 val items : t -> item list
@@ -75,7 +73,7 @@
75 73 val recovery : t -> Recovery.duration Progression.prescribed
76 74 (** The rest window prescribed before the next session. *)
77 75
78 Removed: val start : (module Recovery.CLOCK) -> t -> Recovery.session -> Logged.t
76 Added: val start : t -> Recovery.session -> Logged.t
79 77 (** Begin logging against this plan. *)
80 78
81 79 val pp : Format.formatter -> t -> unit