5.5 KiB
5.5 KiB
Спецификация 02: микроправка весов — ProgressionRules.EvaluateAfterWorkout
Источники: Ф-12.2–12.3, документ метрик правила 3–5, ограничения — решение гейта 1.3 (module_reqs разд. 2.4).
1. Сигнатура
public ProgramVersion? EvaluateAfterWorkout(int athleteId, Guid workoutId)
Возврат: созданная draft-версия или null (изменений нет / черновик
уже существует). Дедупликация: rules-draft со статусом draft и
based_on_version ≥ номера активной версии блокирует новые
предложения (покрывает и пользовательские draft поверх активной).
2. Входные данные
- тренировка
workout+ подходыworkout_set(репозиторий m01); - предыдущие тренировки атлета (
WorkoutFilter, по убываниюstarted_at) — для правила 1 («2 тренировки подряд»); - активная версия
ProgramStore.GetActive(athleteId)— нет → null; - библиотека атлета
IExerciseRepository.Select(сReplacementIds) — сопоставление и ограничения.
Сопоставление план↔факт: body.exercises[].exercise_id ==
src.exercise.name (exact, Ordinal); упражнение не найдено → решение
не генерируется, пометка в rationale. workout_set.exercise_id
(uuid) → src.exercise → имя → элемент body.
3. Решения по упражнениям (проверяются по порядку, срабатывает первое)
По каждому упражнению тренировки, присутствующему в активной версии
и активному (is_active=true):
| # | Условие | Действие |
|---|---|---|
| 1 | в двух последних тренировках этого упражнения все плановые подходы in_plan=true и rpe ≤ 7 (rpe во всех подходах указан) |
плановый вес +BaseStepKg (is_base=true) / +IsoStepKg (is_base=false) |
| 2 | в последней тренировке упражнения rpe ≥ 9 в ≥ 2 подходах |
плановый вес × (1 − BackoffPercent/100) |
| 3 | все плановые подходы закрыты «по плану» и плановые повторения ≥ 15 (верхняя граница диапазона 15–20) | вес +шаг прогрессии, плановые повторения → 12 |
| 4 | в последней тренировке ≥ 2 подходов с reps < плановых |
плановый вес × (1 − BackoffPercent/100) |
| 5 | иначе | без изменения |
- «Предыдущая тренировка упражнения» — предыдущая по
started_atтренировка атлета, содержащая подходы этого упражнения. - Результат веса округляется к 0.5 кг (
Math.Round(x*2)/2). - Подходы
source=editв расчёт не берутся (правка = новая запись, история: берём исходные voice/watch записи). - Вес плана
null(калибровка) → решение не генерируется.
4. Ограничения (module_reqs разд. 2.4)
is_active=false→ не попадает в draft; если в активной версии сессия состоит только из неактивных — сессия исключается;- у исключаемого упражнения есть активная пара-замена
(
ReplacementIds, замена активна, та же группа) → подставляется замена (имя замены вexercise_idbody, веса исходного плана, пометка в rationale). Примечание (З-40): «та же группа» гарантируетсяexercise_replacement_insert(проверка на вставке пары); повторная runtime-проверка вApplyExclusionне дублируется; body.profile.limitationsпереносится без изменений.
5. Формирование draft
- Нет ни одного решения с изменением и ни одного исключения/замены →
null(черновик не нужен). - Deep-copy активного body (JsonNode), применение решений к week 1+ (все недели: v1 ротации нет — одно упражнение = один плановый вес).
- Дедупликация: существует rules-draft с
based_on_version== номер активной версии →null(черновик не плодится между sync). ProgramStore.CreateDraft(athleteId, newBody, "rules", rationale); rationale — человекочитаемо: по упражнению строка «жим: 100 → 102.5 (+2.5: 2 тренировки подряд закрыты при RPE≤7)», «присед: исключено (is_active=false), замена — жим ногами».
🛡 Активная версия не меняется; активации нет (vision 2.4).