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.

Commit
b4c482d25437b1441bec721327f5da0ea6bed92a
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/core/evidence.ml
index 9625e7ea..107c43f5 100644..100644
@@ -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
index 95696db9..e0f33581 100644..100644
@@ -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
index a7b94685..ba89870a 100644..100644
@@ -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
index 4412d522..7c9d3dc9 100644..100644
@@ -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
index 2a2743b5..8a51a1f8 100644..100644
@@ -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
index 02f8629b..85940c44 100644..100644
@@ -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