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>
This commit is contained in:
142
generator/migrations/0001_initial.py
Normal file
142
generator/migrations/0001_initial.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# Generated by Django 5.1.4 on 2026-02-11 16:54
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0002_workoutequipment'),
|
||||
('exercise', '0008_exercise_video_override'),
|
||||
('muscle', '0002_exercisemuscle'),
|
||||
('registered_user', '0003_registereduser_has_nsfw_toggle'),
|
||||
('workout', '0015_alter_completedworkout_difficulty'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MovementPatternOrder',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('position', models.CharField(choices=[('early', 'Early'), ('middle', 'Middle'), ('late', 'Late')], max_length=10)),
|
||||
('movement_pattern', models.CharField(max_length=100)),
|
||||
('frequency', models.IntegerField(default=0)),
|
||||
('section_type', models.CharField(choices=[('warm_up', 'Warm Up'), ('working', 'Working'), ('cool_down', 'Cool Down')], default='working', max_length=10)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['position', '-frequency'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MuscleGroupSplit',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('muscle_names', models.JSONField(default=list, help_text='List of muscle group names')),
|
||||
('frequency', models.IntegerField(default=0, help_text='How often this combo appeared')),
|
||||
('label', models.CharField(blank=True, default='', max_length=100)),
|
||||
('typical_exercise_count', models.IntegerField(default=6)),
|
||||
('split_type', models.CharField(choices=[('push', 'Push'), ('pull', 'Pull'), ('legs', 'Legs'), ('upper', 'Upper'), ('lower', 'Lower'), ('full_body', 'Full Body'), ('core', 'Core'), ('cardio', 'Cardio')], default='full_body', max_length=20)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WeeklySplitPattern',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('days_per_week', models.IntegerField()),
|
||||
('pattern', models.JSONField(default=list, help_text='Ordered list of MuscleGroupSplit IDs')),
|
||||
('pattern_labels', models.JSONField(default=list, help_text='Ordered list of split labels')),
|
||||
('frequency', models.IntegerField(default=0)),
|
||||
('rest_day_positions', models.JSONField(default=list, help_text='Day indices that are rest days')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WorkoutType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('description', models.TextField(blank=True, default='')),
|
||||
('typical_rest_between_sets', models.IntegerField(default=60, help_text='Seconds')),
|
||||
('typical_intensity', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=10)),
|
||||
('rep_range_min', models.IntegerField(default=8)),
|
||||
('rep_range_max', models.IntegerField(default=12)),
|
||||
('round_range_min', models.IntegerField(default=3)),
|
||||
('round_range_max', models.IntegerField(default=4)),
|
||||
('duration_bias', models.FloatField(default=0.5, help_text='0.0=all rep-based, 1.0=all duration-based')),
|
||||
('superset_size_min', models.IntegerField(default=2)),
|
||||
('superset_size_max', models.IntegerField(default=4)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GeneratedWeeklyPlan',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('week_start_date', models.DateField()),
|
||||
('week_end_date', models.DateField()),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=10)),
|
||||
('preferences_snapshot', models.JSONField(blank=True, default=dict)),
|
||||
('generation_time_ms', models.IntegerField(blank=True, null=True)),
|
||||
('registered_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='generated_plans', to='registered_user.registereduser')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WorkoutStructureRule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('section_type', models.CharField(choices=[('warm_up', 'Warm Up'), ('working', 'Working'), ('cool_down', 'Cool Down')], max_length=10)),
|
||||
('movement_patterns', models.JSONField(default=list)),
|
||||
('typical_rounds', models.IntegerField(default=3)),
|
||||
('typical_exercises_per_superset', models.IntegerField(default=3)),
|
||||
('typical_rep_range_min', models.IntegerField(default=8)),
|
||||
('typical_rep_range_max', models.IntegerField(default=12)),
|
||||
('typical_duration_range_min', models.IntegerField(default=30, help_text='Seconds')),
|
||||
('typical_duration_range_max', models.IntegerField(default=45, help_text='Seconds')),
|
||||
('goal_type', models.CharField(choices=[('strength', 'Strength'), ('hypertrophy', 'Hypertrophy'), ('endurance', 'Endurance'), ('weight_loss', 'Weight Loss'), ('general_fitness', 'General Fitness')], default='general_fitness', max_length=20)),
|
||||
('workout_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='structure_rules', to='generator.workouttype')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserPreference',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('fitness_level', models.IntegerField(choices=[(1, 'Beginner'), (2, 'Intermediate'), (3, 'Advanced'), (4, 'Elite')], default=2)),
|
||||
('primary_goal', models.CharField(choices=[('strength', 'Strength'), ('hypertrophy', 'Hypertrophy'), ('endurance', 'Endurance'), ('weight_loss', 'Weight Loss'), ('general_fitness', 'General Fitness')], default='general_fitness', max_length=20)),
|
||||
('secondary_goal', models.CharField(blank=True, choices=[('strength', 'Strength'), ('hypertrophy', 'Hypertrophy'), ('endurance', 'Endurance'), ('weight_loss', 'Weight Loss'), ('general_fitness', 'General Fitness')], default='', max_length=20)),
|
||||
('days_per_week', models.IntegerField(default=4)),
|
||||
('preferred_workout_duration', models.IntegerField(default=45, help_text='Minutes')),
|
||||
('preferred_days', models.JSONField(blank=True, default=list, help_text='List of weekday ints (0=Mon, 6=Sun)')),
|
||||
('injuries_limitations', models.TextField(blank=True, default='')),
|
||||
('available_equipment', models.ManyToManyField(blank=True, related_name='user_preferences', to='equipment.equipment')),
|
||||
('excluded_exercises', models.ManyToManyField(blank=True, related_name='excluded_by_users', to='exercise.exercise')),
|
||||
('registered_user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='generator_preference', to='registered_user.registereduser')),
|
||||
('target_muscle_groups', models.ManyToManyField(blank=True, related_name='user_preferences', to='muscle.muscle')),
|
||||
('preferred_workout_types', models.ManyToManyField(blank=True, related_name='user_preferences', to='generator.workouttype')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GeneratedWorkout',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('scheduled_date', models.DateField()),
|
||||
('day_of_week', models.IntegerField(help_text='0=Monday, 6=Sunday')),
|
||||
('is_rest_day', models.BooleanField(default=False)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('rejected', 'Rejected'), ('completed', 'Completed')], default='pending', max_length=10)),
|
||||
('focus_area', models.CharField(blank=True, default='', max_length=255)),
|
||||
('target_muscles', models.JSONField(blank=True, default=list)),
|
||||
('user_rating', models.IntegerField(blank=True, null=True)),
|
||||
('user_feedback', models.TextField(blank=True, default='')),
|
||||
('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='generated_workouts', to='generator.generatedweeklyplan')),
|
||||
('workout', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='generated_from', to='workout.workout')),
|
||||
('workout_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='generated_workouts', to='generator.workouttype')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['scheduled_date'],
|
||||
},
|
||||
),
|
||||
]
|
||||
18
generator/migrations/0002_add_display_name_to_workouttype.py
Normal file
18
generator/migrations/0002_add_display_name_to_workouttype.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.2 on 2026-02-20 20:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('generator', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='workouttype',
|
||||
name='display_name',
|
||||
field=models.CharField(blank=True, default='', max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.4 on 2026-02-20 22:55
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('generator', '0002_add_display_name_to_workouttype'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userpreference',
|
||||
name='preferred_workout_duration',
|
||||
field=models.IntegerField(default=45, help_text='Minutes', validators=[django.core.validators.MinValueValidator(15), django.core.validators.MaxValueValidator(120)]),
|
||||
),
|
||||
]
|
||||
18
generator/migrations/0004_add_injury_types.py
Normal file
18
generator/migrations/0004_add_injury_types.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2026-02-21 03:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('generator', '0003_alter_userpreference_preferred_workout_duration'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userpreference',
|
||||
name='injury_types',
|
||||
field=models.JSONField(blank=True, default=list, help_text='Structured injury types: knee, lower_back, upper_back, shoulder, hip, wrist, ankle, neck'),
|
||||
),
|
||||
]
|
||||
28
generator/migrations/0005_add_periodization_fields.py
Normal file
28
generator/migrations/0005_add_periodization_fields.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.4 on 2026-02-21 05:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('generator', '0004_add_injury_types'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='generatedweeklyplan',
|
||||
name='cycle_id',
|
||||
field=models.CharField(blank=True, help_text='Groups weeks into training cycles', max_length=64, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='generatedweeklyplan',
|
||||
name='is_deload',
|
||||
field=models.BooleanField(default=False, help_text='Whether this is a recovery/deload week'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='generatedweeklyplan',
|
||||
name='week_number',
|
||||
field=models.IntegerField(default=1, help_text='Position in training cycle (1-based)'),
|
||||
),
|
||||
]
|
||||
0
generator/migrations/__init__.py
Normal file
0
generator/migrations/__init__.py
Normal file
Reference in New Issue
Block a user