File size: 3,603 Bytes
d64fd55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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
        )