предложение переработано по итогам ревью дизайна

- три решения опровергнуты экспериментами: повтор при SQLITE_BUSY не сходится
  без _txlock=immediate (242 из 800 против 800 из 800), разбор в map[string]any
  держит 197 МиБ против 54, канонизация без json.Number теряет литерал
- канонизации назначен дом: общий internal/canon вместо hae, иначе импорт
  экспорта Apple потребует второй реализации и хеш-детектор станет бесполезен
- вывод слоя вернул шаг наследования, WARN сравнивается только с надёжным
  заголовком; в bucket возвращены units и границы содержимого
This commit is contained in:
av
2026-08-01 15:16:58 +03:00
parent 65351ab6e7
commit 809153e03f
8 changed files with 394 additions and 76 deletions
+1
View File
@@ -12,6 +12,7 @@
одному — прерывать поток ради каждого дороже, чем накопить.
## блокеры
- [Идентичность эпизодных метрик](identichnost-epizodnyh-metrik.md) — Координатный ключ схлопывает эпизоды сна: 31 координата в 21 доставке из 89, тай-брейк неисполним
## высокий
- [Разбор метрик в часовые объекты](razbor-metrik-v-obekty.md) — Доставки копятся непрозрачными телами — точек в хранилище нет вовсе, всё остальное упирается в это
@@ -0,0 +1,77 @@
# Идентичность эпизодных метрик
**Приоритет:** блокеры
Вынуто из задачи `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`) блокером не затронута — у неё метка
на полуночи уникальна.
+8 -2
View File
@@ -24,8 +24,14 @@ Read API, MCP — стоит на этой задаче.
- слияние точек в объект read-modify-write, хеш объекта как детектор изменений;
- `docs/database.md` — ER-схема (её требует шаг гейта `er-schema`).
Готово, когда по существующим 89 доставкам собирается хранилище, а суммы по
часовому слою сходятся с проверкой из `tmp/research/`.
**Границы после ревью дизайна.** Поэпизодный `sleep_analysis` в этой задаче не
сохраняется — модель идентичности вынесена блокером
[identichnost-epizodnyh-metrik](identichnost-epizodnyh-metrik.md). Остальное
делается целиком.
Готово, когда по существующим 89 доставкам собирается хранилище (кроме
эпизодов сна), а суммы по часовому слою сходятся с проверкой из
`tmp/research/`.
Связано: `docs/architecture.md` → «Хранилище», план шаг 3.