Files
healthlog/openspec/changes/razbor-metrik-v-obekty/specs/storage/spec.md
T
av 809153e03f предложение переработано по итогам ревью дизайна
- три решения опровергнуты экспериментами: повтор при SQLITE_BUSY не сходится
  без _txlock=immediate (242 из 800 против 800 из 800), разбор в map[string]any
  держит 197 МиБ против 54, канонизация без json.Number теряет литерал
- канонизации назначен дом: общий internal/canon вместо hae, иначе импорт
  экспорта Apple потребует второй реализации и хеш-детектор станет бесполезен
- вывод слоя вернул шаг наследования, WARN сравнивается только с надёжным
  заголовком; в bucket возвращены units и границы содержимого
2026-08-01 15:16:58 +03:00

11 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: Эпизодные схемы не сохраняются до решения об идентичности

Система MUST NOT сохранять точки схем, у которых метка времени не уникальна, — пока модель их идентичности не определена. Такие точки разбираются и учитываются счётчиком, но в объекты не пишутся.

Известная такая схема одна: поэпизодный sleep_analysis. Измерено: 31 координата в 21 доставке из 89 задвоена внутри одной доставки, где received_at общий и тай-брейк по нему неприменим в принципе. Сохранять их по правилу, о котором известно, что оно теряет, хуже, чем не сохранять: тела остаются в архиве, и после решения их подберёт пересборка.

Развилка вынесена блокером identichnost-epizodnyh-metrik. Суточной сводки (sleep_analysis_summary) ограничение не касается — у неё метка на полуночи уникальна.

Scenario: Поэпизодная запись сна не попадает в объект

  • WHEN разобрана точка метрики sleep_analysis поэпизодной схемы
  • THEN она не сохраняется в часовой объект
  • AND факт учитывается счётчиком в итоге разбора доставки

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 не содержит ни значений точек, ни имён устройств