summaryrefslogtreecommitdiff
path: root/src/recovery.lisp
diff options
context:
space:
mode:
Diffstat (limited to 'src/recovery.lisp')
-rw-r--r--src/recovery.lisp180
1 files changed, 180 insertions, 0 deletions
diff --git a/src/recovery.lisp b/src/recovery.lisp
new file mode 100644
index 0000000..c4edabd
--- /dev/null
+++ b/src/recovery.lisp
@@ -0,0 +1,180 @@
+(in-package #:hito)
+
+;;; --- Recovery Gating System ---
+;;;
+;;; "The muscles DO NOT grow during training. They grow during REST.
+;;; If you are not making progress, you are not overtraining —
+;;; you are under-recovering."
+;;;
+;;; Recovery is not optional. It is where growth occurs. The system enforces
+;;; minimum rest periods and extends them when stalls indicate incomplete recovery.
+
+;;; --- Recovery Configuration ---
+
+(defparameter *base-recovery-days* 3
+ "Minimum rest days between training sessions (Mentzer's HD I starting point).")
+
+(defparameter *stall-extension-days* 2
+ "Additional rest days added when a stall is detected in recent sessions.")
+
+(defparameter *consecutive-stall-extension-days* 3
+ "Additional rest days for consecutive stalls across sessions of the same day.")
+
+(defparameter *max-recovery-days* 14
+ "Maximum recovery period before the system suggests program reassessment.")
+
+;;; --- Recovery Calculation ---
+
+(defun count-recent-stalls (workout-day &optional (lookback 2))
+ "Count sessions of WORKOUT-DAY in the last LOOKBACK completed sessions
+that had at least one stall."
+ (let* ((sessions (mito:select-dao 'training-session
+ (sxql:where (:and (:= :workout-day workout-day)
+ (:= :completed-p 1)))
+ (sxql:order-by (:desc :session-date))
+ (sxql:limit lookback)))
+ (stall-count 0))
+ (dolist (session sessions)
+ (let ((records (mito:select-dao 'set-record
+ (sxql:where (:= :session-id (mito:object-id session))))))
+ (when (some (lambda (rec)
+ (let ((actual (set-record-actual-reps rec))
+ (low (set-record-prescribed-rep-low rec)))
+ (and actual (< actual low))))
+ records)
+ (incf stall-count))))
+ stall-count))
+
+(defun calculate-recovery-days (&optional session)
+ "Calculate the required recovery days based on recent performance.
+If SESSION is provided, uses its workout-day for stall analysis.
+Otherwise uses the user's current workout day.
+
+Returns (values days reason) where reason explains the calculation."
+ (let* ((workout-day (if session
+ (training-session-workout-day session)
+ (let ((profile (first (mito:select-dao 'user-profile))))
+ (if profile
+ (user-profile-current-workout-day profile)
+ 1))))
+ (stalls (count-recent-stalls workout-day))
+ (base *base-recovery-days*)
+ (extension (cond
+ ((>= stalls 2)
+ *consecutive-stall-extension-days*)
+ ((>= stalls 1)
+ *stall-extension-days*)
+ (t 0)))
+ (total (min (+ base extension) *max-recovery-days*))
+ (reason (cond
+ ((>= stalls 2)
+ (format nil "Consecutive stalls detected across recent sessions of Day ~D. ~
+ Extended recovery is not merely suggested — it is required. ~
+ Growth occurs during rest." workout-day))
+ ((>= stalls 1)
+ (format nil "A stall was detected in your last Day ~D session. ~
+ Additional recovery time has been prescribed." workout-day))
+ (t
+ (format nil "Standard recovery period between sessions.")))))
+ (values total reason)))
+
+;;; --- Recovery Status ---
+
+(defstruct recovery-status
+ "Current recovery state for the trainee."
+ ready-p ; t if they can train
+ days-remaining ; days until ready (0 if ready)
+ next-date ; recommended next training date (string)
+ reason ; explanation text
+ last-session-date ; when they last trained
+ recovery-days) ; total prescribed recovery
+
+(defun parse-date-string (date-str)
+ "Parse an ISO date string (YYYY-MM-DD) to a local-time timestamp."
+ (when (and date-str (plusp (length date-str)))
+ (local-time:parse-timestring date-str)))
+
+(defun format-date (timestamp)
+ "Format a local-time timestamp as YYYY-MM-DD."
+ (local-time:format-timestring nil timestamp :format '(:year "-" (:month 2) "-" (:day 2))))
+
+(defun days-since (date-string)
+ "Return the number of days elapsed since DATE-STRING (YYYY-MM-DD format)."
+ (let ((then (parse-date-string date-string))
+ (now (local-time:now)))
+ (when then
+ (let ((diff (local-time:timestamp-difference now then)))
+ (floor diff 86400))))) ; seconds -> days
+
+(defun add-days-to-date (date-string days)
+ "Add DAYS to a date string, return new date string."
+ (let ((timestamp (parse-date-string date-string)))
+ (when timestamp
+ (format-date (local-time:timestamp+ timestamp days :day)))))
+
+(defun recovery-status ()
+ "Compute the current recovery status for the trainee.
+Returns a recovery-status struct."
+ (let* ((last-session (first (mito:select-dao 'training-session
+ (sxql:where (:= :completed-p 1))
+ (sxql:order-by (:desc :session-date))
+ (sxql:limit 1)))))
+ (if (null last-session)
+ ;; No sessions yet — ready to train
+ (make-recovery-status
+ :ready-p t
+ :days-remaining 0
+ :next-date (format-date (local-time:now))
+ :reason "No previous sessions recorded. You are ready to begin."
+ :last-session-date nil
+ :recovery-days 0)
+ ;; Calculate based on last session
+ (let* ((last-date (training-session-session-date last-session))
+ (elapsed (days-since last-date)))
+ (multiple-value-bind (required-days reason)
+ (calculate-recovery-days last-session)
+ (let* ((remaining (max 0 (- required-days elapsed)))
+ (ready (zerop remaining))
+ (next-date (if ready
+ (format-date (local-time:now))
+ (add-days-to-date last-date required-days))))
+ (make-recovery-status
+ :ready-p ready
+ :days-remaining remaining
+ :next-date next-date
+ :reason reason
+ :last-session-date last-date
+ :recovery-days required-days)))))))
+
+;;; --- Override Mechanism ---
+
+(defparameter *override-warning*
+ "You are overriding the prescribed recovery period. Understand: growth occurs ~
+during rest, not during training. The muscles require time to compensate and ~
+supercompensate. If you train before recovery is complete, you are not merely ~
+wasting effort — you are actively undermining your progress. Proceed only if ~
+you are certain your recovery is complete."
+ "Warning text displayed when user overrides recovery gate. Mentzer's voice.")
+
+(defun override-recovery ()
+ "Log that the user has overridden the recovery gate.
+Returns the warning text. The session created after this should be marked
+with override-p = t."
+ (format t "~&[OVERRIDE] Recovery gate overridden at ~A~%"
+ (format-date (local-time:now)))
+ *override-warning*)
+
+;;; --- Recovery Formatting ---
+
+(defun format-recovery-status (status)
+ "Format recovery status for display."
+ (with-output-to-string (out)
+ (if (recovery-status-ready-p status)
+ (format out "READY. You may train today.")
+ (format out "RECOVERY IN PROGRESS.~%~
+ Days remaining: ~D~%~
+ Next session: ~A~%~
+ ~A"
+ (recovery-status-days-remaining status)
+ (recovery-status-next-date status)
+ (recovery-status-reason status)))))
Copyright 2019--2026 Marius PETER