- находка 47: ключ (date,start,end) даёт 174 координаты сна против 170 по метке и ноль столкновений внутри доставки против 33; value в ключе лишний - UUID в выгрузку Apple не попадает, другой идентичности у эпизода нет - поэпизодный sleep_analysis вернулся в scope razbor-metrik-v-obekty
12 KiB
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 не содержит ни значений точек, ни имён устройств