# Werkout Generator - Complete Fix Plan ## Scope Fix every issue found in the end-to-end audit: safety bugs, unused data, preference gaps, data consistency, calibration conflicts, and variety enforcement. 14 phases, ordered by dependency and impact. --- ## Phase 1: Consolidate WorkoutType Defaults (Single Source of Truth) **Problem:** `calibrate_workout_types.py` and `DEFAULT_WORKOUT_TYPES` in `workout_analyzer.py` conflict. Running `analyze_workouts` after calibration overwrites calibrated values. **Files:** - `generator/services/workout_analyzer.py` (~lines 58-195) - Update `DEFAULT_WORKOUT_TYPES` to use calibrated (research-backed) values directly - `generator/management/commands/calibrate_workout_types.py` - Delete this file (no longer needed) - `generator/management/commands/analyze_workouts.py` - Remove any reference to calibration **Changes:** 1. In `workout_analyzer.py`, update `DEFAULT_WORKOUT_TYPES` dict to merge in all calibrated values from `CALIBRATIONS` in `calibrate_workout_types.py`: - `functional_strength_training`: rep_min=6, rep_max=15, rest=90, intensity=high, duration_bias=0.15, superset_size_min=2, superset_size_max=4 - `traditional_strength_training`: rep_min=3, rep_max=8, rest=150, intensity=high, duration_bias=0.0, round_min=4, round_max=6 - `hypertrophy`: rep_min=6, rep_max=15, rest=90, intensity=high, duration_bias=0.1 - `cross_training`: duration_bias=0.5 - `core_training`: duration_bias=0.6 - `flexibility`: duration_bias=1.0 2. Populate `display_name` for all 8 types: - `functional_strength_training` -> "Functional Strength" - `traditional_strength_training` -> "Traditional Strength" - `high_intensity_interval_training` -> "HIIT" - `cross_training` -> "Cross Training" - `core_training` -> "Core Training" - `flexibility` -> "Flexibility" - `cardio` -> "Cardio" - `hypertrophy` -> "Hypertrophy" 3. Change `get_or_create` in analyzer to use `update_or_create` so re-running always applies the latest defaults without needing a separate calibration step. 4. Delete `calibrate_workout_types.py`. **Test:** Run `python manage.py analyze_workouts --dry-run` and verify all 8 types show correct calibrated values and display_names. --- ## Phase 2: Fix Bodyweight Fallback Safety Gap **Problem:** `_get_bodyweight_queryset()` (exercise_selector.py:400-446) skips fitness level and injury filters. A beginner with a knee injury could get advanced plyometric bodyweight exercises. **Files:** - `generator/services/exercise_selector.py` - lines 400-446 **Changes:** 1. Add `fitness_level` parameter to `_get_bodyweight_queryset()` signature (currently only takes `muscle_groups` and `is_duration_based`). 2. After the muscle group filtering (line ~440), add the same fitness level filtering that exists in `_get_filtered_queryset`: ```python # Fitness level safety (same as main queryset lines 367-376) if fitness_level is not None and fitness_level <= 1: for pattern in self.ADVANCED_PATTERNS: qs = qs.exclude(movement_patterns__icontains=pattern) qs = qs.exclude(difficulty_level='advanced') ``` 3. Add injury filtering (same logic as lines 379-397): ```python injuries = (self.user_preference.injuries_limitations or '').lower() if injuries: # Apply same keyword-based filtering as _get_filtered_queryset ... ``` 4. Update the call site at line ~172 to pass `fitness_level=self.user_preference.fitness_level`. **Test:** Create a test with a beginner user who has knee injury + limited equipment. Verify bodyweight fallback never returns advanced or high-impact exercises. --- ## Phase 3: Use `hr_elevation_rating` for Warmup & Cooldown Quality **Problem:** `hr_elevation_rating` (1-10) is populated on every exercise but never used. Warmup could select high-HR exercises, cooldown could select activation exercises. **Files:** - `generator/services/exercise_selector.py` - `select_warmup_exercises()` (lines 193-230) and `select_cooldown_exercises()` (lines 231-279) **Changes:** ### Warmup (lines 193-230): 1. After the existing filtering and before `_weighted_pick` (line ~210), add HR-based preference: ```python # Prefer moderate HR for warmup (gradual elevation) warmup_hr_preferred = preferred_qs.filter(hr_elevation_rating__gte=2, hr_elevation_rating__lte=5) warmup_hr_other = preferred_qs.filter( Q(hr_elevation_rating__isnull=True) | Q(hr_elevation_rating__lt=2) | Q(hr_elevation_rating__gt=5) ) ``` 2. Pass `warmup_hr_preferred` as the preferred queryset to `_weighted_pick`, with `warmup_hr_other` as other. 3. In the fallback path (lines 214-224), also apply the HR preference. ### Cooldown (lines 231-279): 1. After the R11 weight filtering (line ~252), add HR ceiling: ```python # Hard filter: cooldown exercises should have low HR elevation qs = qs.filter(Q(hr_elevation_rating__lte=3) | Q(hr_elevation_rating__isnull=True)) ``` 2. Apply the same filter in the fallback path (lines 260-273). **Test:** Generate workouts and verify warmup exercises have HR ratings 2-5, cooldown exercises have HR ratings <= 3. --- ## Phase 4: Use `complexity_rating` for Beginner Safety **Problem:** `complexity_rating` (1-5) is populated but never used. Beginners can get complexity-5 exercises (Olympic variations) that passed the pattern filter. **Files:** - `generator/services/exercise_selector.py` - `_get_filtered_queryset()` (lines 367-376) **Changes:** 1. After the existing fitness level filtering (line 376), add complexity cap: ```python # Complexity cap by fitness level if fitness_level is not None: complexity_caps = {1: 3, 2: 4, 3: 5, 4: 5} # Beginner max 3, Intermediate max 4 max_complexity = complexity_caps.get(fitness_level, 5) qs = qs.filter( Q(complexity_rating__lte=max_complexity) | Q(complexity_rating__isnull=True) ) ``` 2. Also add this to `_get_bodyweight_queryset()` (from Phase 2 changes). **Test:** Generate workouts for a beginner. Verify no exercises with complexity_rating > 3 appear. --- ## Phase 5: Use `stretch_position` for Hypertrophy Workouts **Problem:** `stretch_position` (lengthened/mid/shortened) is populated but unused. Hypertrophy workouts should balance stretch positions per muscle group for optimal stimulus. **Files:** - `generator/services/exercise_selector.py` - `select_exercises()` (lines 55-192) - `generator/services/workout_generator.py` - `_build_working_supersets()` (lines 1176-1398) **Changes:** ### In exercise_selector.py: 1. Add a new method `_balance_stretch_positions(self, selected, muscle_groups)`: ```python def _balance_stretch_positions(self, selected, muscle_groups, base_qs): """For hypertrophy, ensure we don't have all exercises in the same stretch position for a muscle.""" if len(selected) < 3: return selected positions = [ex.stretch_position for ex in selected if ex.stretch_position] if not positions: return selected # If >66% of exercises have the same stretch position, try to swap one from collections import Counter counts = Counter(positions) most_common_pos, most_common_count = counts.most_common(1)[0] if most_common_count / len(positions) <= 0.66: return selected # Find underrepresented position all_positions = {'lengthened', 'mid', 'shortened'} missing = all_positions - set(positions) target_position = missing.pop() if missing else None if not target_position: return selected # Try to swap last accessory with an exercise of the missing position replacement_qs = base_qs.filter(stretch_position=target_position).exclude( pk__in=self.used_exercise_ids ) replacement = replacement_qs.first() if replacement: # Swap last non-primary exercise for i in range(len(selected) - 1, -1, -1): if selected[i].exercise_tier != 'primary': selected[i] = replacement break return selected ``` ### In workout_generator.py: 2. In `_build_working_supersets()`, after exercise selection (line ~1319), add: ```python # Balance stretch positions for hypertrophy goals if self.preference.primary_goal == 'hypertrophy': exercises = self.exercise_selector._balance_stretch_positions( exercises, superset_muscles, base_qs ) ``` **Test:** Generate hypertrophy workouts. Verify exercises for the same muscle don't all share the same stretch position. --- ## Phase 6: Strengthen Recently-Used Exclusion **Problem:** Recently used exercises are only down-weighted (3x to 1x in `_weighted_pick`), not excluded. Users can get the same exercises repeatedly. **Files:** - `generator/services/exercise_selector.py` - `_weighted_pick()` (lines 447-496) and `_get_filtered_queryset()` (lines 284-399) - `generator/services/workout_generator.py` - lines 577-589 **Changes:** ### In exercise_selector.py: 1. Add a `hard_exclude_ids` set to `__init__` (line 42-46): ```python def __init__(self, user_preference, recently_used_ids=None, hard_exclude_ids=None): self.user_preference = user_preference self.used_exercise_ids = set() self.recently_used_ids = recently_used_ids or set() self.hard_exclude_ids = hard_exclude_ids or set() # Exercises from last 3 workouts ``` 2. In `_get_filtered_queryset()`, after excluding `used_exercise_ids` (line 304-305), add: ```python # Hard exclude exercises from very recent workouts (last 3) if self.hard_exclude_ids: qs = qs.exclude(pk__in=self.hard_exclude_ids) ``` 3. Keep the existing soft penalty in `_weighted_pick` (lines 467-468) for exercises from workouts 4-7. ### In workout_generator.py: 4. Split recently used into two tiers (around line 577-589): ```python # Last 3 workouts: hard exclude very_recent_workout_ids = list( GeneratedWorkout.objects.filter(...) .order_by('-scheduled_date')[:3] .values_list('workout_id', flat=True) ) hard_exclude_ids = set( SupersetExercise.objects.filter(superset__workout_id__in=very_recent_workout_ids) .values_list('exercise_id', flat=True) ) if very_recent_workout_ids else set() # Workouts 4-7: soft penalty (existing behavior) older_recent_ids = list( GeneratedWorkout.objects.filter(...) .order_by('-scheduled_date')[3:7] .values_list('workout_id', flat=True) ) soft_penalty_ids = set( SupersetExercise.objects.filter(superset__workout_id__in=older_recent_ids) .values_list('exercise_id', flat=True) ) if older_recent_ids else set() self.exercise_selector = ExerciseSelector( self.preference, recently_used_ids=soft_penalty_ids, hard_exclude_ids=hard_exclude_ids ) ``` **Test:** Generate 4 weekly plans in sequence. Verify exercises from the most recent 3 workouts never appear in the next plan (unless the pool is too small and fallback kicks in). --- ## Phase 7: Apply Target Muscles to ALL Split Types **Problem:** User's `target_muscle_groups` are only injected on `full_body` days (workout_generator.py:681-694). On push/pull/legs days, they're completely ignored. **Files:** - `generator/services/workout_generator.py` - `generate_single_workout()` (lines 681-694) **Changes:** 1. Replace the full_body-only logic with universal target muscle integration: ```python # Get user's target muscle groups user_target_muscles = list( self.preference.target_muscle_groups.values_list('name', flat=True) ) if user_target_muscles: normalized_targets = [normalize_muscle_name(m) for m in user_target_muscles] if split_type == 'full_body': # Full body: inject all target muscles for m in normalized_targets: if m not in target_muscles: target_muscles.append(m) else: # Other splits: inject target muscles that are RELEVANT to this split type split_relevant_muscles = set() categories = MUSCLE_GROUP_CATEGORIES # from muscle_normalizer # Map split_type to relevant muscle categories split_muscle_map = { 'push': categories.get('upper_push', []), 'pull': categories.get('upper_pull', []), 'upper': categories.get('upper_push', []) + categories.get('upper_pull', []), 'lower': categories.get('lower_push', []) + categories.get('lower_pull', []), 'legs': categories.get('lower_push', []) + categories.get('lower_pull', []), 'core': categories.get('core', []), } relevant = set(split_muscle_map.get(split_type, [])) # Add user targets that overlap with this split's muscle domain for m in normalized_targets: if m in relevant and m not in target_muscles: target_muscles.append(m) ``` **Test:** Set target muscles to ["biceps", "glutes"]. Generate a PPL plan. Verify "biceps" appears in pull day targets and "glutes" appears in legs day targets. Neither should appear in push day targets. --- ## Phase 8: Expand Injury Filtering **Problem:** Only 3 injury types (knee/back/shoulder) with freeform text matching. No support for wrist, ankle, hip, elbow, or neck. **Files:** - `generator/services/exercise_selector.py` - `_get_filtered_queryset()` (lines 379-397) - `generator/models.py` - `UserPreference` model (line 112) **Changes:** ### Model change (generator/models.py): 1. Add structured injury field alongside the existing freeform field: ```python INJURY_CHOICES = [ ('knee', 'Knee'), ('back', 'Back'), ('shoulder', 'Shoulder'), ('wrist', 'Wrist'), ('ankle', 'Ankle'), ('hip', 'Hip'), ('elbow', 'Elbow'), ('neck', 'Neck'), ] injury_types = JSONField(default=list, blank=True, help_text='List of injury type strings') ``` 2. Create migration for the new field. ### Serializer change (generator/serializers.py): 3. Add `injury_types` to `UserPreferenceSerializer` and `UserPreferenceUpdateSerializer` fields lists. ### Filtering logic (exercise_selector.py): 4. Refactor injury filtering at lines 379-397 to use both old text field AND new structured field: ```python # Structured injury types (new) injury_types = set(self.user_preference.injury_types or []) # Also parse freeform text for backward compatibility injuries_text = (self.user_preference.injuries_limitations or '').lower() keyword_map = { 'knee': ['knee', 'acl', 'mcl', 'meniscus', 'patella'], 'back': ['back', 'spine', 'spinal', 'disc', 'herniat'], 'shoulder': ['shoulder', 'rotator', 'labrum', 'impingement'], 'wrist': ['wrist', 'carpal'], 'ankle': ['ankle', 'achilles', 'plantar'], 'hip': ['hip', 'labral', 'hip flexor'], 'elbow': ['elbow', 'tennis elbow', 'golfer'], 'neck': ['neck', 'cervical'], } for injury_type, keywords in keyword_map.items(): if any(kw in injuries_text for kw in keywords): injury_types.add(injury_type) # Apply filters per injury type if 'knee' in injury_types: qs = qs.exclude(impact_level='high') if 'back' in injury_types: qs = qs.exclude(impact_level='high') qs = qs.exclude( Q(movement_patterns__icontains='hip hinge') & Q(is_weight=True) & Q(difficulty_level='advanced') ) if 'shoulder' in injury_types: qs = qs.exclude(movement_patterns__icontains='upper push - vertical') if 'wrist' in injury_types: qs = qs.exclude( Q(movement_patterns__icontains='olympic') | Q(name__icontains='wrist curl') | Q(name__icontains='handstand') ) if 'ankle' in injury_types: qs = qs.exclude(impact_level__in=['high', 'medium']) if 'hip' in injury_types: qs = qs.exclude( Q(movement_patterns__icontains='hip hinge') & Q(difficulty_level='advanced') ) qs = qs.exclude(impact_level='high') if 'elbow' in injury_types: qs = qs.exclude( Q(movement_patterns__icontains='arms') & Q(is_weight=True) & Q(difficulty_level='advanced') ) if 'neck' in injury_types: qs = qs.exclude(name__icontains='neck') qs = qs.exclude( Q(movement_patterns__icontains='olympic') & Q(difficulty_level='advanced') ) ``` **Test:** Set injury_types=["knee", "wrist"]. Verify no high-impact or Olympic/handstand exercises appear. --- ## Phase 9: Fix Cardio Data & Ensure Full Rule Coverage **Problem:** ML extraction can produce broken cardio rules (23-25 rounds). Some workout types may have no rules at all after analysis. **Files:** - `generator/services/workout_analyzer.py` - `_step5_extract_workout_structure_rules()` (~lines 818-1129) - `generator/management/commands/calibrate_structure_rules.py` - Merge into analyzer **Changes:** ### In workout_analyzer.py: 1. In `_step5_extract_workout_structure_rules()`, add sanity bounds for per-superset rounds: ```python # Clamp rounds to per-superset range (not total workout rounds) typical_rounds = max(1, min(8, typical_rounds)) # Was min(50) ``` 2. Add a validation pass after rule extraction: ```python def _ensure_full_rule_coverage(self): """Ensure every WorkoutType has at least one rule per (section, goal) combo.""" for wt in WorkoutType.objects.all(): for section in ['warm_up', 'working', 'cool_down']: for goal, _ in GOAL_CHOICES: exists = WorkoutStructureRule.objects.filter( workout_type=wt, section_type=section, goal_type=goal ).exists() if not exists: self._create_default_rule(wt, section, goal) ``` 3. Merge the essential fixes from `calibrate_structure_rules.py` into the analyzer's default rule creation so they're always applied. ### In calibrate_structure_rules.py: 4. Keep this command but change it to only fix known data issues (rep floor clamping, cardio round clamping) rather than creating rules from scratch. Add a check: if rules already have sane values, skip. **Test:** Run `analyze_workouts`. Verify all 8 workout types x 3 sections x 5 goals = 120 rules exist, and no rule has rounds > 8 or rep_min < 1. --- ## Phase 10: Fix WeeklySplitPattern PK Stability **Problem:** `WeeklySplitPattern.pattern` stores raw `MuscleGroupSplit` PKs. Re-running the analyzer creates new splits with new PKs, making old pattern references stale. **Files:** - `generator/models.py` - `WeeklySplitPattern` (lines 196-204) - `generator/services/workout_analyzer.py` - `_step4_extract_weekly_split_patterns()` (~lines 650-812) - `generator/services/workout_generator.py` - `_pick_weekly_split()` (lines 739-796) **Changes:** ### Option: Store labels instead of PKs 1. In `WeeklySplitPattern`, the `pattern` field currently stores `[5, 12, 5, 8]` (MuscleGroupSplit PKs). Change the analyzer to store split_type strings instead: `["push", "pull", "push", "lower"]`. 2. In the analyzer's `_step4`, when building patterns: ```python # Instead of storing PKs pattern_entry = { 'split_types': [split.split_type for split in matched_splits], 'labels': [split.label for split in matched_splits], 'muscle_sets': [split.muscle_names for split in matched_splits], } ``` 3. Update `WeeklySplitPattern` model: ```python pattern = JSONField(default=list) # Now stores split_type strings pattern_muscles = JSONField(default=list) # Stores muscle name lists per day ``` Create migration. 4. In `_pick_weekly_split()` (workout_generator.py:739-796), resolve patterns using `split_type` + `muscle_names` instead of PK lookups: ```python # Instead of MuscleGroupSplit.objects.get(pk=split_id) # Use the pattern's stored muscle lists directly for i, split_type in enumerate(pattern.pattern): muscles = pattern.pattern_muscles[i] if pattern.pattern_muscles else [] split_days.append({ 'label': pattern.pattern_labels[i], 'muscles': muscles, 'split_type': split_type, }) ``` **Test:** Run `analyze_workouts` twice. Verify the second run doesn't break weekly patterns from the first. --- ## Phase 11: Add Movement Pattern Variety Tracking **Problem:** No tracking of movement patterns within a workout. Could get 3 horizontal presses in one session. **Files:** - `generator/services/exercise_selector.py` - Add tracking - `generator/services/workout_generator.py` - `_build_working_supersets()` (lines 1176-1398) **Changes:** ### In exercise_selector.py: 1. Add pattern tracking to `__init__`: ```python self.used_movement_patterns = Counter() # Track patterns used in current workout ``` 2. Add to `reset()`: ```python self.used_movement_patterns = Counter() ``` 3. In `select_exercises()`, after exercises are selected (line ~122), update tracking: ```python for ex in selected: patterns = get_movement_patterns_for_exercise(ex) for p in patterns: self.used_movement_patterns[p] += 1 ``` 4. Add a new method to penalize overused patterns in `_weighted_pick`: ```python # In _weighted_pick, when building the pool (lines 461-474): for ex in preferred_list: patterns = get_movement_patterns_for_exercise(ex) # If any pattern already used 2+ times, downweight if any(self.used_movement_patterns.get(p, 0) >= 2 for p in patterns): pool.extend([ex] * 1) # Reduced weight else: pool.extend([ex] * weight_preferred) ``` **Test:** Generate a full-body workout. Verify no single movement pattern (e.g., "upper push - horizontal") appears more than twice in working supersets. --- ## Phase 12: Add Minimum Working Superset Validation After Trimming **Problem:** Aggressive trimming could remove all working supersets, leaving only warmup + cooldown. **Files:** - `generator/services/workout_generator.py` - `_trim_to_fit()` (lines 1537-1575) **Changes:** 1. After the trimming loop, add a minimum check: ```python # Ensure at least 1 working superset remains working_supersets = [ ss for ss in workout_spec['supersets'] if ss['name'] not in ('Warm Up', 'Cool Down') ] if not working_supersets: # Re-add the last removed working superset with minimal config # (2 exercises, 2 rounds - absolute minimum) if removed_supersets: minimal = removed_supersets[-1] minimal['exercises'] = minimal['exercises'][:2] minimal['rounds'] = 2 # Insert before cooldown cooldown_idx = next( (i for i, ss in enumerate(workout_spec['supersets']) if ss['name'] == 'Cool Down'), len(workout_spec['supersets']) ) workout_spec['supersets'].insert(cooldown_idx, minimal) ``` 2. Track removed supersets during trimming by adding a `removed_supersets` list. **Test:** Set `preferred_workout_duration=15` (minimum). Generate a workout. Verify it has at least 1 working superset. --- ## Phase 13: Add Generation Warnings to API Response **Problem:** Users never know when their preferences can't be honored (equipment fallback, target muscle ignored, etc.). **Files:** - `generator/services/workout_generator.py` - Multiple methods - `generator/services/exercise_selector.py` - Fallback paths - `generator/views.py` - Response construction - `generator/serializers.py` - Add warnings field **Changes:** ### In workout_generator.py: 1. Add a warnings list to `__init__`: ```python self.warnings = [] ``` 2. Add warnings at each fallback point: - `_build_working_supersets()` line ~1327 (broader muscles fallback): ```python self.warnings.append(f"Not enough {', '.join(superset_muscles)} exercises with your equipment. Used broader muscle group.") ``` - `_build_working_supersets()` line ~1338 (unfiltered fallback): ```python self.warnings.append(f"Very few exercises available for {', '.join(superset_muscles)}. Some exercises may not match your muscle targets.") ``` - `generate_single_workout()` line ~681 (target muscles full_body only - with Phase 7 this becomes the "no relevant overlap" case): ```python if user_targets_not_in_split: self.warnings.append(f"Target muscles {', '.join(user_targets_not_in_split)} don't apply to {split_type} day.") ``` ### In exercise_selector.py: 3. Add a warnings list and populate it: - In bodyweight fallback (line ~172): ```python self.warnings.append("Equipment constraints too restrictive. Using bodyweight alternatives.") ``` ### In views.py: 4. Include warnings in generation response (line ~148): ```python response_data = serializer.data response_data['warnings'] = generator.warnings + generator.exercise_selector.warnings ``` ### In serializers.py: 5. No model change needed - warnings are transient, added to the response dict only. **Test:** Set equipment to only "resistance band" and target muscles to "chest". Generate a plan. Verify response includes warnings about equipment fallback. --- ## Phase 14: Validate Preference Consistency **Problem:** Users can set contradictory preferences (e.g., 4 days/week but only 2 preferred days). **Files:** - `generator/serializers.py` - `UserPreferenceUpdateSerializer` (lines 79-144) **Changes:** 1. Add validation to `UserPreferenceUpdateSerializer.validate()`: ```python def validate(self, attrs): errors = {} # Preferred days vs days_per_week preferred_days = attrs.get('preferred_days') days_per_week = attrs.get('days_per_week') if preferred_days and days_per_week: if len(preferred_days) > 0 and len(preferred_days) < days_per_week: errors['preferred_days'] = ( f"You selected {len(preferred_days)} preferred days " f"but plan to train {days_per_week} days/week. " f"Select at least {days_per_week} days or clear preferred days for auto-scheduling." ) # Validate preferred_days are valid weekday indices if preferred_days: invalid = [d for d in preferred_days if d < 0 or d > 6] if invalid: errors['preferred_days'] = f"Invalid day indices: {invalid}. Must be 0 (Mon) - 6 (Sun)." # Duration sanity duration = attrs.get('preferred_workout_duration') if duration is not None and (duration < 15 or duration > 120): errors['preferred_workout_duration'] = "Duration must be between 15 and 120 minutes." if errors: raise serializers.ValidationError(errors) return attrs ``` **Test:** Try to update preferences with `days_per_week=5, preferred_days=[0, 1]`. Verify it returns a validation error. --- ## Execution Order & Dependencies ``` Phase 1 (Consolidate defaults) - Independent, do first Phase 2 (Bodyweight safety) - Independent Phase 3 (HR warmup/cooldown) - Independent Phase 4 (Complexity cap) - Depends on Phase 2 (same file area) Phase 5 (Stretch position) - Independent Phase 6 (Recently-used exclusion) - Independent Phase 7 (Target muscles all splits)- Independent Phase 8 (Injury filtering) - Requires migration, do after Phase 2 Phase 9 (Cardio rules/coverage) - Depends on Phase 1 Phase 10 (Pattern PK stability) - Requires migration Phase 11 (Movement pattern variety) - Depends on Phase 6 (same tracking area) Phase 12 (Min working superset) - Independent Phase 13 (Generation warnings) - Do last (touches all files modified above) Phase 14 (Preference validation) - Independent ``` ## Recommended Batch Order **Batch 1** (Foundation - no migrations): Phases 1, 2, 3, 4, 12 **Batch 2** (Selection quality): Phases 5, 6, 7, 11 **Batch 3** (Schema changes - requires migrations): Phases 8, 10 **Batch 4** (Integration): Phases 9, 13, 14 ## Files Modified Summary | File | Phases | |------|--------| | `generator/services/exercise_selector.py` | 2, 3, 4, 5, 6, 11, 13 | | `generator/services/workout_generator.py` | 5, 6, 7, 9, 12, 13 | | `generator/services/workout_analyzer.py` | 1, 9 | | `generator/services/muscle_normalizer.py` | (read-only, imported) | | `generator/services/plan_builder.py` | (no changes) | | `generator/models.py` | 8, 10 | | `generator/serializers.py` | 8, 13, 14 | | `generator/views.py` | 13 | | `generator/management/commands/calibrate_workout_types.py` | 1 (delete) | | `generator/management/commands/calibrate_structure_rules.py` | 9 | | `generator/management/commands/analyze_workouts.py` | 1 | ## New Migrations Required 1. Phase 8: Add `injury_types` JSONField to UserPreference 2. Phase 10: Add `pattern_muscles` JSONField to WeeklySplitPattern, change `pattern` semantics ## Management Commands to Run After 1. `python manage.py migrate` 2. `python manage.py analyze_workouts` (picks up Phase 1 + 9 changes) 3. `python manage.py populate_exercise_fields` (ensure all exercise fields populated) 4. `python manage.py calibrate_structure_rules` (Phase 9 rep floor + cardio fixes)