(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)))))