# Спецификация 01: FitParser — канонический факт workout-facts/1 Источники: tech-req 2.3, Ф-8.2, Ф-13.4. SDK — `Garmin.FIT.Sdk` (NuGet 21.218.0), namespace `Dynastream.Fit`, событийный `Decode.MesgEvent`. ## 1. Сигнатура ```csharp public sealed record FitFacts( string FactsJson, // workout-facts/1, разд. 2 DateTime StartedAt, // session start (UTC) int? AvgHr, // среднее records.hr (округление AwayFromZero) int? HrRecoveryS, // разд. 3 string FitFileId, // file_idmesg.time_created (UTC ISO) + серийник string Device); // device_infomesg (продукт/версия) или "" ``` `Parse(string path)` — файл читается один раз (потоково, tech-req 5); `Decode.CheckIntegrity` перед разбором; кривой файл → `FitException` (обрабатывает ImportService — spec 04). ## 2. Схема JSON ```json {"schema": "workout-facts/1", "session": {"type": "strength_training", "start": "2026-10-18T18:00:00Z", "duration_s": 3600}, "laps": [{"index": 0, "start": "...", "duration_s": 120}], "records": [{"t_offset_s": 0, "hr": 95}], "sets": [{"index": 0, "reps": 12, "weight_kg": 100.0}], "fit_file_id": "...", "device": "..."} ``` | Сообщение | Что берём | |---|---| | `FileIdMesg` | `TimeCreated` (UTC) + `SerialNumber` → `fit_file_id` (`"{serial}-{yyyyMMddTHHmmssZ}"`); оба пусты → имя файла | | `SessionMesg` | `Sport`/`SubSport` (строкой), `StartTime`, `TotalTimerTime` → `duration_s` (сек, округление) | | `LapMesg` | index по порядку, `StartTime`, `TotalTimerTime` | | `RecordMesg` | `Timestamp` → `t_offset_s` (от session start), `HeartRate` (есть и > 0) | | `SetMesg` | index по порядку, `NumReps`, `Weight` (кг; 0xFFFF/0 — нет) | | `DeviceInfoMesg` | последний (свежий) `Product`/`GarminProduct` → `device` (строкой) | - Session нет → `FitException` (файл не активность). - `sets`/`laps`/`records` пустые → пустые массивы (не null). - Тип сессии — как есть строкой (`strength_training`, `cardio`, …); фильтра по типу импорт не делает (решает сопоставление). ## 3. hr_recovery_s (Ф-13.4) Конец последнего Lap (`start + duration`) → первый Record строго после него с `hr < 90` (порог `Safety.RestingHrSitting` — конфиг, Р3-А): `hr_recovery_s = t_offset(record) − конец последнего lap в offset`. Нет записей после последнего Lap или не опустился → `null`. ## 4. Тест-фикстуры Генерируются в тестах энкодером SDK (`Encode`, `Mesg`-объекты в прямом порядке FileId → DeviceInfo → Record* → Lap* → Set* → Session): полная активность (3 рекорда до/1 после Lap с hr < 90, Set с reps/weight), активность без Set, битый файл (усечённый байтовый массив).