Codebase hardening: 102 fixes across 35+ files
Deep audit identified 106 findings; 102 fixed, 4 deferred. Covers 8 areas: - Settings & deploy: env-gated DEBUG/SECRET_KEY, HTTPS headers, gunicorn, celery worker - Auth (registered_user): password write_only, request.data fixes, transaction safety, proper HTTP status codes - Workout app: IDOR protection, get_object_or_404, prefetch_related N+1 fixes, transaction.atomic - Video/scripts: path traversal sanitization, HLS trigger guard, auth on cache wipe - Models (exercise/equipment/muscle/superset): null-safe __str__, stable IDs, prefetch support - Generator views: helper for registered_user lookup, logger.exception, bulk_update, transaction wrapping - Generator core (rules/selector/generator): push-pull ratio, type affinity normalization, modality checks, side-pair exact match, word-boundary regex, equipment cache clearing - Generator services (plan_builder/analyzer/normalizer): transaction.atomic, muscle cache, bulk_update, glutes classification fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ workout_research.md. Used by the quality gates in WorkoutGenerator
|
||||
and the check_rules_drift management command.
|
||||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
|
||||
@@ -428,6 +429,9 @@ def _get_working_supersets(supersets: list) -> list:
|
||||
def _count_push_pull(supersets: list) -> Tuple[int, int]:
|
||||
"""Count push and pull exercises across working supersets.
|
||||
|
||||
Exercises with BOTH push AND pull patterns are counted as neutral
|
||||
(neither push nor pull) to avoid double-counting.
|
||||
|
||||
Returns (push_count, pull_count).
|
||||
"""
|
||||
push_count = 0
|
||||
@@ -439,9 +443,14 @@ def _count_push_pull(supersets: list) -> Tuple[int, int]:
|
||||
continue
|
||||
patterns = getattr(ex, 'movement_patterns', '') or ''
|
||||
patterns_lower = patterns.lower()
|
||||
if 'push' in patterns_lower:
|
||||
is_push = 'push' in patterns_lower
|
||||
is_pull = 'pull' in patterns_lower
|
||||
if is_push and is_pull:
|
||||
# Dual pattern — count as neutral to avoid double-counting
|
||||
pass
|
||||
elif is_push:
|
||||
push_count += 1
|
||||
if 'pull' in patterns_lower:
|
||||
elif is_pull:
|
||||
pull_count += 1
|
||||
return push_count, pull_count
|
||||
|
||||
@@ -485,6 +494,31 @@ def _focus_key_for_entry(entry: dict) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _is_recovery_entry(entry: dict) -> bool:
|
||||
"""Return True when an entry is a recovery/stretch movement."""
|
||||
ex = entry.get('exercise')
|
||||
if ex is None:
|
||||
return False
|
||||
|
||||
name = (getattr(ex, 'name', '') or '').lower()
|
||||
# Use word boundary check to avoid over-matching (e.g. "Stretch Band Row"
|
||||
# should not be flagged as recovery).
|
||||
if re.search(r'\bstretch(ing|es|ed)?\b', name):
|
||||
return True
|
||||
|
||||
patterns = (getattr(ex, 'movement_patterns', '') or '').lower()
|
||||
recovery_tokens = (
|
||||
'mobility - static',
|
||||
'static stretch',
|
||||
'cool down',
|
||||
'cooldown',
|
||||
'yoga',
|
||||
'breathing',
|
||||
'massage',
|
||||
)
|
||||
return any(token in patterns for token in recovery_tokens)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Main validation function
|
||||
# ======================================================================
|
||||
@@ -608,7 +642,7 @@ def validate_workout(
|
||||
for ss in working:
|
||||
ex_count = len(ss.get('exercises', []))
|
||||
# Allow 1 extra for sided pairs
|
||||
if ex_count > high + 2:
|
||||
if ex_count > high + 1:
|
||||
violations.append(RuleViolation(
|
||||
rule_id='superset_size',
|
||||
severity='warning',
|
||||
@@ -638,7 +672,7 @@ def validate_workout(
|
||||
actual_value=ratio,
|
||||
expected_range=(min_ratio, None),
|
||||
))
|
||||
elif push_count > 2 and pull_count == 0:
|
||||
elif pull_count == 0 and push_count > 0:
|
||||
violations.append(RuleViolation(
|
||||
rule_id='push_pull_ratio',
|
||||
severity='warning',
|
||||
@@ -651,7 +685,41 @@ def validate_workout(
|
||||
))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 5. Focus spread across working supersets
|
||||
# 5. Working-set guardrails (no recovery movements, non-zero rest)
|
||||
# ------------------------------------------------------------------
|
||||
for ss in working:
|
||||
ss_name = ss.get('name') or 'Working set'
|
||||
rest_between_rounds = ss.get('rest_between_rounds')
|
||||
if rest_between_rounds is None or rest_between_rounds <= 0:
|
||||
violations.append(RuleViolation(
|
||||
rule_id='working_rest_missing',
|
||||
severity='warning',
|
||||
message=(
|
||||
f"{ss_name} is missing rest_between_rounds "
|
||||
"(expected a positive value)."
|
||||
),
|
||||
actual_value=rest_between_rounds,
|
||||
expected_range=(15, None),
|
||||
))
|
||||
|
||||
recovery_names = []
|
||||
for entry in ss.get('exercises', []):
|
||||
if _is_recovery_entry(entry):
|
||||
ex = entry.get('exercise')
|
||||
recovery_names.append(getattr(ex, 'name', 'Unknown Exercise'))
|
||||
if recovery_names:
|
||||
violations.append(RuleViolation(
|
||||
rule_id='working_contains_recovery',
|
||||
severity='error',
|
||||
message=(
|
||||
f"{ss_name} contains recovery/stretch movement(s): "
|
||||
f"{', '.join(sorted(set(recovery_names)))}."
|
||||
),
|
||||
actual_value=sorted(set(recovery_names)),
|
||||
))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 6. Focus spread across working supersets
|
||||
# ------------------------------------------------------------------
|
||||
if working:
|
||||
for ss in working:
|
||||
@@ -697,7 +765,7 @@ def validate_workout(
|
||||
previous_focus = focus_keys
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 6. Compound before isolation ordering
|
||||
# 7. Compound before isolation ordering
|
||||
# ------------------------------------------------------------------
|
||||
if UNIVERSAL_RULES['compound_before_isolation']:
|
||||
if not _check_compound_before_isolation(supersets):
|
||||
@@ -708,7 +776,7 @@ def validate_workout(
|
||||
))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 7. Warmup check
|
||||
# 8. Warmup check
|
||||
# ------------------------------------------------------------------
|
||||
if UNIVERSAL_RULES['warmup_mandatory']:
|
||||
if not _has_warmup(supersets):
|
||||
@@ -719,7 +787,7 @@ def validate_workout(
|
||||
))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 8. Cooldown check
|
||||
# 9. Cooldown check
|
||||
# ------------------------------------------------------------------
|
||||
if not _has_cooldown(supersets):
|
||||
violations.append(RuleViolation(
|
||||
@@ -729,7 +797,7 @@ def validate_workout(
|
||||
))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 9. HIIT duration cap
|
||||
# 10. HIIT duration cap
|
||||
# ------------------------------------------------------------------
|
||||
if wt_key == 'high_intensity_interval_training':
|
||||
max_hiit_min = UNIVERSAL_RULES.get('max_hiit_duration_min', 30)
|
||||
@@ -757,7 +825,7 @@ def validate_workout(
|
||||
))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 10. Total exercise count cap
|
||||
# 11. Total exercise count cap
|
||||
# ------------------------------------------------------------------
|
||||
max_exercises = wt_rules.get(
|
||||
'max_exercises_per_session',
|
||||
@@ -780,13 +848,23 @@ def validate_workout(
|
||||
))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 11. Workout type match percentage (refactored from _validate_workout_type_match)
|
||||
# 12. Workout type match percentage (refactored from _validate_workout_type_match)
|
||||
# ------------------------------------------------------------------
|
||||
_STRENGTH_TYPES = {
|
||||
'traditional_strength_training', 'functional_strength_training',
|
||||
'hypertrophy',
|
||||
}
|
||||
_HIIT_TYPES = {'high_intensity_interval_training'}
|
||||
_CARDIO_TYPES = {'cardio'}
|
||||
_CORE_TYPES = {'core_training'}
|
||||
_FLEXIBILITY_TYPES = {'flexibility'}
|
||||
|
||||
is_strength = wt_key in _STRENGTH_TYPES
|
||||
is_hiit = wt_key in _HIIT_TYPES
|
||||
is_cardio = wt_key in _CARDIO_TYPES
|
||||
is_core = wt_key in _CORE_TYPES
|
||||
is_flexibility = wt_key in _FLEXIBILITY_TYPES
|
||||
|
||||
if working:
|
||||
total_ex = 0
|
||||
matching_ex = 0
|
||||
@@ -799,7 +877,33 @@ def validate_workout(
|
||||
if is_strength:
|
||||
if getattr(ex, 'is_weight', False) or getattr(ex, 'is_compound', False):
|
||||
matching_ex += 1
|
||||
elif is_hiit:
|
||||
# HIIT: favor high HR, compound, or duration-capable exercises
|
||||
hr = getattr(ex, 'hr_elevation_rating', None) or 0
|
||||
if hr >= 5 or getattr(ex, 'is_compound', False) or getattr(ex, 'is_duration', False):
|
||||
matching_ex += 1
|
||||
elif is_cardio:
|
||||
# Cardio: favor duration-capable or high-HR exercises
|
||||
hr = getattr(ex, 'hr_elevation_rating', None) or 0
|
||||
if getattr(ex, 'is_duration', False) or hr >= 5:
|
||||
matching_ex += 1
|
||||
elif is_core:
|
||||
# Core: check if exercise targets core muscles
|
||||
muscles = (getattr(ex, 'muscle_groups', '') or '').lower()
|
||||
patterns = (getattr(ex, 'movement_patterns', '') or '').lower()
|
||||
if any(tok in muscles for tok in ('core', 'abs', 'oblique')):
|
||||
matching_ex += 1
|
||||
elif 'core' in patterns or 'anti' in patterns:
|
||||
matching_ex += 1
|
||||
elif is_flexibility:
|
||||
# Flexibility: favor duration-based, stretch/mobility exercises
|
||||
patterns = (getattr(ex, 'movement_patterns', '') or '').lower()
|
||||
if getattr(ex, 'is_duration', False) or any(
|
||||
tok in patterns for tok in ('stretch', 'mobility', 'yoga', 'flexibility')
|
||||
):
|
||||
matching_ex += 1
|
||||
else:
|
||||
# Unknown type — count all as matching (no false negatives)
|
||||
matching_ex += 1
|
||||
if total_ex > 0:
|
||||
match_pct = matching_ex / total_ex
|
||||
|
||||
Reference in New Issue
Block a user