# Требования к модулю: m04-metrics-engine > Модуль — сервис `MetricsEngine` в `Server.WatchBody.Core`: > детерминированный расчёт метрик тренировки (adherence, тоннаж, > оценочный 1ПМ, пульс-метрики, красные флаги) после каждой > синхронизированной тренировки → `src.workout_metrics` (1:1, upsert). > Источники: Ф-12.1/Ф-12.5, Ф-13.5, документ метрик > (`..\..\..\Метрики_тренировок_и_самоконтроля.md`, п. 2.5, правила > 1–6), архитектура (строка `MetricsEngine`), дизайн LLM-планирования > разд. 4. Самодостаточная копия требований (d-dev-6). ## 1. Назначение и границы **В границах модуля:** - класс `MetricsEngine` (`Server.WatchBody.Core`, каталог `Metrics\`): расчёт метрик одной тренировки атлета по его данным и запись в `workout_metrics` (upsert — пересчёт разрешён); - вызов из m02: после успешного коммита пакета sync MetricsEngine пересчитывает затронутые тренировки (Ф-12.1 «после каждой синхронизированной тренировки»); сбой расчёта не откатывает sync (метрики — производные данные, пересчитываются повторно); - пороги красных флагов — из конфигурации `Safety.*` сервера (Р3-А; умолчания — документ метрик, Ф-13.5); - тесты сервиса (интеграционные, docker `watchbody-pg`). **Вне границ модуля:** правила прогрессии/делоада и создание draft-версий (m05 — читает `workout_metrics` и `workout_set`); парсинг .fit (m06 — кладёт факты в `workout.facts_json`/`avg_hr`/ `hr_recovery_s` до расчёта); отображение метрик (телефон m09/m10, AdminWeb m08 — читают `workout_metrics_select`); расчёт adherence «за период» и еженедельные сводки (п. 2.5 документа метрик, не названные в v1) — по потребности m05/m08; перерывы `auto` по пропускам (Ф-14.1 — отдельная логика, не метрика). ## 2. Состав метрик (контракт документа метрик, v1) | Поле `workout_metrics` | Расчёт (детерминированно) | |---|---| | `adherence` | % подходов «по плану» (`workout_set.in_plan`) от планового числа подходов сессии в активной версии программы (body, схема дизайна LLM-планирования разд. 2); план/сессию не удалось сопоставить → % `in_plan` среди подходов тренировки | | `tonnage` | Σ (weight_kg × reps) по всем подходам с weight>0, reps>0 | | `tonnage_by_group` | тот же Σ по группам упражнений (`exercise.group`), код группы → кг | | `e1pm_by_exercise` | формула Эпли (ОВ-7): `w × (1 + reps/30)` по лучшему (максимальному весу) подходу каждого упражнения | | `hr_metrics` | из `workout.facts_json` (.fit, если уже импортирован): средний/пиковый пульс, отдых между подходами; нет фактов → `{avg_hr, hr_recovery_s}` из `workout` (то, что прислал телефон/.fit-импорт) | | `red_flags` | сработавшие красные флаги (см. разд. 3) | ## 3. Красные флаги (v1 — документ метрик, правила 1, 6; Ф-12.5) | Флаг | Условие | Источник данных | |---|---|---| | `bp_over_150_90` | АД дня тренировки > 150/90 (пороги `Safety.BpCancelSys/Dias`) | `daily_log` дня тренировки | | `bp_elevated` | АД в коридоре 140–150/90–95 | `daily_log` дня тренировки | | `hr_resting_up` | пульс покоя ≥ базового +8 уд/мин (`Safety.HrRestingRise`) 2 дня подряд | `daily_log` (тренд за 2 дня до дня тренировки; базовый = среднее за предыдущие 14 дней, при <3 значениях базового флаг не ставится) | | `wellbeing_low` | самочувствие ≤ 2 (`Safety.WellbeingLow`) 2 дня подряд | `daily_log` | | `cancel_recommendation` | `hr_resting_up`/`wellbeing_low` держится 3+ дня → предложение отмены и совет к врачу | `daily_log` | | `knee_pain_repeat` | боль в колене ≥ 1 (`Safety.KneePainMin`) в двух занятиях подряд (сеты группы «ноги») | `workout_set.knee_pain` предыдущей и текущей тренировки | Флаги — накопленный список фактов сработавших условий с датами; значения порогов в `red_flags` не дублируются. 🛡 Пороги — только из конфигурации, в коде не хардкодятся (Р3-А). ## 4. Инварианты (🛡 — нарушать запрещено) 1. **Изоляция атлета** — расчёт только по данным одного `athleteId`; все входные выборки — по нему (репозитории m01). 2. **Метрики — производные данные** — upsert разрешён всегда (пересчёт); sync атлета не может «сломать» метрики другого. 3. **Без побочных эффектов на план** — MetricsEngine ничего не пишет в `workout`/`workout_set`/`program_version` (это m05); только `workout_metrics`. 4. **Сбой расчёта не ломает sync** — исключение расчёта метрик логируется, пакет sync остаётся принятым. 5. **Детерминированность** — тот же вход → тот же выход; никакого LLM. ## 5. Критерии приёмки модуля 1. После sync тренировки с подходами `workout_metrics` заполнен: adherence/tonnage/tonnage_by_group/e1pm (Эпли проверяется вручную посчитанным значением)/hr_metrics. 2. Повторный расчёт (повторный sync того же пакета) не создаёт вторую строку (upsert 1:1). 3. Красные флаги: сценарии АД>150/90, пульс покоя +8 два дня, самочувствие ≤2, колено ≥1 два занятия — флаги ставятся; чистые данные — `red_flags` пустой. 4. Изоляция: расчёт атлета А не видит данные атлета Б. 5. Сбой расчёта метрик не мешает принятию пакета sync. 6. Тесты интеграционные против docker `watchbody-pg` — зелёные; сборка решения без ошибок.