заведён change на разбор метрик в часовые объекты
- proposal и две capability: parsing (вывод слоя, форматы времени, канонизация) и storage (координатный ключ, слияние по полноте, часовые объекты) - design фиксирует границы: разбор отдельным пакетом, синхронно после записи в архив, конкурентная запись в один час — риск с тестом - вне scope сознательно: тренировки, reindex, словарь кодов, род агрегации
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Разбор секции метрик
|
||||
|
||||
Система SHALL разбирать секцию `data.metrics` тела доставки Health Auto Export
|
||||
в точки. Точка несёт имя метрики, единицы, слой, метку времени и содержимое в
|
||||
том виде, в каком его прислал HAE.
|
||||
|
||||
Незнакомое поле внутри точки MUST сохраняться, а не отбрасываться: сырой архив
|
||||
недолговечен, и отброшенное поле теряется безвозвратно.
|
||||
|
||||
#### Scenario: Метрика с точками разбирается в точки
|
||||
|
||||
- **WHEN** тело содержит `data.metrics[]` с непустым `data[]`
|
||||
- **THEN** каждая точка с непустым `date` становится точкой хранилища
|
||||
- **AND** все поля точки, кроме служебных, сохраняются дословно
|
||||
|
||||
#### Scenario: Незнакомая метрика не ломает разбор
|
||||
|
||||
- **WHEN** приходит метрика с именем, которого разбор не знает
|
||||
- **THEN** её точки разбираются наравне с остальными
|
||||
- **AND** разбор не завершается ошибкой
|
||||
|
||||
#### Scenario: Точка без метки времени пропускается
|
||||
|
||||
- **WHEN** точка не содержит `date` либо `date` не разбирается ни одним из
|
||||
поддерживаемых форматов
|
||||
- **THEN** точка не попадает в хранилище
|
||||
- **AND** факт учитывается в итоге разбора доставки
|
||||
|
||||
### Requirement: Вывод слоя гранулярности
|
||||
|
||||
Система SHALL выводить слой точки из **выравнивания меток времени**, а не из
|
||||
заголовка доставки. Заголовок `automation-aggregation` непригоден: значение
|
||||
`Default` соответствует трём разным режимам выгрузки.
|
||||
|
||||
Слои: `raw` (метка на произвольной секунде), `minute` (секунды нулевые),
|
||||
`hour` (секунды и минуты нулевые).
|
||||
|
||||
Классификация MUST быть **по метрике внутри доставки**, а не по доставке
|
||||
целиком: при перенастройке автоматизации приезжают смешанные доставки, и
|
||||
отнесение такой доставки к одному слою складывает минутные точки с
|
||||
посекундными.
|
||||
|
||||
#### Scenario: Плотная метрика классифицируется сама
|
||||
|
||||
- **WHEN** в доставке у метрики не меньше десяти точек с метками
|
||||
- **THEN** слой определяется выравниванием её собственных меток
|
||||
|
||||
#### Scenario: Редкая метрика наследует преобладающий слой
|
||||
|
||||
- **WHEN** в доставке у метрики меньше десяти точек
|
||||
- **THEN** она получает самый мелкий слой среди плотных метрик этой доставки
|
||||
- **AND** её собственное выравнивание во внимание не принимается
|
||||
|
||||
#### Scenario: В доставке нет плотных метрик
|
||||
|
||||
- **WHEN** ни у одной метрики доставки нет десяти точек
|
||||
- **THEN** слой берётся из заголовка `automation-aggregation`
|
||||
(`Minutes` → `minute`, `Hours` → `hour`, иначе `raw`)
|
||||
|
||||
#### Scenario: Выведенный слой расходится с заголовком
|
||||
|
||||
- **WHEN** выведенный слой не совпадает с тем, что объявляет заголовок доставки
|
||||
- **THEN** система пишет запись уровня `WARN`
|
||||
- **AND** сохраняет точки по выведенному слою, а не по заголовку
|
||||
|
||||
### Requirement: Разбор форматов времени
|
||||
|
||||
Система SHALL понимать три формата времени, встречающиеся в теле доставки, и
|
||||
приводить их к UTC, сохраняя офсет исходной зоны.
|
||||
|
||||
- `2026-07-31 21:03:51 +0300` — метрики и тренировки;
|
||||
- `2026-07-31T18:03:51Z` — RFC 3339 в UTC;
|
||||
- `1785446196.4132624` — Unix-эпоха дробным числом внутри `heartbeatSeries`.
|
||||
|
||||
#### Scenario: Локальное время со смещением
|
||||
|
||||
- **WHEN** метка имеет вид `2026-07-31 21:03:51 +0300`
|
||||
- **THEN** точка получает время в UTC и офсет `+10800` секунд
|
||||
|
||||
#### Scenario: Время внутри серии ударов
|
||||
|
||||
- **WHEN** точка метрики `heart_rate_variability` содержит `heartbeatSeries`
|
||||
- **THEN** элементы серии сохраняются дословно вместе с их эпохой
|
||||
- **AND** серия не разворачивается в отдельные точки
|
||||
|
||||
### Requirement: Разделение схем под одним именем метрики
|
||||
|
||||
Система SHALL разводить на разные имена метрики те схемы, которые Health Auto
|
||||
Export шлёт под одним именем, чтобы одно имя означало одну схему.
|
||||
|
||||
Под именем `sleep_analysis` приезжают две несовместимые схемы: поэпизодная
|
||||
(`start`/`end`/`value`/`qty`) и суточная сводка
|
||||
(`totalSleep`/`core`/`rem`/`deep`/`awake` с меткой на местной полуночи). Общих
|
||||
полей, кроме `date` и `source`, у них нет.
|
||||
|
||||
#### Scenario: Поэпизодная запись сна
|
||||
|
||||
- **WHEN** точка `sleep_analysis` содержит поле `value`
|
||||
- **THEN** она сохраняется под именем `sleep_analysis`
|
||||
|
||||
#### Scenario: Суточная сводка сна
|
||||
|
||||
- **WHEN** точка `sleep_analysis` содержит поле `totalSleep`
|
||||
- **THEN** она сохраняется под именем `sleep_analysis_summary`
|
||||
- **AND** её слой фиксирован как `day`, а не выводится из выравнивания
|
||||
|
||||
### Requirement: Канонизация содержимого
|
||||
|
||||
Система SHALL приводить содержимое точки к канонической форме перед сравнением
|
||||
и хешированием: рекурсивная сортировка ключей и округление чисел до двенадцати
|
||||
значащих цифр.
|
||||
|
||||
Без округления сравнение бесполезно: 63% повторно приехавших точек различались
|
||||
последним разрядом double при одинаковом измерении.
|
||||
|
||||
#### Scenario: Повтор с иным порядком ключей опознаётся как тот же
|
||||
|
||||
- **WHEN** та же точка приезжает с другим порядком ключей в JSON
|
||||
- **THEN** её каноническая форма совпадает с сохранённой
|
||||
|
||||
#### Scenario: Дребезг последнего разряда не считается изменением
|
||||
|
||||
- **WHEN** значение отличается только за пределами двенадцатой значащей цифры
|
||||
- **THEN** каноническая форма совпадает с сохранённой
|
||||
|
||||
### Requirement: Разбор не влияет на код ответа приёма
|
||||
|
||||
Система MUST сохранять правило «сохранили — значит приняли»: исход разбора не
|
||||
меняет код ответа на доставку.
|
||||
|
||||
#### Scenario: Содержимое не разобралось
|
||||
|
||||
- **WHEN** тело сохранено в архив, но разбор его содержимого не удался
|
||||
- **THEN** ответ на приём остаётся `200`
|
||||
- **AND** исход виден в `delivery.parse_status` и в записи лога
|
||||
@@ -0,0 +1,100 @@
|
||||
## 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 оставлять
|
||||
**более полную** точку — ту, у которой больше значащих полей, — а не последнюю
|
||||
пришедшую. Иначе бедная доставка стирает `start`/`end` у богатой.
|
||||
|
||||
Если полнота равна, а значения различаются, исход определяет порядок
|
||||
воспроизведения, и он MUST быть по `received_at` доставки: свёртка по журналу
|
||||
обязана давать то же состояние, что приём в реальном времени.
|
||||
|
||||
#### Scenario: Бедная точка не стирает поля богатой
|
||||
|
||||
- **WHEN** сохранена точка с `qty`, `start` и `end`
|
||||
- **AND** по тем же координатам приезжает точка только с `qty`
|
||||
- **THEN** сохранённая точка остаётся с `start` и `end`
|
||||
|
||||
#### Scenario: Одинаково полные точки с разными значениями
|
||||
|
||||
- **WHEN** по одним координатам приходят две одинаково полные точки с разными
|
||||
значениями
|
||||
- **THEN** побеждает точка из доставки с большим `received_at`
|
||||
|
||||
### Requirement: Хранение часовыми объектами
|
||||
|
||||
Система SHALL хранить точки часовыми объектами с ключом
|
||||
`метрика + слой + час (UTC)`. Содержимое объекта — сжатый gzip блоб; точки
|
||||
внутри упорядочены по времени.
|
||||
|
||||
Запись — чтение объекта, слияние точек, запись обратно. Точки из объекта
|
||||
MUST NOT удаляться.
|
||||
|
||||
#### Scenario: Точки за один час ложатся в один объект
|
||||
|
||||
- **WHEN** приходят точки одной метрики и слоя за один час UTC
|
||||
- **THEN** они хранятся одним объектом
|
||||
|
||||
#### Scenario: Дозапись в существующий час
|
||||
|
||||
- **WHEN** приходят новые точки за уже существующий час
|
||||
- **THEN** объект перечитывается, точки сливаются, объект записывается обратно
|
||||
- **AND** ранее сохранённые точки остаются в объекте
|
||||
|
||||
### Requirement: Хеш как детектор изменений
|
||||
|
||||
Система SHALL хранить хеш канонической формы объекта и пропускать запись, если
|
||||
хеш не изменился. Хеш — детектор, а не ключ.
|
||||
|
||||
Это то, что делает широкие проходы синхронизации дешёвыми: глубокий проход
|
||||
переприсылает неделю, но почти все сравнения сходятся и записи не происходит.
|
||||
|
||||
#### Scenario: Повторная присылка того же часа не пишет в базу
|
||||
|
||||
- **WHEN** приезжает доставка, целиком повторяющая уже сохранённый час
|
||||
- **THEN** хеш совпадает и запись не выполняется
|
||||
|
||||
### Requirement: Признак запечатанного часа
|
||||
|
||||
Система SHALL отмечать признаком `sealed` часы, которые уже не должны
|
||||
меняться. Изменение запечатанного объекта — не отказ, а сигнал.
|
||||
|
||||
#### Scenario: Изменение запечатанного часа
|
||||
|
||||
- **WHEN** приходят точки за час, помеченный `sealed`
|
||||
- **THEN** система пишет запись уровня `WARN`
|
||||
- **AND** данные всё равно сохраняются
|
||||
|
||||
### Requirement: Значения точек не попадают в логи
|
||||
|
||||
Данные о здоровье чувствительнее токенов. Система MUST NOT писать значения
|
||||
точек и тела доставок в записи лога уровня выше `DEBUG`.
|
||||
|
||||
#### Scenario: Разбор доставки логируется без значений
|
||||
|
||||
- **WHEN** доставка разобрана
|
||||
- **THEN** запись лога содержит счётчики (метрик, точек, объектов) и
|
||||
идентификатор доставки
|
||||
- **AND** не содержит ни значений точек, ни имён устройств
|
||||
Reference in New Issue
Block a user