Files
healthlog/docs/adr/ADR-2026-08-04-otvet-tochek-nesyot-rod-i-ego-primenimost.md
T
av 29ca8d415c httpapi: точки метрики за период отдаются одним запросом
- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
  объявляет слой, измеренный род, его применимость к отданному ряду и границу
  окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
  точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
  под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
  хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
  записи: дословность содержимого точки иначе не удерживается, а оборванное
  тело уходило под видом успешного `200`
2026-08-04 18:46:45 +03:00

7.3 KiB
Raw Blame History

Ответ точек несёт измеренный род и его применимость к отданному ряду

  • Дата: 2026-08-04
  • Источник: openspec/changes/archive/2026-08-04-tochki-metriki-za-period/design.md

Решение

Конверт ответа маршрута точек несёт aggregation объектом {style, applicable, last_hour}, а не строкой с применённой свёрткой:

  • style — измеренный род метрики, тот же словарь и то же имя, что у каталога;
  • applicable — применим ли объявленный род к отданному ряду;
  • last_hour — ярлык самого свежего часа окна измерения.

docs/architecture.md до этого изменения обещал "aggregation": "sum" — строку. Решение её пересматривает: строка называет применённое и молчит об основании.

Отвергнуто и названо поимённо: поле applied с именем применённой свёртки (выводится из style и bucket тем же инвариантом; как строка неверно описывает свёртку мгновенной метрики, у которой по архитектуре «среднее с min/max рядом»); полное основание каталога (hours, compared, agreeing, conflicting, first_hour) в конверте точек — второй экземпляр факта, обязанный сходиться с первым.

Почему

Род есть свойство метрики, а слой — свойство ряда, и их сочетание бывает опасным. Конверт {"layer": "raw", "style": "cumulative"} законен и штатен: правило выбора слоя при равном охвате предпочитает самый мелкий. Инвариант «нижний слой HAE не суммируется никогда» система соблюдает, ничего не складывая, — но потребитель об инварианте не знает, а сумма по нижнему слою завышает втрое (находка 34 разведки). Разрыв построен проходом review-rubric на предложении, до кода:

Конверт {"layer": "raw", "aggregation": {"style": "cumulative"}} законен, штатен — и он прямо приглашает главного потребителя (агента с ограниченным контекстом) сложить ряд самому. Система при этом свёртки не делает, инвариант формально цел; результат у потребителя завышен, а решение по нему уже принято.

applicable: false — та самая оговорка, которая едет вместе с данными.

last_hour — единственный след замершего окна. Род считается по 48 самым свежим общим часам, а не по последним 48 часам календаря: выключенная минутная автоматизация HAE останавливает пополнение общих часов, окно замирает и продолжает объявлять род.

Литература расколота, и обе стороны названы. Род вместе с данными: Google Cloud Monitoring объявляет metricKind и valueType в каждом объекте TimeSeries ответа, а не только в дескрипторе метрики; CloudWatch GetMetricData кладёт StatusCode (Complete / PartialData) рядом с рядом — оговорка едет с данными, а не оставляется клиенту на вывод; Home Assistant statistics_during_period держит start и end в ответе всегда, независимо от запрошенных types. Род отдельно от данных: Prometheus отдаёт {resultType, result} без единого слова о типе, а тип живёт в /api/v1/metadata; Graphite render не объявляет ничего. Второе отвергнуто по измеримой причине: клиент обязан сделать второй запрос, а до тех пор не отличает «род известен» от «род не измерен», — и согласованности между двумя ответами всё равно нет, потому что род есть функция окна, а окно едет с часами. Принцип HealthKit HKStatistics («род не тот — свёртки нет») взят, механизм неприменим: у нас стиль источником не объявлен.

Последствия

  • + Потребитель видит не только число, но и на каком основании его можно сворачивать, без второго запроса и без знания инвариантов проекта.
  • + Форма объявлена до того, как её скопируют свёртка по сетке, порог неполного ведра, тренировки, записи и MCP. После копирования это была бы не развилка, а археология.
  • Поле applicable избыточно по построению: клиент, знающий правило «нижний слой HAE не суммируется», вывел бы его из style и layer. Взято сознательно — правило принадлежит нам, и молчаливо перекладывать его на потребителя дороже, чем поле.
  • Чтобы разобрать, почему род unknown, придётся спросить каталог: полное основание живёт там в одном экземпляре.
  • Род в конверте точек и род в каталоге считаются в разные моменты и у клиента, сравнивающего два ответа, могут разойтись. Это свойство измерения, а не дефект; ровно поэтому last_hour едет вместе с родом.

Открыто, решает владелец

Машинно-различимый код причины отказа. Тело отказа несёт только человекочитаемую строку, и агент не отличит «зона не указана» от «слой незнаком» иначе, чем разбором русского текста. Правило общее для всех маршрутов и меняет errorWire, то есть и контракт приёма, — сюда не взято.