Files
healthlog/openspec/changes/archive/2026-08-02-trenirovki-i-zapisi/tasks.md
T
av f8200f7f80 feat: разбор и хранение тренировок и состояния разума
- секции `workouts` и `stateOfMind` покрыты разбором: тренировка лежит одной
  строкой вместе с маршрутом и внутренними рядами, запись — по ключу `род + id`;
  миграция 00007 заводит обе таблицы и возвращает в очередь `partial`-доставки
  с этими ключами
- сущность заменяется целиком, но условно: приехавшая побеждает, если не теряет
  содержания сохранённой (множество ключей и длины верхнеуровневых массивов), а
  при равном содержании выигрывает версия из более поздней доставки ЖУРНАЛА —
  «побеждает приехавшая» было бы функцией порядка свёртки, и живая витрина
  расходилась бы с пересборкой молча
- отпечаток витрины покрывает тренировки и записи и снимается одним снимком
  базы; отчёт `reindex` считает «было и стало» по каждой единице хранения
2026-08-02 13:05:16 +03:00

127 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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);
`retenshen-syrogo-arhiva` — предусловие про `stateOfMind` снова открыто;
отметить в `read-api-tochki`, что отдача тренировок и записей входит в
неё; уточнить `proverka-novyh-sekcij` — модель заложена, остались пять
секций
- [x] 7.5 Удалить `docs/backlog/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 У каждого класса пропуска при разборе сущности — свой счётчик, и
пропуск одного элемента не уносит соседей