предложение переработано по итогам ревью дизайна
- три решения опровергнуты экспериментами: повтор при SQLITE_BUSY не сходится без _txlock=immediate (242 из 800 против 800 из 800), разбор в map[string]any держит 197 МиБ против 54, канонизация без json.Number теряет литерал - канонизации назначен дом: общий internal/canon вместо hae, иначе импорт экспорта Apple потребует второй реализации и хеш-детектор станет бесполезен - вывод слоя вернул шаг наследования, WARN сравнивается только с надёжным заголовком; в bucket возвращены units и границы содержимого
This commit is contained in:
@@ -6,14 +6,20 @@
|
||||
в точки. Точка несёт имя метрики, единицы, слой, метку времени и содержимое в
|
||||
том виде, в каком его прислал HAE.
|
||||
|
||||
Незнакомое поле внутри точки MUST сохраняться, а не отбрасываться: сырой архив
|
||||
недолговечен, и отброшенное поле теряется безвозвратно.
|
||||
Хранимая форма точки — **исходные байты**, как они пришли в теле доставки.
|
||||
Система MUST NOT пересобирать содержимое повторной сериализацией разобранных
|
||||
значений: обход через `map[string]any` теряет литерал (`1.0` становится `1`,
|
||||
целые больше 2^53 сдвигаются, невалидный UTF-8 заменяется на U+FFFD), и потеря
|
||||
не видна тестам на фикстурах — они сравнивают разобранное с разобранным.
|
||||
|
||||
Отсюда же следует, что «служебных» полей у точки нет: отбрасывать нечего,
|
||||
нормализованное время добавляется рядом с исходным содержимым, а не вместо.
|
||||
|
||||
#### Scenario: Метрика с точками разбирается в точки
|
||||
|
||||
- **WHEN** тело содержит `data.metrics[]` с непустым `data[]`
|
||||
- **THEN** каждая точка с непустым `date` становится точкой хранилища
|
||||
- **AND** все поля точки, кроме служебных, сохраняются дословно
|
||||
- **AND** её содержимое сохраняется исходными байтами, без пересборки
|
||||
|
||||
#### Scenario: Незнакомая метрика не ломает разбор
|
||||
|
||||
@@ -34,8 +40,15 @@
|
||||
заголовка доставки. Заголовок `automation-aggregation` непригоден: значение
|
||||
`Default` соответствует трём разным режимам выгрузки.
|
||||
|
||||
Слои: `raw` (метка на произвольной секунде), `minute` (секунды нулевые),
|
||||
`hour` (секунды и минуты нулевые).
|
||||
Выводимые слои: `raw` (метка на произвольной секунде), `minute` (секунды
|
||||
нулевые), `hour` (секунды и минуты нулевые). Слой `day` в перечисление входит,
|
||||
но **не выводится** — он назначается схемам с фиксированной гранулярностью
|
||||
(см. «Разделение схем под одним именем метрики»).
|
||||
|
||||
Разделение схем под одним именем выполняется **до** вывода слоя, и точки с
|
||||
назначенным слоем в определении преобладающего слоя доставки не участвуют:
|
||||
суточных сводок сна бывает больше порога плотности, и их полуночные метки
|
||||
иначе назначили бы всей доставке слой `hour`.
|
||||
|
||||
Классификация MUST быть **по метрике внутри доставки**, а не по доставке
|
||||
целиком: при перенастройке автоматизации приезжают смешанные доставки, и
|
||||
@@ -56,23 +69,54 @@
|
||||
#### Scenario: В доставке нет плотных метрик
|
||||
|
||||
- **WHEN** ни у одной метрики доставки нет десяти точек
|
||||
- **THEN** слой берётся из заголовка `automation-aggregation`
|
||||
(`Minutes` → `minute`, `Hours` → `hour`, иначе `raw`)
|
||||
- **THEN** слой наследуется от последнего надёжно выведенного слоя той же
|
||||
автоматизации (`automation-id`)
|
||||
- **AND** если наследовать нечего, слой берётся из **надёжного** заголовка
|
||||
(`Minutes` → `minute`, `Hours` → `hour`)
|
||||
|
||||
#### Scenario: Выведенный слой расходится с заголовком
|
||||
#### Scenario: Наследовать нечего и заголовок ненадёжен
|
||||
|
||||
- **WHEN** выведенный слой не совпадает с тем, что объявляет заголовок доставки
|
||||
- **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 понимать три формата времени, встречающиеся в теле доставки, и
|
||||
приводить их к UTC, сохраняя офсет исходной зоны.
|
||||
Система SHALL разбирать метку точки формата `2026-07-31 21:03:51 +0300` и
|
||||
приводить её к UTC, сохраняя офсет исходной зоны. В секции `data.metrics`
|
||||
других форматов меток не встречается.
|
||||
|
||||
- `2026-07-31 21:03:51 +0300` — метрики и тренировки;
|
||||
- `2026-07-31T18:03:51Z` — RFC 3339 в UTC;
|
||||
- `1785446196.4132624` — Unix-эпоха дробным числом внутри `heartbeatSeries`.
|
||||
Unix-эпоха дробным числом (`1785446196.4132624`) встречается **внутри**
|
||||
`heartbeatSeries` и меткой точки не является. Система MUST NOT преобразовывать
|
||||
её: элементы серии проходят как исходные байты. Преобразование во `time.Unix`
|
||||
и обратно не гарантирует дословности, а серия составляет 93% объёма метрики
|
||||
`heart_rate_variability`.
|
||||
|
||||
RFC 3339 (`2026-07-31T18:03:51Z`) в этой дельте не нормируется: он встречается
|
||||
только в `data.stateOfMind`, которая выведена из scope. Требование к нему
|
||||
появится вместе с задачей про секции с собственными `id` — вместе с данными,
|
||||
на которых его можно проверить.
|
||||
|
||||
#### Scenario: Локальное время со смещением
|
||||
|
||||
@@ -82,8 +126,9 @@
|
||||
#### Scenario: Время внутри серии ударов
|
||||
|
||||
- **WHEN** точка метрики `heart_rate_variability` содержит `heartbeatSeries`
|
||||
- **THEN** элементы серии сохраняются дословно вместе с их эпохой
|
||||
- **THEN** элементы серии сохраняются исходными байтами вместе с их эпохой
|
||||
- **AND** серия не разворачивается в отдельные точки
|
||||
- **AND** эпоха внутри серии не разбирается и не преобразуется
|
||||
|
||||
### Requirement: Разделение схем под одним именем метрики
|
||||
|
||||
@@ -108,9 +153,16 @@ Export шлёт под одним именем, чтобы одно имя оз
|
||||
|
||||
### Requirement: Канонизация содержимого
|
||||
|
||||
Система SHALL приводить содержимое точки к канонической форме перед сравнением
|
||||
и хешированием: рекурсивная сортировка ключей и округление чисел до двенадцати
|
||||
значащих цифр.
|
||||
Система SHALL вычислять каноническую форму содержимого точки для сравнения и
|
||||
хеширования: сортировка ключей и округление чисел до двенадцати значащих цифр.
|
||||
|
||||
Каноническая форма существует **только в момент вычисления хеша** и хранимую
|
||||
форму не заменяет никогда: хранится исходные байты (см. «Разбор секции
|
||||
метрик»). Числа при канонизации читаются литералом, а не через `float64`, —
|
||||
иначе округление применится к уже испорченному значению.
|
||||
|
||||
Сортировка ключей — свойство `encoding/json`, своей реализации не требует.
|
||||
Собственным остаётся только округление.
|
||||
|
||||
Без округления сравнение бесполезно: 63% повторно приехавших точек различались
|
||||
последним разрядом double при одинаковом измерении.
|
||||
|
||||
Reference in New Issue
Block a user