[OCaml] High Intensity Training Online
docs Apply STE100 to code comments
Split every prose semicolon in a comment or doc-comment into two sentences across lib/ and bin/, keeping semicolons that belong to OCaml code (record and list syntax, sequence expressions, string literals). No code, type, or interface declaration changed; the edits are wording only. dune build @check and @fmt stay clean.
Changed files
- bin/main.ml
- lib/app/service.ml
- lib/app/service.mli
- lib/app/trainee.mli
- lib/core/evidence.ml
- lib/core/evidence.mli
- lib/core/prescription.mli
- lib/core/progression.ml
- lib/core/progression.mli
- lib/persistence/codec.ml
- lib/persistence/codec.mli
- lib/persistence/migrations.ml
- lib/web/decode.ml
- lib/web/decode.mli
- lib/web/handlers.ml
- lib/web/handlers.mli
- lib/web/pages.ml
- lib/web/pages.mli
- lib/web/view_model.ml
- lib/web/workout_client.ml
bin/main.ml
@@ -8,12 +8,12 @@
8
8
unset, a random secret is generated for this run only, so
9
9
sessions do not survive a restart. Set it in production.
10
10
- HITO_PORT TCP port. Default: 8080
11
Removed:
- HITO_SEED_PASSWORD Password for the seeded [blendux] account. Required;
12
Removed:
the server refuses to start when unset. Temporary, until
11
Added:
- HITO_SEED_PASSWORD Password for the seeded [blendux] account. Required.
12
Added:
The server refuses to start when unset. Temporary, until
13
13
public registration returns.
14
14
15
15
The socket binds loopback only. Authentication is local username and
16
Removed:
password; sessions and CSRF tokens are signed with the secret.
16
Added:
password. Sessions and CSRF tokens are signed with the secret.
17
17
18
18
Public sign-up is closed: [Handlers.make] leaves [registration_open] at its
19
19
[false] default, and a single account ([blendux]) is seeded on startup with
lib/app/service.ml
@@ -7,7 +7,7 @@
7
7
8
8
(* Error types are part of the flat public interface, so they live at the top
9
9
level of the functor. The implementation below is grouped into focused
10
Removed:
use-case modules over the shared [t]; the public names are re-exported at
10
Added:
use-case modules over the shared [t]. The public names are re-exported at
11
11
the end, so callers and {!Service.mli} see one flat service. *)
12
12
13
13
type register_error = [ `Username of Trainee.username_error | `Username_taken ]
@@ -58,7 +58,7 @@
58
58
let register t ~username ~password =
59
59
let open Lwt_result.Syntax in
60
60
(* Validate the username, hash the password, then create the account.
61
Removed:
Username validation short-circuits into [register_error]; the password
61
Added:
Username validation short-circuits into [register_error]. The password
62
62
carries no policy, so hashing always succeeds. *)
63
63
let* username =
64
64
Lwt.return
@@ -199,7 +199,7 @@
199
199
let finished = Evidence.Workout.finish workout ~ended_at in
200
200
R.finish_workout t.repo trainee finished >|= fun record -> Some record
201
201
202
Removed:
(* Discard the workout in progress. This clears the slot only; it never
202
Added:
(* Discard the workout in progress. This clears the slot only. It never
203
203
writes to history, so a cancelled workout leaves no record. The doctrine
204
204
keeps plan and record distinct — an abandoned session is not evidence. *)
205
205
let cancel t trainee =
lib/app/service.mli
@@ -113,7 +113,7 @@
113
113
type log_error =
114
114
| No_workout_in_progress
115
115
| Rejected of Evidence.Workout.error
116
Removed:
(** The workout refused the stimulus; see {!Evidence.Workout.error}. *)
116
Added:
(** The workout refused the stimulus. See {!Evidence.Workout.error}. *)
117
117
118
118
val pp_log_error : Format.formatter -> log_error -> unit
119
119
lib/app/trainee.mli
@@ -32,7 +32,7 @@
32
32
33
33
type credential
34
34
(** A password verifier. Carries a salted hash, never the password. The hashing
35
Removed:
algorithm is a {!Password_hash.S} adapter detail; this type only wraps the
35
Added:
algorithm is a {!Password_hash.S} adapter detail. This type only wraps the
36
36
stored verifier so no other layer sees a plaintext password. *)
37
37
38
38
val credential_of_hash : string -> credential
lib/core/evidence.ml
@@ -278,7 +278,7 @@
278
278
279
279
let replace_stimulus t ~slot s =
280
280
check_against (prescription_at t slot) s;
281
Removed:
(* A correction targets the slot; it never adds to the recorded volume.
281
Added:
(* A correction targets the slot. It never adds to the recorded volume.
282
282
Replace the slot's fill in place, keeping its position in performance
283
283
order. When the slot was empty, record it as its first fill. *)
284
284
if List.mem_assoc slot t.performed then
lib/core/evidence.mli
@@ -127,7 +127,7 @@
127
127
[Invalid] for a stimulus the slot does not call for. *)
128
128
129
129
val finish : t -> ended_at:Recovery.timestamp -> t
130
Removed:
(** Sets [ended_at] once; later calls retain the first value. *)
130
Added:
(** Sets [ended_at] once. Later calls retain the first value. *)
131
131
132
132
val prescription : t -> Prescription.Workout.t
133
133
val clearance : t -> Recovery.clearance
lib/core/prescription.mli
@@ -84,7 +84,7 @@
84
84
(** Cycle order. *)
85
85
86
86
val workout_after : t -> Workout.t -> Workout.t
87
Removed:
(** Wraps; unknown workouts fall back to the first. *)
87
Added:
(** Wraps. Unknown workouts fall back to the first. *)
88
88
89
89
val pp : Format.formatter -> t -> unit
90
90
lib/core/progression.ml
@@ -50,7 +50,7 @@
50
50
else Progressing
51
51
| None ->
52
52
(* Never advanced. Only a stall once the record is long enough to say
53
Removed:
so; otherwise two sessions a day apart would condemn the routine. *)
53
Added:
so. Otherwise two sessions a day apart would condemn the routine. *)
54
54
if elapsed_between first latest >= window then Stalled
55
55
else raise (Invalid Insufficient_data))
56
56
lib/core/progression.mli
@@ -23,7 +23,7 @@
23
23
}
24
24
25
25
val remedy : assessment -> remedy option
26
Removed:
(** [Some] only for [Stalled]; it never adds work. *)
26
Added:
(** [Some] only for [Stalled]. It never adds work. *)
27
27
28
28
val pp_remedy : Format.formatter -> remedy -> unit
29
29
@@ -39,7 +39,7 @@
39
39
rep_range:Prescription.Rep_range.t ->
40
40
Evidence.Stimulus.Effort.t ->
41
41
load_verdict
42
Removed:
(** Only the range floor matters: below it is [Too_heavy]; at twelve reps the
42
Added:
(** Only the range floor matters: below it is [Too_heavy]. At twelve reps the
43
43
verdict is [Increase]. *)
44
44
45
45
val pp_load_verdict : Format.formatter -> load_verdict -> unit
lib/persistence/codec.ml
@@ -89,7 +89,7 @@
89
89
| _ -> Error (Malformed "effort arity")
90
90
91
91
(* Decode a stimulus and the slot it filled. The current format prefixes the
92
Removed:
slot; a record from before slots were stored omits it, and replays in
92
Added:
slot. A record from before slots were stored omits it, and replays in
93
93
performance order through [add_stimulus] instead. *)
94
94
let decode_stimulus fields =
95
95
let delivery = function
@@ -133,8 +133,8 @@
133
133
~since:(Recovery.timestamp_of_unix_seconds 0)
134
134
~now:(Recovery.timestamp_of_unix_seconds secs)
135
135
136
Removed:
(* A clearance cannot be minted directly; it is derived from a readiness.
137
Removed:
[Recovered] comes from a [Ready] reading; an override needs a [Recovering]
136
Added:
(* A clearance cannot be minted directly. It is derived from a readiness.
137
Added:
[Recovered] comes from a [Ready] reading. An override needs a [Recovering]
138
138
reading carrying the stored durations. *)
139
139
let decode_clearance = function
140
140
| [ "recovered" ] -> Ok (Option.get (Recovery.clear Recovery.Ready))
@@ -176,7 +176,7 @@
176
176
Buffer.contents buf
177
177
178
178
(* A fully-parsed header, gathered before any replay. Parsing produces this
179
Removed:
typed record; replay consumes it. Keeping the two phases apart removes the
179
Added:
typed record. Replay consumes it. Keeping the two phases apart removes the
180
180
mutable accumulators the old single pass needed. *)
181
181
type parsed = {
182
182
routine_name : string;
@@ -188,7 +188,7 @@
188
188
(** Performance order, each with the slot it filled when known. *)
189
189
}
190
190
191
Removed:
(* Fields accumulated while folding over lines; every field is optional until
191
Added:
(* Fields accumulated while folding over lines. Every field is optional until
192
192
its line is seen. [stimuli] is reversed for O(1) prepend and flipped once at
193
193
the end. *)
194
194
type acc = {
@@ -216,7 +216,7 @@
216
216
| Error _ -> Error (Malformed label)
217
217
218
218
(* Fold one line into the accumulator. A line either sets a header field or
219
Removed:
appends a stimulus; anything else is malformed. *)
219
Added:
appends a stimulus. Anything else is malformed. *)
220
220
let step acc = function
221
221
| [ "routine"; name ] -> Ok { acc with a_routine = Some name }
222
222
| [ "prescription"; name ] -> Ok { acc with a_prescription = Some name }
@@ -284,7 +284,7 @@
284
284
}))
285
285
286
286
(* Drive the core constructors from a typed header. The core raises on a
287
Removed:
rejected stimulus; catch it once here and report it as data corruption. *)
287
Added:
rejected stimulus. Catch it once here and report it as data corruption. *)
288
288
let replay ~prescription parsed =
289
289
try
290
290
let workout =
lib/persistence/codec.mli
@@ -17,7 +17,7 @@
17
17
| Unknown_routine of string
18
18
| Unknown_workout_name of { routine : string; workout : string }
19
19
| Replay_rejected of string
20
Removed:
(** The core refused a stored stimulus; the store is inconsistent. *)
20
Added:
(** The core refused a stored stimulus. The store is inconsistent. *)
21
21
22
22
val pp_error : Format.formatter -> error -> unit
23
23
lib/persistence/migrations.ml
@@ -197,7 +197,7 @@
197
197
Db.exec request (migration.version, migration.name)
198
198
199
199
(* Apply one migration inside a transaction, then record it. The ledger check
200
Removed:
makes this idempotent; the [IF NOT EXISTS] clauses keep an already-populated
200
Added:
makes this idempotent. The [IF NOT EXISTS] clauses keep an already-populated
201
201
file from a pre-ledger build safe. *)
202
202
let apply_migration ((module Db : Caqti_lwt.CONNECTION) as db) migration =
203
203
let open Lwt_result.Syntax in
lib/web/decode.ml
@@ -86,10 +86,10 @@
86
86
87
87
(* --- profile --- *)
88
88
89
Removed:
(* The raw username string; the domain normalizes and validates it downstream. *)
89
Added:
(* The raw username string. The domain normalizes and validates it downstream. *)
90
90
let profile_username = Form.required Form.string "username"
91
91
92
Removed:
(* Current and new password, both required. Passwords carry no policy here; the
92
Added:
(* Current and new password, both required. Passwords carry no policy here. The
93
93
service verifies the current one before storing the new one. *)
94
94
let profile_password =
95
95
let open Form in
lib/web/decode.mli
@@ -4,7 +4,7 @@
4
4
val stimulus : Prescription.Stimulus.t -> Evidence.Stimulus.t Dream_html.Form.t
5
5
6
6
val profile_username : string Dream_html.Form.t
7
Removed:
(** The raw username from the rename form; the domain validates it. *)
7
Added:
(** The raw username from the rename form. The domain validates it. *)
8
8
9
9
val profile_password : (string * string) Dream_html.Form.t
10
10
(** The current and new passwords from the change-password form. *)
lib/web/handlers.ml
@@ -95,7 +95,7 @@
95
95
| Some id -> Service.find_trainee t.service (Trainee.id id)
96
96
97
97
(* Resolve the authenticated trainee, or send an unauthenticated request to
98
Removed:
the sign-in page. [handler] receives the trainee; the request and any path
98
Added:
the sign-in page. [handler] receives the trainee. The request and any path
99
99
captures stay in the caller's scope, so this one combinator serves every
100
100
route regardless of how many captures Dream passes. *)
101
101
let authenticated t request handler =
@@ -179,7 +179,7 @@
179
179
redirect_to request Routes.login
180
180
181
181
let routes t =
182
Removed:
(* Public sign-up is disabled by default; the [/register] routes appear
182
Added:
(* Public sign-up is disabled by default. The [/register] routes appear
183
183
only when [registration_open] is set. Everything else is unconditional. *)
184
184
let register_routes =
185
185
if t.registration_open then
@@ -216,14 +216,14 @@
216
216
217
217
(* The slot a workout view should open on: the first slot still awaiting a
218
218
record, or the first slot when every slot is filled. A caller clamps a
219
Removed:
requested slot against the prescription; this supplies the default. *)
219
Added:
requested slot against the prescription. This supplies the default. *)
220
220
let default_slot workout =
221
221
match Evidence.Workout.outstanding workout with
222
222
| (slot, _) :: _ -> slot
223
223
| [] -> 0
224
224
225
225
(* The slot a workout view should open on. An explicit [?slot=] query
226
Removed:
wins when it names a real slot; otherwise the view defaults to the first
226
Added:
wins when it names a real slot. Otherwise the view defaults to the first
227
227
incomplete slot. A [?slot=] out of range, or absent, falls back to the
228
228
default, so a bookmarked or hand-edited URL never renders an empty view. *)
229
229
let active_slot request workout =
@@ -240,7 +240,7 @@
240
240
241
241
(* Map an [Evidence.Workout.t] into the view-model DTO the presenter renders,
242
242
so no Evidence or Prescription type crosses the boundary into Pages. The
243
Removed:
same mapping serves the live workout and a saved-record view; the caller
243
Added:
same mapping serves the live workout and a saved-record view. The caller
244
244
supplies [record_id]/[editing]/[errors] separately. *)
245
245
246
246
(* The ending code a single-choice form uses, taken from an effort's outcome.
lib/web/handlers.mli
@@ -16,7 +16,7 @@
16
16
pre-seeded accounts until public sign-up returns. *)
17
17
18
18
val routes : t -> Dream.route list
19
Removed:
(** The route table. Session and CSRF middleware are applied per handler; wrap
19
Added:
(** The route table. Session and CSRF middleware are applied per handler. Wrap
20
20
with {!Dream.sql_sessions} or {!Dream.memory_sessions} and a secret at the
21
21
top level. *)
22
22
lib/web/pages.ml
@@ -41,7 +41,7 @@
41
41
(* Three always-available actions. The middle slot points to the routine
42
42
normally and to the workout in progress while logging, so a running
43
43
workout stays one tap away without adding a fourth destination. The logbook
44
Removed:
tab always reads "Logbook"; the username lives on its own profile control. *)
44
Added:
tab always reads "Logbook". The username lives on its own profile control. *)
45
45
let navigation_links () =
46
46
[
47
47
nav_link "home" Routes.home "Home";
@@ -435,7 +435,7 @@
435
435
436
436
(* Beginning under override needs an explicit acknowledgement, so its submit is
437
437
gated behind a confirmation modal. Without script the form submits at once —
438
Removed:
the doctrine's deviation stays possible; the modal only makes it deliberate.
438
Added:
the doctrine's deviation stays possible. The modal only makes it deliberate.
439
439
The client reuses the shared confirm machinery: [data-hito-confirm-cancel]
440
440
opens the dialog, [data-hito-confirm-form] is the form its accept submits. *)
441
441
let override_begin request ~routine_id =
@@ -698,7 +698,7 @@
698
698
let field_id field = Printf.sprintf "slot-%d-%s" slot field in
699
699
let selected = selected_extension in
700
700
(* A single-exercise slot shows one row with a visible "Reps to failure"
701
Removed:
label; a pre-exhaust pair shows a shared column heading and hides the
701
Added:
label. A pre-exhaust pair shows a shared column heading and hides the
702
702
per-row labels. *)
703
703
let hide_labels = List.length movements > 1 in
704
704
let head =
@@ -787,7 +787,7 @@
787
787
in
788
788
(* The early-workout notice. The override was already confirmed in a modal, so
789
789
on the workout screen this is informational only: a toast rather than a
790
Removed:
component in the main content. It is a polite live region; the client shows
790
Added:
component in the main content. It is a polite live region. The client shows
791
791
it briefly then dismisses it. Without script it stays visible as a quiet
792
792
fixed notice, so a no-JS visitor is still told. *)
793
793
let override_note =
@@ -858,7 +858,7 @@
858
858
in
859
859
(* The panel for the active slot. It shows one exercise at a time: a recorded
860
860
slot renders its read-only summary and a pre-filled correction form (which
861
Removed:
replaces the slot rather than adding volume); an outstanding slot renders
861
Added:
replaces the slot rather than adding volume). An outstanding slot renders
862
862
the record form. *)
863
863
let active_panel =
864
864
match List.nth_opt vm.slots active_slot with
@@ -948,7 +948,7 @@
948
948
void "input" [ type_ "submit"; value "Finish workout" ];
949
949
];
950
950
(* Cancel is a native POST form. Without script, it submits
951
Removed:
immediately. The client only opens the confirm dialog; its
951
Added:
immediately. The client only opens the confirm dialog. Its
952
952
acceptance submits this form as a full document navigation so
953
953
the cancelled workout cannot leave a stale app shell behind. *)
954
954
tag "form"
@@ -970,7 +970,7 @@
970
970
];
971
971
];
972
972
(* The confirmation modal. Native dialog for built-in focus trapping
973
Removed:
and Escape-to-close. The client opens it; "Cancel workout" submits
973
Added:
and Escape-to-close. The client opens it. "Cancel workout" submits
974
974
the cancel form, "Keep logging" closes it. *)
975
975
tag "dialog"
976
976
[
@@ -1035,7 +1035,7 @@
1035
1035
]
1036
1036
1037
1037
(* The subjective feedback form. Leveled selects report sleep, appetite,
1038
Removed:
readiness, motivation, and difficulty against a personal baseline; flags
1038
Added:
readiness, motivation, and difficulty against a personal baseline. Flags
1039
1039
report pain, injury, and insufficient preparation. A field left blank reports
1040
1040
nothing, so the trainee submits only what they mean to. Feedback is
1041
1041
standalone — recorded from the Logbook at any time. *)
@@ -1633,7 +1633,7 @@
1633
1633
1634
1634
(* The account profile page. It names the signed-in trainee and offers two
1635
1635
independent forms: rename the account, and change the password. Each form
1636
Removed:
reports its own error inline; [notice] confirms a successful change. Both are
1636
Added:
reports its own error inline. [notice] confirms a successful change. Both are
1637
1637
plain authenticated app-forms, so they work with or without script. *)
1638
1638
let profile request ?(logging = false) ~viewer ?username_error ?password_error
1639
1639
?notice () =
lib/web/pages.mli
@@ -40,12 +40,12 @@
40
40
page
41
41
(** Renders one prescribed slot at a time under a vertical group of named
42
42
exercise buttons. Authenticated pages carry generic progressive-enhancement
43
Removed:
markers; current workouts additionally mark slot links and forms, while
43
Added:
markers. Current workouts additionally mark slot links and forms, while
44
44
saved-record edits use the generic authenticated form path. [active_slot]
45
Removed:
button; a handler clamps a requested slot and falls back to the first
45
Added:
button. A handler clamps a requested slot and falls back to the first
46
46
outstanding slot. [record_id] is [None] for the workout in progress and
47
47
[Some id] for a saved history record. [logging] sets the middle navigation
48
Removed:
action; it defaults to a live workout view (true when [record_id] is
48
Added:
action. It defaults to a live workout view (true when [record_id] is
49
49
[None]). *)
50
50
51
51
type feedback_flow = { step : int; answers : (string * string) list }
@@ -78,7 +78,7 @@
78
78
page
79
79
(** The account profile page: rename the account and change the password, each
80
80
an independent form. [username_error] and [password_error] report a refused
81
Removed:
change inline; [notice] confirms a successful one. *)
81
Added:
change inline. [notice] confirms a successful one. *)
82
82
83
83
val problem : title:string -> detail:string -> page
84
84
@@ -96,4 +96,4 @@
96
96
View_model.App_feedback.t list ->
97
97
page
98
98
(** The authenticated application feedback page. One tab accepts a freeform
99
Removed:
message; the other lists the trainee's submitted messages. *)
99
Added:
message. The other lists the trainee's submitted messages. *)
lib/web/view_model.ml
@@ -7,7 +7,7 @@
7
7
model without ever naming an inner-circle type.
8
8
9
9
Migrated so far: the signed-in viewer, application feedback, and subjective
10
Removed:
feedback. Other pages still take entities directly; they move here page by
10
Added:
feedback. Other pages still take entities directly. They move here page by
11
11
page. *)
12
12
13
13
module Viewer = struct
@@ -75,7 +75,7 @@
75
75
gate : gate;
76
76
}
77
77
(** The overview while no workout is in progress. The recovery policy has
78
Removed:
already decided [gate] in the use case; the page only renders it. *)
78
Added:
already decided [gate] in the use case. The page only renders it. *)
79
79
end
80
80
81
81
module Workout = struct
@@ -98,7 +98,7 @@
98
98
label : string; (** Exercise label for the selector option and heading. *)
99
99
movements : movement list;
100
100
recorded_summary : string option;
101
Removed:
(** [Some description] once the slot is filled; [None] while
101
Added:
(** [Some description] once the slot is filled. [None] while
102
102
outstanding. Drives the recorded/outstanding branch without an
103
103
entity. *)
104
104
selected_extension : string;
lib/web/workout_client.ml
@@ -55,7 +55,7 @@
55
55
(Js.number_of_float 3000.)))
56
56
57
57
(* The workout timer. The sticky bar carries the workout's start time as an
58
Removed:
epoch in [data-started]; the elapsed time is computed from it, so a content
58
Added:
epoch in [data-started]. The elapsed time is computed from it, so a content
59
59
swap that re-renders the bar never resets the count. A single interval runs
60
60
at a time: [start_timer] clears the previous one before it reads the bar the
61
61
latest render produced. When no bar is present, the timer stops. *)
@@ -75,7 +75,7 @@
75
75
| None -> ()
76
76
| Some started -> (
77
77
(* Epoch seconds exceed js_of_ocaml's 31-bit [int] range, so the
78
Removed:
start and the current time are kept as floats; only the small
78
Added:
start and the current time are kept as floats. Only the small
79
79
elapsed difference is narrowed to an [int]. Doing the subtraction
80
80
in [int] would overflow and yield a wrong count. *)
81
81
let started =
@@ -326,8 +326,8 @@
326
326
Js._true
327
327
328
328
(* The cancel confirmation modal. The cancel form submits directly without
329
Removed:
script; with script, its submit is intercepted only to open a native dialog.
330
Removed:
"Cancel workout" submits the native form as a full document POST; "Keep
329
Added:
script. With script, its submit is intercepted only to open a native dialog.
330
Added:
"Cancel workout" submits the native form as a full document POST. "Keep
331
331
logging" and Escape close the dialog. The dialog and the pending form are
332
332
found fresh on each click, so a content swap never leaves a stale
333
333
reference. *)
@@ -411,7 +411,7 @@
411
411
Js._true)))))
412
412
413
413
(* The exercise dropdown navigates the moment its selection changes. It lives in
414
Removed:
a GET form ([data-hito-exercise-form]) that submits to the workout path; the
414
Added:
a GET form ([data-hito-exercise-form]) that submits to the workout path. The
415
415
client turns a change into a workout-scoped content swap to [?slot=N], so no
416
416
submit click is needed. Without script the form's submit button still works. *)
417
417
let exercise_change event =