- род метрики выводится сверкой минутного слоя с часовым: часовое значение сходится с суммой минутных — накопительная, со средним — мгновенная, иначе `unknown` и свёртка не предлагается вовсе. На живом архиве (123 доставки, 31 метрика) 7 накопительных, 9 мгновенных, противоречащих часов ноль - `GET /api/v1/metrics` под токеном чтения отдаёт единицы, слои с границами и род вместе с основанием измерения; род нигде не хранится — он функция витрины, а витрина функция журнала, устаревать в нём нечему - миграция 00009: покрывающий индекс, чтобы каталог отвечал по учётным колонкам, не разжимая содержимое объектов
5.7 KiB
Read API: точки, выбор слоя, свёртка по сетке
Приоритет: высокий
Сейчас данные достаются только sqlite3 на хосте. Все три сценария —
агент-медик, трекер тренировок, фитнес-игра — упираются в отсутствие чтения.
Формы запроса ровно две, и это один запрос с необязательным параметром:
?from&to — все значения за период (вес, лекарства, симптомы), ?from&to&bucket
— с разбивкой (шаги, энергия).
Решение по размеру ответа (вариант «б»): разбивка не задана и ответ не влезает — сервер сам берёт сетку погрубее и называет её в ответе; разбивка задана явно и не влезает — ошибка со списком доступных сеток, а не тихая подмена. Различие существенно: иначе агент, попросивший минутную сетку, получит суточные суммы.
Отдача тренировок и записей входит сюда же. Разбор и хранение сущностей с
собственным id сделаны (change 2026-08-02-trenirovki-i-zapisi), а эндпоинтов
нет: тренировка с маршрутом и записи stateOfMind лежат в витрине и наружу не
отдаются. Вводить их раньше конверта ответа значило бы задать контракт
мимоходом, поэтому GET /workouts, GET /workouts/{id} и
GET /records/{kind} закрываются этой задачей — вместе с формой конверта и
правилом размера ответа. Второй сценарий паспорта (трекер) до тех пор не закрыт.
Готово, когда «шаги за неделю по дням» и «вес за год» отвечаются одним запросом
каждый, тренировка отдаётся одним пакетом вместе с маршрутом, а в ответе всегда
видно layer, bucket и aggregation.
Порог неполного ведра решается здесь, и вместе с ним — его полярность.
Каталог и род агрегации сделаны (change 2026-08-02-katalog-i-rod-agregacii), и
измерению порог заполненности не понадобился: у него две конкурирующие гипотезы,
и неполный час не сходится ни с одной сам собой. Свёртке в ответе он нужен, а
готовые решения задают его противоположно: Graphite xFilesFactor — доля
обязательно известных точек (умолчание 0.5 при роллапе и 0 при рендере, один
параметр с двумя умолчаниями), RRDtool xff — доля допустимо неизвестных. Обе
величины выглядят как «0.5», означая разное; полярность придётся назвать вслух в
architecture.md, иначе через полгода два места кода поймут поле по-разному.
Предел размера ответа тоже здесь. У каталога его нет намеренно: правило размера — общее для маршрутов чтения, и задавать его мимоходом на первой ручке значило бы решить контракт до того, как известна форма тяжёлого ответа. Каталог станет первым его потребителем.
Форма провода наследуется от каталога, и это надо решить один раз. Сегодня
типы internal/catalog сами несут json-теги, а транспорт владеет только
обёрткой: переименование поля в домене меняет публичный контракт без касания
httpapi. Держит это один байтовый тест непустого ответа. Либо объявить в
architecture.md, что типы чтения и есть форма провода для всех транспортов
(HTTP и MCP отдают её байт в байт), либо завести DTO в транспорте — но выбрать до
того, как образец скопирует эта задача.
Клиент обязан смотреть на границы окна измерения. Род метрики измерен по
48 самым свежим ОБЩИМ часам, а не по последним 48 часам календаря: если минутная
автоматизация HAE выключена, множество общих часов не пополняется и окно
замирает. Род при этом продолжает объявляться, и единственный след — last_hour
в ответе. Правило выбора свёртки в Read API обязано это учитывать (или явно
объявить, что не учитывает).
Связано: docs/architecture.md → «Read API», «Измерение рода агрегации»,
план → шаг «Read API».