## 1. Фикстуры и схема - [ ] 1.1 Скрипт `tmp/research/fixtures.py`: собирает фикстуры из `data/raw`, вычищая измеренные значения и сохраняя порядок ключей, точность чисел, неразрывные пробелы и форматы времени - [ ] 1.2 Набор `internal/hae/testdata`: минутная доставка, посекундная, часовая, смешанная (перенастройка автоматизации), обе схемы `sleep_analysis`, точка с `heartbeatSeries` - [ ] 1.3 Миграция `internal/store/migrations/00003_bucket.sql`: таблица `bucket` (`metric`, `layer`, `hour_utc`, `payload` BLOB, `content_hash`, `points`, `sealed`, `created_at`, `updated_at`), уникальность по `(metric, layer, hour_utc)` - [ ] 1.4 `docs/database.md` — ER-схема с `delivery` и `bucket` (шаг гейта `er-schema` требует её при изменении миграций) ## 2. Разбор — пакет `internal/hae` - [ ] 2.1 Разбор трёх форматов времени в UTC с сохранением офсета исходной зоны; неразобранная метка — не паника, а пропуск точки со счётчиком - [ ] 2.2 Вывод слоя: выравнивание меток, плотная метрика (≥10 точек) сама, редкая наследует преобладающий слой, при отсутствии плотных — заголовок доставки - [ ] 2.3 Расхождение выведенного слоя с заголовком доставки — запись `WARN` без значений точек - [ ] 2.4 Разделение `sleep_analysis` на поэпизодную и `sleep_analysis_summary` со слоем `day` - [ ] 2.5 Канонизация: рекурсивная сортировка ключей, округление чисел до 12 значащих цифр - [ ] 2.6 `hae.Parse` возвращает точки со слоем, метрикой, меткой и содержимым как пришло; незнакомые поля и метрики сохраняются - [ ] 2.7 Тесты разбора на фикстурах из 1.2, включая смешанную доставку и обе схемы сна ## 3. Хранение — часовые объекты - [ ] 3.1 Модель точки и объекта в `internal/store`; сериализация содержимого в gzip-BLOB, точки внутри упорядочены по времени - [ ] 3.2 Слияние: координатный ключ `метрика + слой + метка`, победа более полной точки, при равной полноте — по `received_at` - [ ] 3.3 Хеш канонической формы объекта как детектор изменений: совпал — записи нет - [ ] 3.4 Запись объекта в транзакции; повтор при `SQLITE_BUSY` - [ ] 3.5 Признак `sealed`: изменение запечатанного часа пишет `WARN` и всё равно сохраняет - [ ] 3.6 Тест конкурентной записи в один `hour_utc`: точки обеих сторон на месте - [ ] 3.7 Тест идемпотентности: повторное слияние того же набора не меняет ни содержимое, ни хеш ## 4. Сшивка с приёмом - [ ] 4.1 `internal/ingest` вызывает разбор после записи тела в архив и строки `delivery` - [ ] 4.2 Отказ и паника разбора не меняют код ответа: `200`, `parse_status=failed`, запись лога уровня `ERROR` без значений - [ ] 4.3 Единственный логирующий чекпоинт на границе: счётчики метрик, точек и объектов, идентификатор доставки; ни значений, ни имён устройств - [ ] 4.4 Тест приёма: битый JSON — 400, непонятое содержимое — 200 с `parse_status=failed` ## 5. Сходимость на реальных данных - [ ] 5.1 Скрипт `tmp/research/verify_buckets.py`: прогоняет архив через разбор и сверяет суммы по часовому слою с прежней проверкой из разведки - [ ] 5.2 Прогон на всех накопленных доставках: расхождений по накопительным метрикам нет - [ ] 5.3 `task gate` зелёный; `task restart` поднимает сервис, новая доставка с телефона разбирается ## 6. Приёмочные критерии ревью дизайна