Files
healthlog/openspec/specs/parsing/spec.md
T
av 37413bb551 change razbor-metrik-v-obekty заархивирован
Дельты влиты в openspec/specs (parsing, storage), задача убрана из беклога,
план отражает сделанную часть шага 3.

Не закрыт один пункт: живая доставка с телефона не разобрана — поток молчит
с 17:13, пауза началась до перезапуска сервиса.
2026-08-01 19:03:46 +03:00

18 KiB
Raw Blame History

parsing Specification

Purpose

TBD - created by archiving change razbor-metrik-v-obekty. Update Purpose after archive.

Requirements

Requirement: Разбор секции метрик

Система SHALL разбирать секцию data.metrics тела доставки Health Auto Export в точки. Точка несёт имя метрики, единицы, слой, метку времени и содержимое в том виде, в каком его прислал HAE.

Хранимая форма точки — исходные байты, как они пришли в теле доставки. Система MUST NOT пересобирать содержимое повторной сериализацией разобранных значений: обход через map[string]any теряет литерал (1.0 становится 1, целые больше 2^53 сдвигаются, невалидный UTF-8 заменяется на U+FFFD), и потеря не видна тестам на фикстурах — они сравнивают разобранное с разобранным.

Отсюда же следует, что «служебных» полей у точки нет: отбрасывать нечего, нормализованное время добавляется рядом с исходным содержимым, а не вместо.

Scenario: Метрика с точками разбирается в точки

  • WHEN тело содержит data.metrics[] с непустым data[]
  • THEN каждая точка с непустым date становится точкой хранилища
  • AND её содержимое сохраняется исходными байтами, без пересборки

Scenario: Незнакомая метрика не ломает разбор

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

Scenario: Точка без метки времени пропускается

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

Scenario: Точка с нечитаемым концом интервала пропускается

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

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

Requirement: Вывод слоя гранулярности

Система SHALL выводить слой точки из выравнивания меток времени, а не из заголовка доставки. Заголовок automation-aggregation непригоден: значение Default соответствует трём разным режимам выгрузки.

Выводимые слои: raw (метка на произвольной секунде), minute (секунды нулевые), hour (секунды и минуты нулевые). Слой day в перечисление входит, но не выводится — он назначается схемам с фиксированной гранулярностью (см. «Разделение схем под одним именем метрики»).

Выравнивание SHALL считаться по метке в исходной зоне, а не по метке в UTC. HAE строит сетку по местному времени; ровный местный час в зоне со смещением на половину (+0530, +0545, +0930) даёт UTC-метку на середине часа, и часовая выгрузка целиком уехала бы в слой minute — где столкнулась бы с настоящей минутной автоматизацией и завысила сумму минутного слоя вдвое.

Слоем метрики SHALL становиться самое мелкое выравнивание среди её меток, а не преобладающее. Измерено: у плотных метрик выравнивания перемешаны (active_energy — 1320 минутных меток и 21 часовая, heart_rate — 654 посекундных и 10 минутных), потому что метка ровно на часе одновременно является и минутной. Метрика, у которой хоть одна метка стоит на середине часа, часовой не является.

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

Классификация MUST быть по метрике внутри доставки, а не по доставке целиком: при перенастройке автоматизации приезжают смешанные доставки, и отнесение такой доставки к одному слою складывает минутные точки с посекундными.

Scenario: Плотная метрика классифицируется сама

  • WHEN в доставке у метрики не меньше десяти точек с метками
  • THEN слой определяется выравниванием её собственных меток

Scenario: Зона с получасовым смещением не делает часовую выгрузку минутной

  • WHEN метки стоят на ровном местном часе, а смещение зоны равно +0530
  • THEN слой метрики hour

Scenario: Одна метка на середине часа делает метрику минутной

  • WHEN у плотной метрики десять меток стоят ровно на часе, а одна — на середине часа
  • THEN слой метрики minute, а не hour

Scenario: Редкая метрика наследует преобладающий слой

  • WHEN в доставке у метрики меньше десяти точек
  • THEN она получает самый мелкий слой среди плотных метрик этой доставки
  • AND её собственное выравнивание во внимание не принимается

Scenario: Доставка, где всем метрикам слой назначен схемой

  • WHEN в доставке нет метрик, которым слой надо выводить, — все точки принадлежат схемам с фиксированной гранулярностью
  • THEN доставка сохраняется, а слой доставки не выводится и не требуется

Иначе доставка автоматизации, настроенной только на сон, отвергалась бы целиком — и необратимо: исход детерминирован, и пересборка повторяла бы его вечно.

Scenario: В доставке нет плотных метрик

  • WHEN ни у одной метрики доставки нет десяти точек
  • THEN слой наследуется от последнего надёжно выведенного слоя той же автоматизации (automation-id) среди доставок, предшествующих этой
  • AND если наследовать нечего, слой берётся из надёжного заголовка (Minutesminute, Hourshour)

Граница «предшествующих» обязательна: слой обязан быть функцией от префикса журнала. Наследование от последней доставки вообще делает свёртку зависящей от истории, и пересборка даёт не то состояние, что живой приём — измерено на архиве, 1737 объектов против 1742.

