diff --git a/CLAUDE.md b/CLAUDE.md index 09f033a..efc1355 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ [docs/passport.md](docs/passport.md) (цель, сценарии, референсы), [README.md](README.md), [docs/architecture.md](docs/architecture.md), [docs/conventions/README.md](docs/conventions/README.md), -[docs/security.md](docs/security.md) и [docs/tasks/PLAN.md](docs/tasks/PLAN.md). +[docs/security.md](docs/security.md) и [docs/tasks/ROADMAP.md](docs/tasks/ROADMAP.md). Документация ведётся по канону `av-dev-pm` (версия в `docs/.pm.json`); раскладку проверяет `av-dev-pm:canon`, содержимое ведёт `av-dev-pm:docs`. diff --git a/README.md b/README.md index fc048d7..a8250ab 100644 --- a/README.md +++ b/README.md @@ -67,13 +67,17 @@ iPhone ──HTTPS POST──► healthlog ──► журнал доставо витрину и сверяет её отпечаток с накопленной — на живом архиве из 116 тел пересборка воспроизводима и повторный прогон ничего не меняет. -Первый маршрут чтения открыт: **каталог разрезов** (`GET /api/v1/metrics`) под +Маршрутов чтения открыто два. **Каталог разрезов** (`GET /api/v1/metrics`) под токеном чтения отдаёт слои с диапазонами и измеренный род агрегации, а повтор неизменившегося отвечает `304` по `ETag` — снимок витрины при этом не -открывается. Журнал WAL разбирается фоновым чекпойнтом по таймеру. +открывается. **Точки метрики за период** (`GET /api/v1/metrics/{name}`) едут +одним запросом: слой выбирается по охвату точек внутри периода, а род агрегации +приезжает вместе с данными и с явным указанием, применим ли он к ряду. Журнал +WAL разбирается фоновым чекпойнтом по таймеру. -Чего ещё нет: **read API точек**, тренировок и записей — сами данные наружу -пока не отдаются. План в [docs/tasks/PLAN.md](docs/tasks/PLAN.md). +Чего ещё нет: свёртки по сетке, условного запроса по точкам, тренировок и +записей наружу. Что умеет и чего не умеет — +[docs/tasks/ROADMAP.md](docs/tasks/ROADMAP.md). Разведка формата закончена: 50 находок на живом потоке, половина расходится с документацией Health Auto Export — [docs/research/apple-health.md](docs/research/apple-health.md). @@ -81,7 +85,7 @@ iPhone ──HTTPS POST──► healthlog ──► журнал доставо ## Команды ``` -healthlog serve приём + read API + MCP +healthlog serve приём + read API (MCP — в планах) healthlog import родной экспорт Apple Health (в планах) healthlog reindex пересборка витрины из журнала healthlog uncovered перечень секций, которых разбор не покрыл @@ -154,7 +158,8 @@ curl -i -H 'If-None-Match: W/"…"' localhost:8080/api/v1/metrics # 304 Если `auth.write_tokens` пуст, проверка токена выключена — для доверенной локальной сети этого достаточно, сервис пишет об этом `write auth disabled` -на старте. Для доступа снаружи понадобится и токен, и TLS — это шаг «Деплой». +на старте. Для доступа снаружи понадобится и токен, и TLS — это цель +«Сервис доступен телефону из любой сети». ## Документация @@ -170,7 +175,8 @@ curl -i -H 'If-None-Match: W/"…"' localhost:8080/api/v1/metrics # 304 - [docs/conventions/](docs/conventions/README.md) — как пишем код - [docs/security.md](docs/security.md) — периметр и модель угроз - [docs/review.md](docs/review.md) — настройка конвейера ревью и журнал дефектов -- [docs/tasks/PLAN.md](docs/tasks/PLAN.md) — цели и обоснование их порядка +- [docs/tasks/ROADMAP.md](docs/tasks/ROADMAP.md) — что приложение уже умеет и + чего ещё не умеет - [docs/tasks/BACKLOG.md](docs/tasks/BACKLOG.md) — что брать следующим, включая отложенные идеи - [docs/research/apple-health.md](docs/research/apple-health.md) — что показал реальный поток diff --git a/docs/.pm.json b/docs/.pm.json index 29317f1..5d8b431 100644 --- a/docs/.pm.json +++ b/docs/.pm.json @@ -1,4 +1,4 @@ { - "canon": 2, + "canon": 3, "migrations": "internal/store/migrations" } diff --git a/docs/passport.md b/docs/passport.md index 8433e6e..fb5eef7 100644 --- a/docs/passport.md +++ b/docs/passport.md @@ -1,8 +1,8 @@ # Паспорт проекта Куда мы идём, как выглядит работа проекта в жизни и у кого подсматривать, -когда упёрлись. Самый верхний документ: [tasks/PLAN.md](tasks/PLAN.md) отвечает «в каком -порядке», [architecture.md](architecture.md) — «как устроено», паспорт — +когда упёрлись. Самый верхний документ: [tasks/ROADMAP.md](tasks/ROADMAP.md) отвечает «что +приложение умеет», [architecture.md](architecture.md) — «как устроено», паспорт — **«зачем и для кого»**. ## Цель @@ -51,23 +51,25 @@ ## Типовые сценарии -Ситуации, ради которых всё написано. В скобках — шаги [tasks/PLAN.md](tasks/PLAN.md), -которыми сценарий закрывается; названы, а не пронумерованы, потому что план -живой и нумерация в нём поедет. +Ситуации, ради которых всё написано. В скобках — цели [tasks/ROADMAP.md](tasks/ROADMAP.md), +которыми сценарий закрывается: достигнутые названы слагом из «Готово», открытые — +заголовком цели. Названы, а не пронумерованы, потому что роадмап живой и +нумерация в нём сдвинется на первой же вставке. -**1. Молчаливый приём** (приём, разбор и хранилище). Телефон каждые 5 минут шлёт доставку; +**1. Молчаливый приём** (`ingest`, `parsing-and-storage` — сделаны). Телефон каждые 5 минут шлёт доставку; сервис кладёт тело в архив, отвечает `200`, разбирает метрики в часовые объекты. Никто ничего не спрашивает и не смотрит. *Успех:* сутки работы не порождают ни одной строки лога уровня `WARN` и ни одного действия человека. -**2. Дыра закрывается сама** (разбор и хранилище). Телефон был заблокирован ночью, +**2. Дыра закрывается сама** (`parsing-and-storage` — сделано). Телефон был заблокирован ночью, автоматизация не отработала, часть дня отсутствует. Средний проход (сутки) и глубокий (неделя) переприсылают окно целиком, точки доезжают. *Успех:* дыра моложе недели закрывается без вмешательства; никто о ней даже не узнаёт. -**3. Квартальный экспорт** (`healthlog import`, устаревание нижнего слоя). +**3. Квартальный экспорт** (История из родного экспорта Apple лежит в +хранилище; Нижний слой чистится после проверенного экспорта). Изредка владелец выгружает родной экспорт Apple Health и скармливает его `healthlog import`. Нижний слой за прошлое становится честным (настоящие сэмплы вместо посекундной развёртки @@ -75,32 +77,34 @@ HAE), а сырой архив получает право быть подчищ *Успех:* экспорт разобран, покрытие периода проверено, объём архива вернулся к норме, ничего не потеряно. -**4. Агент спрашивает про здоровье** (каталог и род агрегации, Read API, MCP). Агент-медик по MCP +**4. Агент спрашивает про здоровье** (`catalog` — сделан; Клиенты читают данные +через HTTP и MCP). Агент-медик по MCP спрашивает каталог («что у тебя вообще есть»), затем «шаги по дням за месяц» или «пульс за вчера». Получает свёрнутый ряд с честным указанием слоя, сетки и рода агрегации. *Успех:* ответ влезает в контекст агента, число не завышено вдвое, и агенту не пришлось знать про слои, чтобы спросить правильно. -**5. Приложение берёт тренировки** (Read API). Разборщик тренировок запрашивает +**5. Приложение берёт тренировки** (Клиенты читают данные через HTTP и MCP). Разборщик тренировок запрашивает заголовки за период, потом одну тренировку целиком — с маршрутом и рядом пульса. *Успех:* тренировка отдана одним пакетом в том виде, в каком её прислал Apple, без нашей интерпретации того, что в ней главное. -**6. Разбор поменялся** (`healthlog reindex`). Мы начали разбирать секцию, которую +**6. Разбор поменялся** (`reindex` — сделан). Мы начали разбирать секцию, которую раньше пропускали, или нашли ошибку в старом разборе. Запускается пересборка по сырому архиву: `import(экспорт) + replay(доставки по received_at)`. *Успех:* состояние пересобрано детерминированно, повтор даёт то же самое, доставки со снятым статусом `partial` подобраны. -**7. Владелец проверяет, жив ли поток** (наблюдаемость). Раз в сколько-то дней — +**7. Владелец проверяет, жив ли поток** (Приложение сообщает о своём состоянии). Раз в сколько-то дней — взгляд в `/stats`: когда была последняя доставка, сколько точек, есть ли тишина, какие строки не легли в словарь кодов. *Успех:* один экран отвечает «всё идёт» или «встало тогда-то», без залезания в SQLite. -**8. Приехало незнакомое** (разбор и хранилище). HAE обновился и прислал новую метрику, +**8. Приехало незнакомое** (`parsing-and-storage` — сделано; Новая форма от +источника не теряется молча). HAE обновился и прислал новую метрику, новую форму точки или новую секцию. Тело сохраняется, ответ — `200`, разбор честно помечает доставку `partial` и перечисляет непокрытое. *Успех:* данные в архиве и восстановимы, факт виден в логе и `/stats`, а diff --git a/docs/review.md b/docs/review.md index ef60501..1aeaf29 100644 --- a/docs/review.md +++ b/docs/review.md @@ -34,8 +34,10 @@ сам же меняет, без границы по `received_at` разбираемой доставки. - Правило выбора между версиями — функция множества версий либо явно функция порядка журнала; третьего состояния нет. -- Столкновение разрешается полнотой, а не свежестью; изменение запечатанного - часа пишется `WARN`, но данные пишутся. +- Столкновение разрешается полнотой, а при равной полноте — положением в + журнале: побеждает стоящая позже + ([ADR](adr/ADR-2026-08-04-tie-break-po-poryadku-zhurnala.md)). Изменение + запечатанного часа пишется `WARN`, но данные пишутся. - Транзакция не держит блокировку дольше `busy_timeout`: канонизация и сжатие — вне её. @@ -52,7 +54,8 @@ - Расход памяти не растёт вместе с длиной журнала. - Подмена базы — решение человека при остановленном сервисе, не команды. -**Обработчик чтения и адаптер MCP** (Read API, MCP — ещё не написаны) +**Обработчик чтения и адаптер MCP** (`internal/httpapi`: каталог и точки +написаны; свёртка по сетке, тренировки, записи и MCP — ещё нет) - Агрегат считается только там, где род свёртки измерен; нижний слой HAE не суммируется никогда. @@ -105,31 +108,41 @@ - `triage`: перечислены ли запущенные проходы поимённо и с исходом; непущенный проход идёт в границы покрытия строкой «не запускался» (запись 2026-08-02, чекпоинт кода прошёл без трёх проходов). +- `specs`: считается ли внешним поведением **состояние, которое даёт + пересборка** — витрина наблюдаема через пересборку, поэтому расхождение с + журналом не внутренняя деталь, а поведение, которого спека не заказывала. + Внешнее здесь — ещё и код ответа приёма, форма ответа чтения и содержимое + архива (переселено из триггеров профиля, канон 3). ### Триггеры профиля -Уточняет умолчания конвейера, не отменяет их. +Уточняет умолчания конвейера, не отменяет их. Рабочее умолчание — `standard`: +миграция схемы, публичный контракт и инвариант ступень **не** поднимают, их +проверяют проходы, которые в `standard` и так есть. -- **`deep`** — изменения в правиле разбора, идентичности, слияния или вывода - слоя; миграции схемы; всё, что трогает `internal/store`, `internal/fold`, - `internal/replay`. -- **«Поведение, видимое снаружи»** здесь — код ответа приёма, форма ответа - чтения, содержимое архива и **состояние, которое даёт пересборка**: витрина - наблюдаема через пересборку, поэтому расхождение с журналом — внешнее - поведение, а не внутренняя деталь. -- **`reimpl`** запускается по триггеру «новое правило слияния, идентичности или - разбора». Единственный раз, когда триаж назвал его отсутствие дырой - покрытия, — это была задача с новым правилом слияния сущностей. - Второй замер (2026-08-03, словарь категориальных значений): триггер сработал - на новом правиле разбора и ключе реестра, проход **окупился** — он независимо - подтвердил замером две находки, до того имевшие только одно измерение (пик - памяти накопителя: 1002 МиБ против 780 на базе; единицы счётчика отброшенных), - и отдельно назвал семь мест, где существующее решение оказалось **лучше** его - собственного. Второе ценно не меньше первого: оно показывает, где проход - соглашается, а не только где спорит. +- **Новое понятие или структурная единица** (`wide`) — новый пакет в + `internal/`, новый род узла из перечня выше, новый тип провода в + `internal/httpapi`, новая единица хранения, входящая в отпечаток, новый + транспорт рядом с HTTP. +- **Правила идентичности, слияния и разбора** (`deep`) живут в трёх местах: + `internal/hae` — разбор пакета и вывод слоя; `internal/fold` — выбор между + версиями точки; `internal/store` — координатный ключ и запись часового + объекта. Правку правила в любом из них ступень поднимает; перенос кода без + правки правила — нет. - **`quick`** — правка документов, конфигурации, сообщений; ничего, что меняет хранимое. +`reimpl` живёт за барьером `deep` и по тому же триггеру — новое правило слияния, +идентичности или разбора. Замеры окупаемости: единственный раз, когда триаж +назвал его отсутствие дырой покрытия, — задача с новым правилом слияния +сущностей. Второй замер (2026-08-03, словарь категориальных значений): триггер +сработал на новом правиле разбора и ключе реестра, проход **окупился** — он +независимо подтвердил замером две находки, до того имевшие только одно измерение +(пик памяти накопителя: 1002 МиБ против 780 на базе; единицы счётчика +отброшенных), и отдельно назвал семь мест, где существующее решение оказалось +**лучше** его собственного. Второе ценно не меньше первого: оно показывает, где +проход соглашается, а не только где спорит. + ### Недоступно проверке **Не проверит ни один проход.** Реальный профиль нагрузки: телефон шлёт молча и diff --git a/docs/tasks/BACKLOG.md b/docs/tasks/BACKLOG.md index 487878e..5433186 100644 --- a/docs/tasks/BACKLOG.md +++ b/docs/tasks/BACKLOG.md @@ -1,51 +1,53 @@ # Беклог Что **можно взять**. Одна задача = один файл `items/.md` -+ строка здесь. Целей тут нет — они в [PLAN.md](PLAN.md): беклог — то, что берут, -план — то, подо что берут. Порядка внутри секции нет: «что делать ++ строка здесь. Целей тут нет — они в [ROADMAP.md](ROADMAP.md): беклог — то, что +берут, роадмап — то, подо что берут. Порядка внутри секции нет: «что делать дальше» отвечает набор спринта. Ведётся скиллом `tasks`. Секции «блокеры» здесь нет и не заводится: блокер — это состояние (спринт не может продолжаться ни одной задачей), оно живёт до ответа человека, а его следы — вопросами в файлах задач. -## ядро +## Ядро + - [[idea] Человеческие аннотации поверх выведенных схем](items/schema-annotations.md) — Выведенная схема говорит форму, но не смысл метрики — нужно ли описание сверху, зависит от стабильности формата -- [Проверка целостности собранной витрины перед подменой](items/integrity-before-swap.md) — Подмена файла необратима, а годность выхода подтверждена отпечатком на ещё открытом дескрипторе -- [Цена слияния на широкой доставке](items/merge-cost-wide-delivery.md) — 63 МБ на одной координате держат транзакцию 5.15 с при busy_timeout 5 с — соседние доставки уходят в failed -- [Сущность с 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-архиве и никуда не едут +- [Проверять целостность собранной витрины до подмены](items/integrity-before-swap.md) — Подмена файла необратима, а годность выхода подтверждена отпечатком на ещё открытом дескрипторе +- [Снизить цену слияния на широкой доставке](items/merge-cost-wide-delivery.md) — 63 МБ на одной координате держат транзакцию 5.15 с при busy_timeout 5 с — соседние доставки уходят в failed +- [Не терять сущность с 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-архиве и никуда не едут - [[idea] Месячный проход по ручным секциям](items/monthly-manual-sections-pass.md) — Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит - [[idea] NDJSON-поток для больших выборок Read API](items/ndjson-stream.md) — Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация -- [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 ради данных, которых нет ни в одном планируемом запросе -- [Пересборка держит весь журнал в памяти](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет +- [Не держать весь журнал в памяти при пересборке](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет - [[idea] Пересекающиеся источники одной метрики](items/overlapping-sources.md) — Сон пишут и часы, и стороннее приложение — сумма по обоим задвоит ночь - [[idea] Порог sealed: с какого возраста час считается запечатанным](items/sealed-threshold.md) — WARN на изменение старого часа уже пишется, но порог не выбран — ставим по факту, когда накопится статистика досчёта -- [Порядок журнала при конкурентных приёмах](items/journal-order-on-ingest.md) — Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой -- [Предел на размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним -- [Пределы на размер сущности и потоковый расчёт формы](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе +- [Держать порядок журнала при конкурентных приёмах](items/journal-order-on-ingest.md) — Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой +- [Ограничить размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним +- [Ограничить размер сущности и считать форму потоково](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе - [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом -- [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке +- [Выводить схемы содержимого из данных](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке - [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено -- [Сверка живой витрины с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит -- [Устаревание нижнего слоя после экспорта](items/lower-layer-expiry.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен +- [Сверять живую витрину с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит +- [Помечать нижний слой устаревшим после экспорта](items/lower-layer-expiry.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен - [[idea] Выгрузка в parquet отдельной командой](items/parquet-export.md) — Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно -- [Заголовки доставки в архиве рядом с телом](items/delivery-headers-in-archive.md) — Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке -- [Правило полноты против last wins](items/last-wins-over-completeness.md) — Полнота решает 1,2% спорных координат, и неизвестно, была ли более полная точка более поздней — от этого зависит, нужна ли она вообще -- [MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем -- [Рукописная OpenAPI-спека](items/openapi-spec.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате -- [Гейт против расхождения спеки с маршрутами](items/openapi-gate-check.md) — Рукописная спека — источник истины, а расходится она с кодом молча: без гейта решение о рукописной спеке не держится -- [Swagger UI без внешней сети](items/swagger-ui.md) — Контракт читается машиной, но человеку нечем ткнуть в живой сервис, а внешних CDN в локальной сети нет +- [Класть заголовки доставки в архив рядом с телом](items/delivery-headers-in-archive.md) — Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке +- [Измерить, нужно ли правило полноты рядом с LWW](items/last-wins-over-completeness.md) — Полнота решает 1,2% спорных координат, и неизвестно, была ли более полная точка более поздней — от этого зависит, нужна ли она вообще +- [Поднять MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем +- [Написать OpenAPI-спеку руками](items/openapi-spec.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате +- [Ловить гейтом расхождение спеки с маршрутами](items/openapi-gate-check.md) — Рукописная спека — источник истины, а расходится она с кодом молча: без гейта решение о рукописной спеке не держится +- [Поднять Swagger UI без внешней сети](items/swagger-ui.md) — Контракт читается машиной, но человеку нечем выполнить запрос к живому сервису из браузера, а внешних CDN в локальной сети нет -## инфра -- [Активный алерт «данных нет N часов»](items/stream-silence-alert.md) — Пропажу потока сейчас замечает человек, а не сервис -- [Деплой на rivendell](items/deploy-rivendell.md) — Сервис живёт на рабочей машине — телефон достаёт до него только дома -- [Счётчики слияния переживают ротацию логов](items/merge-counters-in-db.md) — единственный след несравнимых наборов — строка WARN в docker-логе с ротацией 3×10 МБ: событие может произойти и не оставить ничего -- [Остановка и миграция: раздельные бюджеты и следы в логе](items/shutdown-and-migration-traces.md) — Долгий запрос чтения съедает бюджет остановки, и WARN обвиняет воркер свёртки; миграция молчит и не прерывается SIGTERM -- [Чем откатывать релиз после наката миграции](items/release-rollback-after-migration.md) — Страж версии схемы делает возврат старого бинаря отказом, а понизить схему нечем — аварийный путь пришлось бы изобретать в аварии -- [Ретеншен сырого архива](items/raw-archive-retention.md) — Архив не подчищается вовсе — 14-дневный срок объявлен, но не работает -- [Наблюдаемость: /stats](items/stats-endpoint.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах -- [Умолчания конфига указывают на прежнюю раскладку](items/config-defaults-data-dir.md) — Запуск без конфига заведёт пустую базу в корне рядом с настоящей — тихая ловушка -- [Управление токенами и секретами](items/token-and-secret-management.md) — Проверка токенов выключена, config.docker.toml коммитится — так нельзя выезжать наружу +## Инфра + +- [Слать уведомление, когда данных нет N часов](items/stream-silence-alert.md) — Пропажу потока сейчас замечает человек, а не сервис +- [Выложить сервис на rivendell](items/deploy-rivendell.md) — Сервис живёт на рабочей машине — телефон достаёт до него только дома +- [Хранить счётчики слияния вне логов](items/merge-counters-in-db.md) — Единственный след несравнимых наборов — строка WARN в docker-логе с ротацией 3×10 МБ: событие может произойти и не оставить ничего +- [Развести бюджеты остановки и оставить следы миграции в логе](items/shutdown-and-migration-traces.md) — Долгий запрос чтения съедает бюджет остановки, и WARN обвиняет воркер свёртки; миграция молчит и не прерывается SIGTERM +- [Назвать механизм отката релиза после наката миграции](items/release-rollback-after-migration.md) — Страж версии схемы делает возврат старого бинаря отказом, а понизить схему нечем — аварийный путь пришлось бы изобретать в аварии +- [Подчищать сырой архив до последнего проверенного экспорта](items/raw-archive-retention.md) — Архив не подчищается вовсе, а резать его раньше даты проверенного экспорта нельзя — в журнале останется дыра, которую нечем пересобрать +- [Отдавать состояние сервиса маршрутом /stats](items/stats-endpoint.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах +- [Свести умолчания конфига с рабочей раскладкой данных](items/config-defaults-data-dir.md) — Запуск без конфига заведёт пустую базу в корне рядом с настоящей — тихая ловушка +- [Развести токены контуров и убрать секреты из репозитория](items/token-and-secret-management.md) — Проверка токенов выключена, config.docker.toml коммитится — так нельзя выезжать наружу diff --git a/docs/tasks/PLAN.md b/docs/tasks/PLAN.md deleted file mode 100644 index 34a4041..0000000 --- a/docs/tasks/PLAN.md +++ /dev/null @@ -1,54 +0,0 @@ -# План - -Оглавление целей. Цель — файл `[goal]` в `items/`; её задачи -здесь **не перечисляются** — перечень даёт `tasks.py list --goal <слаг>`. -В первой секции («порядок») очередь значима и обосновывается -прозой; в остальных порядка нет — это тематические цели. - -## Что уже пройдено - -Каркас и приём без разбора закрыты. Метрики, тренировки и записи со своими `id` -разбираются и ложатся в часовые объекты. `reindex` проигрывает журнал в свежую -витрину, отпечатки сравниваются, повторный прогон ничего не меняет. Род -агрегации **измерен**: сверка минутного слоя с часовым разложила метрики живого -корпуса на накопительные и мгновенные, не сойдясь ни на одной, и каталог -разрезов отдаётся первым маршрутом чтения. Разведка закончена — правило вывода -слоя, модель идентичности и формы точки проверены на живом потоке -([research/apple-health.md](../research/apple-health.md)). - -**Разбор и хранилище закрыты 2026-08-04.** Ни одна секция живого потока не -числится неразобранной, категориальные значения несут стабильный код рядом с -переведённой строкой, а первая встреча незнакомой секции стала наблюдаемым -событием. То, что заканчиваться не умеет по природе — новые формы от источника -и ручные секции задним числом, — переехало в тему -[«Полнота разбора потока»](items/parsing-completeness.md). - -Эти звенья целями не заведены: закрытая цель записи не оставляет, ей хватает -коммита и спеки. - -## Почему в таком порядке - -- **Каталог и род агрегации — перед Read API.** Без измеренного рода свёртка в - ответе неотличима от угадывания, а ошибиться здесь дорого: просуммировать - нижний слой значит завысить втрое. Это звено уже закрыто. -- **`healthlog import` — перед устареванием нижнего слоя.** Пока импорт - экспорта не написан, помечать что-либо устаревшим не на основании чего. -- **MCP — внутри чтения, а не отдельной целью.** Прежде было две цели, и - разделяла их очередь: адаптер собственной логики не несёт, он переводит - вызовы в те же обработчики, и переводить было нечего. Очередь осталась — - порядком задач внутри цели, — а отдельная цель под адаптер описывала не - направление, а последний шаг того же направления. - -## порядок -- [[goal] Чтение данных клиентами](items/read-api.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может -- [[goal] Самоописание](items/self-description.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке -- [[goal] Импорт родного экспорта Apple](items/native-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут -- [[goal] Устаревание нижнего слоя](items/lower-layer-cleanup.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен -- [[goal] Наблюдаемость](items/observability.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах -- [[goal] Деплой](items/deploy.md) — Сервис живёт на рабочей машине — телефон достаёт до него только дома, и оба контура открыты - -## темы -- [[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/ROADMAP.md b/docs/tasks/ROADMAP.md new file mode 100644 index 0000000..7b4cc7c --- /dev/null +++ b/docs/tasks/ROADMAP.md @@ -0,0 +1,51 @@ +# Роадмап + +Что приложение уже умеет и чего ещё не умеет. Цель — возможность приложения, +файл `[goal]` в `items/`; её задачи здесь **не перечисляются** — перечень даёт +`tasks.py list --goal <слаг>`. Очередь значима только в «Запланировано» и +обосновывается прозой рядом. + +## Готово + +- 2026-08-01 `ingest` — Сервис принимает доставки HAE и кладёт тела в архив. + Приём отвечает `200` до разбора, свёртку ведёт фоновый воркер: код ответа + отражает доставку, а не её понимание. +- 2026-08-02 `reindex` — `healthlog reindex` проигрывает журнал в свежую витрину + и печатает оба отпечатка. Повторный прогон ничего не меняет. +- 2026-08-02 `catalog` — Клиент видит перечень разрезов с измеренным родом + агрегации. Сверка минутного слоя с часовым разложила метрики живого корпуса на + накопительные и мгновенные, не сойдясь ни на одной. +- 2026-08-04 `parsing-and-storage` — Метрики, тренировки и записи со своими `id` + разобраны и лежат в часовых объектах. Ни одна секция живого потока не числится + неразобранной, категориальные значения несут стабильный код рядом с + переведённой строкой, первая встреча незнакомой секции наблюдаема. + +Разведка формата закончена там же и записана в +[research/apple-health.md](../research/apple-health.md): правило вывода слоя, +модель идентичности и формы точки проверены на живом потоке. Возможностью +приложения она не была, поэтому строки среди достигнутых целей не занимает. + +## Запланировано + +Очередь держится на двух зависимостях. **`healthlog import` идёт перед чисткой +нижнего слоя:** пока импорт экспорта не написан, помечать что-либо устаревшим не +на основании чего. **MCP входит в чтение, а не идёт отдельной целью:** адаптер +собственной логики не несёт, он переводит вызовы в те же обработчики, и очередь +осталась порядком задач внутри цели. + +- [[goal] Клиенты читают данные через HTTP и MCP](items/read-api.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может +- [[goal] Клиент узнаёт форму данных из ответа](items/self-description.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке +- [[goal] История из родного экспорта Apple лежит в хранилище](items/native-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут +- [[goal] Нижний слой чистится после проверенного экспорта](items/lower-layer-cleanup.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен +- [[goal] Приложение сообщает о своём состоянии](items/observability.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах + +## Направления + +- [[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 поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы + +## Разработка + +- [[goal] Сервис доступен телефону из любой сети](items/deploy.md) — Сервис живёт на рабочей машине — телефон достаёт до него только дома, и оба контура открыты diff --git a/docs/tasks/SPRINT.md b/docs/tasks/SPRINT.md index 5402b2b..d0c3d34 100644 --- a/docs/tasks/SPRINT.md +++ b/docs/tasks/SPRINT.md @@ -1,15 +1,16 @@ # Спринт -- **Цель:** [[goal] Чтение данных клиентами](items/read-api.md) +- **Цель:** [[goal] Клиенты читают данные через HTTP и MCP](items/read-api.md) - **Начат:** 2026-08-04 - **Спринт:** `2026-08-04` -Урожай спринта поднимается `tasks.py list --tag sprint:2026-08-04` — это первая порция переоценки на сессии. +Урожай спринта перечисляет `tasks.py list --tag sprint:2026-08-04`; в наборе — первая порция задач, переоценённых в этой сессии. ## Набор -- [Условный запрос по точкам](items/read-api-points-conditional.md) — Агент опрашивает по расписанию, а каждый повтор стоит полного чтения: на каталоге это 693 мс и +153 МиБ -- [Свёртка по сетке](items/read-api-points-bucket.md) — «Шаги за неделю по дням» — базовый запрос трекера и игры, и сегодня его нечем задать -- [Порог неполного ведра](items/read-api-partial-bucket.md) — Текущий час неполон всегда, и без порога свёртка отдаёт его наравне с полными — клиент видит провал вместо неизвестности -- [Предел размера ответа](items/read-api-response-limit.md) — У маршрутов чтения нет ни одного потолка: множители «метрики × окно × точки × одновременные запросы» ничем не ограничены -- [Тренировки наружу](items/read-api-workouts.md) — Тренировки с маршрутами разобраны и лежат в витрине, а эндпоинтов нет — сценарий трекера не закрыт -- [Записи наружу](items/read-api-records.md) — stateOfMind разобран и хранится, но наружу не отдаётся — а восстановить его нечем: в экспорте Apple его нет + +- [Отвечать 304 на повторный запрос точек](items/read-api-points-conditional.md) — Агент опрашивает по расписанию, а каждый повтор стоит полного чтения: на каталоге это 693 мс и +153 МиБ +- [Сворачивать точки по заданной сетке](items/read-api-points-bucket.md) — «Шаги за неделю по дням» — базовый запрос трекера и игры, и сегодня его нечем задать +- [Отличать неполное ведро от полного](items/read-api-partial-bucket.md) — Текущий час неполон всегда, и без порога свёртка отдаёт его наравне с полными — клиент видит провал вместо неизвестности +- [Ограничить размер ответа маршрутов чтения](items/read-api-response-limit.md) — У маршрутов чтения нет ни одного потолка: множители «метрики × окно × точки × одновременные запросы» ничем не ограничены +- [Отдавать тренировки вместе с маршрутом](items/read-api-workouts.md) — Тренировки с маршрутами разобраны и лежат в витрине, а маршрутов чтения нет — сценарий трекера не закрыт +- [Отдавать записи со своим id за период](items/read-api-records.md) — stateOfMind разобран и хранится, но наружу не отдаётся — а восстановить его нечем: в экспорте Apple его нет diff --git a/docs/tasks/items/apple-export-import.md b/docs/tasks/items/apple-export-import.md index 7dcda8b..317c0b8 100644 --- a/docs/tasks/items/apple-export-import.md +++ b/docs/tasks/items/apple-export-import.md @@ -1,6 +1,6 @@ -# Импорт родного экспорта Apple Health +# Импортировать родной экспорт Apple Health -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут - **Теги:** goal:native-export-import @@ -37,6 +37,8 @@ должен ничего менять; - `export_cda.xml` игнорируем — это клинический формат тех же данных. +Двигает строку «Завершения» цели: «Слой `sample` наполнен историей с 2019 года, повторный импорт того же экспорта ничего не меняет». + ## Импорт выставляет пометку покрытия Импорт — единственный, кто знает, какой период каким слоем обеспечен, поэтому diff --git a/docs/tasks/items/config-defaults-data-dir.md b/docs/tasks/items/config-defaults-data-dir.md index 48b9e4e..65f3525 100644 --- a/docs/tasks/items/config-defaults-data-dir.md +++ b/docs/tasks/items/config-defaults-data-dir.md @@ -1,6 +1,6 @@ -# Умолчания конфига указывают на прежнюю раскладку +# Свести умолчания конфига с рабочей раскладкой данных -- **Секция:** инфра +- **Секция:** Инфра - **Зачем:** Запуск без конфига заведёт пустую базу в корне рядом с настоящей — тихая ловушка - **Теги:** goal:deploy @@ -16,3 +16,5 @@ Готово, когда запуск без конфига использует `./data` и не создаёт ничего в корне репозитория. Тогда же снимается предупреждение из `config.example.toml`. + +Двигает строку «Завершения» цели: «Запуск без конфига не заводит базу мимо `./data`». diff --git a/docs/tasks/items/data-migration-row-selection.md b/docs/tasks/items/data-migration-row-selection.md index 4b06629..d54a7e3 100644 --- a/docs/tasks/items/data-migration-row-selection.md +++ b/docs/tasks/items/data-migration-row-selection.md @@ -1,12 +1,14 @@ -# Data-миграции не отбирают строки по обрезаемым спискам +# Не отбирать строки в data-миграциях по обрезаемым спискам -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону - **Теги:** goal:journal-and-rebuild Остаток задачи «Дозакрыть находки ревью по слиянию сущностей» (архивный change `dozakryt-nahodki-sushchnostej`). +Двигает строку «Завершения» цели: «Data-миграции не наследуют слепые зоны обрезаемых списков». + ## Оракул: механизм доказан, дефект пока пустой Миграция `00007` переводит в `pending` доставки, у которых имя ставшей покрытой diff --git a/docs/tasks/items/day-boundary-timezone.md b/docs/tasks/items/day-boundary-timezone.md index 9b6a06e..360db55 100644 --- a/docs/tasks/items/day-boundary-timezone.md +++ b/docs/tasks/items/day-boundary-timezone.md @@ -1,6 +1,6 @@ # [idea] Что считать сутками при смене часового пояса -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено - **Теги:** goal:read-api diff --git a/docs/tasks/items/delivery-header-limits.md b/docs/tasks/items/delivery-header-limits.md index bbbe999..9bc04d3 100644 --- a/docs/tasks/items/delivery-header-limits.md +++ b/docs/tasks/items/delivery-header-limits.md @@ -1,6 +1,6 @@ -# Предел на размер и число заголовков доставки +# Ограничить размер и число заголовков доставки -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним - **Теги:** goal:limits-and-load @@ -27,3 +27,5 @@ не оставляя следа в базе, а обычная доставка проходит как раньше. Связано: `internal/httpapi`, `internal/ingest`, `docs/architecture.md` → «Приём». + +Двигает строку «Завершения» цели: «У заголовков доставки есть названный предел». diff --git a/docs/tasks/items/delivery-headers-in-archive.md b/docs/tasks/items/delivery-headers-in-archive.md index 121aa21..48c1d96 100644 --- a/docs/tasks/items/delivery-headers-in-archive.md +++ b/docs/tasks/items/delivery-headers-in-archive.md @@ -1,6 +1,6 @@ -# Заголовки доставки в архиве рядом с телом +# Класть заголовки доставки в архив рядом с телом -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке - **Теги:** goal:journal-and-rebuild @@ -32,7 +32,7 @@ Prior art прямой: **WARC** (формат веб-архивов) храни операциям, но появляется третья сущность. Цена ошибки высокая: правится **путь приёма**, а доставка, не попавшая в архив, -теряется навсегда. Значит профиль ревью — `deep`, и менять надо так, чтобы +теряется навсегда. Значит менять надо так, чтобы старые тела без заголовков продолжали читаться. Готово, когда пересборка на архиве, у которого рабочей базы нет вовсе, даёт то @@ -40,3 +40,5 @@ Prior art прямой: **WARC** (формат веб-архивов) храни Связано: `docs/architecture.md` → «Сырой архив и восстановление состояния», `internal/replay`. + +Двигает строку «Завершения» цели: «Пересборка восстановима без базы: заголовки доставки лежат в архиве рядом с телом». diff --git a/docs/tasks/items/deploy-rivendell.md b/docs/tasks/items/deploy-rivendell.md index e128108..adfe970 100644 --- a/docs/tasks/items/deploy-rivendell.md +++ b/docs/tasks/items/deploy-rivendell.md @@ -1,6 +1,6 @@ -# Деплой на rivendell +# Выложить сервис на rivendell -- **Секция:** инфра +- **Секция:** Инфра - **Зачем:** Сервис живёт на рабочей машине — телефон достаёт до него только дома - **Теги:** goal:deploy @@ -30,3 +30,5 @@ Том стоит смонтировать так, чтобы серверный бекап забирал его без отдельной настройки, и снимать копию SQLite через `VACUUM INTO`, а не `cp`: телефон шлёт непрерывно, и файл под записью копировать нельзя. + +Двигает строку «Завершения» цели: «Телефон шлёт на публичный адрес из любой сети, агент читает по тому же домену». diff --git a/docs/tasks/items/deploy.md b/docs/tasks/items/deploy.md index 27e602d..09cf702 100644 --- a/docs/tasks/items/deploy.md +++ b/docs/tasks/items/deploy.md @@ -1,17 +1,17 @@ -# [goal] Деплой +# [goal] Сервис доступен телефону из любой сети -- **Секция:** порядок +- **Секция:** Разработка - **Зачем:** Сервис живёт на рабочей машине — телефон достаёт до него только дома, и оба контура открыты - **Теги:** decomposed Сервис переезжает на rivendell и становится доступен телефону из любой сети. -Выведена из шага 11 плана. - -Завершена, когда оба контура закрыты разными токенами, откат релиза имеет -названный механизм, а запуск без конфига не заводит базу мимо данных. - ## Завершение -Оба контура закрыты разными токенами, откат релиза имеет названный механизм, -а запуск без конфига не заводит базу мимо данных. +- Телефон шлёт на публичный адрес из любой сети, агент читает по тому же домену +- Оба контура закрыты разными токенами, и без токенов сервис стартует только на + localhost +- Откат релиза после наката миграции имеет названный механизм +- Запуск без конфига не заводит базу мимо `./data` +- Остановка сервиса называет виновный этап честно, а накат миграций виден в логе + старта diff --git a/docs/tasks/items/derived-content-schemas.md b/docs/tasks/items/derived-content-schemas.md index 544da0d..6eea5aa 100644 --- a/docs/tasks/items/derived-content-schemas.md +++ b/docs/tasks/items/derived-content-schemas.md @@ -1,6 +1,6 @@ -# Выведенные из данных схемы содержимого +# Выводить схемы содержимого из данных -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке - **Теги:** goal:self-description @@ -25,3 +25,5 @@ Связано: `docs/architecture.md` → «Самоописание». Форму конверта API описывает не эта задача, а OpenAPI. + +Двигает строку «Завершения» цели: «Формы содержимого метрик выведены из данных, а не описаны руками». diff --git a/docs/tasks/items/drop-heartbeat-series.md b/docs/tasks/items/drop-heartbeat-series.md index 36b08b5..cc2b593 100644 --- a/docs/tasks/items/drop-heartbeat-series.md +++ b/docs/tasks/items/drop-heartbeat-series.md @@ -1,6 +1,6 @@ # [idea] Отказ от heartbeatSeries -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе - **Теги:** goal:lower-layer-cleanup diff --git a/docs/tasks/items/entity-size-limits.md b/docs/tasks/items/entity-size-limits.md index 78afcca..4824a7b 100644 --- a/docs/tasks/items/entity-size-limits.md +++ b/docs/tasks/items/entity-size-limits.md @@ -1,6 +1,6 @@ -# Пределы на размер сущности и потоковый расчёт формы +# Ограничить размер сущности и считать форму потоково -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе - **Теги:** goal:limits-and-load @@ -10,6 +10,8 @@ структурное: **предела на размер одной сущности нет вовсе**, а форма и хеш считаются материализацией значения целиком. +Двигает строку «Завершения» цели: «У тела, сущности и секции доставки есть названный предел». + ## Оракул: измерено Оракулы жили в `tmp/adv/mem_test.go` и `tmp/adv/lock_test.go`; числа снимались diff --git a/docs/tasks/items/entity-without-parsed-label.md b/docs/tasks/items/entity-without-parsed-label.md index 2a4284d..b1318c7 100644 --- a/docs/tasks/items/entity-without-parsed-label.md +++ b/docs/tasks/items/entity-without-parsed-label.md @@ -1,6 +1,6 @@ -# Сущность с id, но неразобранной меткой +# Не терять сущность с id и неразобранной меткой -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым - **Теги:** goal:parsing-completeness @@ -22,6 +22,8 @@ разбор кладёт сущность в `ts_utc`/`start_utc`, колонки `NOT NULL`, и сущность с неразбираемой меткой по-прежнему пропускается целиком. +Двигает строку «Завершения» цели: «Сущность с `id` и неразобранной меткой не пропадает целиком». + ## Что известно - Оракул: `internal/hae/entity_test.go`, случаи «метка в ином формате», «метка diff --git a/docs/tasks/items/integrity-before-swap.md b/docs/tasks/items/integrity-before-swap.md index 97a3a00..bf72cc3 100644 --- a/docs/tasks/items/integrity-before-swap.md +++ b/docs/tasks/items/integrity-before-swap.md @@ -1,6 +1,6 @@ -# Проверка целостности собранной витрины перед подменой +# Проверять целостность собранной витрины до подмены -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Подмена файла необратима, а годность выхода подтверждена отпечатком на ещё открытом дескрипторе - **Теги:** goal:journal-and-rebuild @@ -27,3 +27,5 @@ называть результат годным, а на здоровом — не замедляется заметно. Связано: `cmd/healthlog/reindex.go`, `docs/architecture.md` → «Пересборка». + +Двигает строку «Завершения» цели: «Годность собранной витрины подтверждена до подмены файла». diff --git a/docs/tasks/items/journal-and-rebuild.md b/docs/tasks/items/journal-and-rebuild.md index 6edd7e3..4922324 100644 --- a/docs/tasks/items/journal-and-rebuild.md +++ b/docs/tasks/items/journal-and-rebuild.md @@ -1,20 +1,26 @@ -# [goal] Журнал и пересборка +# [goal] Расхождение витрины с журналом не молчит -- **Секция:** темы +- **Секция:** Направления - **Зачем:** Расхождение витрины с журналом сейчас молчит: reindex печатает оба отпечатка, а сравнивать их некому - **Теги:** decomposed -Тема: инвариант «`import` + `replay` даёт то же состояние» и всё, что его +Направление: инвариант «`import` + `replay` даёт то же состояние» и всё, что его держит — архив, ретеншен, отпечаток витрины, расход памяти пересборки. -В порядок не встаёт: работа приходит находками и растёт вместе с +В «Запланировано» не встаёт: работа приходит находками и растёт вместе с журналом. -Завершена не бывает: закрывается по мере того, как расхождение витрины с -журналом перестаёт быть молчащим. - ## Завершение -Завершена не бывает — это тема. Закрывается по мере того, как расхождение витрины -с журналом перестаёт быть молчащим, а расход пересборки — расти вместе с -журналом. +Завершена не бывает — это направление. Закрывается по мере того, как расхождение +витрины с журналом перестаёт быть молчащим, а расход пересборки — расти вместе с +журналом. Открыто сегодня: + +- Расхождение живой витрины с пересборкой замечает сервис, а не человек +- Годность собранной витрины подтверждена до подмены файла +- Порядок журнала держится при конкурентных приёмах +- Пересборка восстановима без базы: заголовки доставки лежат в архиве рядом с + телом +- Сырой архив подчищается до последнего проверенного экспорта +- Расход пересборки не растёт вместе с журналом +- Data-миграции не наследуют слепые зоны обрезаемых списков diff --git a/docs/tasks/items/journal-order-on-ingest.md b/docs/tasks/items/journal-order-on-ingest.md index a66057f..3f29e4a 100644 --- a/docs/tasks/items/journal-order-on-ingest.md +++ b/docs/tasks/items/journal-order-on-ingest.md @@ -1,6 +1,6 @@ -# Порядок журнала при конкурентных приёмах +# Держать порядок журнала при конкурентных приёмах -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой - **Теги:** goal:journal-and-rebuild, question @@ -13,6 +13,8 @@ Вынут ревью кода задачи «Разнести ответ приёма и свёртку доставки» (профиль `deep`, враждебный проход, находка с построенным путём и прогоном). +Двигает строку «Завершения» цели: «Порядок журнала держится при конкурентных приёмах». + ## Вопросы **Решение (в) порядок журнала не восстанавливает, а цена окна выросла.** diff --git a/docs/tasks/items/last-wins-over-completeness.md b/docs/tasks/items/last-wins-over-completeness.md index 60f7913..764db16 100644 --- a/docs/tasks/items/last-wins-over-completeness.md +++ b/docs/tasks/items/last-wins-over-completeness.md @@ -1,6 +1,6 @@ -# Правило полноты против last wins +# Измерить, нужно ли правило полноты рядом с LWW -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Полнота решает 1,2% спорных координат, и неизвестно, была ли более полная точка более поздней — от этого зависит, нужна ли она вообще - **Теги:** goal:merge-robustness, sprint:2026-08-03 @@ -8,17 +8,21 @@ зависит ровно там, где замер показал, что без этого теряются данные. Сегодня неизвестно, какой из двух случаев верен. +Двигает строку «Завершения» цели: «Правило выбора между версиями измерено: полнота либо нужна, либо снята». + ## Откуда задача -Владелец предложил 2026-08-04 держаться стратегии **last wins**: экспорт Apple +Владелец предложил 2026-08-04 держаться стратегии **LWW** («выигрывает +последняя»): экспорт Apple Health — база снапшота, новые доставки HAE затирают предыдущие. Это отменяет `critical`-инвариант `CLAUDE.md` «при столкновении выигрывает более полная точка, а не последняя», и потому меняется не молча, а этой задачей. -Соседняя задача [tie-break-equal-completeness](tie-break-equal-completeness.md) -двигает то же правило в ту же сторону, но осторожнее: она меняет только -тай-брейк при **равной** полноте, оставляя саму полноту первичной. Эта задача -решает, надо ли снимать и её. +Соседняя задача «Тай-брейк при равной полноте» двигала то же правило в ту же +сторону, но осторожнее: она поменяла только тай-брейк при **равной** полноте, +оставив саму полноту первичной. Она сделана 2026-08-04 — решение записано в +[ADR о тай-брейке по порядку журнала](../../adr/ADR-2026-08-04-tie-break-po-poryadku-zhurnala.md). +Эта задача решает, надо ли снимать и саму полноту. ## Замер — первый шаг, и от него ветвится всё остальное @@ -32,11 +36,11 @@ Health — база снапшота, новые доставки HAE затир Вопрос ровно один: **в этих 1 022 случаях более полная точка была более поздней или более ранней?** -- **Всегда более поздней** — полнота ничего не решает сверх порядка, `last wins` +- **Всегда более поздней** — полнота ничего не решает сверх порядка, LWW строго проще и ничего не теряет. Ветка полноты удаляется, инвариант в `CLAUDE.md` переписывается. - **Иногда более ранней** — значит HAE присылает обеднённые версии задним - числом, и `last wins` будет молча стирать поля. Тогда полнота остаётся, а + числом, и LWW будет молча стирать поля. Тогда полнота остаётся, а граница её применения записывается числом: сколько таких случаев, у каких метрик, какие поля пропадали. @@ -77,9 +81,8 @@ Health — база снапшота, новые доставки HAE затир Схему не трогаем. Отпечаток витрины изменится — пересборка обязательна и делается человеком при остановленном сервисе; подмена файла базы необратима и в -задаче не выполняется. Берётся **после** -[tie-break-equal-completeness](tie-break-equal-completeness.md): та меняет то же -место, и замер до её вливания ответит на вопрос про уже неактуальное правило. +задаче не выполняется. Тай-брейк при равной полноте уже влит, поэтому замер +отвечает про действующее правило, а не про снятое. Связано: находки 10, 47, 49, 53; `docs/architecture.md` → «Разрешение столкновений»; `docs/review.md`, запись 2026-08-04. diff --git a/docs/tasks/items/limits-and-load.md b/docs/tasks/items/limits-and-load.md index a1567e3..8036e2d 100644 --- a/docs/tasks/items/limits-and-load.md +++ b/docs/tasks/items/limits-and-load.md @@ -1,18 +1,19 @@ -# [goal] Пределы и поведение под объёмом +# [goal] У каждого входа есть названный предел -- **Секция:** темы +- **Секция:** Направления - **Зачем:** Предела на одну сущность нет вовсе: 63 МиБ держат блокировку 5.019 с, соседние доставки уходят в failed - **Теги:** decomposed -Тема: названные пределы на размер тела, сущности, заголовков и ответа плюс +Направление: названные пределы на размер тела, сущности, заголовков и ответа плюс поведение под удерживаемой блокировкой. -В порядок не встаёт: пределы всплывают замерами, а не планом. - -Завершена не бывает: закрывается по мере того, как каждый вход получает -названный предел вместо подразумеваемого. +В «Запланировано» не встаёт: предел находит замер, а не очередь. ## Завершение -Завершена не бывает — это тема. Закрывается по мере того, как каждый вход получает -названный предел вместо подразумеваемого. +Завершена не бывает — это направление. Закрывается по мере того, как каждый вход +получает названный предел вместо подразумеваемого. Открыто сегодня: + +- У тела, сущности и секции доставки есть названный предел +- У заголовков доставки есть названный предел +- Занятость базы не выводит доставку из очереди diff --git a/docs/tasks/items/lower-layer-cleanup.md b/docs/tasks/items/lower-layer-cleanup.md index 280bdc2..8b83781 100644 --- a/docs/tasks/items/lower-layer-cleanup.md +++ b/docs/tasks/items/lower-layer-cleanup.md @@ -1,18 +1,15 @@ -# [goal] Устаревание нижнего слоя +# [goal] Нижний слой чистится после проверенного экспорта -- **Секция:** порядок +- **Секция:** Запланировано - **Зачем:** Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен - **Теги:** decomposed -После проверенного экспорта нижний слой HAE избыточен и подлежит чистке. +После проверенного экспорта нижний слой HAE избыточен, и его можно чистить. -Выведена из шага 9 плана. Нижний слой растёт на ~100 тысяч координат в сутки. - -Завершена, когда чистка идёт по правилу, а не по календарю, и решение о -удалении опирается на колонку, отличающую ноль от «не измерялось». +Нижний слой растёт на ~100 тысяч координат в сутки. ## Завершение -Чистка идёт по правилу «до следующего проверенного экспорта», а не по -календарю, и решение об удалении опирается на колонку, отличающую ноль от -«не измерялось». +- Нижний слой помечен покрытым после проверенного экспорта +- Чистка идёт по правилу «до следующего проверенного экспорта», а не по календарю +- Решение об удалении опирается на колонку, отличающую ноль от «не измерялось» diff --git a/docs/tasks/items/lower-layer-expiry.md b/docs/tasks/items/lower-layer-expiry.md index bf439a6..e914454 100644 --- a/docs/tasks/items/lower-layer-expiry.md +++ b/docs/tasks/items/lower-layer-expiry.md @@ -1,6 +1,6 @@ -# Устаревание нижнего слоя после экспорта +# Помечать нижний слой устаревшим после экспорта -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен - **Теги:** goal:lower-layer-cleanup @@ -17,6 +17,8 @@ - пометка ≠ удаление. Удаление включается только после того, как восстановление из экспорта отработает на живых данных хотя бы раз. +Двигает строку «Завершения» цели: «Нижний слой помечен покрытым после проверенного экспорта». + ## Чем помечать: разряд на диапазон, а не провенанс на точку Решено при постановке 2026-08-04. Пометка — **одна строка на диапазон**: diff --git a/docs/tasks/items/mcp-server.md b/docs/tasks/items/mcp-server.md index 8afce09..d2ff93c 100644 --- a/docs/tasks/items/mcp-server.md +++ b/docs/tasks/items/mcp-server.md @@ -1,6 +1,6 @@ -# MCP-сервер поверх Read API +# Поднять MCP-сервер поверх Read API -- **Секция:** ядро — набор ограничен HTTP-слоем чтения после дробления; адаптер берётся следующим спринтом по той же цели +- **Секция:** Ядро — набор ограничен HTTP-слоем чтения после дробления; адаптер берётся следующим спринтом по той же цели - **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем - **Теги:** goal:read-api @@ -8,7 +8,7 @@ на дату последнего ручного экспорта. Транспорт — **Streamable HTTP**, не stdio: сервис живёт на VPS, агент ходит по -сети. Отсюда: MCP — эндпоинт того же процесса и того же порта, аутентификация — +сети. Отсюда: MCP — маршрут того же процесса и того же порта, аутентификация — тот же токен чтения, что у Read API. Отдельного контура доступа не заводим: MCP не даёт ничего, чего не даёт HTTP, и права обязаны совпадать. @@ -21,6 +21,8 @@ MCP не даёт ничего, чего не даёт HTTP, и права об Готово, когда агент подключается по URL и отвечает на «как я спал на прошлой неделе» без промежуточного кода. +Двигает строку «Завершения» цели: «Агент-медик читает то же самое через MCP тем же токеном чтения». + ## Критерии приёмки - живой агент подключается по URL и отвечает на «как я спал на прошлой неделе» diff --git a/docs/tasks/items/merge-cost-wide-delivery.md b/docs/tasks/items/merge-cost-wide-delivery.md index 688ce58..12be732 100644 --- a/docs/tasks/items/merge-cost-wide-delivery.md +++ b/docs/tasks/items/merge-cost-wide-delivery.md @@ -1,12 +1,14 @@ -# Цена слияния на широкой доставке +# Снизить цену слияния на широкой доставке -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** 63 МБ на одной координате держат транзакцию 5.15 с при busy_timeout 5 с — соседние доставки уходят в failed - **Теги:** goal:limits-and-load Вынуто ревью кода задачи `pravilo-sliyaniya-tochek` (профиль `deep`, враждебный проход и независимая реализация — независимо друг от друга). +Двигает строку «Завершения» цели: «Занятость базы не выводит доставку из очереди». + ## Оракул: измерено Тело 63 МБ (в запросе ~200 КБ gzip — предел приёма 64 МиБ), 119 точек на ОДНОЙ diff --git a/docs/tasks/items/merge-counters-in-db.md b/docs/tasks/items/merge-counters-in-db.md index 25be49f..c9a9ad2 100644 --- a/docs/tasks/items/merge-counters-in-db.md +++ b/docs/tasks/items/merge-counters-in-db.md @@ -1,12 +1,14 @@ -# Счётчики слияния переживают ротацию логов +# Хранить счётчики слияния вне логов -- **Секция:** инфра -- **Зачем:** единственный след несравнимых наборов — строка WARN в docker-логе с ротацией 3×10 МБ: событие может произойти и не оставить ничего +- **Секция:** Инфра +- **Зачем:** Единственный след несравнимых наборов — строка WARN в docker-логе с ротацией 3×10 МБ: событие может произойти и не оставить ничего - **Теги:** goal:observability Вынуто ревью кода задачи `pravilo-sliyaniya-tochek` (профиль `deep`, проход негативного пространства, подтверждено эксплуатационным). +Двигает строку «Завершения» цели: «Счётчики слияния переживают ротацию логов». + ## Что не так Решение не реализовывать объединение полей при несравнимых наборах стоит на diff --git a/docs/tasks/items/merge-robustness.md b/docs/tasks/items/merge-robustness.md index 4f1c1c8..18c812a 100644 --- a/docs/tasks/items/merge-robustness.md +++ b/docs/tasks/items/merge-robustness.md @@ -1,17 +1,18 @@ -# [goal] Прочность слияния и идентичности +# [goal] Исход слияния не зависит от порядка элементов на проводе -- **Секция:** темы +- **Секция:** Направления - **Зачем:** Правила слияния держатся на замерах, и каждый новый замер находит зависимость от порядка на проводе - **Теги:** decomposed -Тема: правила, по которым две версии одних данных превращаются в одну. -В порядок не встаёт — работа приходит находками ревью и замерами на +Направление: правила, по которым две версии одних данных превращаются в одну. +В «Запланировано» не встаёт — очереди у направления нет: работа приходит находками ревью и замерами на живом корпусе. -Завершена не бывает: закрывается по мере того, как правила перестают зависеть -от порядка на проводе. - ## Завершение -Завершена не бывает — это тема. Закрывается по мере того, как правила выбора между -версиями перестают зависеть от порядка элементов на проводе. +Завершена не бывает — это направление. Закрывается по мере того, как правила +выбора между версиями перестают зависеть от порядка элементов на проводе. +Открыто сегодня: + +- Правило выбора между версиями измерено: полнота либо нужна, либо снята +- Порог `sealed` выбран по накопленной статистике досчёта diff --git a/docs/tasks/items/monthly-manual-sections-pass.md b/docs/tasks/items/monthly-manual-sections-pass.md index 6a29dc0..5d248f2 100644 --- a/docs/tasks/items/monthly-manual-sections-pass.md +++ b/docs/tasks/items/monthly-manual-sections-pass.md @@ -1,6 +1,6 @@ # [idea] Месячный проход по ручным секциям -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит - **Теги:** goal:parsing-completeness diff --git a/docs/tasks/items/native-export-import.md b/docs/tasks/items/native-export-import.md index 9d03177..ab67648 100644 --- a/docs/tasks/items/native-export-import.md +++ b/docs/tasks/items/native-export-import.md @@ -1,19 +1,17 @@ -# [goal] Импорт родного экспорта Apple +# [goal] История из родного экспорта Apple лежит в хранилище -- **Секция:** порядок +- **Секция:** Запланировано - **Зачем:** Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут - **Теги:** decomposed `healthlog import`: снапшот всей истории из родного экспорта Apple Health ложится в хранилище перед проигрыванием хвоста доставок. -Выведена из шага 8 плана. Идёт перед устареванием нижнего слоя намеренно: пока +Идёт перед чисткой нижнего слоя намеренно: пока импорт экспорта не написан, помечать что-либо устаревшим не на основании чего. -Завершена, когда слой `sample` наполнен историей с 2019 года, а повторный -импорт того же экспорта ничего не меняет. - ## Завершение -Слой `sample` наполнен историей с 2019 года, повторный импорт того же экспорта -ничего не меняет, а тренировки из экспорта не задваивают приехавшие от HAE. +- Слой `sample` наполнен историей с 2019 года +- Повторный импорт того же экспорта ничего не меняет +- Тренировки из экспорта не задваивают приехавшие от HAE diff --git a/docs/tasks/items/ndjson-stream.md b/docs/tasks/items/ndjson-stream.md index 75485f0..2c9f51b 100644 --- a/docs/tasks/items/ndjson-stream.md +++ b/docs/tasks/items/ndjson-stream.md @@ -1,6 +1,6 @@ # [idea] NDJSON-поток для больших выборок Read API -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация - **Теги:** goal:read-api diff --git a/docs/tasks/items/observability.md b/docs/tasks/items/observability.md index 447846e..0d50ece 100644 --- a/docs/tasks/items/observability.md +++ b/docs/tasks/items/observability.md @@ -1,18 +1,15 @@ -# [goal] Наблюдаемость +# [goal] Приложение сообщает о своём состоянии -- **Секция:** порядок +- **Секция:** Запланировано - **Зачем:** Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах - **Теги:** decomposed Тихо сломавшаяся автоматизация — главный эксплуатационный риск: телефон шлёт молча, и молчание неотличимо от нормы. -Выведена из шага 10 плана. - -Завершена, когда пропажа потока и расхождение витрины с журналом видны -владельцу без чтения логов. - ## Завершение -Пропажа потока и расхождение витрины с журналом видны владельцу без чтения -логов и переживают ротацию логов. +- Пропажа потока видна владельцу без чтения логов +- Состояние сервиса — последняя доставка, счётчики, тишина — читается одним + запросом +- Счётчики слияния переживают ротацию логов diff --git a/docs/tasks/items/openapi-gate-check.md b/docs/tasks/items/openapi-gate-check.md index 53f0d58..49f1784 100644 --- a/docs/tasks/items/openapi-gate-check.md +++ b/docs/tasks/items/openapi-gate-check.md @@ -1,6 +1,6 @@ -# Гейт против расхождения спеки с маршрутами +# Ловить гейтом расхождение спеки с маршрутами -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Рукописная спека — источник истины, а расходится она с кодом молча: без гейта решение о рукописной спеке не держится - **Теги:** goal:read-api, sprint:2026-08-04 @@ -14,14 +14,18 @@ Класс отказа тот же, что у остальных безусловных шагов гейта проекта: не виден глазами и стоит дорого. Место ему там же. +Двигает строку «Завершения» цели: «Контракт чтения читается машиной». + ## Критерии приёмки - добавленный маршрут без правки спеки красит гейт — оракул: намеренно рассогласованный маршрут в прогоне гейта - переименованное поле ответа красит гейт — оракул: намеренное переименование в прогоне гейта -- проверка не ходит в сеть и укладывается в бюджет гейта — оракул: замер шага по - логу `tmp/gate/` +- проверка укладывается в бюджет гейта — оракул: замер шага по логу + `tmp/gate/` +- проверка работает без внешней сети — оракул: прогон гейта в контейнере без + доступа наружу ## Рамки diff --git a/docs/tasks/items/openapi-spec.md b/docs/tasks/items/openapi-spec.md index 81b6ca7..067e639 100644 --- a/docs/tasks/items/openapi-spec.md +++ b/docs/tasks/items/openapi-spec.md @@ -1,6 +1,6 @@ -# Рукописная OpenAPI-спека +# Написать OpenAPI-спеку руками -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате - **Теги:** goal:read-api, sprint:2026-08-04 @@ -18,13 +18,15 @@ Заодно снимает вопрос, чем быть «схеме контракта API» из раздела самоописания: ею и будет OpenAPI-документ, а не собственный формат. +Двигает строку «Завершения» цели: «Контракт чтения читается машиной». + ## Критерии приёмки - по спеке генерируется клиент, и он выполняет запрос к живому сервису — оракул: прогон генератора плюс запрос сгенерированным клиентом -- спека покрывает все существующие маршруты: приём, каталог, точки, тренировки, - записи — оракул: сверка перечня путей спеки с таблицей маршрутов в - `docs/architecture.md` +- спека покрывает все маршруты, которые сервис действительно регистрирует — + оракул: сверка перечня путей спеки с обходом роутера поднятого сервиса + (`chi.Walk`) - спека проходит валидатор OpenAPI 3.1 — оракул: прогон валидатора ## Рамки diff --git a/docs/tasks/items/overlapping-sources.md b/docs/tasks/items/overlapping-sources.md index 3891d3d..e90d0f3 100644 --- a/docs/tasks/items/overlapping-sources.md +++ b/docs/tasks/items/overlapping-sources.md @@ -1,6 +1,6 @@ # [idea] Пересекающиеся источники одной метрики -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Сон пишут и часы, и стороннее приложение — сумма по обоим задвоит ночь - **Теги:** goal:read-api diff --git a/docs/tasks/items/parquet-export.md b/docs/tasks/items/parquet-export.md index 6aed1e6..e6ed94a 100644 --- a/docs/tasks/items/parquet-export.md +++ b/docs/tasks/items/parquet-export.md @@ -1,6 +1,6 @@ # [idea] Выгрузка в parquet отдельной командой -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно - **Теги:** goal:read-api diff --git a/docs/tasks/items/parsing-completeness.md b/docs/tasks/items/parsing-completeness.md index 9b2ca12..5f3be36 100644 --- a/docs/tasks/items/parsing-completeness.md +++ b/docs/tasks/items/parsing-completeness.md @@ -1,9 +1,10 @@ -# [goal] Полнота разбора потока +# [goal] Новая форма от источника не теряется молча -- **Секция:** темы +- **Секция:** Направления - **Зачем:** Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы +- **Теги:** decomposed -Тема: всё, что приезжает от источника, разобрано и доехало до витрины — не +Направление: всё, что приезжает от источника, разобрано и доехало до витрины — не только сегодня, но и после того, как источник изменится. Выделена из цели «Разбор и хранилище», когда та достигла своего критерия @@ -12,11 +13,15 @@ вправе прислать форму, которой раньше не было, а часть секций заводится человеком задним числом. -В порядок не встаёт: работа приходит от потока, а не от плана. Первая встреча -новой секции наблюдаема (`healthlog uncovered` и `WARN` на свёртке) — тема -кормится этими событиями. +В «Запланировано» не встаёт: работа приходит от потока, а не от очереди. Первая встреча +новой секции наблюдаема (`healthlog uncovered` и `WARN` на свёртке) — работа +направления приходит от этих событий. ## Завершение -Завершена не бывает — это тема. Закрывается по мере того, как каждая приезжающая -форма доезжает до витрины, а не теряется между «принято» и «разобрано». +Завершена не бывает — это направление. Закрывается по мере того, как каждая +приезжающая форма доезжает до витрины, а не теряется между «принято» и +«разобрано». Открыто сегодня: + +- Сущность с `id` и неразобранной меткой не пропадает целиком +- Ручные секции, заведённые задним числом, доезжают до витрины diff --git a/docs/tasks/items/raw-archive-retention.md b/docs/tasks/items/raw-archive-retention.md index bbae8fa..a3707f7 100644 --- a/docs/tasks/items/raw-archive-retention.md +++ b/docs/tasks/items/raw-archive-retention.md @@ -1,7 +1,7 @@ -# Ретеншен сырого архива +# Подчищать сырой архив до последнего проверенного экспорта -- **Секция:** инфра -- **Зачем:** Архив не подчищается вовсе — 14-дневный срок объявлен, но не работает +- **Секция:** Инфра +- **Зачем:** Архив не подчищается вовсе, а резать его раньше даты проверенного экспорта нельзя — в журнале останется дыра, которую нечем пересобрать - **Теги:** goal:journal-and-rebuild Срок жизни сырого архива объявлен (14 дней, `storage.raw_retention`), но @@ -28,6 +28,8 @@ экспорта, записи `stateOfMind` не трогаются вовсе, а `/stats` показывает глубину архива и дату снапшота, до которой он подрезан. +Двигает строку «Завершения» цели: «Сырой архив подчищается до последнего проверенного экспорта». + ## Предусловие снова открыто Признак «доставка с непокрытой секцией» появился в change diff --git a/docs/tasks/items/read-api-partial-bucket.md b/docs/tasks/items/read-api-partial-bucket.md index a6672b2..0c7df6d 100644 --- a/docs/tasks/items/read-api-partial-bucket.md +++ b/docs/tasks/items/read-api-partial-bucket.md @@ -1,8 +1,8 @@ -# Порог неполного ведра +# Отличать неполное ведро от полного -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Текущий час неполон всегда, и без порога свёртка отдаёт его наравне с полными — клиент видит провал вместо неизвестности -- **Теги:** goal:read-api, sprint:2026-08-04 +- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature Ведро, в котором известна не вся сетка, отличимо от полного — а полярность порога названа вслух, а не выводится читателем из умолчания. @@ -13,11 +13,19 @@ показывает за него провал вместо неизвестности. **Готовые решения задают порог противоположно.** Graphite `xFilesFactor` — доля -обязательно известных точек (умолчание 0.5 при роллапе и 0 при рендере: один -параметр с двумя умолчаниями), RRDtool `xff` — доля допустимо неизвестных. Обе +обязательно известных точек (умолчание 0.5 при свёртке на записи и 0 при +отдаче ответа: один параметр с двумя умолчаниями), RRDtool `xff` — доля допустимо неизвестных. Обе величины выглядят как «0.5», означая разное. Полярность придётся назвать вслух, иначе через полгода два места кода поймут поле по-разному — и разойдутся молча. +Двигает строку «Завершения» цели: «Неполное ведро отличимо от полного, и полярность порога названа». + +## Затрагивает + +Форма ответа свёртки — признак неполного ведра рядом со значением. Конфиг и его +образцы — порог с названной полярностью. Раздел о свёртке в +`docs/architecture.md`. Схемы и формата на диске не трогает. + ## Критерии приёмки - полярность и умолчание порога названы в `docs/architecture.md` одной diff --git a/docs/tasks/items/read-api-points-bucket.md b/docs/tasks/items/read-api-points-bucket.md index aa7af83..11e2a34 100644 --- a/docs/tasks/items/read-api-points-bucket.md +++ b/docs/tasks/items/read-api-points-bucket.md @@ -1,8 +1,8 @@ -# Свёртка по сетке +# Сворачивать точки по заданной сетке -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** «Шаги за неделю по дням» — базовый запрос трекера и игры, и сегодня его нечем задать -- **Теги:** goal:read-api, sprint:2026-08-04 +- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature «Шаги за неделю по дням» отвечаются одним запросом `?from&to&bucket`, и род свёртки берётся измеренным, а не угаданным. @@ -17,7 +17,15 @@ завышает втрое. Порог неполного ведра и предел размера ответа — соседние задачи; здесь они -берутся в том виде, в каком есть на момент мерджа, и не проектируются. +берутся в том виде, в каком есть на момент вливания, и не проектируются. + +Двигает строку «Завершения» цели: «Точки сворачиваются по заданной сетке измеренным родом агрегации». + +## Затрагивает + +Маршрут `GET /api/v1/metrics/{name}` — параметр запроса `bucket`, поля +`bucket` и `aggregation` в конверте ответа, код отказа на метрике с неизвестным +родом. Схемы и формата на диске не трогает. ## Критерии приёмки diff --git a/docs/tasks/items/read-api-points-conditional.md b/docs/tasks/items/read-api-points-conditional.md index 900ca19..3c38872 100644 --- a/docs/tasks/items/read-api-points-conditional.md +++ b/docs/tasks/items/read-api-points-conditional.md @@ -1,8 +1,8 @@ -# Условный запрос по точкам +# Отвечать 304 на повторный запрос точек -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Агент опрашивает по расписанию, а каждый повтор стоит полного чтения: на каталоге это 693 мс и +153 МиБ -- **Теги:** goal:read-api, sprint:2026-08-04 +- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature Повторный опрос точек с той же меткой стоит `304` вместо полного чтения, и метка не может ответить на другой набор данных. @@ -19,6 +19,15 @@ враждебном запросе. Агент опрашивает по расписанию, и без условного запроса каждый его повтор стоит полного чтения. +Двигает строку «Завершения» цели: «Повторный запрос тех же точек стоит `304`, а не полного чтения». + +## Затрагивает + +Маршрут `GET /api/v1/metrics/{name}` — заголовки `ETag` и `If-None-Match`, +код ответа `304`. Область действия метки: набор параметров запроса (метрика, +окно, слой) и версия витрины, из которых метка считается, и их каноническая +форма. Схемы и формата на диске не трогает. + ## Критерии приёмки - повторный запрос с `If-None-Match` при неизменной витрине даёт `304` — оракул: diff --git a/docs/tasks/items/read-api-records.md b/docs/tasks/items/read-api-records.md index 18f8192..2413bdf 100644 --- a/docs/tasks/items/read-api-records.md +++ b/docs/tasks/items/read-api-records.md @@ -1,13 +1,13 @@ -# Записи наружу +# Отдавать записи со своим id за период -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** stateOfMind разобран и хранится, но наружу не отдаётся — а восстановить его нечем: в экспорте Apple его нет -- **Теги:** goal:read-api, sprint:2026-08-04 +- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature Записи со своим `id` — сегодня это `stateOfMind` — достаются за период через `GET /records/{kind}`. -Разбор и хранение сделаны тем же change, что у тренировок; наружу не отдаётся +Разбор и хранение сделаны тем же изменением, что у тренировок; наружу не отдаётся ничего. У этих данных есть особенность, которой нет больше ни у чего в проекте: **`stateOfMind` нет в экспорте Apple**, он не восстанавливается пересборкой из снапшота, и единственный его источник — доставки HAE. Отдача наружу — не @@ -15,6 +15,14 @@ Конверт наследуется от точек; собственной формы у записей нет. +Двигает строку «Завершения» цели: «Тренировки с маршрутом и записи со своим `id` отдаются за период». + +## Затрагивает + +Новый маршрут `GET /api/v1/records/{kind}` и код отказа на неизвестном `kind`. +Публичный тип провода в `internal/httpapi` — конверт записей. Чтение таблицы +записей; схемы и формата на диске не трогает. + ## Критерии приёмки - записи `stateOfMind` за период отдаются одним запросом — оракул: запрос к diff --git a/docs/tasks/items/read-api-response-limit.md b/docs/tasks/items/read-api-response-limit.md index 8980323..f039bae 100644 --- a/docs/tasks/items/read-api-response-limit.md +++ b/docs/tasks/items/read-api-response-limit.md @@ -1,8 +1,8 @@ -# Предел размера ответа +# Ограничить размер ответа маршрутов чтения -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** У маршрутов чтения нет ни одного потолка: множители «метрики × окно × точки × одновременные запросы» ничем не ограничены -- **Теги:** goal:read-api, sprint:2026-08-04 +- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature У маршрутов чтения появляется названный потолок: сетка не задана и ответ не влезает — сервер огрубляет её и **называет** в ответе; сетка задана явно и не @@ -16,20 +16,31 @@ приём в том же процессе уже даёт пик 768 МиБ на теле 40 МиБ. Множители «метрики × окно × точки × одновременные запросы» сегодня без потолка ни у одного маршрута — включая уже живой каталог, у которого предела нет намеренно: правило размера -общее, и задавать его мимоходом на первой ручке значило бы решить контракт до +общее, и задавать его мимоходом на первом маршруте значило бы решить контракт до того, как известна форма тяжёлого ответа. Правило распространяется на все маршруты чтения сразу — каталог, точки, тренировки, записи, — а не только на тот, где написано. +Двигает строку «Завершения» цели: «У ответа любого маршрута чтения есть объявленный предел размера». + +## Затрагивает + +Все маршруты чтения сразу — каталог, точки, а следом тренировки и записи: код и +тело отказа на запросе за пределом, поле огрублённой сетки в ответе. Конфиг и +его образцы — сам предел. Раздел о пределах в `docs/architecture.md`. Схемы и +формата на диске не трогает. + ## Критерии приёмки - запрос без сетки, не влезающий в предел, отвечает огрублённой сеткой и называет её в ответе — оракул: враждебный запрос на живом архиве - явно заданная сетка за пределом даёт ошибку со списком доступных сеток — оракул: тест -- предел объявлен в конфиге и в `docs/architecture.md`, а не зашит числом в - обработчике — оракул: образцы конфига в гейте +- предел читается из конфига: два разных значения дают две разные границы + отказа — оракул: тест с подменой значения предела +- предел назван в образцах конфига и в `docs/architecture.md` — оракул: шаг + образцов конфига в гейте и глазами по разделу - каталог подчиняется тому же пределу, что и точки — оракул: тест на враждебном запросе к каталогу @@ -37,5 +48,5 @@ Схема не трогается, данные только читаются. Берётся после свёртки по сетке. Собственный дедлайн маршрута сюда **не входит**: он в задаче -[«Остановка и миграция»](shutdown-and-migration-traces.md) вместе с `BaseContext` +[«Развести бюджеты остановки»](shutdown-and-migration-traces.md) вместе с `BaseContext` и раздельными бюджетами остановки. diff --git a/docs/tasks/items/read-api-workouts.md b/docs/tasks/items/read-api-workouts.md index b5cec5d..b901dc2 100644 --- a/docs/tasks/items/read-api-workouts.md +++ b/docs/tasks/items/read-api-workouts.md @@ -1,14 +1,14 @@ -# Тренировки наружу +# Отдавать тренировки вместе с маршрутом -- **Секция:** ядро -- **Зачем:** Тренировки с маршрутами разобраны и лежат в витрине, а эндпоинтов нет — сценарий трекера не закрыт -- **Теги:** goal:read-api, sprint:2026-08-04 +- **Секция:** Ядро +- **Зачем:** Тренировки с маршрутами разобраны и лежат в витрине, а маршрутов чтения нет — сценарий трекера не закрыт +- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature Трекер забирает тренировку одним пакетом вместе с маршрутом: `GET /workouts` за период и `GET /workouts/{id}` поштучно. Разбор и хранение тренировок сделаны (change `2026-08-02-trenirovki-i-zapisi`), -эндпоинтов нет: тренировка с маршрутом лежит в витрине и наружу не отдаётся. +маршрутов чтения нет: тренировка с маршрутом лежит в витрине и наружу не отдаётся. Второй сценарий паспорта — трекер тренировок — до тех пор не закрыт. **Это первый по-настоящему тяжёлый ответ проекта.** Маршрут лежит блобом внутри @@ -19,6 +19,15 @@ Конверт и форма провода наследуются, а не изобретаются. +Двигает строку «Завершения» цели: «Тренировки с маршрутом и записи со своим `id` отдаются за период». + +## Затрагивает + +Два новых маршрута: `GET /api/v1/workouts` за период и +`GET /api/v1/workouts/{id}` поштучно. Публичные типы провода в +`internal/httpapi` — конверт тренировки и элемент списка. Чтение таблицы +тренировок; схемы и формата на диске не трогает. + ## Критерии приёмки - тренировка отдаётся одним пакетом вместе с маршрутом — оракул: запрос к diff --git a/docs/tasks/items/read-api.md b/docs/tasks/items/read-api.md index 44475fa..6a6e832 100644 --- a/docs/tasks/items/read-api.md +++ b/docs/tasks/items/read-api.md @@ -1,13 +1,13 @@ -# [goal] Чтение данных клиентами +# [goal] Клиенты читают данные через HTTP и MCP -- **Секция:** порядок +- **Секция:** Запланировано - **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может - **Теги:** decomposed Потребители читают данные: точки с выбором слоя и свёрткой по сетке, тренировки и записи, машиночитаемый контракт — и всё то же самое через MCP. -Выведена из шагов 5 и 7 плана. Идёт после каталога и рода агрегации намеренно: +Идёт после каталога и рода агрегации намеренно: без измеренного рода свёртка в ответе неотличима от угадывания, а ошибиться здесь дорого — просуммировать нижний слой значит завысить втрое. @@ -20,7 +20,13 @@ ## Завершение -Любой из трёх потребителей получает точки, тренировки и записи за период без -доступа к файлу базы; предел размера ответа объявлен, а не подразумевается; -агент-медик читает то же самое через MCP тем же токеном чтения, и собственной -логики адаптер не несёт. +- Точки метрики за период отдаются по HTTP без доступа к файлу базы +- Повторный запрос тех же точек стоит `304`, а не полного чтения +- Точки сворачиваются по заданной сетке измеренным родом агрегации +- Неполное ведро отличимо от полного, и полярность порога названа +- У ответа любого маршрута чтения есть объявленный предел размера +- Тренировки с маршрутом и записи со своим `id` отдаются за период +- Контракт чтения читается машиной: спека, гейт против её расхождения с + маршрутами, UI без внешней сети +- Агент-медик читает то же самое через MCP тем же токеном чтения, и собственной + логики адаптер не несёт diff --git a/docs/tasks/items/rebuild-comparison-check.md b/docs/tasks/items/rebuild-comparison-check.md index f3ef590..eac2acb 100644 --- a/docs/tasks/items/rebuild-comparison-check.md +++ b/docs/tasks/items/rebuild-comparison-check.md @@ -1,6 +1,6 @@ -# Сверка живой витрины с пересборкой +# Сверять живую витрину с пересборкой -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит - **Теги:** goal:journal-and-rebuild @@ -10,7 +10,7 @@ только тем, что кто-то вручную запустил пересборку и посмотрел на два числа. Между тем расхождение — не гипотеза. Известный путь к нему записан блокером -[«Порядок журнала при конкурентных приёмах»](journal-order-on-ingest.md): +[«Держать порядок журнала при конкурентных приёмах»](journal-order-on-ingest.md): доставка, свёрнутая раньше своей предшественницы, уходит в `failed` навсегда, и живая витрина расходится с пересборкой молча. Пока тот предел не закрыт, сверка — единственный способ узнать, что он сработал. @@ -30,3 +30,5 @@ Связано: `cmd/healthlog/reindex.go`, [наблюдаемость](stats-endpoint.md), [деплой](deploy-rivendell.md). + +Двигает строку «Завершения» цели: «Расхождение живой витрины с пересборкой замечает сервис, а не человек». diff --git a/docs/tasks/items/rebuild-memory-footprint.md b/docs/tasks/items/rebuild-memory-footprint.md index 3d44183..6882246 100644 --- a/docs/tasks/items/rebuild-memory-footprint.md +++ b/docs/tasks/items/rebuild-memory-footprint.md @@ -1,6 +1,6 @@ -# Пересборка держит весь журнал в памяти +# Не держать весь журнал в памяти при пересборке -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет - **Теги:** goal:journal-and-rebuild @@ -26,3 +26,5 @@ памяти, не зависящим от его длины. Связано: `internal/replay`, `cmd/healthlog/reindex.go`. + +Двигает строку «Завершения» цели: «Расход пересборки не растёт вместе с журналом». diff --git a/docs/tasks/items/release-rollback-after-migration.md b/docs/tasks/items/release-rollback-after-migration.md index f5e00a7..d949d17 100644 --- a/docs/tasks/items/release-rollback-after-migration.md +++ b/docs/tasks/items/release-rollback-after-migration.md @@ -1,6 +1,6 @@ -# Чем откатывать релиз после наката миграции +# Назвать механизм отката релиза после наката миграции -- **Секция:** инфра +- **Секция:** Инфра - **Зачем:** Страж версии схемы делает возврат старого бинаря отказом, а понизить схему нечем — аварийный путь пришлось бы изобретать в аварии - **Теги:** goal:deploy @@ -15,6 +15,8 @@ Вынуто ревью кода задачи «Дозакрыть находки ревью по слиянию сущностей» (проходы `ops` и `negative`, профиль `deep`). +Двигает строку «Завершения» цели: «Откат релиза после наката миграции имеет названный механизм». + ## Что именно решить Та задача перенесла в `store.Open` стража версии схемы: база новее бинаря — diff --git a/docs/tasks/items/schema-annotations.md b/docs/tasks/items/schema-annotations.md index 881d54e..036bb9c 100644 --- a/docs/tasks/items/schema-annotations.md +++ b/docs/tasks/items/schema-annotations.md @@ -1,6 +1,6 @@ # [idea] Человеческие аннотации поверх выведенных схем -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Выведенная схема говорит форму, но не смысл метрики — нужно ли описание сверху, зависит от стабильности формата - **Теги:** goal:self-description diff --git a/docs/tasks/items/sealed-threshold.md b/docs/tasks/items/sealed-threshold.md index ea0971d..904052f 100644 --- a/docs/tasks/items/sealed-threshold.md +++ b/docs/tasks/items/sealed-threshold.md @@ -1,6 +1,6 @@ # [idea] Порог sealed: с какого возраста час считается запечатанным -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** WARN на изменение старого часа уже пишется, но порог не выбран — ставим по факту, когда накопится статистика досчёта - **Теги:** goal:merge-robustness diff --git a/docs/tasks/items/self-description.md b/docs/tasks/items/self-description.md index f2c872a..809b81f 100644 --- a/docs/tasks/items/self-description.md +++ b/docs/tasks/items/self-description.md @@ -1,17 +1,12 @@ -# [goal] Самоописание +# [goal] Клиент узнаёт форму данных из ответа -- **Секция:** порядок +- **Секция:** Запланировано - **Зачем:** Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке - **Теги:** decomposed Клиент узнаёт форму данных из ответа сервиса, а не угадывает её по выборке. -Выведена из шага 6 плана. - -Завершена, когда контракт читается машиной, а формы содержимого метрик -выведены из данных, а не описаны руками. - ## Завершение -Контракт читается машиной, а формы содержимого метрик выведены из данных, а не -описаны руками. +- Формы содержимого метрик выведены из данных, а не описаны руками +- Клиент узнаёт форму одной метрики и всего хранилища одним запросом diff --git a/docs/tasks/items/shutdown-and-migration-traces.md b/docs/tasks/items/shutdown-and-migration-traces.md index a2b5f75..f1b2452 100644 --- a/docs/tasks/items/shutdown-and-migration-traces.md +++ b/docs/tasks/items/shutdown-and-migration-traces.md @@ -1,6 +1,6 @@ -# Остановка и миграция: раздельные бюджеты и следы в логе +# Развести бюджеты остановки и оставить следы миграции в логе -- **Секция:** инфра +- **Секция:** Инфра - **Зачем:** Долгий запрос чтения съедает бюджет остановки, и WARN обвиняет воркер свёртки; миграция молчит и не прерывается SIGTERM - **Теги:** goal:deploy @@ -49,3 +49,5 @@ Связано: `cmd/healthlog/serve.go`, `internal/store/store.go`, change `2026-08-02-cena-chitayushchego-marshruta` (архив). + +Двигает строку «Завершения» цели: «Остановка сервиса называет виновный этап честно, а накат миграций виден в логе старта». diff --git a/docs/tasks/items/stats-endpoint.md b/docs/tasks/items/stats-endpoint.md index a25fe54..16b4bcb 100644 --- a/docs/tasks/items/stats-endpoint.md +++ b/docs/tasks/items/stats-endpoint.md @@ -1,6 +1,6 @@ -# Наблюдаемость: /stats +# Отдавать состояние сервиса маршрутом /stats -- **Секция:** инфра +- **Секция:** Инфра - **Зачем:** Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах - **Теги:** goal:observability @@ -59,3 +59,5 @@ сколько страниц лежит и сколько перенесено) и — тем же полем — доля ответов чтения, которые удалось подписать `ETag`: механизм условного запроса может перестать окупаться под плотным потоком, и снаружи это неотличимо от нормы. + +Двигает строку «Завершения» цели: «Состояние сервиса — последняя доставка, счётчики, тишина — читается одним запросом». diff --git a/docs/tasks/items/stream-silence-alert.md b/docs/tasks/items/stream-silence-alert.md index 53986a0..bf36276 100644 --- a/docs/tasks/items/stream-silence-alert.md +++ b/docs/tasks/items/stream-silence-alert.md @@ -1,6 +1,6 @@ -# Активный алерт «данных нет N часов» +# Слать уведомление, когда данных нет N часов -- **Секция:** инфра +- **Секция:** Инфра - **Зачем:** Пропажу потока сейчас замечает человек, а не сервис - **Теги:** goal:observability @@ -18,10 +18,12 @@ После деплоя на rivendell поднимется. -## Источник алерта не может жить внутри `serve` +Двигает строку «Завершения» цели: «Пропажа потока видна владельцу без чтения логов». + +## Источник уведомления не может жить внутри `serve` Отказ стража версии схемы (база новее бинаря) останавливает процесс, а -`restart: unless-stopped` даёт цикл перезапуска. Значит алерт «данных нет N +`restart: unless-stopped` даёт цикл перезапуска. Значит уведомление «данных нет N часов», живущий внутри сервиса, на эту причину остановки не сработает **по построению** — он не поднимется вместе с ним. Обоснование стража («откат делает оператор, он в этот момент рядом») верно для ручного отката и не покрывает diff --git a/docs/tasks/items/swagger-ui.md b/docs/tasks/items/swagger-ui.md index 65f2d35..bb25823 100644 --- a/docs/tasks/items/swagger-ui.md +++ b/docs/tasks/items/swagger-ui.md @@ -1,7 +1,7 @@ -# Swagger UI без внешней сети +# Поднять Swagger UI без внешней сети -- **Секция:** ядро -- **Зачем:** Контракт читается машиной, но человеку нечем ткнуть в живой сервис, а внешних CDN в локальной сети нет +- **Секция:** Ядро +- **Зачем:** Контракт читается машиной, но человеку нечем выполнить запрос к живому сервису из браузера, а внешних CDN в локальной сети нет - **Теги:** goal:read-api, sprint:2026-08-04 Человек открывает UI на отдельном пути и выполняет запрос к живому сервису — не @@ -11,18 +11,22 @@ статика отдаётся самим сервисом и лежит в бинаре: внешние CDN здесь означают «работает, пока работает чужой сайт». -**UI — новый адресат недоверенного входа наизнанку:** он даёт человеку в один -клик дёрнуть любой описанный маршрут, включая приём. Права на запись из -браузера не должны появляться сами собой — это ровно та граница, которую +**UI — новый адресат недоверенного входа наизнанку:** он даёт человеку одним +нажатием выполнить запрос к любому описанному маршруту, включая приём. Права +на запись из браузера не должны появляться сами собой — это ровно та граница, которую описывает `docs/security.md`. +Двигает строку «Завершения» цели: «Контракт чтения читается машиной». + ## Критерии приёмки - UI открывается и выполняет запрос при отключённой внешней сети — оракул: запуск контейнера без доступа наружу - бинарь работает без каталога со статикой рядом — оракул: запуск одного файла из пустого каталога -- маршрут приёма из UI не вызывается без явного токена приёма — оракул: тест +- запрос к маршруту приёма без заголовка с токеном приёма отклоняется, откуда + бы он ни пришёл, а отдаваемая UI статика токена приёма в себе не держит — + оракул: тест на обработчике приёма плюс поиск токена в отдаваемой статике ## Рамки diff --git a/docs/tasks/items/token-and-secret-management.md b/docs/tasks/items/token-and-secret-management.md index 494a522..21dfa9b 100644 --- a/docs/tasks/items/token-and-secret-management.md +++ b/docs/tasks/items/token-and-secret-management.md @@ -1,6 +1,6 @@ -# Управление токенами и секретами +# Развести токены контуров и убрать секреты из репозитория -- **Секция:** инфра +- **Секция:** Инфра - **Зачем:** Проверка токенов выключена, config.docker.toml коммитится — так нельзя выезжать наружу - **Теги:** goal:deploy @@ -29,3 +29,5 @@ Готово, когда запуск без токенов возможен только на localhost, а на rivendell оба контура закрыты разными токенами. + +Двигает строку «Завершения» цели: «Оба контура закрыты разными токенами, и без токенов сервис стартует только на localhost». diff --git a/docs/tasks/items/workout-identity-on-import.md b/docs/tasks/items/workout-identity-on-import.md index e37c5b8..c3ffe72 100644 --- a/docs/tasks/items/workout-identity-on-import.md +++ b/docs/tasks/items/workout-identity-on-import.md @@ -1,6 +1,6 @@ -# Идентичность тренировок при импорте родного экспорта +# Не задваивать тренировки при импорте родного экспорта -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE - **Теги:** goal:native-export-import @@ -37,3 +37,5 @@ Prior art: `dogsheep/healthkit-to-sqlite` адресует тренировку Связано: `docs/architecture.md` → «Тренировки и прочие секции», задача `apple-export-import`. + +Двигает строку «Завершения» цели: «Тренировки из экспорта не задваивают приехавшие от HAE». diff --git a/docs/tasks/items/workout-routes-table.md b/docs/tasks/items/workout-routes-table.md index 460d9be..c8cc92e 100644 --- a/docs/tasks/items/workout-routes-table.md +++ b/docs/tasks/items/workout-routes-table.md @@ -1,6 +1,6 @@ # [idea] Разворачивание маршрутов тренировок в отдельную таблицу -- **Секция:** ядро +- **Секция:** Ядро - **Зачем:** Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом - **Теги:** goal:read-api diff --git a/internal/ingest/ingest_test.go b/internal/ingest/ingest_test.go index fe3fd28..a0c88ac 100644 --- a/internal/ingest/ingest_test.go +++ b/internal/ingest/ingest_test.go @@ -65,7 +65,7 @@ func TestAcceptStoresBodyVerbatim(t *testing.T) { } // Повтор того же тела пока принимается — отсев идентичных доставок отложен -// (docs/tasks/PLAN.md). Проверяем, что повтор не ломается и не затирает первую. +// (docs/tasks/ROADMAP.md). Проверяем, что повтор не ломается и не затирает первую. func TestAcceptAllowsRepeatedBody(t *testing.T) { svc, _, st := newService(t) body := []byte(`{"data":{"metrics":[]}}`)