Files
healthlog/docs/backlog/identichnost-epizodnyh-metrik.md
T
av 809153e03f предложение переработано по итогам ревью дизайна
- три решения опровергнуты экспериментами: повтор при SQLITE_BUSY не сходится
  без _txlock=immediate (242 из 800 против 800 из 800), разбор в map[string]any
  держит 197 МиБ против 54, канонизация без json.Number теряет литерал
- канонизации назначен дом: общий internal/canon вместо hae, иначе импорт
  экспорта Apple потребует второй реализации и хеш-детектор станет бесполезен
- вывод слоя вернул шаг наследования, WARN сравнивается только с надёжным
  заголовком; в bucket возвращены units и границы содержимого
2026-08-01 15:16:58 +03:00

5.4 KiB
Raw Blame History

Идентичность эпизодных метрик

Приоритет: блокеры

Вынуто из задачи razbor-metrik-v-obekty ревью дизайна (профиль design, находка №1 триажа, severity critical). Пока не решено — эпизодные схемы не хранятся, см. «Что стоит без решения».

Что решить

Состав координатного ключа и правило слияния для метрик, у которых метка времени не уникальна. Принятая модель — метрика + слой + метка — для них неверна.

Оракул: измерено на живых данных

Из 173 координат сна три несут разное содержимое, одна — три разных эпизода:

sleep_analysis 2026-07-31 22:04:00
   start=22:04 end=22:16  qty=0.2   value=Во сне
   start=22:04 end=02:21  qty=4.28  value=В кровати
   start=22:04 end=07:51  qty=9.78  value=В кровати

Хуже: 31 координата в 21 доставке из 89 (почти четверть) задвоена внутри одной доставки. Там received_at у обеих точек один и тот же, поэтому предписанный тай-брейк «побеждает больший received_at» неприменим в принципе — исход решил бы порядок элементов в JSON-массиве, а он нестабилен (находка 2). Это ломало бы и детерминированность свёртки: пересборка из архива давала бы другое состояние, чем живой приём.

Правило полноты не спасает: точка сна несёт одновременно start/end и startDate/endDate, так что два набора разных полей равной мощности равнополны.

Варианты и цена

(А) Включить start/end в координату для эпизодных схем. Закрывает и междоставочные, и внутридоставочные дубли — все 31 относятся к sleep_analysis. Цена: ключ разной формы для разных классов метрик, и надо определить, что делает метрику «эпизодной» (наличие start/end? список?).

(Б) Эпизодные метрики — множество с идентичностью по хешу канонической формы (append-only). Ничего не теряется по построению. Цена: две модели идентичности в store и рост объекта — эпизоды не схлопываются никогда, даже когда повтор действительно повтор.

(В) Принять потерю: нынешнее правило плюс обязательный WARN. Дёшево. Цена: необратимая потеря эпизодов примерно в четверти доставок сна, обнаружимая только сверкой с экспортом Apple, то есть месяцами позже. Прямо противоречит инварианту «ничего не теряем молча».

Независимо от выбора: тай-брейк received_at надо заменить или дополнить детерминированным (например, лексикографически по канонической форме) — без провенанса в схеме нынешний неисполним даже между доставками.

Рекомендация

(А). Она закрывает оба класса дублей, не плодит вторую модель хранения и не требует принимать потерю. «Эпизодность» выводится из данных, а не курируется: точка, несущая start и end, отличные от date, — эпизод. Это в духе того, как в проекте уже выводится слой — из формы данных, а не из объявления.

Против (Б): рост объекта ради случая, которого можно избежать. Против (В): это ровно тот класс молчаливой потери, ради защиты от которого заведён инвариант.

Что стоит без решения

sleep_analysis (поэпизодная схема) в задаче razbor-metrik-v-obekty не сохраняется: точки разбираются и считаются, но в объекты не пишутся. Сохранять их по правилу, о котором известно, что оно теряет, — хуже, чем не сохранять: тела лежат в архиве, и после решения блокера их подберёт reindex.

Суточная сводка (sleep_analysis_summary) блокером не затронута — у неё метка на полуночи уникальна.