Files
WerkoutAPI/generator/management/commands/calibrate_structure_rules.py
Trey t 1c61b80731 workout generator audit: rules engine, structure rules, split patterns, injury UX, metadata cleanup
- Add rules_engine.py with quantitative rules for all 8 workout types
- Add quality gate retry loop in generate_single_workout()
- Expand calibrate_structure_rules to all 120 combinations (8 types × 5 goals × 3 sections)
- Wire WeeklySplitPattern DB records into _pick_weekly_split()
- Enforce movement patterns from WorkoutStructureRule in exercise selection
- Add straight-set strength support (single main lift, 4-6 rounds)
- Add modality consistency check for duration-dominant workout types
- Add InjuryStep component to onboarding and preferences
- Add sibling exercise exclusion in regenerate and preview_day endpoints
- Display generator warnings on dashboard
- Expand fix_rep_durations, fix_exercise_flags, fix_movement_pattern_typo
- Add audit_exercise_data and check_rules_drift management commands
- Add Next.js frontend with dashboard, onboarding, preferences, history pages
- Add generator app with ML-powered workout generation pipeline
- 96 new tests across 7 test modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:07:40 -06:00

1376 lines
46 KiB
Python

"""
Calibrate WorkoutStructureRule DB records for ALL 8 workout types.
Creates the full 120-rule matrix (8 types x 5 goals x 3 sections).
Values are based on exercise science research (workout_research.md),
not ML extraction. Uses update_or_create for full idempotency.
Workout types: traditional_strength_training, hypertrophy,
high_intensity_interval_training, functional_strength_training,
cross_training, core_training, flexibility, cardio
Goals: strength, hypertrophy, endurance, weight_loss, general_fitness
Sections: warm_up, working, cool_down
"""
from django.core.management.base import BaseCommand
from generator.models import WorkoutType, WorkoutStructureRule
# ======================================================================
# Research-backed structure rules — all 8 workout types
# ======================================================================
# traditional_strength_training: heavy compound lifts, low reps, long rest
# Reference: NSCA Essentials of Strength Training (4th ed.), Schoenfeld 2021
TRADITIONAL_STRENGTH_RULES = {
'warm_up': {
'strength': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 6, 'rep_max': 8,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 30,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 6,
'rep_min': 10, 'rep_max': 12,
'dur_min': 30, 'dur_max': 40,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'cardio/locomotion',
'lower push',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 10, 'rep_max': 12,
'dur_min': 30, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 30,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
},
'working': {
'strength': {
'rounds': 5, 'ex_per_ss': 2,
'rep_min': 6, 'rep_max': 8,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push - horizontal', 'upper push - vertical',
'upper pull - horizontal', 'upper pull - vertical',
'arms',
],
},
'hypertrophy': {
'rounds': 4, 'ex_per_ss': 2,
'rep_min': 6, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push - horizontal', 'upper pull - horizontal',
'upper push - vertical', 'upper pull - vertical',
'arms',
],
},
'endurance': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 10, 'rep_max': 15,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push', 'upper pull', 'arms', 'core',
],
},
'weight_loss': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 12,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push', 'upper pull', 'arms', 'core',
],
},
'general_fitness': {
'rounds': 4, 'ex_per_ss': 2,
'rep_min': 6, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push - horizontal', 'upper pull - horizontal',
'upper push - vertical', 'upper pull - vertical',
'arms',
],
},
},
'cool_down': {
'strength': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 8,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 10, 'rep_max': 12,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 10, 'rep_max': 12,
'dur_min': 25, 'dur_max': 40,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
},
}
# hypertrophy: moderate-heavy loads, controlled tempo, volume-focused
# Reference: Schoenfeld "Science and Development of Muscle Hypertrophy" (2nd ed.)
HYPERTROPHY_RULES = {
'warm_up': {
'strength': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 6, 'rep_max': 8,
'dur_min': 25, 'dur_max': 30,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 30,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 6,
'rep_min': 10, 'rep_max': 12,
'dur_min': 30, 'dur_max': 40,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'cardio/locomotion', 'lower push',
'core - anti-extension',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 10, 'rep_max': 12,
'dur_min': 30, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 30,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
},
'working': {
'strength': {
'rounds': 4, 'ex_per_ss': 2,
'rep_min': 6, 'rep_max': 8,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'upper push - horizontal', 'upper push - vertical',
'upper pull - horizontal', 'upper pull - vertical',
'lower push - squat', 'lower pull - hip hinge',
'arms',
],
},
'hypertrophy': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 12,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'upper push', 'upper pull', 'lower push',
'lower pull', 'arms', 'core',
'lower push - squat', 'lower pull - hip hinge',
],
},
'endurance': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 12, 'rep_max': 15,
'dur_min': 25, 'dur_max': 40,
'patterns': [
'upper push', 'upper pull', 'lower push',
'lower pull', 'arms', 'core', 'cardio/locomotion',
],
},
'weight_loss': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 10, 'rep_max': 15,
'dur_min': 20, 'dur_max': 35,
'patterns': [
'upper push', 'upper pull', 'lower push',
'lower pull', 'arms', 'core',
],
},
'general_fitness': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 12,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'upper push', 'upper pull', 'lower push',
'lower pull', 'arms', 'core',
'lower push - squat', 'lower pull - hip hinge',
],
},
},
'cool_down': {
'strength': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 8,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 10, 'rep_max': 12,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge', 'lower push',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 10, 'rep_max': 12,
'dur_min': 25, 'dur_max': 40,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
},
}
# high_intensity_interval_training: short work intervals, high tempo circuits
# Reference: Biddle & Batterham (2015), Tabata protocol research
# Key: 20-30 min total, 30:30 "Goldilocks ratio", 4-6 exercises per circuit
HIIT_RULES = {
'warm_up': {
'strength': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 12,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'cardio/locomotion',
'core', 'lower push',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 12,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'cardio/locomotion',
'core', 'lower push',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 10, 'rep_max': 15,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'cardio/locomotion',
'core', 'lower push', 'mobility',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 10, 'rep_max': 15,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'cardio/locomotion',
'core', 'lower push', 'mobility',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 12,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'cardio/locomotion',
'core', 'lower push',
],
},
},
'working': {
'strength': {
'rounds': 4, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 12,
'dur_min': 20, 'dur_max': 40,
'patterns': [
'lower pull - hip hinge', 'upper push',
'core', 'upper pull', 'lower push - squat',
'plyometric',
],
},
'hypertrophy': {
'rounds': 4, 'ex_per_ss': 5,
'rep_min': 10, 'rep_max': 15,
'dur_min': 20, 'dur_max': 40,
'patterns': [
'upper push', 'upper pull', 'lower push',
'lower pull', 'core', 'plyometric',
],
},
'endurance': {
'rounds': 5, 'ex_per_ss': 5,
'rep_min': 12, 'rep_max': 20,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'cardio/locomotion', 'upper push', 'upper pull',
'lower push', 'lower pull', 'core', 'plyometric',
],
},
'weight_loss': {
'rounds': 5, 'ex_per_ss': 5,
'rep_min': 10, 'rep_max': 20,
'dur_min': 20, 'dur_max': 40,
'patterns': [
'cardio/locomotion', 'upper push', 'upper pull',
'lower push', 'lower pull', 'core', 'plyometric',
],
},
'general_fitness': {
'rounds': 4, 'ex_per_ss': 4,
'rep_min': 10, 'rep_max': 15,
'dur_min': 20, 'dur_max': 40,
'patterns': [
'lower pull - hip hinge', 'upper push',
'core', 'upper pull', 'lower push - squat',
'plyometric',
],
},
},
'cool_down': {
'strength': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 12,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'cardio/locomotion',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 12,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
},
}
# functional_strength_training: compound movements, carries, 7 movement patterns
# Reference: Dan John, Pavel Tsatsouline, NSCA
# Key: 3-5 sets, 60-180s rest, all 7 patterns represented, carries mandatory
FUNCTIONAL_STRENGTH_RULES = {
'warm_up': {
'strength': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 6, 'rep_max': 8,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 30,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 6,
'rep_min': 10, 'rep_max': 12,
'dur_min': 30, 'dur_max': 40,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'cardio/locomotion',
'lower push',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 10, 'rep_max': 12,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 30,
'patterns': [
'mobility', 'mobility - dynamic', 'core',
'core - anti-extension', 'lower push',
],
},
},
'working': {
'strength': {
'rounds': 4, 'ex_per_ss': 2,
'rep_min': 6, 'rep_max': 8,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push - horizontal', 'upper push - vertical',
'upper pull - horizontal', 'upper pull - vertical',
'carry',
],
},
'hypertrophy': {
'rounds': 4, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push - horizontal', 'upper pull - horizontal',
'upper push - vertical', 'upper pull - vertical',
'carry', 'arms',
],
},
'endurance': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 12, 'rep_max': 20,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push', 'upper pull', 'carry',
'core', 'cardio/locomotion',
],
},
'weight_loss': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 12,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push', 'upper pull', 'carry',
'core',
],
},
'general_fitness': {
'rounds': 4, 'ex_per_ss': 2,
'rep_min': 6, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push - horizontal', 'upper pull - horizontal',
'upper push - vertical', 'upper pull - vertical',
'carry',
],
},
},
'cool_down': {
'strength': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 8,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 10, 'rep_max': 12,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 10, 'rep_max': 12,
'dur_min': 25, 'dur_max': 40,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility', 'mobility - static', 'yoga',
'lower pull - hip hinge',
],
},
},
}
# cross_training: mixed modality — strength + WOD formats (AMRAP, EMOM, For Time)
# Reference: CrossFit methodology, Glassman
# Key: complexity decreases with fatigue, pull:press 1.5:1, varied formats
CROSS_TRAINING_RULES = {
'warm_up': {
'strength': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 6, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'core', 'lower push',
'cardio/locomotion', 'mobility',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 8, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'core', 'lower push',
'cardio/locomotion', 'mobility',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 6,
'rep_min': 10, 'rep_max': 15,
'dur_min': 25, 'dur_max': 35,
'patterns': [
'mobility - dynamic', 'core', 'lower push',
'cardio/locomotion', 'mobility',
'core - anti-extension',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 10, 'rep_max': 12,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'core', 'lower push',
'cardio/locomotion', 'mobility',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 5,
'rep_min': 8, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'core', 'lower push',
'cardio/locomotion', 'mobility',
],
},
},
'working': {
'strength': {
'rounds': 4, 'ex_per_ss': 3,
'rep_min': 6, 'rep_max': 10,
'dur_min': 20, 'dur_max': 40,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push - horizontal', 'upper push - vertical',
'upper pull - horizontal', 'upper pull - vertical',
'plyometric',
],
},
'hypertrophy': {
'rounds': 4, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 12,
'dur_min': 20, 'dur_max': 40,
'patterns': [
'upper push', 'upper pull', 'lower push',
'lower pull', 'core', 'arms',
'plyometric',
],
},
'endurance': {
'rounds': 3, 'ex_per_ss': 4,
'rep_min': 10, 'rep_max': 15,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'cardio/locomotion', 'upper push', 'upper pull',
'lower push', 'lower pull', 'core',
'plyometric',
],
},
'weight_loss': {
'rounds': 4, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 15,
'dur_min': 20, 'dur_max': 40,
'patterns': [
'cardio/locomotion', 'upper push', 'upper pull',
'lower push', 'lower pull', 'core',
'plyometric',
],
},
'general_fitness': {
'rounds': 4, 'ex_per_ss': 3,
'rep_min': 6, 'rep_max': 15,
'dur_min': 20, 'dur_max': 40,
'patterns': [
'lower push - squat', 'lower pull - hip hinge',
'upper push', 'upper pull', 'core',
'cardio/locomotion', 'plyometric',
],
},
},
'cool_down': {
'strength': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 10,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 12,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'cardio/locomotion',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 12,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
},
}
# core_training: anti-movement focus, moderate reps, holds + reps mix
# Reference: McGill Big 3, anti-movement research
# Key: anti-extension + anti-rotation + anti-lateral each session, 20-30 min
CORE_TRAINING_RULES = {
'warm_up': {
'strength': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'mobility', 'core',
'cardio/locomotion',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'mobility', 'core',
'cardio/locomotion',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 10, 'rep_max': 12,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'mobility', 'core',
'cardio/locomotion',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 10, 'rep_max': 12,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'mobility', 'core',
'cardio/locomotion',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'mobility', 'core',
'cardio/locomotion',
],
},
},
'working': {
'strength': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 12,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'core - anti-extension', 'core - anti-rotation',
'core - anti-lateral flexion', 'core',
'carry',
],
},
'hypertrophy': {
'rounds': 3, 'ex_per_ss': 4,
'rep_min': 10, 'rep_max': 15,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'core - anti-extension', 'core - anti-rotation',
'core - anti-lateral flexion', 'core',
'core - hip flexion', 'carry',
],
},
'endurance': {
'rounds': 3, 'ex_per_ss': 4,
'rep_min': 15, 'rep_max': 20,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'core - anti-extension', 'core - anti-rotation',
'core - anti-lateral flexion', 'core',
'core - hip flexion', 'carry',
],
},
'weight_loss': {
'rounds': 3, 'ex_per_ss': 4,
'rep_min': 12, 'rep_max': 20,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'core - anti-extension', 'core - anti-rotation',
'core - anti-lateral flexion', 'core',
'carry', 'cardio/locomotion',
],
},
'general_fitness': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 10, 'rep_max': 15,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'core - anti-extension', 'core - anti-rotation',
'core - anti-lateral flexion', 'core',
'carry',
],
},
},
'cool_down': {
'strength': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 10, 'rep_max': 12,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 10, 'rep_max': 12,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 45,
'patterns': [
'mobility - static', 'yoga', 'mobility',
],
},
},
}
# flexibility: duration-dominant, stretch holds, 1-2 rounds
# Reference: Cipriani et al. (2012), Bandy & Irion (1994)
# Key: 45-60s holds, RPE 5-6, hip mobility gets most time, PNF 2-3x/week
FLEXIBILITY_RULES = {
'warm_up': {
'strength': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'mobility', 'core',
'cardio/locomotion',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'mobility', 'core',
'cardio/locomotion',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'mobility', 'core',
'cardio/locomotion',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 8, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'mobility', 'core',
'cardio/locomotion',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 10,
'dur_min': 20, 'dur_max': 30,
'patterns': [
'mobility - dynamic', 'mobility', 'core',
'cardio/locomotion',
],
},
},
'working': {
'strength': {
'rounds': 2, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 8,
'dur_min': 45, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'mobility - dynamic',
],
},
'hypertrophy': {
'rounds': 2, 'ex_per_ss': 5,
'rep_min': 6, 'rep_max': 10,
'dur_min': 45, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'mobility - dynamic',
'lower pull - hip hinge',
],
},
'endurance': {
'rounds': 2, 'ex_per_ss': 5,
'rep_min': 6, 'rep_max': 10,
'dur_min': 45, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'mobility - dynamic',
'cardio/locomotion',
],
},
'weight_loss': {
'rounds': 2, 'ex_per_ss': 5,
'rep_min': 6, 'rep_max': 10,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'mobility - dynamic',
'cardio/locomotion',
],
},
'general_fitness': {
'rounds': 2, 'ex_per_ss': 5,
'rep_min': 6, 'rep_max': 10,
'dur_min': 45, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'mobility - dynamic',
'lower pull - hip hinge',
],
},
},
'cool_down': {
'strength': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 8,
'dur_min': 45, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 8,
'dur_min': 45, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 10,
'dur_min': 45, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 10,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 4,
'rep_min': 6, 'rep_max': 8,
'dur_min': 45, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
'lower pull - hip hinge',
],
},
},
}
# cardio: duration-dominant, polarized training (70-80% Zone 2, 20-30% Zone 4-5)
# Reference: Stöggl & Sperlich (2015), Inigo San Millan Zone 2 research
# Key: rounds 2-3 (NOT 23-25 from ML), 30-90s duration, steady state
CARDIO_RULES = {
'warm_up': {
'strength': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 6, 'rep_max': 10,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - dynamic', 'cardio/locomotion',
'mobility',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 6, 'rep_max': 10,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - dynamic', 'cardio/locomotion',
'mobility',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - dynamic', 'cardio/locomotion',
'mobility',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - dynamic', 'cardio/locomotion',
'mobility',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 6, 'rep_max': 10,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - dynamic', 'cardio/locomotion',
'mobility',
],
},
},
'working': {
'strength': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 6, 'rep_max': 10,
'dur_min': 40, 'dur_max': 90,
'patterns': [
'cardio/locomotion', 'lower push', 'lower pull',
],
},
'hypertrophy': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 12,
'dur_min': 40, 'dur_max': 90,
'patterns': [
'cardio/locomotion', 'lower push', 'lower pull',
],
},
'endurance': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 12,
'dur_min': 45, 'dur_max': 120,
'patterns': [
'cardio/locomotion', 'lower push', 'lower pull',
'core',
],
},
'weight_loss': {
'rounds': 3, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 12,
'dur_min': 45, 'dur_max': 90,
'patterns': [
'cardio/locomotion', 'lower push', 'lower pull',
'core',
],
},
'general_fitness': {
'rounds': 2, 'ex_per_ss': 3,
'rep_min': 6, 'rep_max': 10,
'dur_min': 45, 'dur_max': 90,
'patterns': [
'cardio/locomotion', 'lower push', 'lower pull',
],
},
},
'cool_down': {
'strength': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 6, 'rep_max': 8,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
],
},
'hypertrophy': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 6, 'rep_max': 8,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
],
},
'endurance': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'cardio/locomotion',
],
},
'weight_loss': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 8, 'rep_max': 10,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
],
},
'general_fitness': {
'rounds': 1, 'ex_per_ss': 3,
'rep_min': 6, 'rep_max': 8,
'dur_min': 30, 'dur_max': 60,
'patterns': [
'mobility - static', 'yoga', 'mobility',
],
},
},
}
# ======================================================================
# Master mapping: workout_type DB name -> rule dict
# ======================================================================
ALL_RULES = {
'traditional_strength_training': TRADITIONAL_STRENGTH_RULES,
'hypertrophy': HYPERTROPHY_RULES,
'high_intensity_interval_training': HIIT_RULES,
'functional_strength_training': FUNCTIONAL_STRENGTH_RULES,
'cross_training': CROSS_TRAINING_RULES,
'core_training': CORE_TRAINING_RULES,
'flexibility': FLEXIBILITY_RULES,
'cardio': CARDIO_RULES,
}
# Minimum rep floor — any rule with rep_min below this gets clamped.
MIN_REPS = 6
class Command(BaseCommand):
help = (
'Create/update all 120 WorkoutStructureRule records '
'(8 types x 5 goals x 3 sections). Fully idempotent.'
)
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would change without applying',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
prefix = '[DRY RUN] ' if dry_run else ''
created_total = 0
updated_total = 0
# ----- Create/update all 8 workout types -----
for type_name, rules_dict in ALL_RULES.items():
c, u = self._upsert_rules_for_type(
type_name, rules_dict, dry_run, prefix,
)
created_total += c
updated_total += u
# ----- Fix all sub-floor rep_min values -----
fixed = self._fix_rep_floors(dry_run, prefix)
self.stdout.write(
f'\n{prefix}Done: created {created_total}, '
f'updated {updated_total}, fixed {fixed} rep floors'
)
total = WorkoutStructureRule.objects.count()
self.stdout.write(f'{prefix}Total rules in DB: {total}')
def _upsert_rules_for_type(self, type_name, rules_dict, dry_run, prefix):
"""Create or update all WorkoutStructureRules for a workout type."""
try:
wt = WorkoutType.objects.get(name=type_name)
except WorkoutType.DoesNotExist:
self.stderr.write(
f' WorkoutType "{type_name}" not found, skipping'
)
return 0, 0
created_count = 0
updated_count = 0
self.stdout.write(f'\n{prefix}Processing {type_name}:')
for section_type, goals in rules_dict.items():
for goal_type, vals in goals.items():
defaults = {
'typical_rounds': vals['rounds'],
'typical_exercises_per_superset': vals['ex_per_ss'],
'typical_rep_range_min': vals['rep_min'],
'typical_rep_range_max': vals['rep_max'],
'typical_duration_range_min': vals['dur_min'],
'typical_duration_range_max': vals['dur_max'],
'movement_patterns': vals['patterns'],
}
if dry_run:
exists = WorkoutStructureRule.objects.filter(
workout_type=wt,
section_type=section_type,
goal_type=goal_type,
).exists()
action = 'update' if exists else 'create'
else:
_, was_created = WorkoutStructureRule.objects.update_or_create(
workout_type=wt,
section_type=section_type,
goal_type=goal_type,
defaults=defaults,
)
action = 'create' if was_created else 'update'
if action == 'create':
created_count += 1
else:
updated_count += 1
self.stdout.write(
f' {action}: {section_type}/{goal_type} '
f'rounds={vals["rounds"]}, ex/ss={vals["ex_per_ss"]}, '
f'reps={vals["rep_min"]}-{vals["rep_max"]}, '
f'dur={vals["dur_min"]}-{vals["dur_max"]}s'
)
return created_count, updated_count
def _fix_rep_floors(self, dry_run, prefix):
"""Clamp all rep_min values below MIN_REPS to MIN_REPS."""
fixed = 0
rules = WorkoutStructureRule.objects.filter(
typical_rep_range_min__lt=MIN_REPS,
typical_rep_range_min__gt=0,
)
if rules.exists():
self.stdout.write(
f'\n{prefix}Fixing sub-floor rep_min values '
f'(minimum {MIN_REPS}):'
)
for rule in rules:
old_min = rule.typical_rep_range_min
old_max = rule.typical_rep_range_max
new_min = MIN_REPS
new_max = max(old_max, new_min)
changes = [f'rep_min: {old_min} -> {new_min}']
if new_max != old_max:
changes.append(f'rep_max: {old_max} -> {new_max}')
wt_name = rule.workout_type.name if rule.workout_type else 'Any'
self.stdout.write(
f' {wt_name}/{rule.section_type}/{rule.goal_type}: '
f'{", ".join(changes)}'
)
if not dry_run:
rule.typical_rep_range_min = new_min
rule.typical_rep_range_max = new_max
rule.save()
fixed += 1
if not rules.exists():
self.stdout.write(
f'\n{prefix}No sub-floor rep_min values found'
)
return fixed