- три решения опровергнуты экспериментами: повтор при SQLITE_BUSY не сходится без _txlock=immediate (242 из 800 против 800 из 800), разбор в map[string]any держит 197 МиБ против 54, канонизация без json.Number теряет литерал - канонизации назначен дом: общий internal/canon вместо hae, иначе импорт экспорта Apple потребует второй реализации и хеш-детектор станет бесполезен - вывод слоя вернул шаг наследования, WARN сравнивается только с надёжным заголовком; в bucket возвращены units и границы содержимого
5.4 KiB
Идентичность эпизодных метрик
Приоритет: блокеры
Вынуто из задачи 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) блокером не затронута — у неё метка
на полуночи уникальна.