49 файлов, миграция сделана командой tasks.py check --fix — той самой, ради которой в скрипте оставлена читаемость старой формы. Побочно тот же прогон проставил тег decomposed целям, у которых есть задачи: это его штатная работа. check после миграции зелёный, индексы согласованы. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.7 KiB
Read API: точки, выбор слоя, свёртка по сетке
- Секция: ядро
- Зачем: Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
- Теги: goal: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, иначе через полгода два места кода поймут поле по-разному.
Предел размера ответа тоже здесь, и он унаследовал измеренную цену. У каталога предела нет намеренно: правило размера — общее для маршрутов чтения, и задавать его мимоходом на первой ручке значило бы решить контракт до того, как известна форма тяжёлого ответа. Каталог станет первым его потребителем.
Цена измерена на каталоге (задача «цена читающего маршрута», закрыта чекпойнтом WAL и условным запросом): 693 мс и +153 МиБ живой кучи на враждебном запросе (20 метрик × 8 часов × 5000 точек), при том что приём в том же процессе уже даёт пик 768 МиБ на теле 40 МиБ. Условный запрос снял повтор, но первый запрос стоит столько же, а множители «метрики × окно × точки × одновременные запросы» по-прежнему без потолка. Сюда же уезжают отложенные варианты той задачи: собственный дедлайн маршрута и потоковое измерение по метрике (второе — только если счётчик заговорит).
Машинерия условного запроса готова, и её надо взять, а не написать заново.
store.VersionedRead держит правило «версией, снятой после чтения, не
подписывать»; httpapi — разбор If-None-Match и 304. Метка обязана нести
область действия: у точек ответ есть функция параметров запроса, и
etag(scope, version) требует их канонизированную форму — иначе 304 ответит
на другой набор данных. Детали — docs/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».