feat Distinguish decompensation from a stall

HD1 names two failure states, not one: a halt, where progress stops but strength holds, and decompensation, where strength is lost ("The Economics of Growth and Recovery": "before decompensating, i.e., lose strength and mass"). assess folded both into Stalled; add Decompensating and separate them. Once the stall window passes without an advance, compare the latest effort to the reference — the peak reached, or the first observation when the record never advanced. Strength below it is decompensation; strength holding at it is a stall. Add `declines` as the mirror of `beats`. HD1 applies one corrective to both ("when progress comes to a halt or you start decompensating ... decrease them"), so the remedy is unchanged.

Commit
f162e3dac65eb02fa2709b0e2cc0c7eb3b80154c
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/core/progress.ml
index ea30a83f..24922570 100644..100644
@@ -5,7 +5,14 @@
5 5 let reps = Int.compare (Effort.reps current) (Effort.reps previous) in
6 6 load > 0 || (load = 0 && reps > 0)
7 7
8 Removed: type assessment = Progressing | Stalled
8 Added: (* The mirror of [beats]: [current] is strictly worse than [previous] — less
9 Added: load, or equal load and fewer reps. The mark of decompensation. *)
10 Added: let declines ~previous ~current =
11 Added: let load = Float.compare (Effort.load current) (Effort.load previous) in
12 Added: let reps = Int.compare (Effort.reps current) (Effort.reps previous) in
13 Added: load < 0 || (load = 0 && reps < 0)
14 Added:
15 Added: type assessment = Progressing | Stalled | Decompensating
9 16 type error = Insufficient_data
10 17
11 18 exception Invalid of error
@@ -13,6 +20,7 @@
13 20 let pp_assessment ppf = function
14 21 | Progressing -> Format.pp_print_string ppf "progressing"
15 22 | Stalled -> Format.pp_print_string ppf "stalled"
23 Added: | Decompensating -> Format.pp_print_string ppf "decompensating"
16 24
17 25 let pp_error ppf Insufficient_data =
18 26 Format.pp_print_string ppf "not enough record to judge"
@@ -48,14 +56,26 @@
48 56 match (observations, last observations) with
49 57 | ([] | [ _ ]), _ | _, None -> raise (Invalid Insufficient_data)
50 58 | first :: _, Some latest -> (
59 Added: (* Once the window has passed without progress, separate a halt from a
60 Added: loss: strength below the reference is decompensation, strength holding
61 Added: at it is a stall. The reference is the peak reached (the last advance),
62 Added: or the first observation when the record never advanced. *)
63 Added: let effort = Evidence.Observation.effort in
64 Added: let verdict ~reference =
65 Added: if declines ~previous:(effort reference) ~current:(effort latest) then
66 Added: Decompensating
67 Added: else Stalled
68 Added: in
51 69 match last_advance observations with
52 70 | Some advance ->
53 Removed: if elapsed_between advance latest >= window then Stalled
71 Added: if elapsed_between advance latest >= window then
72 Added: verdict ~reference:advance
54 73 else Progressing
55 74 | None ->
56 75 (* Never advanced. Only a stall once the record is long enough to say
57 76 so. Otherwise two sessions a day apart would condemn the routine. *)
58 Removed: if elapsed_between first latest >= window then Stalled
77 Added: if elapsed_between first latest >= window then
78 Added: verdict ~reference:first
59 79 else raise (Invalid Insufficient_data))
60 80
61 81 type remedy =
@@ -67,7 +87,7 @@
67 87
68 88 let remedy = function
69 89 | Progressing -> None
70 Removed: | Stalled ->
90 Added: | Stalled | Decompensating ->
71 91 Some
72 92 (Lay_off_then_reduce
73 93 {
lib/core/progress.mli
index a03323d6..b4f33f35 100644..100644
@@ -1,6 +1,14 @@
1 1 (** Interprets recorded training against Heavy Duty rules. *)
2 2
3 Removed: type assessment = Progressing | Stalled
3 Added: type assessment =
4 Added: | Progressing
5 Added: | Stalled
6 Added: | Decompensating
7 Added: (** [Stalled] is a halt: progress has stopped but strength holds.
8 Added: [Decompensating] is worse: strength is being lost (HD1, "The Economics
9 Added: of Growth and Recovery": "before decompensating, i.e., lose strength
10 Added: and mass"). HD1 names both and applies the same corrective. *)
11 Added:
4 12 type error = Insufficient_data
5 13
6 14 exception Invalid of error
@@ -23,7 +31,9 @@
23 31 }
24 32
25 33 val remedy : assessment -> remedy option
26 Removed: (** [Some] only for [Stalled]. It never adds work. *)
34 Added: (** [Some] for [Stalled] and [Decompensating]. HD1 applies one corrective to a
35 Added: halt and to decompensation alike ("when progress comes to a halt or you
36 Added: start decompensating ... you should decrease them"). It never adds work. *)
27 37
28 38 val pp_remedy : Format.formatter -> remedy -> unit
29 39