4.9 KiB
Спецификация: 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)
workouts.Get(athleteId, workoutId)— карточка + подходы (агрегат workout_select).exercises.Select(athleteId)+ группы — map exercise_id → код группы;is_baseдля шага прогрессии (нужно m05, тут не используется).dailyLogs.Select(athleteId, dateFrom = дата тренировки − safety.HrBaselineDays, dateTo = дата тренировки)— АД дня, тренды пульса покоя/самочувствия, базовый пульс. Примечание (З-42 п. 6): фактическое окно шире наlightDayCount—dateFrom = дата − HrBaselineDays − LightDayCount(поиск более раннего старта серии, spec 03 #3; MetricsEngine.cs:42).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).