[OCaml] High Intensity Training Online
refactor Add canonical evidence observations
Promote the log's observation record into an abstract Evidence.Observation type. Native workouts produce this canonical evidence through Evidence.Log, and Progression judges the same type through accessors. This creates the insight boundary needed for promoted imports without introducing import, CSV, or provenance concepts into the core.
Changed files
lib/core/evidence.ml
@@ -307,15 +307,22 @@
307
307
(List.length (Prescription.Workout.stimuli t.prescription))
308
308
end
309
309
310
Removed:
module Log = struct
311
Removed:
type t = Workout.t list
312
Removed:
313
Removed:
type observation = {
310
Added:
module Observation = struct
311
Added:
type t = {
314
312
exercise : Exercise.t;
315
313
effort : Stimulus.Effort.t;
316
314
performed_at : Recovery.timestamp;
317
315
}
318
316
317
Added:
let make ~exercise ~effort ~performed_at = { exercise; effort; performed_at }
318
Added:
let exercise t = t.exercise
319
Added:
let effort t = t.effort
320
Added:
let performed_at t = t.performed_at
321
Added:
end
322
Added:
323
Added:
module Log = struct
324
Added:
type t = Workout.t list
325
Added:
319
326
let empty = []
320
327
let add t workout = workout :: t
321
328
@@ -337,16 +344,15 @@
337
344
Workout.stimuli workout
338
345
|> List.concat_map Stimulus.efforts
339
346
|> List.map (fun effort ->
340
Removed:
{
341
Removed:
exercise = Stimulus.Effort.exercise effort;
342
Removed:
effort;
343
Removed:
performed_at = Workout.started_at workout;
344
Removed:
})
347
Added:
Observation.make
348
Added:
~exercise:(Stimulus.Effort.exercise effort)
349
Added:
~effort
350
Added:
~performed_at:(Workout.started_at workout))
345
351
346
352
let observations t exercise =
347
353
chronological t
348
354
|> List.concat_map observations_of
349
Removed:
|> List.filter (fun o -> Exercise.equal o.exercise exercise)
355
Added:
|> List.filter (fun o -> Exercise.equal (Observation.exercise o) exercise)
350
356
351
357
let readiness t ~now ~recommended =
352
358
let finished =
lib/core/evidence.mli
@@ -161,6 +161,22 @@
161
161
val pp : Format.formatter -> t -> unit
162
162
end
163
163
164
Added:
(** One effort for one exercise, observed at a point in time. The canonical unit
165
Added:
that {!Progression} judges. *)
166
Added:
module Observation : sig
167
Added:
type t
168
Added:
169
Added:
val make :
170
Added:
exercise:Exercise.t ->
171
Added:
effort:Stimulus.Effort.t ->
172
Added:
performed_at:Recovery.timestamp ->
173
Added:
t
174
Added:
175
Added:
val exercise : t -> Exercise.t
176
Added:
val effort : t -> Stimulus.Effort.t
177
Added:
val performed_at : t -> Recovery.timestamp
178
Added:
end
179
Added:
164
180
(** Workout history. *)
165
181
module Log : sig
166
182
type t
@@ -173,13 +189,7 @@
173
189
174
190
val last_prescription : t -> Prescription.Workout.t option
175
191
176
Removed:
type observation = {
177
Removed:
exercise : Exercise.t;
178
Removed:
effort : Stimulus.Effort.t;
179
Removed:
performed_at : Recovery.timestamp;
180
Removed:
}
181
Removed:
182
Removed:
val observations : t -> Exercise.t -> observation list
192
Added:
val observations : t -> Exercise.t -> Observation.t list
183
193
(** Oldest first. *)
184
194
185
195
val readiness :
lib/core/progression.ml
@@ -23,10 +23,13 @@
23
23
(* The most recent observation that improved on the one before it. *)
24
24
let last_advance observations =
25
25
let rec scan found = function
26
Removed:
| (previous : Evidence.Log.observation) :: (current :: _ as rest) ->
26
Added:
| (previous : Evidence.Observation.t) :: (current :: _ as rest) ->
27
27
let found =
28
Removed:
if beats ~previous:previous.effort ~current:current.effort then
29
Removed:
Some current
28
Added:
if
29
Added:
beats
30
Added:
~previous:(Evidence.Observation.effort previous)
31
Added:
~current:(Evidence.Observation.effort current)
32
Added:
then Some current
30
33
else found
31
34
in
32
35
scan found rest
@@ -34,10 +37,11 @@
34
37
in
35
38
scan None observations
36
39
37
Removed:
let elapsed_between (a : Evidence.Log.observation)
38
Removed:
(b : Evidence.Log.observation) =
40
Added:
let elapsed_between (a : Evidence.Observation.t) (b : Evidence.Observation.t) =
39
41
Recovery.duration_to_seconds
40
Removed:
(Recovery.elapsed ~since:a.performed_at ~now:b.performed_at)
42
Added:
(Recovery.elapsed
43
Added:
~since:(Evidence.Observation.performed_at a)
44
Added:
~now:(Evidence.Observation.performed_at b))
41
45
42
46
let assess observations =
43
47
let window = Recovery.duration_to_seconds stall_window in
lib/core/progression.mli
@@ -11,7 +11,7 @@
11
11
val stall_window : Recovery.duration
12
12
(** 14 days. *)
13
13
14
Removed:
val assess : Evidence.Log.observation list -> assessment
14
Added:
val assess : Evidence.Observation.t list -> assessment
15
15
(** Raises [Invalid Insufficient_data] when the record cannot support a
16
16
judgment. *)
17
17
test/test_evidence.ml
@@ -4,6 +4,7 @@
4
4
module Workout = Evidence.Workout
5
5
module Feedback = Evidence.Feedback
6
6
module Log = Evidence.Log
7
Added:
module Observation = Evidence.Observation
7
8
8
9
let ok = function Ok v -> v | Error _ -> Alcotest.fail "expected Ok"
9
10
@@ -424,6 +425,29 @@
424
425
(Prescription.Routine.workout_after routine last)) );
425
426
]
426
427
428
Added:
let observation_type_tests =
429
Added:
[
430
Added:
( "make and accessors round-trip the fields",
431
Added:
`Quick,
432
Added:
fun () ->
433
Added:
let exercise = get "laterals" in
434
Added:
let effort = move "laterals" 12. 8 in
435
Added:
let performed_at = day 3 in
436
Added:
let o = Observation.make ~exercise ~effort ~performed_at in
437
Added:
Alcotest.(check bool)
438
Added:
"exercise" true
439
Added:
(Exercise.equal exercise (Observation.exercise o));
440
Added:
Alcotest.(check (float 0.001))
441
Added:
"load" 12.
442
Added:
(Stimulus.Effort.load (Observation.effort o));
443
Added:
Alcotest.(check int)
444
Added:
"reps" 8
445
Added:
(Stimulus.Effort.reps (Observation.effort o));
446
Added:
Alcotest.(check int)
447
Added:
"performed_at" 259_200
448
Added:
(Recovery.timestamp_to_unix_seconds (Observation.performed_at o)) );
449
Added:
]
450
Added:
427
451
let observation_tests =
428
452
[
429
453
( "observations for a movement come back oldest first",
@@ -444,7 +468,7 @@
444
468
Alcotest.(check (list (float 0.001)))
445
469
"12kg then 14kg" [ 12.; 14. ]
446
470
(List.map
447
Removed:
(fun (o : Log.observation) -> Stimulus.Effort.load o.effort)
471
Added:
(fun o -> Stimulus.Effort.load (Observation.effort o))
448
472
history) );
449
473
( "observations are dated, so a stall can be measured",
450
474
`Quick,
@@ -458,7 +482,7 @@
458
482
| [ o ] ->
459
483
Alcotest.(check int)
460
484
"day 2" 172_800
461
Removed:
(Recovery.timestamp_to_unix_seconds o.performed_at)
485
Added:
(Recovery.timestamp_to_unix_seconds (Observation.performed_at o))
462
486
| _ -> Alcotest.fail "expected one observation" );
463
487
( "a movement never performed has no observations",
464
488
`Quick,
@@ -576,6 +600,7 @@
576
600
("evidence.workout.clearance", clearance_tests);
577
601
("evidence.workout.completion_feedback", completion_feedback_tests);
578
602
("evidence.log.basics", log_basic_tests);
603
Added:
("evidence.observation", observation_type_tests);
579
604
("evidence.log.observations", observation_tests);
580
605
("evidence.log.readiness", readiness_tests);
581
606
]
test/test_progression.ml
@@ -23,9 +23,10 @@
23
23
Stimulus.Effort.make ~exercise:(get id) ~load:(load load_kg)
24
24
~reps:(reps rep_count) ~outcome
25
25
26
Removed:
(* An observation is a public record, so evidence can be written directly. *)
27
Removed:
let seen ~on load r : Evidence.Log.observation =
28
Removed:
{ exercise = get "laterals"; effort = move load r; performed_at = day on }
26
Added:
(* An observation is the canonical unit Progression judges. *)
27
Added:
let seen ~on load r : Evidence.Observation.t =
28
Added:
Evidence.Observation.make ~exercise:(get "laterals") ~effort:(move load r)
29
Added:
~performed_at:(day on)
29
30
30
31
let raises_insufficient_data f =
31
32
try