- 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>
29 KiB
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) - UpdateDEFAULT_WORKOUT_TYPESto use calibrated (research-backed) values directlygenerator/management/commands/calibrate_workout_types.py- Delete this file (no longer needed)generator/management/commands/analyze_workouts.py- Remove any reference to calibration
Changes:
- In
workout_analyzer.py, updateDEFAULT_WORKOUT_TYPESdict to merge in all calibrated values fromCALIBRATIONSincalibrate_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=4traditional_strength_training: rep_min=3, rep_max=8, rest=150, intensity=high, duration_bias=0.0, round_min=4, round_max=6hypertrophy: rep_min=6, rep_max=15, rest=90, intensity=high, duration_bias=0.1cross_training: duration_bias=0.5core_training: duration_bias=0.6flexibility: duration_bias=1.0
- Populate
display_namefor 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"
- Change
get_or_createin analyzer to useupdate_or_createso re-running always applies the latest defaults without needing a separate calibration step. - 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:
- Add
fitness_levelparameter to_get_bodyweight_queryset()signature (currently only takesmuscle_groupsandis_duration_based). - After the muscle group filtering (line ~440), add the same fitness level filtering that exists in
_get_filtered_queryset:# 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') - Add injury filtering (same logic as lines 379-397):
injuries = (self.user_preference.injuries_limitations or '').lower() if injuries: # Apply same keyword-based filtering as _get_filtered_queryset ... - 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) andselect_cooldown_exercises()(lines 231-279)
Changes:
Warmup (lines 193-230):
- After the existing filtering and before
_weighted_pick(line ~210), add HR-based preference:# 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) ) - Pass
warmup_hr_preferredas the preferred queryset to_weighted_pick, withwarmup_hr_otheras other. - In the fallback path (lines 214-224), also apply the HR preference.
Cooldown (lines 231-279):
- After the R11 weight filtering (line ~252), add HR ceiling:
# Hard filter: cooldown exercises should have low HR elevation qs = qs.filter(Q(hr_elevation_rating__lte=3) | Q(hr_elevation_rating__isnull=True)) - 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:
- After the existing fitness level filtering (line 376), add complexity cap:
# 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) ) - 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:
- Add a new method
_balance_stretch_positions(self, selected, muscle_groups):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:
- In
_build_working_supersets(), after exercise selection (line ~1319), add:# 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:
-
Add a
hard_exclude_idsset to__init__(line 42-46):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 -
In
_get_filtered_queryset(), after excludingused_exercise_ids(line 304-305), add:# Hard exclude exercises from very recent workouts (last 3) if self.hard_exclude_ids: qs = qs.exclude(pk__in=self.hard_exclude_ids) -
Keep the existing soft penalty in
_weighted_pick(lines 467-468) for exercises from workouts 4-7.
In workout_generator.py:
- Split recently used into two tiers (around line 577-589):
# 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:
- Replace the full_body-only logic with universal target muscle integration:
# 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-UserPreferencemodel (line 112)
Changes:
Model change (generator/models.py):
-
Add structured injury field alongside the existing freeform field:
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') -
Create migration for the new field.
Serializer change (generator/serializers.py):
- Add
injury_typestoUserPreferenceSerializerandUserPreferenceUpdateSerializerfields lists.
Filtering logic (exercise_selector.py):
- Refactor injury filtering at lines 379-397 to use both old text field AND new structured field:
# 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:
-
In
_step5_extract_workout_structure_rules(), add sanity bounds for per-superset rounds:# Clamp rounds to per-superset range (not total workout rounds) typical_rounds = max(1, min(8, typical_rounds)) # Was min(50) -
Add a validation pass after rule extraction:
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) -
Merge the essential fixes from
calibrate_structure_rules.pyinto the analyzer's default rule creation so they're always applied.
In calibrate_structure_rules.py:
- 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
-
In
WeeklySplitPattern, thepatternfield currently stores[5, 12, 5, 8](MuscleGroupSplit PKs). Change the analyzer to store split_type strings instead:["push", "pull", "push", "lower"]. -
In the analyzer's
_step4, when building patterns:# 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], } -
Update
WeeklySplitPatternmodel:pattern = JSONField(default=list) # Now stores split_type strings pattern_muscles = JSONField(default=list) # Stores muscle name lists per dayCreate migration.
-
In
_pick_weekly_split()(workout_generator.py:739-796), resolve patterns usingsplit_type+muscle_namesinstead of PK lookups:# 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 trackinggenerator/services/workout_generator.py-_build_working_supersets()(lines 1176-1398)
Changes:
In exercise_selector.py:
-
Add pattern tracking to
__init__:self.used_movement_patterns = Counter() # Track patterns used in current workout -
Add to
reset():self.used_movement_patterns = Counter() -
In
select_exercises(), after exercises are selected (line ~122), update tracking:for ex in selected: patterns = get_movement_patterns_for_exercise(ex) for p in patterns: self.used_movement_patterns[p] += 1 -
Add a new method to penalize overused patterns in
_weighted_pick:# 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:
-
After the trimming loop, add a minimum check:
# 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) -
Track removed supersets during trimming by adding a
removed_supersetslist.
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 methodsgenerator/services/exercise_selector.py- Fallback pathsgenerator/views.py- Response constructiongenerator/serializers.py- Add warnings field
Changes:
In workout_generator.py:
-
Add a warnings list to
__init__:self.warnings = [] -
Add warnings at each fallback point:
_build_working_supersets()line ~1327 (broader muscles fallback):self.warnings.append(f"Not enough {', '.join(superset_muscles)} exercises with your equipment. Used broader muscle group.")_build_working_supersets()line ~1338 (unfiltered fallback):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):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:
- Add a warnings list and populate it:
- In bodyweight fallback (line ~172):
self.warnings.append("Equipment constraints too restrictive. Using bodyweight alternatives.")
- In bodyweight fallback (line ~172):
In views.py:
- Include warnings in generation response (line ~148):
response_data = serializer.data response_data['warnings'] = generator.warnings + generator.exercise_selector.warnings
In serializers.py:
- 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:
- Add validation to
UserPreferenceUpdateSerializer.validate():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
- Phase 8: Add
injury_typesJSONField to UserPreference - Phase 10: Add
pattern_musclesJSONField to WeeklySplitPattern, changepatternsemantics
Management Commands to Run After
python manage.py migratepython manage.py analyze_workouts(picks up Phase 1 + 9 changes)python manage.py populate_exercise_fields(ensure all exercise fields populated)python manage.py calibrate_structure_rules(Phase 9 rep floor + cardio fixes)