VEU.WathBody/1-docs/4-modules/m04-metrics-engine/api_specs/02-compute-for-workout.md

4.9 KiB
Raw Blame History

Спецификация: MetricsEngine.ComputeForWorkout

Класс Core\Metrics\MetricsEngine.cs; источники — таблицы src.workout (+sets агрегатом), src.exercise, src.daily_log, активная версия программы (ProgramStore, m03); выход — upsert в src.workout_metrics (репозиторий m01).

1. Сигнатура

public class MetricsEngine(
    IWorkoutRepository workouts,
    IExerciseRepository exercises,
    IExerciseGroupRepository groups,
    IDailyLogRepository dailyLogs,
    IWorkoutMetricsRepository metrics,
    ProgramStore programs,
    MetricsConfig safety)
{
    public WorkoutMetrics ComputeForWorkout(int athleteId, Guid workoutId);
}

Тренировка не найдена или чужая → InvalidOperationException (вызывающий — sync — оборачивает в try/catch, 🛡4). Возвращает доменный WorkoutMetrics (m01) с заполненными полями; строка уже записана upsert-ом.

2. Входные данные (все выборки по athleteId, 🛡1)

  1. workouts.Get(athleteId, workoutId) — карточка + подходы (агрегат workout_select).
  2. exercises.Select(athleteId) + группы — map exercise_id → код группы; is_base для шага прогрессии (нужно m05, тут не используется).
  3. dailyLogs.Select(athleteId, dateFrom = дата тренировки − safety.HrBaselineDays, dateTo = дата тренировки) — АД дня, тренды пульса покоя/самочувствия, базовый пульс. Примечание (З-42 п. 6): фактическое окно шире на lightDayCount — dateFrom = дата − HrBaselineDays − LightDayCount (поиск более раннего старта серии, spec 03 #3; MetricsEngine.cs:42).
  4. programs.GetActive(athleteId) — активная версия (body) для плановых подходов; null → план отсутствует (см. 3.1).

3. Расчёт полей

3.1 adherence (numeric 0–100, один знак)

  • Знаменатель (план): из body активной версии берётся сессия дня недели даты тренировки (weeks[].sessions[], day — понедельник…воскресенье в нижнем регистре); недель несколько → берётся сессия первой недели, содержащая этот день (ротация недель — v2). Плановое число подходов = Σ exercises[].sets сессии.
  • Числитель (факт): подходы тренировки с in_plan = true.
  • План найден: adherence = round(числитель / знаменатель × 100, 1); знаменатель 0 → adherence = 0.
  • План не найден (нет активной версии / сессии дня / body не парсится): adherence = доля in_plan среди всех подходов тренировки; подходов нет → 0.

3.2 tonnage (Σ вес×повторы)

Σ weight_kg × reps по подходам с weight_kg > 0 и reps > 0; один знак после запятой.

3.3 tonnage_by_group

Тот же Σ по коду группы упражнения (группа из 2): {"push": 4200}, группы без подходов не включаются; пусто → {}.

3.4 e1pm_by_exercise (Эпли, ОВ-7)

По каждому упражнению: лучший подход = max weight_kg (при равном весе — max reps, reps > 0); e1pm = round(w × (1 + reps/30), 1). Ключ — exercise_id (uuid) строкой. Подходов с весом нет → упражнение не включается; пусто → {}.

3.5 hr_metrics

  • workout.facts_json непустой и содержит hr-данные → из фактов (.fit, m06): {"avg_hr", "peak_hr", "rest_between_s"} (что есть — то попадает; поля отсутствуют в фактах → не включаются).
  • Фактов нет → {"avg_hr": <workout.avg_hr>, "hr_recovery_s": <workout.hr_recovery_s>} (null-поля пропускаются).
  • Всё отсутствует → {}.

3.6 red_flags — см. 03-red-flags.md

4. Запись

metrics.Upsert(athleteId, ...) — 1:1, повторный расчёт обновляет строку (🛡2). ComputedAt — текущее время сервера.

5. Детерминированность (🛡5)

Тот же состояние БД → тот же результат; случайности и времени в расчёте нет (только в computed_at).