# Спецификация: MetricsEngine.ComputeForWorkout > Класс `Core\Metrics\MetricsEngine.cs`; источники — таблицы > `src.workout` (+sets агрегатом), `src.exercise`, `src.daily_log`, > активная версия программы (ProgramStore, m03); выход — upsert в > `src.workout_metrics` (репозиторий m01). ## 1. Сигнатура ```csharp 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": , "hr_recovery_s": }` (null-поля пропускаются). - Всё отсутствует → `{}`. ### 3.6 red_flags — см. [03-red-flags.md](03-red-flags.md) ## 4. Запись `metrics.Upsert(athleteId, ...)` — 1:1, повторный расчёт обновляет строку (🛡2). `ComputedAt` — текущее время сервера. ## 5. Детерминированность (🛡5) Тот же состояние БД → тот же результат; случайности и времени в расчёте нет (только в `computed_at`).