157 lines
4.4 KiB
Go
157 lines
4.4 KiB
Go
package ai
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
// validateWorkouts ensures AI-generated workouts meet quality standards
|
|
func validateWorkouts(workouts []AIWorkout) error {
|
|
if len(workouts) == 0 {
|
|
return fmt.Errorf("no workouts generated")
|
|
}
|
|
|
|
for i, w := range workouts {
|
|
if err := validateWorkout(w, i); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateWorkout(w AIWorkout, index int) error {
|
|
// Required fields
|
|
if w.Title == "" {
|
|
return fmt.Errorf("workout %d: title is required", index)
|
|
}
|
|
|
|
if w.Duration <= 0 {
|
|
return fmt.Errorf("workout %d: invalid duration %d", index, w.Duration)
|
|
}
|
|
|
|
if w.Duration > 86400 {
|
|
return fmt.Errorf("workout %d: duration exceeds 24 hours", index)
|
|
}
|
|
|
|
// Validate date format
|
|
if _, err := time.Parse("2006-01-02", w.ScheduledDate); err != nil {
|
|
return fmt.Errorf("workout %d: invalid date format %s", index, w.ScheduledDate)
|
|
}
|
|
|
|
// Validate segments
|
|
if len(w.Segments) == 0 {
|
|
return fmt.Errorf("workout %d: must have at least one segment", index)
|
|
}
|
|
|
|
if len(w.Segments) > 100 {
|
|
return fmt.Errorf("workout %d: too many segments (%d), maximum 100", index, len(w.Segments))
|
|
}
|
|
|
|
totalDuration := 0
|
|
for j, seg := range w.Segments {
|
|
if seg.Duration <= 0 {
|
|
return fmt.Errorf("workout %d, segment %d: invalid duration %d", index, j, seg.Duration)
|
|
}
|
|
|
|
// Validate power values are reasonable (0-300% FTP)
|
|
if seg.Power < 0 || seg.Power > 3.0 {
|
|
return fmt.Errorf("workout %d, segment %d: power %.2f out of range (0-3.0)", index, j, seg.Power)
|
|
}
|
|
|
|
if seg.PowerLow < 0 || seg.PowerLow > 3.0 {
|
|
return fmt.Errorf("workout %d, segment %d: power_low %.2f out of range (0-3.0)", index, j, seg.PowerLow)
|
|
}
|
|
|
|
if seg.PowerHigh < 0 || seg.PowerHigh > 3.0 {
|
|
return fmt.Errorf("workout %d, segment %d: power_high %.2f out of range (0-3.0)", index, j, seg.PowerHigh)
|
|
}
|
|
|
|
// Validate that power_low <= power_high (except for ramps/cooldowns where power descends)
|
|
// For warmup, cooldown, and ramp types, power can go from high to low
|
|
if seg.PowerLow > 0 && seg.PowerHigh > 0 && seg.PowerLow > seg.PowerHigh {
|
|
// Allow descending power for warmup, cooldown, and ramp segments
|
|
if seg.Type != "warmup" && seg.Type != "cooldown" && seg.Type != "ramp" {
|
|
return fmt.Errorf("workout %d, segment %d: power_low (%.2f) > power_high (%.2f)", index, j, seg.PowerLow, seg.PowerHigh)
|
|
}
|
|
}
|
|
|
|
// Validate segment type
|
|
validTypes := map[string]bool{
|
|
"warmup": true,
|
|
"cooldown": true,
|
|
"steadystate": true,
|
|
"interval": true,
|
|
"ramp": true,
|
|
"rest": true,
|
|
"freeride": true,
|
|
}
|
|
if !validTypes[seg.Type] {
|
|
return fmt.Errorf("workout %d, segment %d: invalid type %s", index, j, seg.Type)
|
|
}
|
|
|
|
totalDuration += seg.Duration
|
|
}
|
|
|
|
// Verify total duration matches sum of segments (within 5% tolerance)
|
|
tolerance := float64(w.Duration) * 0.05
|
|
diff := math.Abs(float64(totalDuration - w.Duration))
|
|
if diff > tolerance {
|
|
return fmt.Errorf("workout %d: duration mismatch (declared %d, segments total %d)",
|
|
index, w.Duration, totalDuration)
|
|
}
|
|
|
|
// Validate TSS is reasonable (0-500)
|
|
if w.EstimatedTSS < 0 || w.EstimatedTSS > 500 {
|
|
return fmt.Errorf("workout %d: unrealistic TSS %.1f (expected 0-500)", index, w.EstimatedTSS)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateGenerateRequest validates user input
|
|
func validateGenerateRequest(req GenerateRequest) error {
|
|
if req.PlanDuration < 1 || req.PlanDuration > 28 {
|
|
return fmt.Errorf("plan_duration must be between 1 and 28 days")
|
|
}
|
|
|
|
if req.WeeklyHours < 1 || req.WeeklyHours > 40 {
|
|
return fmt.Errorf("weekly_hours must be between 1 and 40")
|
|
}
|
|
|
|
validIntensities := map[string]bool{"easy": true, "moderate": true, "hard": true}
|
|
if !validIntensities[req.IntensityLevel] {
|
|
return fmt.Errorf("intensity_level must be 'easy', 'moderate', or 'hard'")
|
|
}
|
|
|
|
if len(req.FocusAreas) == 0 {
|
|
return fmt.Errorf("at least one focus area is required")
|
|
}
|
|
|
|
if len(req.FocusAreas) > 3 {
|
|
return fmt.Errorf("maximum 3 focus areas allowed")
|
|
}
|
|
|
|
validFocusAreas := map[string]bool{
|
|
"endurance": true,
|
|
"threshold": true,
|
|
"vo2max": true,
|
|
"recovery": true,
|
|
"sprint": true,
|
|
"sweet_spot": true,
|
|
}
|
|
for _, focus := range req.FocusAreas {
|
|
if !validFocusAreas[focus] {
|
|
return fmt.Errorf("invalid focus area: %s", focus)
|
|
}
|
|
}
|
|
|
|
// Validate start date format
|
|
if _, err := time.Parse("2006-01-02", req.StartDate); err != nil {
|
|
return fmt.Errorf("invalid start_date format, use YYYY-MM-DD")
|
|
}
|
|
|
|
return nil
|
|
}
|