lots of stuff, don't truly remember

This commit is contained in:
Blake Ridgway
2026-05-17 20:39:47 -05:00
parent 178ffb3425
commit dc4fe558b7
35 changed files with 3501 additions and 112 deletions

156
internal/ai/validator.go Normal file
View File

@@ -0,0 +1,156 @@
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
}