from typing import Optional, Literal from pydantic import BaseModel from datetime import date from enum import Enum class TrainingPhase(str, Enum): BASE = "base" BUILD = "build" PEAK = "peak" RECOVERY = "recovery" PLATEAU = "plateau" class RunnerPositioning(BaseModel): """ Domain model for representing a runner's positioning assessment. This is a deterministic interpretation of snapshots, trends, and goals. """ week_start_date: date # Core signals health_signal: Literal["RECOVERING", "OPTIMAL", "OVERREACHING", "UNKNOWN"] position_status: Literal["AHEAD", "ON_TRACK", "FALLING_BEHIND", "UNKNOWN"] goal_trajectory: Literal["IMPROVING", "STABLE", "DECLINING", "UNKNOWN"] recommended_focus: Literal["RECOVERY", "CONSISTENCY", "INTENSITY", "MAINTENANCE"] training_phase: TrainingPhase # Metadata comparison_available: bool = False signal_strength: float = 1.0 llm_used: bool = False summary: Optional[str] = None @classmethod def compute( cls, week_start: date, total_distance: float, target_distance: Optional[float], consistency_score: float, pace_delta: float, hr_delta: Optional[float], distance_delta_pct: float, comparison_available: bool, training_phase: TrainingPhase = TrainingPhase.BASE ) -> "RunnerPositioning": """ Pure deterministic logic to compute positioning signals. No imports from outside the domain layer allowed. """ # ... (logic remains same, passed as arg from service) # 1. Health Signal if distance_delta_pct > 20.0 or (hr_delta is not None and hr_delta > 5.0): health_signal = "OVERREACHING" elif consistency_score < 0.6: health_signal = "RECOVERING" elif consistency_score > 0.8 and (hr_delta is None or hr_delta <= 0): health_signal = "OPTIMAL" else: health_signal = "UNKNOWN" # 2. Position Status (relative to goal distance) if target_distance and target_distance > 0: diff_pct = (total_distance - target_distance) / target_distance if diff_pct > 0.1: position_status = "AHEAD" elif diff_pct < -0.1: position_status = "FALLING_BEHIND" else: position_status = "ON_TRACK" else: position_status = "UNKNOWN" # 3. Goal Trajectory if pace_delta < -5.0 and consistency_score > 0.7: goal_trajectory = "IMPROVING" elif abs(pace_delta) <= 5.0 and consistency_score > 0.6: goal_trajectory = "STABLE" elif pace_delta > 5.0 or consistency_score < 0.5: goal_trajectory = "DECLINING" else: goal_trajectory = "UNKNOWN" # 4. Recommended Focus if health_signal == "OVERREACHING": recommended_focus = "RECOVERY" elif consistency_score < 0.7: recommended_focus = "CONSISTENCY" elif health_signal == "OPTIMAL" and goal_trajectory != "DECLINING": recommended_focus = "INTENSITY" else: recommended_focus = "MAINTENANCE" return cls( week_start_date=week_start, health_signal=health_signal, position_status=position_status, goal_trajectory=goal_trajectory, recommended_focus=recommended_focus, comparison_available=comparison_available, training_phase=training_phase )