Files
healthlog/openspec/changes/razbor-metrik-v-obekty/specs/storage/spec.md
T
av 3c9d226a34 идентичность эпизода — интервал: блокер снят измерением
- находка 47: ключ (date,start,end) даёт 174 координаты сна против 170 по
  метке и ноль столкновений внутри доставки против 33; value в ключе лишний
- UUID в выгрузку Apple не попадает, другой идентичности у эпизода нет
- поэпизодный sleep_analysis вернулся в scope razbor-metrik-v-obekty
2026-08-01 17:09:45 +03:00

12 KiB
Raw Blame History

ADDED Requirements

Requirement: Идентичность точки по координатам

Система SHALL адресовать точку-измерение координатами метрика + слой + метка времени. Поле source в ключ входить MUST NOT: оно нестабильно — то же измерение с тем же значением приезжает то как Apple Watch Ultra 3|iPad (Anton), то как Apple Watch Ultra 3, потому что Health переосмысливает атрибуцию задним числом.

Идентичность по хешу содержимого проверялась и отвергнута: она задваивала минутный слой целиком — 120 точек в часе вместо 60.

Scenario: Повторная доставка той же точки ничего не меняет

  • WHEN точка с теми же координатами и тем же содержимым приезжает снова
  • THEN хранилище не изменяется

Scenario: Смена источника не создаёт вторую точку

  • WHEN точка с теми же координатами приезжает с другой строкой source
  • THEN она остаётся одной точкой, а не превращается в две

Requirement: Идентичность эпизода по интервалу

Система SHALL адресовать точку-интервал координатами метрика + слой + начало + конец. Метки времени для неё недостаточно: под одной меткой лежит до трёх разных эпизодов сна.

Эпизодность SHALL выводиться из формы точки — точка несёт start и end, отличные от date, — а не назначаться списком имён метрик. Список был бы вторым способом описывать то, что уже сказано формой точки, и разошёлся бы с ней на первой же новой метрике HAE.

Измерено на всех 94 доставках (1880 эпизодных точек, находка 47): ключ по метке даёт 170 координат и 33 столкновения внутри одной доставки, ключ по интервалу — 174 координаты и ноль столкновений. Внутридоставочные столкновения и делают ключ по метке неисправимым: received_at там общий, тай-брейк по нему неприменим в принципе, и исход решал бы порядок элементов в JSON-массиве — а он нестабилен.

Час объекта для точки-интервала SHALL определяться по началу эпизода: эпизод пересекает границы часов, и любой другой выбор сделал бы принадлежность объекту зависящей от длительности.

Scenario: Эпизоды с одной меткой и разными интервалами не схлопываются

  • WHEN в доставке приходят точки sleep_analysis с одинаковым date и разными парами start/end
  • THEN каждая сохраняется отдельной точкой

Scenario: Повтор эпизода в следующей доставке не задваивает

  • WHEN точка с тем же start и end приезжает следующей доставкой
  • THEN она остаётся одной точкой

Requirement: Разрешение столкновений по полноте

Когда по одним координатам приходят разные содержимые, система SHALL оставлять более полную точку — ту, у которой больше значащих полей, — а не последнюю пришедшую. Иначе бедная доставка стирает start/end у богатой.

Если полнота равна, а значения различаются, исход MUST быть детерминированным и не зависеть от порядка, в котором доставки дошли до хранилища: свёртка по журналу обязана давать то же состояние, что приём в реальном времени.

Сравнение по received_at для этого не годится: у сохранённой точки нет провенанса — ни времени приёма, ни идентификатора доставки, — и сравнивать не с чем. Детерминизм обеспечивается свойством самих значений (например, порядком канонических форм), а не порядком событий.

Scenario: Бедная точка не стирает поля богатой

  • WHEN сохранена точка с qty, start и end
  • AND по тем же координатам приезжает точка только с qty
  • THEN сохранённая точка остаётся с start и end

Scenario: Одинаково полные точки с разными значениями

  • WHEN по одним координатам приходят две одинаково полные точки с разными значениями
  • THEN исход определяется детерминированно и не зависит от порядка воспроизведения доставок

Scenario: Столкновение с различием содержимого оставляет след

  • WHEN по одним координатам сохраняется точка, каноническая форма которой отличается от уже сохранённой
  • THEN система пишет запись уровня WARN без значений точки
  • AND увеличивает счётчик перезаписей в итоге разбора доставки

Без этого следа допущение «меньше полей не значит новее» не получит ни одного наблюдения, а отказ правила будет неотличим от нормальной работы до сверки с экспортом Apple — то есть месяцами.

Requirement: Хранение часовыми объектами

Система SHALL хранить точки часовыми объектами с ключом метрика + слой + час (UTC). Содержимое объекта — сжатый gzip блоб; точки внутри упорядочены по времени.

Объект SHALL нести единицы измерения метрики. Внутри точки их нет — они живут на уровне метрики (проверено: поле units не встретилось ни в одной точке за 89 доставок), поэтому дословное хранение точек их не сохраняет. Без колонки единицы восстановимы только из архива, а для метрик, переставших приходить, — теряются навсегда.

Объект SHALL нести границы содержимого (первая и последняя метка) и идентификатор доставки, создавшей его. Первое нужно каталогу разрезов, чтобы не разжимать каждый блоб ради диапазона; второе — провенанс для разбора слияний.

Запись — чтение объекта, слияние точек, запись обратно. Точки из объекта MUST NOT удаляться.

Scenario: Точки за один час ложатся в один объект

  • WHEN приходят точки одной метрики и слоя за один час UTC
  • THEN они хранятся одним объектом

Scenario: Дозапись в существующий час

  • WHEN приходят новые точки за уже существующий час
  • THEN объект перечитывается, точки сливаются, объект записывается обратно
  • AND ранее сохранённые точки остаются в объекте

Requirement: Хеш как детектор изменений

Система SHALL хранить хеш канонической формы объекта и пропускать запись, если хеш не изменился. Хеш — детектор, а не ключ.

Это то, что делает широкие проходы синхронизации дешёвыми: глубокий проход переприсылает неделю, но почти все сравнения сходятся и записи не происходит.

Scenario: Повторная присылка того же часа не пишет в базу

  • WHEN приезжает доставка, целиком повторяющая уже сохранённый час
  • THEN хеш совпадает и запись не выполняется

Requirement: Признак запечатанного часа

Система SHALL хранить признак sealed у часового объекта и SHALL реагировать на изменение запечатанного объекта сигналом, а не отказом.

Правило перевода часа в sealed в этой дельте не определяется: порог глубины досчёта ставится по наблюдениям, которых пока нет (наблюдалось до 22 минут). До появления правила признак остаётся невыставленным, и сценарий ниже проверяется только явной установкой в тесте — это осознанная граница, а не упущение.

Scenario: Изменение запечатанного часа

  • WHEN приходят точки за час, помеченный sealed
  • THEN система пишет запись уровня WARN
  • AND данные всё равно сохраняются

Requirement: Значения точек не попадают в логи

Данные о здоровье чувствительнее токенов. Система MUST NOT писать значения точек и тела доставок в записи лога уровня выше DEBUG.

Scenario: Разбор доставки логируется без значений

  • WHEN доставка разобрана
  • THEN запись лога содержит счётчики (метрик, точек, объектов) и идентификатор доставки
  • AND не содержит ни значений точек, ни имён устройств