# Спецификация 04: payload-контракт sync (фикстуры) Источники: tech-req 2.1/2.2 (конверт, составы data), m02 spec 03 (серверный приёмник). Тест сериализации — каждый payload сравнивается с фикстурой (ключи/типы snake_case). ## 1. Конверт ```json {"client_guid": "", "entity": "workout_set", "op": "upsert", "data": { …поля сущности… }} ``` ## 2. data по сущностям (обязательные/опциональные поля) | entity | data | Примечание | |---|---|---| | `workout` | `program_version_id?`, `started_at` (ISO UTC), `ended_at?`, `status`, `session_kind`, `avg_hr?` | status: active/finished/cancelled | | `workout_set` | `workout_id`, `exercise_id?`, `seq`, `weight_kg`, `reps`, `done_at`, `source`, `in_plan`, `rpe?`, `knee_pain?` | вес — число с точкой | | `command_log` | `workout_id`, `t_offset_s`, `recognized_text`, `semantics` (объект), `accepted` | | | `daily_log` | `date` (yyyy-MM-dd), `bp_sys?`, `bp_dias?`, `weight_kg?`, `hr_resting?`, `wellbeing?` | день уникален — сервер upsert | | `break` | `kind?`, `source` (**только manual**), `started_on`, `planned_return?`, `ended_on?`, `note` | 🛡 авто — сервер | | `exercise` | `name`, `group` (push/pull/legs/core/cardio), `is_base`, `is_active` | id = client_guid (серверу не передаётся) | | `program_version` | `version` (число, **обязательно** — значение считает БД), `based_on_version?`, `author` (user), `rationale?` + **поля программы (weeks…) на верхнем уровне** | errata m10 2026-10-19: контракт m02 spec 03 — data хранится сервером целиком как body; вложенного `body` НЕТ; локальные draft (в v1 телефона нет писателя — решение гейта 1.3 m10) | | `profile` | `sex?`, `birth_year?`, `weight_kg?`, `hr_max?` | singleton | Уточнения против m02 spec 03: `exercise_id` у `workout_set` — локальный UUID упражнения (Р2-Б, строкой); `program_version_id` — UUID версии; `break.kind` ∈ illness/vacation/workload/injury/other. `profile.sex` ∈ `m`/`f` (валидатор m02), null — не передаётся. **Errata 2026-10-19 (смок против живого сервера):** `client_guid` — UUID v4 для ВСЕХ сущностей, включая `daily_log` и `profile` (даты и строки-слова сервер отвергает — «client_guid отсутствует или не uuid»); поэтому в LocalDb у daily_log/profile есть стабильная колонка `guid` (spec 01) — WorkoutCore сохраняет существующий guid при повторных правках, `client_guid = guid`. Примечание: сервер (m02 spec 06: «лишние поля игнорируются») не разбирает `break.ended_on` — локальное закрытие перерыва в v1 на сервер не уходит (дату возврата выводит серверный BreakEngine по тренировкам); кандидат v2. ## 3. Даты/время - `started_at`/`done_at`/`ended_at` — `yyyy-MM-dd'T'HH:mm:ss'Z'` UTC (сервер парсит UtcDate); - `date`/`started_on`/`planned_return`/`ended_on` — `yyyy-MM-dd`. ## 4. Фикстуры теста Проверки полей — инлайн в тест-классе `SyncPayloadTest` (app/src/test): `PayloadFactory.*` → JsonObject → assert ключей/типов/значений snake_case, optional-поля — только при наличии; отдельно недопустимые варианты: `break.source="auto"` → исключение фабрики (🛡); конверт `envelope(Pending)` — форма `client_guid/entity/op/data`.