## 1. Фикстуры на реальных пакетах - [x] 1.1 Расширить `tmp/research/fixtures.py`: вычистка UUID (`id`), меток RFC 3339, `route[].timestamp` и словарных значений `stateOfMind` (`kind`, `valenceClassification`, `labels`, `associations`) при сохранении формы литерала, структуры и порядка ключей - [x] 1.2 Собрать `internal/hae/testdata/workout_route.json` — уличная тренировка с маршрутом (проверяет дословность маршрута и внутренних рядов) - [x] 1.3 Собрать `internal/hae/testdata/workout_indoor.json` — тренировка без маршрута, с полями, которых нет у уличной (`temperature`, `humidity`, `intensity`) - [x] 1.4 Собрать `internal/hae/testdata/state_of_mind.json` — состояние разума (RFC 3339, отсутствие `source`) - [x] 1.5 Дописать в `handmade_edge.json` случаи, которых поток не даёт: сущность без `id`, с пустым и со слишком длинным `id`, с неразбираемой меткой, с неразбираемым `end`, с нечисловой длительностью, два элемента с одним `id` в одном теле - [x] 1.6 Обновить `internal/hae/testdata/README.md`: новые файлы и что именно вычищено ## 2. Разбор (`internal/hae`) - [x] 2.1 Множество покрытых секций: `metrics`, `workouts`, `stateOfMind` — одно объявление на разбор и на перечисление непокрытых - [x] 2.2 Типы `Workout` и `Record` в `Result`; счётчики `SkippedNoID`, `SkippedNoTime` для сущностей - [x] 2.3 `parseEntityTime`: формат HAE, затем RFC 3339; офсет из разобранной зоны. Парсер точек остаётся строгим — причина записана в спеке - [x] 2.4 Разбор `workouts`: заголовок (`id`, `name`, `start`, `end`, `duration`), содержимое — исходные байты элемента; `end` нечитаем → равен началу; длительность отсутствует → не заполнена (не ноль) - [x] 2.5 Разбор `stateOfMind` в записи рода `stateOfMind` (имя секции дословно) - [x] 2.6 `Parse` отдаёт сущности и при отсутствии секции `metrics`; при ошибке (обрыв тела, невыводимый слой) не отдаёт ни точек, ни сущностей - [x] 2.7 Повтор ключа покрытой секции даёт объединение; повтор члена `data` **накапливает** результаты, а не присваивает последний (существующий дефект `decodeEnvelope`) - [x] 2.8 Предел длины `id`: сущность сверх него пропускается тем же счётчиком - [x] 2.9 Тесты на фикстурах: маршрут дословно, пульс тренировки не стал метрикой, оба формата времени, пропуски со счётчиками - [x] 2.10 Тест: доставка из одних тренировок и из одного `stateOfMind` непокрытых ключей не даёт; из одного `ecg` — даёт ## 3. Схема (`internal/store/migrations`) - [x] 3.1 Миграция `00007_workout_record.sql`: таблицы `workout` и `record` (провенанс `delivery_id` + `delivery_received_at`, `content_hash`, nullable `duration_sec`), индексы по времени, перевод `partial`-доставок с ключами `workouts` и `stateOfMind` в `pending` - [x] 3.2 Проверить, что перевод в `pending` отбирает строки точно (по элементу JSON-массива, а не по подстроке тела) - [x] 3.3 Обновить `docs/database.md`: обе таблицы, смысл колонок, ключ `род + id`, провенанс, `NULL` у длительности, офсет `0` у `stateOfMind` ## 4. Хранение (`internal/store`, `internal/canon`) - [x] 4.1 `canon`: сравнение множеств содержательных ключей **без** условия о совпадении значений (поверх существующего `relateKeys`) плюс сравнение длин верхнеуровневых массивов - [x] 4.2 `Incoming{Points, Workouts, Records}` и `Merge` вместо `MergePoints`: одна транзакция на доставку - [x] 4.3 Правило замены версии: хеш-детектор без чтения блоба, условие «не теряет содержания», тай-брейк по позиции журнала `(received_at, id)`, внутридоставочный тай-брейк по канонической форме, счётчик и координаты для `WARN` - [x] 4.4 Чтение и запись тренировки и записи; содержимое — сжатый блоб; хеш приехавшей считается один раз на доставку, до входа в транзакцию - [x] 4.5 `Fingerprint` покрывает тренировки и записи, читает одной read-only транзакцией, строки разделов различаются константным признаком - [x] 4.6 Тесты: замещение маршрутом; досчёт при том же наборе полей; обеднённая версия **с изменившимися значениями** не затирает; усечённый маршрут не затирает; несравнимые наборы; повтор не пишет; две версии в одном теле; перестановка **трёх** версий в двух порядках подачи (конвенция `docs/conventions.md`) - [x] 4.7 Тест: отпечаток расходится при расхождении одной тренировки ## 5. Свёртка и пересборка (`internal/fold`, `internal/replay`, `cmd`) - [x] 5.1 `Stats` несёт счётчики сущностей; свёртка зовёт `Merge` один раз; счётчики доезжают до лога без ручного копирования (или это покрыто тестом) - [x] 5.2 Единственный логирующий чекпоинт: атрибуты сущностей, ветка `WARN` для удержанной обеднённой версии — координатами, без содержимого - [x] 5.3 Отчёт `reindex`: «до и после» по тренировкам и записям, «покрыта новая секция» в перечне ожидаемых классов расхождения - [x] 5.4 Тест: содержимое сущности в лог не попадает ## 6. Сходимость и проверка на живом архиве - [x] 6.1 `task gate` — зелёный - [x] 6.2 `task verify:archive` — прогон всего `./data/raw`, повтор даёт то же состояние; он же оракул того, что живой приём и пересборка применяют к сущностям один порядок - [x] 6.3 Проверка на копии рабочей базы в отдельном каталоге данных: миграция накатывается, `partial`-доставки пересворачиваются, тренировки и записи появляются ## 7. Документация и беклог - [x] 7.1 `docs/architecture.md` — раздел «Тренировки и прочие секции»: правило замены версии с обоснованием, отвергнутые варианты, предел `import`; **плюс блок схемы БД** (`record(kind, id)`, сжатый блоб, `content_hash`, провенанс) - [x] 7.2 `docs/conventions.md` — строка про ключ `record` устарела, поправить - [x] 7.3 `docs/local-research.md` — находка о поведении тренировки при переприсылке (числа замера) и пересчёт находки 50 на 118 доставок - [x] 7.4 Беклог: задача про идентичность тренировок при импорте родного экспорта (в `export.xml` `id` нет — `dogsheep` считает hash_id); `raw-archive-retention` — предусловие про `stateOfMind` снова открыто; отметить в `read-api-points`, что отдача тренировок и записей входит в неё; уточнить `unseen-sections-check` — модель заложена, остались пять секций - [x] 7.5 Удалить `docs/tasks/items/trenirovki-i-zapisi.md` и строку индекса ## 8. Приёмочные критерии (рубрика ревью) - [x] 8.1 Тренировка с маршрутом переживает круг «разбор → хранение → чтение» побайтово - [x] 8.2 Повторная свёртка того же журнала не меняет отпечатка витрины - [x] 8.3 Ни один путь не пишет содержимое сущности в лог выше `DEBUG` - [x] 8.4 Доставка из одних тренировок получает `parsed`, а не `partial` - [x] 8.5 Доставка с непокрытой секцией по-прежнему `partial`, и её тело ретеншену трогать нельзя - [x] 8.6 Исход правила замены не зависит от порядка свёртки в пунктах 2–4 правила; зависимость в пункте 5 наблюдаема счётчиком - [x] 8.7 У каждого класса пропуска при разборе сущности — свой счётчик, и пропуск одного элемента не уносит соседей