- три решения опровергнуты экспериментами: повтор при SQLITE_BUSY не сходится без _txlock=immediate (242 из 800 против 800 из 800), разбор в map[string]any держит 197 МиБ против 54, канонизация без json.Number теряет литерал - канонизации назначен дом: общий internal/canon вместо hae, иначе импорт экспорта Apple потребует второй реализации и хеш-детектор станет бесполезен - вывод слоя вернул шаг наследования, WARN сравнивается только с надёжным заголовком; в bucket возвращены units и границы содержимого
13 KiB
ADDED 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 факт учитывается в итоге разбора доставки
Requirement: Вывод слоя гранулярности
Система SHALL выводить слой точки из выравнивания меток времени, а не из
заголовка доставки. Заголовок automation-aggregation непригоден: значение
Default соответствует трём разным режимам выгрузки.
Выводимые слои: raw (метка на произвольной секунде), minute (секунды
нулевые), hour (секунды и минуты нулевые). Слой day в перечисление входит,
но не выводится — он назначается схемам с фиксированной гранулярностью
(см. «Разделение схем под одним именем метрики»).
Разделение схем под одним именем выполняется до вывода слоя, и точки с
назначенным слоем в определении преобладающего слоя доставки не участвуют:
суточных сводок сна бывает больше порога плотности, и их полуночные метки
иначе назначили бы всей доставке слой hour.
Классификация MUST быть по метрике внутри доставки, а не по доставке целиком: при перенастройке автоматизации приезжают смешанные доставки, и отнесение такой доставки к одному слою складывает минутные точки с посекундными.
Scenario: Плотная метрика классифицируется сама
- WHEN в доставке у метрики не меньше десяти точек с метками
- THEN слой определяется выравниванием её собственных меток
Scenario: Редкая метрика наследует преобладающий слой
- WHEN в доставке у метрики меньше десяти точек
- THEN она получает самый мелкий слой среди плотных метрик этой доставки
- AND её собственное выравнивание во внимание не принимается
Scenario: В доставке нет плотных метрик
- WHEN ни у одной метрики доставки нет десяти точек
- THEN слой наследуется от последнего надёжно выведенного слоя той же
автоматизации (
automation-id) - AND если наследовать нечего, слой берётся из надёжного заголовка
(
Minutes→minute,Hours→hour)
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, у них нет.
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и в записи лога