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