## 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 У каждого класса пропуска при разборе сущности — свой счётчик, и
      пропуск одного элемента не уносит соседей
