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

78 lines
5.4 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.
# Идентичность эпизодных метрик
**Приоритет:** блокеры
Вынуто из задачи `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`) блокером не затронута — у неё метка
на полуночи уникальна.