идентичность эпизода — интервал: блокер снят измерением
- находка 47: ключ (date,start,end) даёт 174 координаты сна против 170 по метке и ноль столкновений внутри доставки против 33; value в ключе лишний - UUID в выгрузку Apple не попадает, другой идентичности у эпизода нет - поэпизодный sleep_analysis вернулся в scope razbor-metrik-v-obekty
This commit is contained in:
+20
-1
@@ -24,7 +24,8 @@ healthlog принимает выгрузки Apple Health из приложен
|
||||
- **Сохранили — значит приняли.** Код ответа отражает доставку, а не разбор
|
||||
(см. «Приём»).
|
||||
- **Ничего не теряем молча.** Идентичность — устойчивые координаты
|
||||
(`метрика + слой + метка времени`); `source` в ключ не входит, он
|
||||
(`метрика + слой + метка времени`), а у точки-интервала — `метрика + слой +
|
||||
начало + конец`; `source` в ключ не входит, он
|
||||
нестабилен. Хеш канонизированного содержимого остаётся детектором изменений,
|
||||
чтобы не писать зря. При столкновении выигрывает более полная точка, а не
|
||||
последняя пришедшая: бедная доставка не должна стирать поля у богатой.
|
||||
@@ -464,6 +465,24 @@ hour метки выровнены на час heart_rate 00:00:00
|
||||
значения: qty / Min / Avg / Max / source / … ← перезаписываются
|
||||
```
|
||||
|
||||
**У точки-интервала ключ — интервал.** Метка на эпизоде не уникальна: под одним
|
||||
`date` лежит до трёх записей сна, и это не дефект, а способ Apple выразить
|
||||
вложенность «в кровати» и фазы внутри неё. Ключ
|
||||
`метрика + слой + начало + конец` измерен на всём корпусе (находка 47): 174
|
||||
координаты против 170 по метке, ноль столкновений против 33 **внутри одной
|
||||
доставки**, где тай-брейк по времени приёма неприменим в принципе. `value` в
|
||||
ключе ничего не добавляет.
|
||||
|
||||
Эпизодность выводится из формы точки — есть `start` и `end`, отличные от `date`,
|
||||
— а не из списка имён метрик: список был бы вторым способом описывать то, что
|
||||
уже сказано данными. Час объекта берётся по началу эпизода, иначе
|
||||
принадлежность объекту зависела бы от длительности.
|
||||
|
||||
`HKObject.uuid` дал бы идентичность даром, но в выгрузку Apple он не попадает —
|
||||
там у записи только `type`, `sourceName`, `sourceVersion`, `creationDate`,
|
||||
`startDate`, `endDate`, `value`. Значит модель обязана выражаться через
|
||||
`start`/`end`, иначе `import(экспорт)` не сойдётся с `replay(HAE)`.
|
||||
|
||||
Мы дважды пробовали адресовать точку хешем её содержимого и дважды получали
|
||||
задвоение на живых данных:
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
|
||||
<!-- - ГГГГ-ММ-ДД `slug` — Заголовок. Причина: … Был приоритет: … -->
|
||||
- 2026-08-01 `bekap-dannyh` — Резервное копирование ./data. Причина: бекап обеспечивает готовый механизм на сервере пет-проектов — своего заводить не нужно, задача снимается деплоем. Был приоритет: высокий.
|
||||
- 2026-08-01 `identichnost-epizodnyh-metrik` — Идентичность эпизодных метрик. Причина: решён измерением и prior art: ключ эпизода — метрика+слой+start+end (находка 47), вариант А; вернулся в scope razbor-metrik-v-obekty. Был приоритет: блокеры.
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
одному — прерывать поток ради каждого дороже, чем накопить.
|
||||
|
||||
## блокеры
|
||||
- [Идентичность эпизодных метрик](identichnost-epizodnyh-metrik.md) — Координатный ключ схлопывает эпизоды сна: 31 координата в 21 доставке из 89, тай-брейк неисполним
|
||||
|
||||
## высокий
|
||||
- [Разбор метрик в часовые объекты](razbor-metrik-v-obekty.md) — Доставки копятся непрозрачными телами — точек в хранилище нет вовсе, всё остальное упирается в это
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Идентичность эпизодных метрик
|
||||
|
||||
**Приоритет:** блокеры
|
||||
|
||||
Вынуто из задачи `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`) блокером не затронута — у неё метка
|
||||
на полуночи уникальна.
|
||||
|
||||
@@ -9,7 +9,8 @@ Read API, MCP — стоит на этой задаче.
|
||||
Правила выведены на живом потоке и проверены, изобретать заново не нужно
|
||||
(`docs/local-research.md`, находки 33, 35, 36, 38, 39, 41):
|
||||
|
||||
- ключ точки — координаты `метрика + слой + метка`; `source` в ключ не входит;
|
||||
- ключ точки — координаты `метрика + слой + метка`, у эпизода — `метрика + слой
|
||||
+ начало + конец` (находка 47); `source` в ключ не входит;
|
||||
- слой выводится из выравнивания меток: плотная метрика (≥10 точек)
|
||||
классифицируется сама, редкая наследует преобладающий слой доставки;
|
||||
- при столкновении выигрывает более полная точка, а не последняя пришедшая
|
||||
@@ -24,14 +25,9 @@ Read API, MCP — стоит на этой задаче.
|
||||
- слияние точек в объект read-modify-write, хеш объекта как детектор изменений;
|
||||
- `docs/database.md` — ER-схема (её требует шаг гейта `er-schema`).
|
||||
|
||||
**Границы после ревью дизайна.** Поэпизодный `sleep_analysis` в этой задаче не
|
||||
сохраняется — модель идентичности вынесена блокером
|
||||
[identichnost-epizodnyh-metrik](identichnost-epizodnyh-metrik.md). Остальное
|
||||
делается целиком.
|
||||
|
||||
Готово, когда по существующим 89 доставкам собирается хранилище (кроме
|
||||
эпизодов сна), а суммы по часовому слою сходятся с проверкой из
|
||||
`tmp/research/`.
|
||||
Готово, когда по существующим доставкам собирается хранилище, суммы по часовому
|
||||
слою сходятся с проверкой из `tmp/research/`, а эпизодов сна выходит 174, а не
|
||||
170 (находка 47).
|
||||
|
||||
Связано: `docs/architecture.md` → «Хранилище», план шаг 3.
|
||||
|
||||
|
||||
@@ -1459,6 +1459,59 @@ type="HKWorkoutEventTypeMotionPaused|Resumed" ← совпадение по
|
||||
следующего проверенного экспорта, иначе между концом ретеншена и датой
|
||||
снапшота в журнале образуется дыра.
|
||||
|
||||
## 47. Идентичность эпизода сна — `start` и `end`, и другой у Apple нет
|
||||
|
||||
Координата `метрика + слой + метка` для поэпизодного `sleep_analysis` неверна:
|
||||
под одной меткой лежит до трёх разных эпизодов. Замер по всем 94 доставкам
|
||||
(1880 эпизодных точек, суточные сводки исключены):
|
||||
|
||||
| Ключ | Координат | Схлопнуто точек | Дублей **внутри одной** доставки |
|
||||
|---|---|---|---|
|
||||
| `date` | 170 | 1710 | 33 |
|
||||
| `date + start + end` | 174 | 1706 | **0** |
|
||||
| `date + start + end + value` | 174 | 1706 | 0 |
|
||||
|
||||
`start`/`end` закрывают всё; `value` в ключе не добавляет ни одной координаты.
|
||||
Сильнее: из 174 координат **ни одна не несёт двух разных содержимых** — ни с
|
||||
`source`, ни без него. Правило разрешения столкновений на эпизодном сне за весь
|
||||
корпус не срабатывает ни разу.
|
||||
|
||||
Внутридоставочные дубли важнее междоставочных: там `received_at` общий, и любой
|
||||
тай-брейк по времени приёма неприменим в принципе — исход решал бы порядок
|
||||
элементов в JSON-массиве, а он нестабилен (находка 2).
|
||||
|
||||
**Это не дефект данных, а задуманное представление.** Записи с одним `startDate`
|
||||
и разными `endDate` — вложенность «в кровати» и фазы сна внутри неё; HealthKit
|
||||
намеренно допускает перекрывающиеся сэмплы, чтобы выразить «в кровати» и
|
||||
«спит» одновременно.
|
||||
|
||||
**Другой идентичности Apple не даёт.** Атрибуты записи сна в экспорте:
|
||||
|
||||
```
|
||||
type sourceName sourceVersion creationDate startDate endDate value
|
||||
```
|
||||
|
||||
`HKObject.uuid` существует в API, но **в выгрузку не попадает**. Значит модель
|
||||
идентичности обязана выражаться через `start`/`end`: иначе `import(экспорт)` не
|
||||
сойдётся с `replay(HAE)` и журнал перестанет быть журналом.
|
||||
|
||||
### Как это решают другие
|
||||
|
||||
- **Health CSV Importer** дедуплицирует по `Start Date + End Date + Data Type +
|
||||
Type Identifier`. Совпадает с нашим замером, включая отсутствие источника в
|
||||
ключе.
|
||||
- **Ингесторы поверх InfluxDB** (`irvinlim/apple-health-ingester`,
|
||||
`joeecarter/health-import-server`) ключуют по `measurement + tags + timestamp`
|
||||
— то есть имеют ровно эту коллизию и разрешают её молчаливым last-write-wins
|
||||
движка. Один обходит её тем, что кладёт сон плоской точкой
|
||||
(`inBed`/`inBedStart`/`inBedEnd`), то есть поддерживает только суточную схему.
|
||||
- **Пайплайн HAE → FastAPI → Postgres** (ladvien) даёт каждой записи
|
||||
суррогатный UUID-PK без уникального ограничения: обещанная идемпотентность не
|
||||
реализована, повторная доставка задваивает строки.
|
||||
|
||||
То есть обе распространённые схемы хранения теряют данные ровно там, где мы это
|
||||
измерили, а единственная принятая схема дедупликации — это `start`+`end`.
|
||||
|
||||
## Инструмент
|
||||
|
||||
Разбор ведётся скриптом `tmp/research/hl.py` (Python 3, только стандартная
|
||||
|
||||
Reference in New Issue
Block a user