Scenario: Пересборка журнала даёт то же состояние

  • WHEN те же доставки сворачиваются повторно в том же порядке
  • THEN число объектов и их содержимое не меняются

Scenario: Наследовать нечего и заголовок ненадёжен

  • WHEN плотных метрик нет, предыдущего слоя автоматизации нет, а заголовок равен Default
  • THEN точки доставки не сохраняются, а исход учитывается счётчиком и записью WARN
  • AND тело остаётся в архиве, откуда доставку подберёт пересборка

Молчаливый выбор raw в этой ветке недопустим: заголовок Default наблюдался одновременно у посекундного, минутного и часового режимов, поэтому он не доказывает ничего, а призрачный raw-разрез попадёт в каталог и в правило Read API «самый мелкий слой, покрывающий диапазон».

Scenario: Выведенный слой расходится с надёжным заголовком

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

Scenario: Заголовок Default в сравнении не участвует

  • WHEN заголовок доставки равен Default
  • THEN расхождение не фиксируется и WARN не пишется

Иначе сигнал утонул бы в собственном шуме: Default не означает режима, и сравнение с ним давало бы WARN на каждой доставке потока в пять минут.

Requirement: Разбор форматов времени

Система SHALL разбирать метку точки формата 2026-07-31 21:03:51 +0300 и приводить её к UTC, сохраняя офсет исходной зоны. В секции data.metrics других форматов меток не встречается.

Unix-эпоха дробным числом (1785446196.4132624) встречается внутри heartbeatSeries и меткой точки не является. Система MUST NOT преобразовывать её: элементы серии проходят как исходные байты. Преобразование во time.Unix и обратно не гарантирует дословности, а серия составляет 93% объёма метрики heart_rate_variability.

RFC 3339 (2026-07-31T18:03:51Z) в этой дельте не нормируется: он встречается только в data.stateOfMind, которая выведена из scope. Требование к нему появится вместе с задачей про секции с собственными id — вместе с данными, на которых его можно проверить.

Scenario: Локальное время со смещением

  • WHEN метка имеет вид 2026-07-31 21:03:51 +0300
  • THEN точка получает время в UTC и офсет +10800 секунд

Scenario: Время внутри серии ударов

  • WHEN точка метрики heart_rate_variability содержит heartbeatSeries
  • THEN элементы серии сохраняются исходными байтами вместе с их эпохой
  • AND серия не разворачивается в отдельные точки
  • AND эпоха внутри серии не разбирается и не преобразуется

Requirement: Разделение схем под одним именем метрики

Система SHALL разводить на разные имена метрики те схемы, которые Health Auto Export шлёт под одним именем, чтобы одно имя означало одну схему.

Под именем sleep_analysis приезжают две несовместимые схемы: поэпизодная (start/end/value/qty) и суточная сводка (totalSleep/core/rem/deep/awake с меткой на местной полуночи). Общих полей, кроме date и source, у них нет.

Схема точки SHALL определяться по самой точке, а не по её месту в исходном массиве. Список разобранных точек отфильтрован пропусками, и соответствие по индексу съезжало бы от одной пропущенной точки: эпизод уезжал бы под имя суточной сводки со слоем day, сводка — под имя эпизода. Метрика и слой входят в координату, поэтому ошибка необратима — точки из объекта не удаляются, и пересборка воспроизвела бы её.

Scenario: Пропущенная точка не сдвигает разметку схем

  • WHEN в метрике sleep_analysis перед суточной сводкой стоит точка без разбираемой метки
  • THEN сводка всё равно сохраняется под именем sleep_analysis_summary со слоем day

Scenario: Поэпизодная запись сна

  • WHEN точка sleep_analysis содержит поле value
  • THEN она сохраняется под именем sleep_analysis

Scenario: Суточная сводка сна

  • WHEN точка sleep_analysis содержит поле totalSleep
  • THEN она сохраняется под именем sleep_analysis_summary
  • AND её слой фиксирован как day, а не выводится из выравнивания

Requirement: Канонизация содержимого

Система SHALL вычислять каноническую форму содержимого точки для сравнения и хеширования: сортировка ключей и округление чисел до двенадцати значащих цифр.

Каноническая форма существует только в момент вычисления хеша и хранимую форму не заменяет никогда: хранится исходные байты (см. «Разбор секции метрик»). Числа при канонизации читаются литералом, а не через float64, — иначе округление применится к уже испорченному значению.

Сортировка ключей — свойство encoding/json, своей реализации не требует. Собственным остаётся только округление.

Без округления сравнение бесполезно: 63% повторно приехавших точек различались последним разрядом double при одинаковом измерении.

Scenario: Повтор с иным порядком ключей опознаётся как тот же

  • WHEN та же точка приезжает с другим порядком ключей в JSON
  • THEN её каноническая форма совпадает с сохранённой

Scenario: Дребезг последнего разряда не считается изменением

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

Requirement: Разбор не влияет на код ответа приёма

Система MUST сохранять правило «сохранили — значит приняли»: исход разбора не меняет код ответа на доставку.

Scenario: Содержимое не разобралось

  • WHEN тело сохранено в архив, но разбор его содержимого не удался
  • THEN ответ на приём остаётся 200
  • AND исход виден в delivery.parse_status и в записи лога