summaryrefslogtreecommitdiff
path: root/src/models.lisp
diff options
context:
space:
mode:
authorMarius Peter <dev@marius-peter.com>2026-07-18 23:23:35 +0200
committerMarius Peter <dev@marius-peter.com>2026-07-18 23:23:35 +0200
commit753e25e99ddf57be78ff27ad89cb7e1bbc99d041 (patch)
tree43db69ec341520f2923f2f13d0cd8846a9acac35 /src/models.lisp
Initial commit: HITO MVP
Heavy Duty I prescriptive training system encoding Mentzer's methodology. - 4-day split program (Chest & Back, Legs, Shoulders, Arms) - Prescription engine with pre-exhaust superset structure - Strict progression logic (+5kg large / +2.5kg small muscles) - Recovery gating with stall-based extension and override - Onboarding via experience level (Novice/Intermediate/Advanced) - Session logging with full progression analysis - Exercise substitution (33 alternatives for 17 canonical movements) - Objectivist aesthetic: black/gold, serif, rectilinear Stack: Hunchentoot, Mito/SQLite, Djula, SBCL
Diffstat (limited to 'src/models.lisp')
-rw-r--r--src/models.lisp186
1 files changed, 186 insertions, 0 deletions
diff --git a/src/models.lisp b/src/models.lisp
new file mode 100644
index 0000000..72a128c
--- /dev/null
+++ b/src/models.lisp
@@ -0,0 +1,186 @@
+(in-package #:hito)
+
+;;; --- Database Configuration ---
+
+(defvar *db-path*
+ (asdf:system-relative-pathname "hito" "hito.db")
+ "Path to the SQLite database file.")
+
+;;; --- Table Definitions ---
+
+(mito:deftable exercise ()
+ ((name :col-type (:varchar 128)
+ :documentation "Exercise name, e.g. 'Pec Deck'")
+ (muscle-group :col-type (:varchar 64)
+ :documentation "Primary muscle group: chest, back, legs, shoulders, arms")
+ (is-default-p :col-type :boolean
+ :initform t
+ :documentation "Whether this is a Mentzer canonical exercise")
+ (substitution-for :col-type (or :bigint :null)
+ :initform nil
+ :documentation "FK to exercise this substitutes for (nil if canonical)"))
+ (:documentation "An exercise in the HITO system. Mentzer's canonical movements are seeded by default."))
+
+(mito:deftable user-profile ()
+ ((name :col-type (:varchar 128)
+ :initform "Trainee"
+ :documentation "User's name")
+ (current-workout-day :col-type :integer
+ :initform 1
+ :documentation "Next workout day in the rotation (1-4)")
+ (last-session-date :col-type (or :varchar :null)
+ :initform nil
+ :documentation "ISO date string of last completed session")
+ (training-start-date :col-type (or :varchar :null)
+ :initform nil
+ :documentation "ISO date string of when training began"))
+ (:documentation "Single user profile. Architected for multi-user extension."))
+
+(mito:deftable training-session ()
+ ((session-date :col-type (:varchar 32)
+ :documentation "ISO date string of this session")
+ (workout-day :col-type :integer
+ :documentation "Which day in the rotation (1-4)")
+ (completed-p :col-type :boolean
+ :initform nil
+ :documentation "Whether the session was completed")
+ (notes :col-type (or :text :null)
+ :initform nil
+ :documentation "Optional session notes")
+ (override-p :col-type :boolean
+ :initform nil
+ :documentation "Whether recovery gate was overridden for this session"))
+ (:documentation "A single training session, one workout day."))
+
+(mito:deftable set-record ()
+ ((session-id :col-type :bigint
+ :documentation "FK to training-session")
+ (exercise-id :col-type :bigint
+ :documentation "FK to exercise")
+ (set-number :col-type :integer
+ :documentation "Order within the exercise (1, 2, ...)")
+ (prescribed-weight :col-type :real
+ :documentation "Target weight in kg")
+ (prescribed-rep-low :col-type :integer
+ :initform 6
+ :documentation "Lower bound of rep range")
+ (prescribed-rep-high :col-type :integer
+ :initform 10
+ :documentation "Upper bound of rep range")
+ (actual-weight :col-type (or :real :null)
+ :initform nil
+ :documentation "Weight actually used (filled after session)")
+ (actual-reps :col-type (or :integer :null)
+ :initform nil
+ :documentation "Reps actually achieved (filled after session)")
+ (to-failure-p :col-type (or :boolean :null)
+ :initform nil
+ :documentation "Whether the set was taken to muscular failure"))
+ (:documentation "A single set within a session — both prescription and result."))
+
+;;; --- Database Initialization ---
+
+(defun connect-db ()
+ "Connect to the SQLite database."
+ (mito:connect-toplevel :sqlite3
+ :database-name *db-path*))
+
+(defun ensure-tables ()
+ "Create or migrate all tables."
+ (mito:ensure-table-exists 'exercise)
+ (mito:ensure-table-exists 'user-profile)
+ (mito:ensure-table-exists 'training-session)
+ (mito:ensure-table-exists 'set-record))
+
+;;; --- Seed Data: Mentzer's HD I Canonical Exercises ---
+
+(defparameter *hd1-exercises*
+ '(;; Day 1: Chest & Back
+ ("Pec Deck" "chest")
+ ("Incline Barbell Press" "chest")
+ ("Straight-Arm Pulldowns" "back")
+ ("Close-Grip Palms-Up Pulldowns" "back")
+ ("Bent-Over Barbell Rows" "back")
+ ;; Day 2: Legs
+ ("Leg Extensions" "legs")
+ ("Leg Press" "legs")
+ ("Squats" "legs")
+ ("Leg Curls" "legs")
+ ("Standing Calf Raises" "legs")
+ ("Toe Presses" "legs")
+ ;; Day 3: Shoulders
+ ("Lateral Raises" "shoulders")
+ ("Overhead Press" "shoulders")
+ ("Bent-Over Lateral Raises" "shoulders")
+ ;; Day 4: Arms
+ ("Barbell Curls" "arms")
+ ("Triceps Pushdowns" "arms")
+ ("Dips" "arms"))
+ "Mentzer's canonical Heavy Duty I exercises.")
+
+(defparameter *substitution-options*
+ '(;; Each entry: (canonical-exercise-name . list-of-substitute-names)
+ ("Pec Deck" "Cable Crossovers" "Dumbbell Flyes")
+ ("Incline Barbell Press" "Incline Dumbbell Press" "Flat Barbell Press")
+ ("Straight-Arm Pulldowns" "Dumbbell Pullovers" "Straight-Arm Cable Pushdowns")
+ ("Close-Grip Palms-Up Pulldowns" "Chin-Ups" "Close-Grip Cable Rows")
+ ("Bent-Over Barbell Rows" "T-Bar Rows" "Seated Cable Rows")
+ ("Leg Extensions" "Sissy Squats")
+ ("Leg Press" "Hack Squats" "Front Squats")
+ ("Squats" "Belt Squats" "Bulgarian Split Squats")
+ ("Leg Curls" "Romanian Deadlifts" "Glute-Ham Raises")
+ ("Standing Calf Raises" "Seated Calf Raises" "Donkey Calf Raises")
+ ("Toe Presses" "Calf Press on Leg Press")
+ ("Lateral Raises" "Cable Lateral Raises" "Machine Lateral Raises")
+ ("Overhead Press" "Dumbbell Shoulder Press" "Machine Shoulder Press")
+ ("Bent-Over Lateral Raises" "Reverse Pec Deck" "Face Pulls")
+ ("Barbell Curls" "Dumbbell Curls" "EZ-Bar Curls")
+ ("Triceps Pushdowns" "Overhead Triceps Extension" "Close-Grip Bench Press")
+ ("Dips" "Weighted Dips" "Decline Close-Grip Press"))
+ "Available substitutions for each canonical exercise.
+The movement pattern is what matters, not brand loyalty to specific equipment.")
+
+(defun seed-exercises ()
+ "Seed the database with Mentzer's canonical exercises if not already present."
+ (when (zerop (mito:count-dao 'exercise))
+ (dolist (entry *hd1-exercises*)
+ (mito:create-dao 'exercise
+ :name (first entry)
+ :muscle-group (second entry)
+ :is-default-p t
+ :substitution-for nil))
+ (format t "~&Seeded ~D canonical exercises.~%" (length *hd1-exercises*))))
+
+(defun find-exercise-by-name (name)
+ "Look up an exercise DAO by name. Returns nil if not found."
+ (first (mito:select-dao 'exercise
+ (sxql:where (:= :name name)))))
+
+(defun seed-substitution-options ()
+ "Seed available substitution exercises (not active, just available for selection).
+These are stored with is-default-p = nil and substitution-for = nil until activated."
+ (dolist (entry *substitution-options*)
+ (let* ((canonical-name (first entry))
+ (canonical (find-exercise-by-name canonical-name))
+ (subs (rest entry)))
+ (when canonical
+ (dolist (sub-name subs)
+ ;; Only create if not already in DB
+ (unless (find-exercise-by-name sub-name)
+ (let ((muscle-group (exercise-muscle-group canonical)))
+ (mito:create-dao 'exercise
+ :name sub-name
+ :muscle-group muscle-group
+ :is-default-p nil
+ :substitution-for nil)))))))
+ (format t "~&Seeded substitution options.~%"))
+
+;;; --- Top-Level Initialization ---
+
+(defun init-db ()
+ "Initialize the database: connect, create tables, seed data."
+ (connect-db)
+ (ensure-tables)
+ (seed-exercises)
+ (seed-substitution-options)
+ (format t "~&Database initialized at ~A~%" *db-path*))
Copyright 2019--2026 Marius PETER