tasks: закрыт разбор и хранилище, начат спринт по чтению данных клиентами

- цель parsing-and-storage закрыта по своему критерию; незакрываемый остаток
  (новые формы от источника, ручные секции задним числом) переехал в тему
  parsing-completeness
- цель mcp поглощена целью read-api, переименованной в «Чтение данных
  клиентами»: адаптер — последний шаг того же направления, а не своё
- read-api-points разложена на конверт с точками, свёртку по сетке и
  тренировки с записями; спринт 2026-08-04 набран пятью задачами
This commit is contained in:
av
2026-08-04 14:01:38 +03:00
parent 637eb38bce
commit 79331ac670
17 changed files with 276 additions and 144 deletions
-3
View File
@@ -16,10 +16,8 @@
- [Сущность с id, но неразобранной меткой](items/entity-without-parsed-label.md) — Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым - [Сущность с id, но неразобранной меткой](items/entity-without-parsed-label.md) — Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым
- [Идентичность тренировок при импорте родного экспорта](items/workout-identity-on-import.md) — В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE - [Идентичность тренировок при импорте родного экспорта](items/workout-identity-on-import.md) — В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE
- [Импорт родного экспорта Apple Health](items/apple-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут - [Импорт родного экспорта Apple Health](items/apple-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
- [MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
- [[idea] Месячный проход по ручным секциям](items/monthly-manual-sections-pass.md) — Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит - [[idea] Месячный проход по ручным секциям](items/monthly-manual-sections-pass.md) — Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит
- [[idea] NDJSON-поток для больших выборок Read API](items/ndjson-stream.md) — Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация - [[idea] NDJSON-поток для больших выборок Read API](items/ndjson-stream.md) — Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация
- [OpenAPI-спека и Swagger UI](items/openapi-swagger.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
- [Data-миграции не отбирают строки по обрезаемым спискам](items/data-migration-row-selection.md) — Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону - [Data-миграции не отбирают строки по обрезаемым спискам](items/data-migration-row-selection.md) — Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону
- [[idea] Отказ от heartbeatSeries](items/drop-heartbeat-series.md) — 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе - [[idea] Отказ от heartbeatSeries](items/drop-heartbeat-series.md) — 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе
- [Пересборка держит весь журнал в памяти](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет - [Пересборка держит весь журнал в памяти](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет
@@ -29,7 +27,6 @@
- [Предел на размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним - [Предел на размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним
- [Пределы на размер сущности и потоковый расчёт формы](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе - [Пределы на размер сущности и потоковый расчёт формы](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе
- [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом - [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом
- [Read API: точки, выбор слоя, свёртка по сетке](items/read-api-points.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
- [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке - [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
- [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено - [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено
- [Сверка живой витрины с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит - [Сверка живой витрины с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит
+14 -5
View File
@@ -16,6 +16,13 @@
слоя, модель идентичности и формы точки проверены на живом потоке слоя, модель идентичности и формы точки проверены на живом потоке
([research/apple-health.md](../research/apple-health.md)). ([research/apple-health.md](../research/apple-health.md)).
**Разбор и хранилище закрыты 2026-08-04.** Ни одна секция живого потока не
числится неразобранной, категориальные значения несут стабильный код рядом с
переведённой строкой, а первая встреча незнакомой секции стала наблюдаемым
событием. То, что заканчиваться не умеет по природе — новые формы от источника
и ручные секции задним числом, — переехало в тему
[«Полнота разбора потока»](items/parsing-completeness.md).
Эти звенья целями не заведены: закрытая цель записи не оставляет, ей хватает Эти звенья целями не заведены: закрытая цель записи не оставляет, ей хватает
коммита и спеки. коммита и спеки.
@@ -26,14 +33,15 @@
нижний слой значит завысить втрое. Это звено уже закрыто. нижний слой значит завысить втрое. Это звено уже закрыто.
- **`healthlog import` — перед устареванием нижнего слоя.** Пока импорт - **`healthlog import` — перед устареванием нижнего слоя.** Пока импорт
экспорта не написан, помечать что-либо устаревшим не на основании чего. экспорта не написан, помечать что-либо устаревшим не на основании чего.
- **Read API — перед MCP.** Адаптер собственной логики не несёт, он переводит - **MCP — внутри чтения, а не отдельной целью.** Прежде было две цели, и
вызовы в те же обработчики; переводить пока нечего. разделяла их очередь: адаптер собственной логики не несёт, он переводит
вызовы в те же обработчики, и переводить было нечего. Очередь осталась —
порядком задач внутри цели, — а отдельная цель под адаптер описывала не
направление, а последний шаг того же направления.
## порядок ## порядок
- [[goal] Разбор и хранилище](items/parsing-and-storage.md) — Секции живого потока разобраны все, кроме словаря категориальных значений и пяти невиденных - [[goal] Чтение данных клиентами](items/read-api.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может
- [[goal] Read API](items/read-api.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
- [[goal] Самоописание](items/self-description.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке - [[goal] Самоописание](items/self-description.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
- [[goal] MCP](items/mcp.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
- [[goal] Импорт родного экспорта Apple](items/native-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут - [[goal] Импорт родного экспорта Apple](items/native-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
- [[goal] Устаревание нижнего слоя](items/lower-layer-cleanup.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен - [[goal] Устаревание нижнего слоя](items/lower-layer-cleanup.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
- [[goal] Наблюдаемость](items/observability.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах - [[goal] Наблюдаемость](items/observability.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах
@@ -43,3 +51,4 @@
- [[goal] Прочность слияния и идентичности](items/merge-robustness.md) — Правила слияния держатся на замерах, и каждый новый замер находит зависимость от порядка на проводе - [[goal] Прочность слияния и идентичности](items/merge-robustness.md) — Правила слияния держатся на замерах, и каждый новый замер находит зависимость от порядка на проводе
- [[goal] Журнал и пересборка](items/journal-and-rebuild.md) — Расхождение витрины с журналом сейчас молчит: reindex печатает оба отпечатка, а сравнивать их некому - [[goal] Журнал и пересборка](items/journal-and-rebuild.md) — Расхождение витрины с журналом сейчас молчит: reindex печатает оба отпечатка, а сравнивать их некому
- [[goal] Пределы и поведение под объёмом](items/limits-and-load.md) — Предела на одну сущность нет вовсе: 63 МиБ держат блокировку 5.019 с, соседние доставки уходят в failed - [[goal] Пределы и поведение под объёмом](items/limits-and-load.md) — Предела на одну сущность нет вовсе: 63 МиБ держат блокировку 5.019 с, соседние доставки уходят в failed
- [[goal] Полнота разбора потока](items/parsing-completeness.md) — Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы
+2
View File
@@ -8,3 +8,5 @@
- 2026-08-01 `bekap-dannyh` — Резервное копирование ./data. Причина: бекап обеспечивает готовый механизм на сервере пет-проектов — своего заводить не нужно, задача снимается деплоем. Была секция: высокий. - 2026-08-01 `bekap-dannyh` — Резервное копирование ./data. Причина: бекап обеспечивает готовый механизм на сервере пет-проектов — своего заводить не нужно, задача снимается деплоем. Была секция: высокий.
- 2026-08-01 `identichnost-epizodnyh-metrik` — Идентичность эпизодных метрик. Причина: решён измерением и prior art: ключ эпизода — метрика+слой+start+end (находка 47), вариант А; вернулся в scope razbor-metrik-v-obekty. Была секция: блокеры. - 2026-08-01 `identichnost-epizodnyh-metrik` — Идентичность эпизодных метрик. Причина: решён измерением и prior art: ключ эпизода — метрика+слой+start+end (находка 47), вариант А; вернулся в scope razbor-metrik-v-obekty. Была секция: блокеры.
- 2026-08-01 `edinicy-metriki-v-razreze` — Единицы метрики: часть координаты или свойство объекта. Причина: измерено: на 99 доставках единицы не менялись ни у одной из 30 метрик (находка 49 → 48); реализованное правило «сохранённое побеждает + WARN + счётчик» делает событие наблюдаемым. Была секция: блокеры. - 2026-08-01 `edinicy-metriki-v-razreze` — Единицы метрики: часть координаты или свойство объекта. Причина: измерено: на 99 доставках единицы не менялись ни у одной из 30 метрик (находка 49 → 48); реализованное правило «сохранённое побеждает + WARN + счётчик» делает событие наблюдаемым. Была секция: блокеры.
- 2026-08-04 `mcp` — [goal] MCP. Причина: поглощена целью read-api («Чтение данных клиентами»): MCP — не направление, а последний шаг того же направления; адаптер переводит вызовы в те же обработчики и собственной логики не несёт. Очередь «Read API перед MCP» стала порядком задач внутри цели. Задача mcp-server жива и перевешена на read-api. Была секция: порядок.
- 2026-08-04 `read-api-points` — Read API: точки, выбор слоя, свёртка по сетке. Причина: разложена на read-api-envelope-and-points (конверт, точки за период, форма провода, условный запрос), read-api-bucketing (свёртка по сетке, предел размера ответа, порог неполного ведра) и read-api-workouts-and-records (тренировки и записи наружу). Одним заходом не мерджилась: десяток критериев приёмки и три развилки в одном файле. Была секция: ядро.
+9 -2
View File
@@ -1,7 +1,14 @@
# Спринт # Спринт
**Цель:** [[goal] Разбор и хранилище](items/parsing-and-storage.md) · **Начат:** 2026-08-03 · **Спринт:** `2026-08-03` - **Цель:** [[goal] Чтение данных клиентами](items/read-api.md)
- **Начат:** 2026-08-04
- **Спринт:** `2026-08-04`
Урожай спринта поднимается `tasks.py list --tag sprint:2026-08-03` — это первая порция переоценки на сессии. Урожай спринта поднимается `tasks.py list --tag sprint:2026-08-04` — это первая порция переоценки на сессии.
## Набор ## Набор
- [Конверт ответа и точки за период](items/read-api-envelope-and-points.md) — Точки лежат в витрине и наружу не отдаются: ни один потребитель не может спросить «вес за год» иначе как sqlite3 на хосте
- [Свёртка по сетке и предел размера ответа](items/read-api-bucketing.md) — Враждебный запрос к каталогу стоил 693 мс и +153 МиБ кучи, а у точек множители те же и потолка нет ни у одного
- [Тренировки и записи наружу](items/read-api-workouts-and-records.md) — Тренировки с маршрутами и stateOfMind разобраны и лежат в витрине, а эндпоинтов нет — второй сценарий паспорта не закрыт
- [OpenAPI-спека и Swagger UI](items/openapi-swagger.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
- [MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
@@ -2,7 +2,7 @@
- **Секция:** ядро - **Секция:** ядро
- **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым - **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым
- **Теги:** goal:parsing-and-storage - **Теги:** goal:parsing-completeness
**Решение принято владельцем 2026-08-03: вариант (1) — хранить с NULL-меткой.** **Решение принято владельцем 2026-08-03: вариант (1) — хранить с NULL-меткой.**
`start_utc`/`ts_utc` становятся NULLABLE, содержимое (включая маршрут) хранится, `start_utc`/`ts_utc` становятся NULLABLE, содержимое (включая маршрут) хранится,
@@ -10,7 +10,7 @@
отвергнут при постановке: подстановка метки доставки — выдуманное измерение в отвергнут при постановке: подстановка метки доставки — выдуманное измерение в
колонке, по которой идёт выборка. колонке, по которой идёт выборка.
**Берётся после [Read API по точкам и сущностям](read-api-points.md).** Правило **Берётся после [тренировок и записей наружу](read-api-workouts-and-records.md).** Правило
чтения — что выборка «за период» делает со строками без метки — обязано чтения — что выборка «за период» делает со строками без метки — обязано
проектироваться вместе с читателем, иначе такие строки молча исчезнут из любого проектироваться вместе с читателем, иначе такие строки молча исчезнут из любого
ответа. Порядок тот же, что у [journal-order-on-ingest](journal-order-on-ingest.md) ответа. Порядок тот же, что у [journal-order-on-ingest](journal-order-on-ingest.md)
+23 -2
View File
@@ -2,7 +2,7 @@
- **Секция:** ядро - **Секция:** ядро
- **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем - **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
- **Теги:** goal:mcp - **Теги:** goal:read-api
Агент-медик — первый заказчик проекта. Ему нужна актуальная сводка, а не срез Агент-медик — первый заказчик проекта. Ему нужна актуальная сводка, а не срез
на дату последнего ручного экспорта. на дату последнего ручного экспорта.
@@ -21,4 +21,25 @@ MCP не даёт ничего, чего не даёт HTTP, и права об
Готово, когда агент подключается по URL и отвечает на «как я спал на прошлой Готово, когда агент подключается по URL и отвечает на «как я спал на прошлой
неделе» без промежуточного кода. неделе» без промежуточного кода.
Связано: `docs/architecture.md` → «MCP», план → шаг «MCP». ## Критерии приёмки
- живой агент подключается по URL и отвечает на «как я спал на прошлой неделе»
без промежуточного кода — оракул: подключение реального MCP-клиента к
поднятому сервису
- вызов инструмента и соответствующий HTTP-запрос дают одни и те же данные —
оракул: тест, сравнивающий выход инструмента с ответом маршрута на тех же
параметрах
- запрос без токена чтения отклоняется обоими транспортами одинаково — оракул:
тест на паре «MCP без токена / HTTP без токена»
- правило размера ответа действует и в MCP: слишком широкий запрос получает
названную сетку или ошибку со списком, а не обрезанный ответ — оракул: тест на
запросе за пределом
## Рамки
Схема не трогается, данные только читаются, сервис перезапускается. Собственной
логики адаптер не несёт — новое поведение здесь признак того, что оно должно
было появиться в маршруте чтения. Берётся последней в цели: переводить нечего,
пока обработчиков нет.
Связано: `docs/architecture.md` → «MCP».
-18
View File
@@ -1,18 +0,0 @@
# [goal] MCP
- **Секция:** порядок
- **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
- **Теги:** decomposed
Агент-медик — первый заказчик проекта — подключается к хранилищу.
Выведена из шага 7 плана. Идёт после Read API намеренно: адаптер собственной
логики не несёт, он переводит вызовы в те же обработчики, и переводить пока
нечего.
Завершена, когда агент читает данные через MCP тем же токеном чтения.
## Завершение
Агент читает данные через MCP тем же токеном чтения, и собственной логики
адаптер не несёт.
@@ -2,7 +2,7 @@
- **Секция:** ядро - **Секция:** ядро
- **Зачем:** Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит - **Зачем:** Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит
- **Теги:** goal:parsing-and-storage - **Теги:** goal:parsing-completeness
Окно досчёта не единое, и это измеренное различие, а не предположение. Окно досчёта не единое, и это измеренное различие, а не предположение.
Количественные метрики (пульс, шаги, энергия) человек руками не правит — они Количественные метрики (пульс, шаги, энергия) человек руками не правит — они
+1 -1
View File
@@ -19,4 +19,4 @@ Read API отдаёт ответ одним JSON. Для выборок нижн
последовательно или с возвратами. последовательно или с возвратами.
Связано: `docs/architecture.md` → «Свёртка и размер ответа», задача Связано: `docs/architecture.md` → «Свёртка и размер ответа», задача
`read-api-points`. `read-api-bucketing` (правило размера ответа проектируется там).
+24 -2
View File
@@ -18,8 +18,30 @@
он должен работать в локальной сети без интернета); он должен работать в локальной сети без интернета);
- проверка актуальности спеки в гейте: контракт разъезжается молча. - проверка актуальности спеки в гейте: контракт разъезжается молча.
**Решено владельцем 2026-08-04: спека пишется руками и она источник истины.**
Для API из шести ручек это честнее вывода из кода: контракт проектируется, а не
фотографируется с того, что вышло, — опечатка в имени поля иначе становится
частью спеки. Совпадает с тем, как в проекте уже устроен OpenSpec: спека
первична к коду. Плата названа: спека расходится с кодом молча, и именно поэтому
проверка её актуальности идёт в гейт третьим шагом, а не остаётся регламентом.
Готово, когда по спеке можно сгенерировать клиент, а Swagger UI открывается Готово, когда по спеке можно сгенерировать клиент, а Swagger UI открывается
локально и выполняет запрос к живому сервису. локально и выполняет запрос к живому сервису.
Развилка на решение: спека пишется руками как источник истины или выводится из ## Критерии приёмки
кода. Для маленького API рукописная спека честнее — но это стоит обсудить.
- по спеке генерируется клиент, и сгенерированный клиент выполняет запрос к
живому сервису — оракул: прогон генератора плюс запрос сгенерированным
клиентом
- Swagger UI открывается и выполняет запрос **без внешней сети** — оракул:
запуск с отключённым интернетом
- маршрут, разошедшийся со спекой, красит гейт — оракул: намеренно
рассогласованный маршрут в прогоне гейта
- спека покрывает приём, каталог, точки, тренировки, записи и `/stats`
оракул: сверка перечня путей спеки с таблицей маршрутов в `architecture.md`
## Рамки
Схема не трогается, данные только читаются, сервис перезапускается. Берётся
после того, как маршруты чтения существуют: спека рукописная, но описывать
нечего, пока конверт не задан.
-20
View File
@@ -1,20 +0,0 @@
# [goal] Разбор и хранилище
- **Секция:** порядок
- **Зачем:** Секции живого потока разобраны все, кроме словаря категориальных значений и пяти невиденных
- **Теги:** decomposed
Метрики, тренировки и записи со своими `id` разбираются и ложатся в часовые
объекты; тела перестали быть недифференцированной кучей.
Выведена из шага 3 плана. Сделано: разбор метрик в объекты, тренировки и
записи, `reindex`. Осталось: словарь категориальных значений и секции, которых
поток ещё не приносил.
Завершена, когда ни одна секция живого потока не числится неразобранной, а
категориальные значения имеют стабильный код рядом с переведённой строкой.
## Завершение
Ни одна секция живого потока не числится неразобранной, а категориальные
значения несут стабильный код рядом с переведённой строкой.
+22
View File
@@ -0,0 +1,22 @@
# [goal] Полнота разбора потока
- **Секция:** темы
- **Зачем:** Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы
Тема: всё, что приезжает от источника, разобрано и доехало до витрины — не
только сегодня, но и после того, как источник изменится.
Выделена из цели «Разбор и хранилище», когда та достигла своего критерия
завершения: секции живого потока разобраны, категориальные значения несут
стабильный код. Осталось то, что заканчиваться не умеет по природе — источник
вправе прислать форму, которой раньше не было, а часть секций заводится
человеком задним числом.
В порядок не встаёт: работа приходит от потока, а не от плана. Первая встреча
новой секции наблюдаема (`healthlog uncovered` и `WARN` на свёртке) — тема
кормится этими событиями.
## Завершение
Завершена не бывает — это тема. Закрывается по мере того, как каждая приезжающая
форма доезжает до витрины, а не теряется между «принято» и «разобрано».
+59
View File
@@ -0,0 +1,59 @@
# Свёртка по сетке и предел размера ответа
- **Секция:** ядро
- **Зачем:** Враждебный запрос к каталогу стоил 693 мс и +153 МиБ кучи, а у точек множители те же и потолка нет ни у одного
- **Теги:** goal:read-api
Потребитель просит разбивку (`?from&to&bucket`) и получает свёртку по сетке —
либо честную ошибку вместо тихо подменённой сетки, — а сервис получает
названный предел на то, сколько он готов отдать за один запрос.
Вторая из трёх частей `read-api-points`. Берётся после
[конверта ответа](read-api-envelope-and-points.md): сетка — параметр того же
маршрута.
**Решение по размеру ответа принято, вариант «б».** Разбивка не задана и ответ
не влезает — сервер сам берёт сетку погрубее и **называет её в ответе**;
разбивка задана явно и не влезает — ошибка со списком доступных сеток, а не
тихая подмена. Различие существенно: иначе агент, попросивший минутную сетку,
получит суточные суммы и не узнает об этом.
**Порог неполного ведра решается здесь, и вместе с ним — его полярность.**
Измерению рода агрегации порог не понадобился, свёртке в ответе он нужен, а
готовые решения задают его **противоположно**: Graphite `xFilesFactor` — доля
обязательно известных точек (умолчание 0.5 при роллапе и 0 при рендере, один
параметр с двумя умолчаниями), RRDtool `xff` — доля допустимо неизвестных. Обе
величины выглядят как «0.5», означая разное; полярность придётся назвать вслух в
`architecture.md`, иначе через полгода два места кода поймут поле по-разному.
**Цена измерена, и она унаследована.** На каталоге враждебный запрос
(20 метрик × 8 часов × 5000 точек) дал 693 мс и +153 МиБ живой кучи, при том что
приём в том же процессе уже даёт пик 768 МиБ на теле 40 МиБ. Условный запрос
снял повтор, но первый запрос стоит столько же, а множители «метрики × окно ×
точки × одновременные запросы» по-прежнему без потолка. Сюда же уезжают
отложенные варианты задачи «цена читающего маршрута»: собственный дедлайн
маршрута и потоковое измерение по метрике — второе только если счётчик
заговорит.
Инвариант, который здесь легче всего нарушить: **нижний слой HAE не
суммируется** ни при какой сетке — это интерполяция, а не сэмплы.
## Критерии приёмки
- «шаги за неделю по дням» отвечаются одним запросом, и в ответе названа
фактическая сетка — оракул: запрос к поднятому сервису на живом архиве
- явно заданная сетка, которая не влезает в предел, даёт ошибку со списком
доступных сеток, а не подменённый ответ — оракул: тест
- неполное ведро обрабатывается объявленным порогом, и полярность порога
названа в `docs/architecture.md` вслух — оракул: тест на границе плюс глазами
по разделу
- нижний слой HAE не суммируется ни при какой сетке — оракул: тест на
накопительной метрике, у которой есть и нижний, и часовой слой
## Рамки
Схема не трогается, данные только читаются, сервис перезапускается. Берётся
после конверта ответа. Против `./data` — только `task up` / `task run`.
Связано: `docs/architecture.md` → «Свёртка и размер ответа»; идея
[NDJSON-потока](ndjson-stream.md) закрывает другую сторону того же предела.
@@ -0,0 +1,56 @@
# Конверт ответа и точки за период
- **Секция:** ядро
- **Зачем:** Точки лежат в витрине и наружу не отдаются: ни один потребитель не может спросить «вес за год» иначе как sqlite3 на хосте
- **Теги:** goal:read-api
Потребитель получает значения метрики за период одним запросом
`?from&to`, и из ответа видно, что именно ему отдали: слой, род свёртки и
границу окна, в котором род измерен.
Первая из трёх частей, на которые разложена `read-api-points`. Здесь решается
**конверт** — форма, которую унаследуют все остальные маршруты чтения и оба
транспорта.
**Форма провода решается здесь, один раз.** Сегодня типы `internal/catalog`
сами несут json-теги, а транспорт владеет только обёрткой: переименование поля
в домене меняет публичный контракт без касания `httpapi`, и держит это один
байтовый тест непустого ответа. Либо объявить в `architecture.md`, что типы
чтения и есть форма провода для всех транспортов (HTTP и MCP отдают её байт в
байт), либо завести DTO в транспорте. Выбрать надо **до** того, как образец
скопируют соседние задачи.
**Машинерия условного запроса готова — её берут, а не пишут заново.**
`store.VersionedRead` держит правило «версией, снятой после чтения, не
подписывать»; `httpapi` умеет `If-None-Match` и `304`. Новое здесь одно: метка
обязана нести **область действия** — у точек ответ есть функция параметров
запроса, поэтому `etag(scope, version)` требует их канонизированной формы, иначе
`304` ответит на другой набор данных. Детали — `docs/architecture.md`,
«Условный запрос».
**Клиент обязан видеть границы окна измерения.** Род метрики измерен по 48 самым
свежим **общим** часам, а не по последним 48 часам календаря: выключенная
минутная автоматизация HAE останавливает пополнение множества общих часов, и
окно замирает, продолжая объявлять род. Единственный след — `last_hour` в
ответе; конверт обязан его нести.
## Критерии приёмки
- «вес за год» отвечается одним запросом без доступа к файлу базы — оракул:
запрос к поднятому сервису на живом архиве
- в ответе всегда видны `layer`, `aggregation` и `last_hour` — оракул: тест на
форме ответа
- повторный запрос с `If-None-Match` даёт `304`, а тот же запрос с другими
параметрами — `200` с другим телом — оракул: тест на паре запросов с разной
областью действия метки
- форма провода объявлена в `docs/architecture.md` — доменные типы или DTO, —
и соседние маршруты ссылаются на это решение, а не повторяют выбор — оракул:
глазами по разделу
## Рамки
Схема не трогается, данные только читаются, сервис перезапускается. Против
`./data` — только `task up` / `task run`.
Связано: `docs/architecture.md` → «Read API», «Условный запрос», «Измерение рода
агрегации».
-78
View File
@@ -1,78 +0,0 @@
# 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».
@@ -0,0 +1,46 @@
# Тренировки и записи наружу
- **Секция:** ядро
- **Зачем:** Тренировки с маршрутами и stateOfMind разобраны и лежат в витрине, а эндпоинтов нет — второй сценарий паспорта не закрыт
- **Теги:** goal:read-api
Трекер тренировок забирает тренировку одним пакетом вместе с маршрутом, а
записи `stateOfMind` достаются за период — без доступа к файлу базы.
Третья из трёх частей `read-api-points`. Разбор и хранение сущностей с
собственным `id` сделаны (change `2026-08-02-trenirovki-i-zapisi`), эндпоинтов
нет: данные лежат в витрине и наружу не отдаются. Второй сценарий паспорта до
тех пор не закрыт.
Маршруты: `GET /workouts`, `GET /workouts/{id}`, `GET /records/{kind}`.
**Конверт не изобретается заново** — наследуется от
[конверта ответа](read-api-envelope-and-points.md) вместе с формой провода и
условным запросом. Вводить эти маршруты раньше конверта значило бы задать
контракт мимоходом; именно поэтому они здесь, а не в задаче о разборе.
**Маршрут — первый по-настоящему тяжёлый ответ проекта.** Он лежит блобом внутри
тренировки, и правило размера ответа
([свёртка по сетке](read-api-bucketing.md)) обязано распространяться и на него —
иначе одна тренировка с длинным треком проходит мимо предела, названного для
точек. Разворачивание маршрута в отдельную таблицу остаётся идеей и этой задачей
не решается.
## Критерии приёмки
- тренировка отдаётся одним пакетом вместе с маршрутом — оракул: запрос к
поднятому сервису на живом архиве
- записи `stateOfMind` за период достаются `GET /records/{kind}` — оракул: тест
на пакетах `internal/hae/testdata`
- конверт, форма провода и условный запрос те же, что у точек, а не собственные
— оракул: тест, сравнивающий форму ответа обоих маршрутов
- тренировка с длинным маршрутом подчиняется тому же пределу размера ответа,
что и точки — оракул: тест на синтетическом треке за пределом
## Рамки
Схема не трогается, данные только читаются, сервис перезапускается. Берётся
после конверта ответа. Против `./data` — только `task up` / `task run`.
Связано: `docs/architecture.md` → «Read API»; идея
[разворачивания маршрутов в таблицу](workout-routes-table.md).
+17 -10
View File
@@ -1,19 +1,26 @@
# [goal] Read API # [goal] Чтение данных клиентами
- **Секция:** порядок - **Секция:** порядок
- **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может - **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может
- **Теги:** decomposed - **Теги:** decomposed
Потребители читают точки: выбор слоя, свёртка по сетке, предел размера ответа. Потребители читают данные: точки с выбором слоя и свёрткой по сетке, тренировки
и записи, машиночитаемый контракт — и всё то же самое через MCP.
Выведена из шага 5 плана. Идёт после каталога и рода агрегации намеренно: без Выведена из шагов 5 и 7 плана. Идёт после каталога и рода агрегации намеренно:
измеренного рода свёртка в ответе неотличима от угадывания, а ошибиться здесь без измеренного рода свёртка в ответе неотличима от угадывания, а ошибиться
дорого — просуммировать нижний слой значит завысить втрое. здесь дорого — просуммировать нижний слой значит завысить втрое.
Завершена, когда любой из трёх потребителей получает точки за период без **MCP входит в эту цель, а не идёт отдельной.** Прежде их было две, и разделяла
доступа к файлу базы. их очередь: адаптер собственной логики не несёт, он переводит вызовы в те же
обработчики, и переводить было нечего. Очередь никуда не делась — она стала
порядком задач внутри цели, — а вот отдельная цель под адаптер описывала не
направление, а последний шаг этого же направления. Заказчик у обоих транспортов
один: три потребителя, из которых первый — агент.
## Завершение ## Завершение
Любой из трёх потребителей получает точки за период без доступа к файлу базы, Любой из трёх потребителей получает точки, тренировки и записи за период без
и предел размера ответа объявлен, а не подразумевается. доступа к файлу базы; предел размера ответа объявлен, а не подразумевается;
агент-медик читает то же самое через MCP тем же токеном чтения, и собственной
логики адаптер не несёт.