From 79331ac67062124f7117aca77cc6f32193d37aa5 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Tue, 4 Aug 2026 14:01:38 +0300 Subject: [PATCH] =?UTF-8?q?tasks:=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=20=D0=B8=20=D1=85=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=BB=D0=B8=D1=89=D0=B5,=20=D0=BD=D0=B0?= =?UTF-8?q?=D1=87=D0=B0=D1=82=20=D1=81=D0=BF=D1=80=D0=B8=D0=BD=D1=82=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8E=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - цель parsing-and-storage закрыта по своему критерию; незакрываемый остаток (новые формы от источника, ручные секции задним числом) переехал в тему parsing-completeness - цель mcp поглощена целью read-api, переименованной в «Чтение данных клиентами»: адаптер — последний шаг того же направления, а не своё - read-api-points разложена на конверт с точками, свёртку по сетке и тренировки с записями; спринт 2026-08-04 набран пятью задачами --- docs/tasks/BACKLOG.md | 3 - docs/tasks/PLAN.md | 19 +++-- docs/tasks/REJECTED.md | 2 + docs/tasks/SPRINT.md | 11 ++- .../items/entity-without-parsed-label.md | 4 +- docs/tasks/items/mcp-server.md | 25 +++++- docs/tasks/items/mcp.md | 18 ----- .../items/monthly-manual-sections-pass.md | 2 +- docs/tasks/items/ndjson-stream.md | 2 +- docs/tasks/items/openapi-swagger.md | 26 ++++++- docs/tasks/items/parsing-and-storage.md | 20 ----- docs/tasks/items/parsing-completeness.md | 22 ++++++ docs/tasks/items/read-api-bucketing.md | 59 ++++++++++++++ .../items/read-api-envelope-and-points.md | 56 +++++++++++++ docs/tasks/items/read-api-points.md | 78 ------------------- .../items/read-api-workouts-and-records.md | 46 +++++++++++ docs/tasks/items/read-api.md | 27 ++++--- 17 files changed, 276 insertions(+), 144 deletions(-) delete mode 100644 docs/tasks/items/mcp.md delete mode 100644 docs/tasks/items/parsing-and-storage.md create mode 100644 docs/tasks/items/parsing-completeness.md create mode 100644 docs/tasks/items/read-api-bucketing.md create mode 100644 docs/tasks/items/read-api-envelope-and-points.md delete mode 100644 docs/tasks/items/read-api-points.md create mode 100644 docs/tasks/items/read-api-workouts-and-records.md diff --git a/docs/tasks/BACKLOG.md b/docs/tasks/BACKLOG.md index 8d24e59..ffd9bbd 100644 --- a/docs/tasks/BACKLOG.md +++ b/docs/tasks/BACKLOG.md @@ -16,10 +16,8 @@ - [Сущность с id, но неразобранной меткой](items/entity-without-parsed-label.md) — Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым - [Идентичность тренировок при импорте родного экспорта](items/workout-identity-on-import.md) — В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE - [Импорт родного экспорта Apple Health](items/apple-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут -- [MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем - [[idea] Месячный проход по ручным секциям](items/monthly-manual-sections-pass.md) — Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит - [[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 — следующая покрытая секция унаследует слепую зону - [[idea] Отказ от heartbeatSeries](items/drop-heartbeat-series.md) — 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе - [Пересборка держит весь журнал в памяти](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет @@ -29,7 +27,6 @@ - [Предел на размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним - [Пределы на размер сущности и потоковый расчёт формы](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе - [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом -- [Read API: точки, выбор слоя, свёртка по сетке](items/read-api-points.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может - [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке - [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено - [Сверка живой витрины с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит diff --git a/docs/tasks/PLAN.md b/docs/tasks/PLAN.md index 2b87bfb..34a4041 100644 --- a/docs/tasks/PLAN.md +++ b/docs/tasks/PLAN.md @@ -16,6 +16,13 @@ слоя, модель идентичности и формы точки проверены на живом потоке ([research/apple-health.md](../research/apple-health.md)). +**Разбор и хранилище закрыты 2026-08-04.** Ни одна секция живого потока не +числится неразобранной, категориальные значения несут стабильный код рядом с +переведённой строкой, а первая встреча незнакомой секции стала наблюдаемым +событием. То, что заканчиваться не умеет по природе — новые формы от источника +и ручные секции задним числом, — переехало в тему +[«Полнота разбора потока»](items/parsing-completeness.md). + Эти звенья целями не заведены: закрытая цель записи не оставляет, ей хватает коммита и спеки. @@ -26,14 +33,15 @@ нижний слой значит завысить втрое. Это звено уже закрыто. - **`healthlog import` — перед устареванием нижнего слоя.** Пока импорт экспорта не написан, помечать что-либо устаревшим не на основании чего. -- **Read API — перед MCP.** Адаптер собственной логики не несёт, он переводит - вызовы в те же обработчики; переводить пока нечего. +- **MCP — внутри чтения, а не отдельной целью.** Прежде было две цели, и + разделяла их очередь: адаптер собственной логики не несёт, он переводит + вызовы в те же обработчики, и переводить было нечего. Очередь осталась — + порядком задач внутри цели, — а отдельная цель под адаптер описывала не + направление, а последний шаг того же направления. ## порядок -- [[goal] Разбор и хранилище](items/parsing-and-storage.md) — Секции живого потока разобраны все, кроме словаря категориальных значений и пяти невиденных -- [[goal] Read API](items/read-api.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может +- [[goal] Чтение данных клиентами](items/read-api.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может - [[goal] Самоописание](items/self-description.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке -- [[goal] MCP](items/mcp.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем - [[goal] Импорт родного экспорта Apple](items/native-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут - [[goal] Устаревание нижнего слоя](items/lower-layer-cleanup.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен - [[goal] Наблюдаемость](items/observability.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах @@ -43,3 +51,4 @@ - [[goal] Прочность слияния и идентичности](items/merge-robustness.md) — Правила слияния держатся на замерах, и каждый новый замер находит зависимость от порядка на проводе - [[goal] Журнал и пересборка](items/journal-and-rebuild.md) — Расхождение витрины с журналом сейчас молчит: reindex печатает оба отпечатка, а сравнивать их некому - [[goal] Пределы и поведение под объёмом](items/limits-and-load.md) — Предела на одну сущность нет вовсе: 63 МиБ держат блокировку 5.019 с, соседние доставки уходят в failed +- [[goal] Полнота разбора потока](items/parsing-completeness.md) — Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы diff --git a/docs/tasks/REJECTED.md b/docs/tasks/REJECTED.md index 7255bda..0e3269f 100644 --- a/docs/tasks/REJECTED.md +++ b/docs/tasks/REJECTED.md @@ -8,3 +8,5 @@ - 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 `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 (тренировки и записи наружу). Одним заходом не мерджилась: десяток критериев приёмки и три развилки в одном файле. Была секция: ядро. diff --git a/docs/tasks/SPRINT.md b/docs/tasks/SPRINT.md index 9438c7f..f3dcd60 100644 --- a/docs/tasks/SPRINT.md +++ b/docs/tasks/SPRINT.md @@ -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) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем diff --git a/docs/tasks/items/entity-without-parsed-label.md b/docs/tasks/items/entity-without-parsed-label.md index 977944c..4a7095b 100644 --- a/docs/tasks/items/entity-without-parsed-label.md +++ b/docs/tasks/items/entity-without-parsed-label.md @@ -2,7 +2,7 @@ - **Секция:** ядро - **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым -- **Теги:** goal:parsing-and-storage +- **Теги:** goal:parsing-completeness **Решение принято владельцем 2026-08-03: вариант (1) — хранить с NULL-меткой.** `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) diff --git a/docs/tasks/items/mcp-server.md b/docs/tasks/items/mcp-server.md index e45aad5..d6c8e30 100644 --- a/docs/tasks/items/mcp-server.md +++ b/docs/tasks/items/mcp-server.md @@ -2,7 +2,7 @@ - **Секция:** ядро - **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем -- **Теги:** goal:mcp +- **Теги:** goal:read-api Агент-медик — первый заказчик проекта. Ему нужна актуальная сводка, а не срез на дату последнего ручного экспорта. @@ -21,4 +21,25 @@ MCP не даёт ничего, чего не даёт HTTP, и права об Готово, когда агент подключается по URL и отвечает на «как я спал на прошлой неделе» без промежуточного кода. -Связано: `docs/architecture.md` → «MCP», план → шаг «MCP». +## Критерии приёмки + +- живой агент подключается по URL и отвечает на «как я спал на прошлой неделе» + без промежуточного кода — оракул: подключение реального MCP-клиента к + поднятому сервису +- вызов инструмента и соответствующий HTTP-запрос дают одни и те же данные — + оракул: тест, сравнивающий выход инструмента с ответом маршрута на тех же + параметрах +- запрос без токена чтения отклоняется обоими транспортами одинаково — оракул: + тест на паре «MCP без токена / HTTP без токена» +- правило размера ответа действует и в MCP: слишком широкий запрос получает + названную сетку или ошибку со списком, а не обрезанный ответ — оракул: тест на + запросе за пределом + +## Рамки + +Схема не трогается, данные только читаются, сервис перезапускается. Собственной +логики адаптер не несёт — новое поведение здесь признак того, что оно должно +было появиться в маршруте чтения. Берётся последней в цели: переводить нечего, +пока обработчиков нет. + +Связано: `docs/architecture.md` → «MCP». diff --git a/docs/tasks/items/mcp.md b/docs/tasks/items/mcp.md deleted file mode 100644 index 257b694..0000000 --- a/docs/tasks/items/mcp.md +++ /dev/null @@ -1,18 +0,0 @@ -# [goal] MCP - -- **Секция:** порядок -- **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем -- **Теги:** decomposed - -Агент-медик — первый заказчик проекта — подключается к хранилищу. - -Выведена из шага 7 плана. Идёт после Read API намеренно: адаптер собственной -логики не несёт, он переводит вызовы в те же обработчики, и переводить пока -нечего. - -Завершена, когда агент читает данные через MCP тем же токеном чтения. - -## Завершение - -Агент читает данные через MCP тем же токеном чтения, и собственной логики -адаптер не несёт. diff --git a/docs/tasks/items/monthly-manual-sections-pass.md b/docs/tasks/items/monthly-manual-sections-pass.md index 9963fc1..6a29dc0 100644 --- a/docs/tasks/items/monthly-manual-sections-pass.md +++ b/docs/tasks/items/monthly-manual-sections-pass.md @@ -2,7 +2,7 @@ - **Секция:** ядро - **Зачем:** Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит -- **Теги:** goal:parsing-and-storage +- **Теги:** goal:parsing-completeness Окно досчёта не единое, и это измеренное различие, а не предположение. Количественные метрики (пульс, шаги, энергия) человек руками не правит — они diff --git a/docs/tasks/items/ndjson-stream.md b/docs/tasks/items/ndjson-stream.md index e4e2c77..cff77f7 100644 --- a/docs/tasks/items/ndjson-stream.md +++ b/docs/tasks/items/ndjson-stream.md @@ -19,4 +19,4 @@ Read API отдаёт ответ одним JSON. Для выборок нижн последовательно или с возвратами. Связано: `docs/architecture.md` → «Свёртка и размер ответа», задача -`read-api-points`. +`read-api-bucketing` (правило размера ответа проектируется там). diff --git a/docs/tasks/items/openapi-swagger.md b/docs/tasks/items/openapi-swagger.md index 9a4c57f..5e350f0 100644 --- a/docs/tasks/items/openapi-swagger.md +++ b/docs/tasks/items/openapi-swagger.md @@ -18,8 +18,30 @@ он должен работать в локальной сети без интернета); - проверка актуальности спеки в гейте: контракт разъезжается молча. +**Решено владельцем 2026-08-04: спека пишется руками и она источник истины.** +Для API из шести ручек это честнее вывода из кода: контракт проектируется, а не +фотографируется с того, что вышло, — опечатка в имени поля иначе становится +частью спеки. Совпадает с тем, как в проекте уже устроен OpenSpec: спека +первична к коду. Плата названа: спека расходится с кодом молча, и именно поэтому +проверка её актуальности идёт в гейт третьим шагом, а не остаётся регламентом. + Готово, когда по спеке можно сгенерировать клиент, а Swagger UI открывается локально и выполняет запрос к живому сервису. -Развилка на решение: спека пишется руками как источник истины или выводится из -кода. Для маленького API рукописная спека честнее — но это стоит обсудить. +## Критерии приёмки + +- по спеке генерируется клиент, и сгенерированный клиент выполняет запрос к + живому сервису — оракул: прогон генератора плюс запрос сгенерированным + клиентом +- Swagger UI открывается и выполняет запрос **без внешней сети** — оракул: + запуск с отключённым интернетом +- маршрут, разошедшийся со спекой, красит гейт — оракул: намеренно + рассогласованный маршрут в прогоне гейта +- спека покрывает приём, каталог, точки, тренировки, записи и `/stats` — + оракул: сверка перечня путей спеки с таблицей маршрутов в `architecture.md` + +## Рамки + +Схема не трогается, данные только читаются, сервис перезапускается. Берётся +после того, как маршруты чтения существуют: спека рукописная, но описывать +нечего, пока конверт не задан. diff --git a/docs/tasks/items/parsing-and-storage.md b/docs/tasks/items/parsing-and-storage.md deleted file mode 100644 index 8b2e37d..0000000 --- a/docs/tasks/items/parsing-and-storage.md +++ /dev/null @@ -1,20 +0,0 @@ -# [goal] Разбор и хранилище - -- **Секция:** порядок -- **Зачем:** Секции живого потока разобраны все, кроме словаря категориальных значений и пяти невиденных -- **Теги:** decomposed - -Метрики, тренировки и записи со своими `id` разбираются и ложатся в часовые -объекты; тела перестали быть недифференцированной кучей. - -Выведена из шага 3 плана. Сделано: разбор метрик в объекты, тренировки и -записи, `reindex`. Осталось: словарь категориальных значений и секции, которых -поток ещё не приносил. - -Завершена, когда ни одна секция живого потока не числится неразобранной, а -категориальные значения имеют стабильный код рядом с переведённой строкой. - -## Завершение - -Ни одна секция живого потока не числится неразобранной, а категориальные -значения несут стабильный код рядом с переведённой строкой. diff --git a/docs/tasks/items/parsing-completeness.md b/docs/tasks/items/parsing-completeness.md new file mode 100644 index 0000000..9b2ca12 --- /dev/null +++ b/docs/tasks/items/parsing-completeness.md @@ -0,0 +1,22 @@ +# [goal] Полнота разбора потока + +- **Секция:** темы +- **Зачем:** Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы + +Тема: всё, что приезжает от источника, разобрано и доехало до витрины — не +только сегодня, но и после того, как источник изменится. + +Выделена из цели «Разбор и хранилище», когда та достигла своего критерия +завершения: секции живого потока разобраны, категориальные значения несут +стабильный код. Осталось то, что заканчиваться не умеет по природе — источник +вправе прислать форму, которой раньше не было, а часть секций заводится +человеком задним числом. + +В порядок не встаёт: работа приходит от потока, а не от плана. Первая встреча +новой секции наблюдаема (`healthlog uncovered` и `WARN` на свёртке) — тема +кормится этими событиями. + +## Завершение + +Завершена не бывает — это тема. Закрывается по мере того, как каждая приезжающая +форма доезжает до витрины, а не теряется между «принято» и «разобрано». diff --git a/docs/tasks/items/read-api-bucketing.md b/docs/tasks/items/read-api-bucketing.md new file mode 100644 index 0000000..a74ab45 --- /dev/null +++ b/docs/tasks/items/read-api-bucketing.md @@ -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) закрывает другую сторону того же предела. diff --git a/docs/tasks/items/read-api-envelope-and-points.md b/docs/tasks/items/read-api-envelope-and-points.md new file mode 100644 index 0000000..bffa803 --- /dev/null +++ b/docs/tasks/items/read-api-envelope-and-points.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», «Условный запрос», «Измерение рода +агрегации». diff --git a/docs/tasks/items/read-api-points.md b/docs/tasks/items/read-api-points.md deleted file mode 100644 index 846f94a..0000000 --- a/docs/tasks/items/read-api-points.md +++ /dev/null @@ -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». diff --git a/docs/tasks/items/read-api-workouts-and-records.md b/docs/tasks/items/read-api-workouts-and-records.md new file mode 100644 index 0000000..cbc44b7 --- /dev/null +++ b/docs/tasks/items/read-api-workouts-and-records.md @@ -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). diff --git a/docs/tasks/items/read-api.md b/docs/tasks/items/read-api.md index 7eedbe9..44475fa 100644 --- a/docs/tasks/items/read-api.md +++ b/docs/tasks/items/read-api.md @@ -1,19 +1,26 @@ -# [goal] Read API +# [goal] Чтение данных клиентами - **Секция:** порядок -- **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может +- **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может - **Теги:** decomposed -Потребители читают точки: выбор слоя, свёртка по сетке, предел размера ответа. +Потребители читают данные: точки с выбором слоя и свёрткой по сетке, тренировки +и записи, машиночитаемый контракт — и всё то же самое через MCP. -Выведена из шага 5 плана. Идёт после каталога и рода агрегации намеренно: без -измеренного рода свёртка в ответе неотличима от угадывания, а ошибиться здесь -дорого — просуммировать нижний слой значит завысить втрое. +Выведена из шагов 5 и 7 плана. Идёт после каталога и рода агрегации намеренно: +без измеренного рода свёртка в ответе неотличима от угадывания, а ошибиться +здесь дорого — просуммировать нижний слой значит завысить втрое. -Завершена, когда любой из трёх потребителей получает точки за период без -доступа к файлу базы. +**MCP входит в эту цель, а не идёт отдельной.** Прежде их было две, и разделяла +их очередь: адаптер собственной логики не несёт, он переводит вызовы в те же +обработчики, и переводить было нечего. Очередь никуда не делась — она стала +порядком задач внутри цели, — а вот отдельная цель под адаптер описывала не +направление, а последний шаг этого же направления. Заказчик у обоих транспортов +один: три потребителя, из которых первый — агент. ## Завершение -Любой из трёх потребителей получает точки за период без доступа к файлу базы, -и предел размера ответа объявлен, а не подразумевается. +Любой из трёх потребителей получает точки, тренировки и записи за период без +доступа к файлу базы; предел размера ответа объявлен, а не подразумевается; +агент-медик читает то же самое через MCP тем же токеном чтения, и собственной +логики адаптер не несёт.