идентичность эпизода — интервал: блокер снят измерением
- находка 47: ключ (date,start,end) даёт 174 координаты сна против 170 по метке и ноль столкновений внутри доставки против 33; value в ключе лишний - UUID в выгрузку Apple не попадает, другой идентичности у эпизода нет - поэпизодный sleep_analysis вернулся в scope razbor-metrik-v-obekty
This commit is contained in:
@@ -36,7 +36,10 @@ Module path — `git.vakhrushev.me/av/healthlog`.
|
||||
- **Сохранили — значит приняли.** Код ответа отражает доставку, а не разбор:
|
||||
битый JSON — 400, непонятое содержимое — 200.
|
||||
- **Ничего не теряем молча.** Идентичность — координаты
|
||||
(`метрика + слой + метка`); `source` в ключ не входит, он нестабилен. Хеш
|
||||
(`метрика + слой + метка`), а у точки-интервала — `метрика + слой + начало +
|
||||
конец`: под одной меткой лежит до трёх эпизодов сна, и эпизодность выводится
|
||||
из формы точки, а не из списка метрик. `source` в ключ не входит, он
|
||||
нестабилен. Хеш
|
||||
канонизированного содержимого остался детектором изменений. При
|
||||
столкновении выигрывает **более полная** точка, а не последняя. Изменение
|
||||
запечатанного часа — `WARN`, но данные всё равно пишутся.
|
||||
|
||||
+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, только стандартная
|
||||
|
||||
@@ -30,10 +30,6 @@
|
||||
- Словарь категориальных значений: строка пока хранится дословно и без кода.
|
||||
- Род агрегации и каталог разрезов.
|
||||
- Своя агрегация при записи: слои не сводятся друг к другу никогда.
|
||||
- **Хранение эпизодных схем** (поэпизодный `sleep_analysis`): модель их
|
||||
идентичности вынесена блокером `identichnost-epizodnyh-metrik`. Точки
|
||||
разбираются и считаются, но не сохраняются; тела в архиве, подберёт
|
||||
пересборка.
|
||||
|
||||
## Decisions
|
||||
|
||||
@@ -125,6 +121,41 @@ double, и хеш-детектор начнёт видеть изменения
|
||||
|
||||
Сжатие наблюдалось около 25 раз — ~2 МБ в сутки вместо ~50 МБ.
|
||||
|
||||
### Эпизод адресуется интервалом, а не меткой
|
||||
|
||||
Координата `метрика + слой + метка` верна для точки-измерения и неверна для
|
||||
точки-интервала: под одной меткой лежит до трёх разных эпизодов сна. Поэтому
|
||||
у точки, несущей `start` и `end`, координата — `метрика + слой + start + end`.
|
||||
|
||||
Замер по всем 94 доставкам (1880 эпизодных точек, находка 47): метка одна даёт
|
||||
170 координат и 33 столкновения **внутри одной доставки**, интервал — 174
|
||||
координаты и ноль столкновений; `value` в ключе не добавляет ни одной
|
||||
координаты. Из 174 координат ни одна не несёт двух разных содержимых, то есть
|
||||
на эпизодном сне правило разрешения столкновений не срабатывает ни разу.
|
||||
|
||||
**Эпизодность выводится из данных, а не курируется списком** — так же, как слой
|
||||
выводится из выравнивания меток, а не из заголовка. Признак: точка несёт `start`
|
||||
и `end`, отличные от `date`. Список имён метрик здесь был бы вторым способом
|
||||
описывать то, что уже сказано формой точки, и разошёлся бы с ней на первой же
|
||||
новой метрике HAE.
|
||||
|
||||
Почему не «принять потерю и писать `WARN`»: в дублях внутри одной доставки
|
||||
`received_at` общий, и тай-брейк по времени приёма неприменим в принципе —
|
||||
исход решал бы порядок элементов в JSON-массиве, а он нестабилен (находка 2).
|
||||
Свёртка перестала бы быть детерминированной: пересборка из архива давала бы не
|
||||
то состояние, что живой приём.
|
||||
|
||||
Почему не append-only по хешу содержимого: это вторая модель идентичности в
|
||||
`store` ради случая, которого можно избежать, и эпизоды не схлопывались бы
|
||||
никогда — даже когда повтор действительно повтор, а их здесь 1706 из 1880.
|
||||
|
||||
Проверено против чужих решений (находка 47): единственная принятая схема
|
||||
дедупликации Apple Health — `Start + End + тип`, без источника в ключе;
|
||||
популярные ингесторы поверх InfluxDB ключуют по метке и теряют эпизоды
|
||||
молчаливым last-write-wins движка. `HKObject.uuid` дал бы идентичность даром,
|
||||
но **в выгрузку Apple он не попадает** — значит модель обязана выражаться через
|
||||
`start`/`end`, иначе `import(экспорт)` не сойдётся с `replay(HAE)`.
|
||||
|
||||
### Слияние — по полноте, при равенстве — по `received_at`
|
||||
|
||||
Координатный ключ означает перезапись значения. Кто побеждает — решает
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
- **Идентичность точки — координаты** (`метрика + слой + метка`). `source` в
|
||||
ключ не входит: он нестабилен и меняется задним числом. При столкновении
|
||||
выигрывает **более полная** точка, а не последняя пришедшая.
|
||||
- **Точка-интервал адресуется интервалом**: `метрика + слой + начало + конец`.
|
||||
Под одной меткой лежит до трёх эпизодов сна, и 33 столкновения из измеренных
|
||||
происходят внутри одной доставки, где тай-брейк по времени приёма неприменим.
|
||||
Эпизодность выводится из формы точки, а не из списка имён метрик.
|
||||
- **Канонизация с округлением** чисел до ~12 значащих цифр; хеш канонической
|
||||
формы — детектор изменений, а не ключ.
|
||||
- `sleep_analysis` разводится на два имени: поэпизодное и суточную сводку —
|
||||
@@ -43,13 +47,7 @@
|
||||
Сходимость на них проверяется скриптом поверх архива, а не командой сервиса;
|
||||
- **словарь категориальных значений** (переведённые строки → коды HealthKit) —
|
||||
отдельная задача; пока строка хранится дословно и без кода рядом;
|
||||
- **род агрегации и каталог разрезов** — отдельная задача, она следующая;
|
||||
- **хранение поэпизодного `sleep_analysis`** — вынуто ревью дизайна блокером
|
||||
`identichnost-epizodnyh-metrik`: координатный ключ его схлопывает (измерено:
|
||||
31 координата в 21 доставке из 89 задвоена внутри одной доставки, где
|
||||
тай-брейк по времени приёма неприменим в принципе). Точки разбираются и
|
||||
считаются, но не сохраняются; тела в архиве, подберёт пересборка.
|
||||
Суточной сводки это не касается.
|
||||
- **род агрегации и каталог разрезов** — отдельная задача, она следующая.
|
||||
|
||||
## Capabilities
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
### Requirement: Идентичность точки по координатам
|
||||
|
||||
Система SHALL адресовать точку координатами `метрика + слой + метка времени`.
|
||||
Поле `source` в ключ входить MUST NOT: оно нестабильно — то же измерение с тем
|
||||
же значением приезжает то как `Apple Watch Ultra 3|iPad (Anton)`, то как
|
||||
`Apple Watch Ultra 3`, потому что Health переосмысливает атрибуцию задним
|
||||
числом.
|
||||
Система SHALL адресовать точку-измерение координатами
|
||||
`метрика + слой + метка времени`. Поле `source` в ключ входить MUST NOT: оно
|
||||
нестабильно — то же измерение с тем же значением приезжает то как
|
||||
`Apple Watch Ultra 3|iPad (Anton)`, то как `Apple Watch Ultra 3`, потому что
|
||||
Health переосмысливает атрибуцию задним числом.
|
||||
|
||||
Идентичность по хешу содержимого проверялась и отвергнута: она задваивала
|
||||
минутный слой целиком — 120 точек в часе вместо 60.
|
||||
@@ -21,27 +21,38 @@
|
||||
- **WHEN** точка с теми же координатами приезжает с другой строкой `source`
|
||||
- **THEN** она остаётся одной точкой, а не превращается в две
|
||||
|
||||
### Requirement: Эпизодные схемы не сохраняются до решения об идентичности
|
||||
### Requirement: Идентичность эпизода по интервалу
|
||||
|
||||
Система MUST NOT сохранять точки схем, у которых метка времени не уникальна, —
|
||||
пока модель их идентичности не определена. Такие точки разбираются и
|
||||
учитываются счётчиком, но в объекты не пишутся.
|
||||
Система SHALL адресовать точку-интервал координатами
|
||||
`метрика + слой + начало + конец`. Метки времени для неё недостаточно: под
|
||||
одной меткой лежит до трёх разных эпизодов сна.
|
||||
|
||||
Известная такая схема одна: поэпизодный `sleep_analysis`. Измерено: 31
|
||||
координата в 21 доставке из 89 задвоена **внутри одной доставки**, где
|
||||
`received_at` общий и тай-брейк по нему неприменим в принципе. Сохранять их по
|
||||
правилу, о котором известно, что оно теряет, хуже, чем не сохранять: тела
|
||||
остаются в архиве, и после решения их подберёт пересборка.
|
||||
Эпизодность SHALL выводиться из формы точки — точка несёт `start` и `end`,
|
||||
отличные от `date`, — а не назначаться списком имён метрик. Список был бы
|
||||
вторым способом описывать то, что уже сказано формой точки, и разошёлся бы с
|
||||
ней на первой же новой метрике HAE.
|
||||
|
||||
Развилка вынесена блокером `identichnost-epizodnyh-metrik`. Суточной сводки
|
||||
(`sleep_analysis_summary`) ограничение не касается — у неё метка на полуночи
|
||||
уникальна.
|
||||
Измерено на всех 94 доставках (1880 эпизодных точек, находка 47): ключ по метке
|
||||
даёт 170 координат и 33 столкновения **внутри одной доставки**, ключ по
|
||||
интервалу — 174 координаты и ноль столкновений. Внутридоставочные столкновения
|
||||
и делают ключ по метке неисправимым: `received_at` там общий, тай-брейк по нему
|
||||
неприменим в принципе, и исход решал бы порядок элементов в JSON-массиве — а он
|
||||
нестабилен.
|
||||
|
||||
#### Scenario: Поэпизодная запись сна не попадает в объект
|
||||
Час объекта для точки-интервала SHALL определяться по началу эпизода: эпизод
|
||||
пересекает границы часов, и любой другой выбор сделал бы принадлежность
|
||||
объекту зависящей от длительности.
|
||||
|
||||
- **WHEN** разобрана точка метрики `sleep_analysis` поэпизодной схемы
|
||||
- **THEN** она не сохраняется в часовой объект
|
||||
- **AND** факт учитывается счётчиком в итоге разбора доставки
|
||||
#### Scenario: Эпизоды с одной меткой и разными интервалами не схлопываются
|
||||
|
||||
- **WHEN** в доставке приходят точки `sleep_analysis` с одинаковым `date` и
|
||||
разными парами `start`/`end`
|
||||
- **THEN** каждая сохраняется отдельной точкой
|
||||
|
||||
#### Scenario: Повтор эпизода в следующей доставке не задваивает
|
||||
|
||||
- **WHEN** точка с тем же `start` и `end` приезжает следующей доставкой
|
||||
- **THEN** она остаётся одной точкой
|
||||
|
||||
### Requirement: Разрешение столкновений по полноте
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
- [ ] 1.1 Скрипт `tmp/research/fixtures.py`: собирает фикстуры из `data/raw`, вычищая измеренные значения и сохраняя порядок ключей, точность чисел, неразрывные пробелы и форматы времени
|
||||
- [ ] 1.2 Набор `internal/hae/testdata`: минутная доставка, посекундная, часовая, смешанная (перенастройка автоматизации), обе схемы `sleep_analysis`, точка с `heartbeatSeries`, доставка без плотных метрик с заголовком `Default`
|
||||
- [ ] 1.3a Фикстура с эпизодами сна: три точки с одной меткой `date` и разными парами `start`/`end`, включая эпизод, пересекающий границу часа (в архиве такие есть — вычистить значения, интервалы сохранить)
|
||||
- [ ] 1.3 Рукотворная фикстура с **выдуманным полем точки** — скрипт из архива её не породит, а без неё требование «незнакомое поле сохраняется» останется без теста
|
||||
- [ ] 1.4 Миграция `internal/store/migrations/00003_bucket.sql`: таблица `bucket` (`metric`, `layer`, `hour_utc`, `units`, `payload` BLOB, `content_hash`, `points`, `first_ts`, `last_ts`, `first_delivery_id`, `sealed`, `created_at`, `updated_at`), уникальность по `(metric, layer, hour_utc)`
|
||||
- [ ] 1.5 `docs/database.md` — ER-схема с `delivery` и `bucket` (шаг гейта `er-schema` требует её при изменении миграций)
|
||||
@@ -32,7 +33,8 @@
|
||||
- [ ] 4.1 Модель объекта в `internal/store`; содержимое — исходные байты точек, gzip-BLOB, точки упорядочены по времени
|
||||
- [ ] 4.2 Слияние: координатный ключ, победа более полной точки, при равной полноте — детерминированный исход по порядку канонических форм
|
||||
- [ ] 4.3 Столкновение с различием канонической формы — `WARN` без значений и счётчик перезаписей
|
||||
- [ ] 4.4 Поэпизодный `sleep_analysis` **не сохраняется** (блокер `identichnost-epizodnyh-metrik`): точки считаются, в объекты не пишутся
|
||||
- [ ] 4.4 Ключ точки-интервала — `метрика + слой + start + end`; эпизодность выводится из формы точки (`start`/`end`, отличные от `date`), а не из списка имён. Час объекта — по началу эпизода
|
||||
- [ ] 4.4a Тест на фикстуре 1.3a: три эпизода с одной меткой и разными интервалами дают три точки, а не одну; повтор той же тройки следующей доставкой не задваивает
|
||||
- [ ] 4.5 Хеш объекта как детектор изменений: совпал — записи нет
|
||||
- [ ] 4.6 `_txlock=immediate` в DSN; повтор оборачивает **всю тройку** чтение-слияние-запись; путь «хеш совпал» — под `TxOptions{ReadOnly: true}`
|
||||
- [ ] 4.7 Распознавание занятости — `errors.As` на `*sqlite.Error`, коды 5 и 517, обёрнуто в `store`
|
||||
@@ -71,3 +73,4 @@
|
||||
- [ ] 7.10 Время нормализовано без потери зоны
|
||||
- [ ] 7.11 Слой выводится из данных, а не из заголовка; неопределимый слой имеет явную судьбу
|
||||
- [ ] 7.12 Парсер детерминирован и чист: ни `time.Now`, ни генерации id; два вызова на одном входе равны
|
||||
- [ ] 7.13 (добавлено после снятия блокера) Точка-интервал не схлопывается по метке: прогон архива даёт 174 координаты сна, а не 170
|
||||
|
||||
Reference in New Issue
Block a user