Files
healthlog/openspec/changes/razbor-metrik-v-obekty/tasks.md
T
av 01d0de59df разбор метрик HAE: фикстуры, канонизация, парсер
- tmp/research/fixtures.py собирает фикстуры из архива, вычищая измерения и
  сохраняя порядок ключей, форму литералов, выравнивание меток и невидимые
  символы; шесть фикстур в internal/hae/testdata
- internal/canon — общий дом канонической формы, полноты и хеша: числа читаются
  литералом через json.Number, округление до 12 значащих цифр
- internal/hae — разбор секции metrics, вывод слоя по метрике, разделение схем
  сна, координаты интервалом; recover внутри Parse, фаззинг
- миграция bucket и docs/database.md
2026-08-01 17:31:41 +03:00

77 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`: собирает фикстуры из `data/raw`, вычищая измеренные значения и сохраняя порядок ключей, точность чисел, неразрывные пробелы и форматы времени
- [x] 1.2 Набор `internal/hae/testdata`: минутная доставка, посекундная, часовая, смешанная (перенастройка автоматизации), обе схемы `sleep_analysis`, точка с `heartbeatSeries`, доставка без плотных метрик с заголовком `Default`
- [x] 1.3a Фикстура с эпизодами сна: три точки с одной меткой `date` и разными парами `start`/`end`, включая эпизод, пересекающий границу часа (в архиве такие есть — вычистить значения, интервалы сохранить)
- [x] 1.3 Рукотворная фикстура с **выдуманным полем точки** — скрипт из архива её не породит, а без неё требование «незнакомое поле сохраняется» останется без теста
- [x] 1.4 Миграция `internal/store/migrations/00003_bucket.sql`: таблица `bucket` (`metric`, `layer`, `hour_utc`, `units`, `payload` BLOB, `content_hash`, `points`, `first_ts`, `last_ts`, `first_delivery_id`, `sealed`, `created_at`, `updated_at`), уникальность по `(metric, layer, hour_utc)`
- [x] 1.5 `docs/database.md` — ER-схема с `delivery` и `bucket` (шаг гейта `er-schema` требует её при изменении миграций)
## 2. Канонизация — общий дом
- [x] 2.1 Пакет `internal/canon`: каноническая форма (числа читаются литералом через `json.Number`, округление до 12 значащих цифр — именованная константа со ссылкой на находку 30), полнота точки, хеш
- [x] 2.2 Сортировку ключей **не писать** — её делает `encoding/json`
- [x] 2.3 Тест канонизации на настоящих парах чисел из находки 30: все три схлопываются
- [x] 2.4 Тест: значение с `1.0`, целым больше 2^53 и невалидным UTF-8 переживает хранение дословно
## 3. Разбор — пакет `internal/hae`
- [x] 3.1 Декодирование конверта в структуру с `Data []json.RawMessage`; точка декодируется по одной. Тест удержания кучи на теле в десятки МиБ
- [x] 3.2 Разбор метки `2006-01-02 15:04:05 -0700` в UTC с сохранением офсета; неразобранная метка — пропуск точки со счётчиком, не паника
- [x] 3.3 Эпоха внутри `heartbeatSeries` **не разбирается**: элементы проходят исходными байтами
- [x] 3.4 Вывод слоя: плотная метрика (≥10 точек) сама, редкая наследует преобладающий, доставка без плотных — наследует последний слой автоматизации по `automation-id`
- [x] 3.5 Наследовать нечего и заголовок `Default` — точки не сохраняются, `WARN` и счётчик
- [x] 3.6 `WARN` о расхождении слоя — только против `Minutes`/`Hours`; `Default` в сравнении не участвует
- [x] 3.7 Разделение `sleep_analysis` на поэпизодную и `sleep_analysis_summary` со слоем `day`; выполняется **до** вывода слоя, точки с назначенным слоем в голосовании не участвуют
- [x] 3.8 `recover` **внутри** `hae.Parse` — превращает панику разбора в ошибку пакета; паника из `store` наверх не перехватывается
- [x] 3.9 Контракт: `error` ненулевая только когда точек нет вовсе; частичные исходы — счётчиками в результате
- [x] 3.10 `FuzzParse` и таблица враждебных входов: усечённое тело, `null` вместо объекта, массив вместо `data`, число вместо строки даты
- [x] 3.11 Тесты разбора на фикстурах 1.2–1.3, включая смешанную доставку и обе схемы сна
## 4. Хранение — часовые объекты
- [ ] 4.1 Модель объекта в `internal/store`; содержимое — исходные байты точек, gzip-BLOB, точки упорядочены по времени
- [ ] 4.2 Слияние: координатный ключ, победа более полной точки, при равной полноте — детерминированный исход по порядку канонических форм
- [ ] 4.3 Столкновение с различием канонической формы — `WARN` без значений и счётчик перезаписей
- [ ] 4.4 Ключ точки — `метрика + слой + начало + конец` одной формы для всех точек: начало из `start`, иначе из `date`; конец из `end`, иначе равен началу. Час объекта — по началу. Ветвления по «классу метрики» быть не должно
- [ ] 4.4a Тест на фикстуре 1.3a: три записи с одной меткой и разными интервалами дают три точки, а не одну; повтор той же тройки следующей доставкой не задваивает; точка без `end` кладётся вырожденным интервалом
- [ ] 4.5 Хеш объекта как детектор изменений: совпал — записи нет
- [ ] 4.6 `_txlock=immediate` в DSN; повтор оборачивает **всю тройку** чтение-слияние-запись; путь «хеш совпал» — под `TxOptions{ReadOnly: true}`
- [ ] 4.7 Распознавание занятости — `errors.As` на `*sqlite.Error`, коды 5 и 517, обёрнуто в `store`
- [ ] 4.8 Тест конкурентной записи: N горутин × M слияний в один `hour_utc`, проверка **суммы** точек, под `-race`
- [ ] 4.9 Тест идемпотентности: повторное слияние того же набора не меняет ни содержимое, ни хеш
- [ ] 4.10 Тесты сценариев слияния: «бедная точка не стирает поля богатой», «смена `source` не создаёт вторую точку»
## 5. Сшивка с приёмом
- [ ] 5.1 Свёртка вызывается по идентификатору доставки, тело читается из архива — один код с будущей пересборкой
- [ ] 5.2 Работа после записи в архив — на `context.WithoutCancel` с собственным дедлайном: обрыв соединения не рвёт запись объектов
- [ ] 5.3 Отказ разбора не меняет код ответа: `200`, `parse_status=failed`, запись `ERROR` без значений
- [ ] 5.4 Единственный логирующий чекпоинт на границе: счётчики метрик, точек, объектов, пропусков, перезаписей; идентификатор доставки; ни значений, ни имён устройств
- [ ] 5.5 Тест приёма: битый JSON — 400, непонятое содержимое — 200 с `parse_status=failed`
## 6. Сходимость на реальных данных
- [ ] 6.1 Скрипт `tmp/research/verify_buckets.py`: прогоняет архив через разбор и сверяет суммы по часовому слою с проверкой из разведки
- [ ] 6.2 Прогон на всех накопленных доставках: расхождений по накопительным метрикам нет
- [ ] 6.3 `task gate` зелёный; `task restart` поднимает сервис, новая доставка с телефона разбирается
## 7. Приёмочные критерии (рубрика ревью дизайна)
Порождена проходом `healthlog-review-rubric` **до** чтения предложения. Каждый
пункт проверяем: понятно, каким тестом его провалить.
- [ ] 7.1 Отсутствие паники на произвольном входе — усечённый JSON, `null` вместо объекта, массив вместо объекта, число вместо строки даты
- [ ] 7.2 Незнакомое поле точки переживает round-trip дословно
- [ ] 7.3 Идемпотентность повторной записи, включая переставленный порядок ключей и точек
- [ ] 7.4 Различие не теряется молча: перезапись и изменение запечатанного часа оставляют след
- [ ] 7.5 Конкурентное слияние того же часа не теряет точки — под `-race`, с проверкой суммы
- [ ] 7.6 Отмена посреди слияния не оставляет половинчатого состояния: объект либо прежний, либо полный
- [ ] 7.7 Граница размера входа явная; вход в сотни мегабайт не кладёт процесс по памяти
- [ ] 7.8 Ошибки различимы по типу, а не по тексту (`errors.Is`/`errors.As`)
- [ ] 7.9 Частично непонятный пакет имеет явную судьбу: что сохранено, что отброшено — видно в счётчиках
- [ ] 7.10 Время нормализовано без потери зоны
- [ ] 7.11 Слой выводится из данных, а не из заголовка; неопределимый слой имеет явную судьбу
- [ ] 7.12 Парсер детерминирован и чист: ни `time.Now`, ни генерации id; два вызова на одном входе равны
- [ ] 7.13 (добавлено после снятия блокера) Точка-интервал не схлопывается по метке: прогон архива даёт 174 координаты сна, а не 170