docs: документация переведена на канон av-dev-pm 3

- роадмап отвечает «что умеет и чего не умеет»: PLAN.md → ROADMAP.md, четыре
  канонические секции, достигнутые звенья строками в «Готово», цели
  переформулированы возможностями приложения
- задачи: род работы и «Затрагивает» набору спринта, 34 заголовка в форму
  действия, «Завершение» целей перечнями со ссылкой из каждой задачи
- вычитка проходами task-form и doc-wording, починены протухшие факты в README,
  паспорте и review.md
This commit is contained in:
av
2026-08-04 20:48:30 +03:00
parent b1d3b25827
commit d33f37249c
63 changed files with 489 additions and 346 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
[docs/passport.md](docs/passport.md) (цель, сценарии, референсы), [docs/passport.md](docs/passport.md) (цель, сценарии, референсы),
[README.md](README.md), [docs/architecture.md](docs/architecture.md), [README.md](README.md), [docs/architecture.md](docs/architecture.md),
[docs/conventions/README.md](docs/conventions/README.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` (версия в `docs/.pm.json`);
раскладку проверяет `av-dev-pm:canon`, содержимое ведёт `av-dev-pm:docs`. раскладку проверяет `av-dev-pm:canon`, содержимое ведёт `av-dev-pm:docs`.
+13 -7
View File
@@ -67,13 +67,17 @@ iPhone ──HTTPS POST──► healthlog ──► журнал доставо
витрину и сверяет её отпечаток с накопленной — на живом архиве из 116 тел витрину и сверяет её отпечаток с накопленной — на живом архиве из 116 тел
пересборка воспроизводима и повторный прогон ничего не меняет. пересборка воспроизводима и повторный прогон ничего не меняет.
Первый маршрут чтения открыт: **каталог разрезов** (`GET /api/v1/metrics`) под Маршрутов чтения открыто два. **Каталог разрезов** (`GET /api/v1/metrics`) под
токеном чтения отдаёт слои с диапазонами и измеренный род агрегации, а повтор токеном чтения отдаёт слои с диапазонами и измеренный род агрегации, а повтор
неизменившегося отвечает `304` по `ETag` — снимок витрины при этом не неизменившегося отвечает `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 находок на живом потоке, половина расходится с Разведка формата закончена: 50 находок на живом потоке, половина расходится с
документацией Health Auto Export — [docs/research/apple-health.md](docs/research/apple-health.md). документацией 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 import родной экспорт Apple Health (в планах)
healthlog reindex пересборка витрины из журнала healthlog reindex пересборка витрины из журнала
healthlog uncovered перечень секций, которых разбор не покрыл healthlog uncovered перечень секций, которых разбор не покрыл
@@ -154,7 +158,8 @@ curl -i -H 'If-None-Match: W/"…"' localhost:8080/api/v1/metrics # 304
Если `auth.write_tokens` пуст, проверка токена выключена — для доверенной Если `auth.write_tokens` пуст, проверка токена выключена — для доверенной
локальной сети этого достаточно, сервис пишет об этом `write auth disabled` локальной сети этого достаточно, сервис пишет об этом `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/conventions/](docs/conventions/README.md) — как пишем код
- [docs/security.md](docs/security.md) — периметр и модель угроз - [docs/security.md](docs/security.md) — периметр и модель угроз
- [docs/review.md](docs/review.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/tasks/BACKLOG.md](docs/tasks/BACKLOG.md) — что брать следующим, включая
отложенные идеи отложенные идеи
- [docs/research/apple-health.md](docs/research/apple-health.md) — что показал реальный поток - [docs/research/apple-health.md](docs/research/apple-health.md) — что показал реальный поток
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"canon": 2, "canon": 3,
"migrations": "internal/store/migrations" "migrations": "internal/store/migrations"
} }
+17 -13
View File
@@ -1,8 +1,8 @@
# Паспорт проекта # Паспорт проекта
Куда мы идём, как выглядит работа проекта в жизни и у кого подсматривать, Куда мы идём, как выглядит работа проекта в жизни и у кого подсматривать,
когда упёрлись. Самый верхний документ: [tasks/PLAN.md](tasks/PLAN.md) отвечает «в каком когда упёрлись. Самый верхний документ: [tasks/ROADMAP.md](tasks/ROADMAP.md) отвечает «что
порядке», [architecture.md](architecture.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`, разбирает метрики в часовые сервис кладёт тело в архив, отвечает `200`, разбирает метрики в часовые
объекты. Никто ничего не спрашивает и не смотрит. объекты. Никто ничего не спрашивает и не смотрит.
*Успех:* сутки работы не порождают ни одной строки лога уровня `WARN` и ни *Успех:* сутки работы не порождают ни одной строки лога уровня `WARN` и ни
одного действия человека. одного действия человека.
**2. Дыра закрывается сама** (разбор и хранилище). Телефон был заблокирован ночью, **2. Дыра закрывается сама** (`parsing-and-storage` — сделано). Телефон был заблокирован ночью,
автоматизация не отработала, часть дня отсутствует. Средний проход (сутки) и автоматизация не отработала, часть дня отсутствует. Средний проход (сутки) и
глубокий (неделя) переприсылают окно целиком, точки доезжают. глубокий (неделя) переприсылают окно целиком, точки доезжают.
*Успех:* дыра моложе недели закрывается без вмешательства; никто о ней даже не *Успех:* дыра моложе недели закрывается без вмешательства; никто о ней даже не
узнаёт. узнаёт.
**3. Квартальный экспорт** (`healthlog import`, устаревание нижнего слоя). **3. Квартальный экспорт** (История из родного экспорта Apple лежит в
хранилище; Нижний слой чистится после проверенного экспорта).
Изредка владелец выгружает Изредка владелец выгружает
родной экспорт Apple Health и скармливает его `healthlog import`. Нижний слой родной экспорт 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, *Успех:* тренировка отдана одним пакетом в том виде, в каком её прислал Apple,
без нашей интерпретации того, что в ней главное. без нашей интерпретации того, что в ней главное.
**6. Разбор поменялся** (`healthlog reindex`). Мы начали разбирать секцию, которую **6. Разбор поменялся** (`reindex` — сделан). Мы начали разбирать секцию, которую
раньше пропускали, или нашли ошибку в старом разборе. Запускается пересборка раньше пропускали, или нашли ошибку в старом разборе. Запускается пересборка
по сырому архиву: `import(экспорт) + replay(доставки по received_at)`. по сырому архиву: `import(экспорт) + replay(доставки по received_at)`.
*Успех:* состояние пересобрано детерминированно, повтор даёт то же самое, *Успех:* состояние пересобрано детерминированно, повтор даёт то же самое,
доставки со снятым статусом `partial` подобраны. доставки со снятым статусом `partial` подобраны.
**7. Владелец проверяет, жив ли поток** (наблюдаемость). Раз в сколько-то дней — **7. Владелец проверяет, жив ли поток** (Приложение сообщает о своём состоянии). Раз в сколько-то дней —
взгляд в `/stats`: когда была последняя доставка, сколько точек, есть ли взгляд в `/stats`: когда была последняя доставка, сколько точек, есть ли
тишина, какие строки не легли в словарь кодов. тишина, какие строки не легли в словарь кодов.
*Успех:* один экран отвечает «всё идёт» или «встало тогда-то», без залезания *Успех:* один экран отвечает «всё идёт» или «встало тогда-то», без залезания
в SQLite. в SQLite.
**8. Приехало незнакомое** (разбор и хранилище). HAE обновился и прислал новую метрику, **8. Приехало незнакомое** (`parsing-and-storage` — сделано; Новая форма от
источника не теряется молча). HAE обновился и прислал новую метрику,
новую форму точки или новую секцию. Тело сохраняется, ответ — `200`, разбор новую форму точки или новую секцию. Тело сохраняется, ответ — `200`, разбор
честно помечает доставку `partial` и перечисляет непокрытое. честно помечает доставку `partial` и перечисляет непокрытое.
*Успех:* данные в архиве и восстановимы, факт виден в логе и `/stats`, а *Успех:* данные в архиве и восстановимы, факт виден в логе и `/stats`, а
+34 -21
View File
@@ -34,8 +34,10 @@
сам же меняет, без границы по `received_at` разбираемой доставки. сам же меняет, без границы по `received_at` разбираемой доставки.
- Правило выбора между версиями — функция множества версий либо явно функция - Правило выбора между версиями — функция множества версий либо явно функция
порядка журнала; третьего состояния нет. порядка журнала; третьего состояния нет.
- Столкновение разрешается полнотой, а не свежестью; изменение запечатанного - Столкновение разрешается полнотой, а при равной полноте — положением в
часа пишется `WARN`, но данные пишутся. журнале: побеждает стоящая позже
([ADR](adr/ADR-2026-08-04-tie-break-po-poryadku-zhurnala.md)). Изменение
запечатанного часа пишется `WARN`, но данные пишутся.
- Транзакция не держит блокировку дольше `busy_timeout`: канонизация и - Транзакция не держит блокировку дольше `busy_timeout`: канонизация и
сжатие — вне её. сжатие — вне её.
@@ -52,7 +54,8 @@
- Расход памяти не растёт вместе с длиной журнала. - Расход памяти не растёт вместе с длиной журнала.
- Подмена базы — решение человека при остановленном сервисе, не команды. - Подмена базы — решение человека при остановленном сервисе, не команды.
**Обработчик чтения и адаптер MCP** (Read API, MCP — ещё не написаны) **Обработчик чтения и адаптер MCP** (`internal/httpapi`: каталог и точки
написаны; свёртка по сетке, тренировки, записи и MCP — ещё нет)
- Агрегат считается только там, где род свёртки измерен; нижний слой HAE не - Агрегат считается только там, где род свёртки измерен; нижний слой HAE не
суммируется никогда. суммируется никогда.
@@ -105,31 +108,41 @@
- `triage`: перечислены ли запущенные проходы поимённо и с исходом; непущенный - `triage`: перечислены ли запущенные проходы поимённо и с исходом; непущенный
проход идёт в границы покрытия строкой «не запускался» (запись 2026-08-02, проход идёт в границы покрытия строкой «не запускался» (запись 2026-08-02,
чекпоинт кода прошёл без трёх проходов). чекпоинт кода прошёл без трёх проходов).
- `specs`: считается ли внешним поведением **состояние, которое даёт
пересборка** — витрина наблюдаема через пересборку, поэтому расхождение с
журналом не внутренняя деталь, а поведение, которого спека не заказывала.
Внешнее здесь — ещё и код ответа приёма, форма ответа чтения и содержимое
архива (переселено из триггеров профиля, канон 3).
### Триггеры профиля ### Триггеры профиля
Уточняет умолчания конвейера, не отменяет их. Уточняет умолчания конвейера, не отменяет их. Рабочее умолчание — `standard`:
миграция схемы, публичный контракт и инвариант ступень **не** поднимают, их
проверяют проходы, которые в `standard` и так есть.
- **`deep`** — изменения в правиле разбора, идентичности, слияния или вывода - **Новое понятие или структурная единица** (`wide`) — новый пакет в
слоя; миграции схемы; всё, что трогает `internal/store`, `internal/fold`, `internal/`, новый род узла из перечня выше, новый тип провода в
`internal/replay`. `internal/httpapi`, новая единица хранения, входящая в отпечаток, новый
- **«Поведение, видимое снаружи»** здесь — код ответа приёма, форма ответа транспорт рядом с HTTP.
чтения, содержимое архива и **состояние, которое даёт пересборка**: витрина - **Правила идентичности, слияния и разбора** (`deep`) живут в трёх местах:
наблюдаема через пересборку, поэтому расхождение с журналом — внешнее `internal/hae` — разбор пакета и вывод слоя; `internal/fold` — выбор между
поведение, а не внутренняя деталь. версиями точки; `internal/store` — координатный ключ и запись часового
- **`reimpl`** запускается по триггеру «новое правило слияния, идентичности или объекта. Правку правила в любом из них ступень поднимает; перенос кода без
разбора». Единственный раз, когда триаж назвал его отсутствие дырой правки правила — нет.
покрытия, — это была задача с новым правилом слияния сущностей.
Второй замер (2026-08-03, словарь категориальных значений): триггер сработал
на новом правиле разбора и ключе реестра, проход **окупился** — он независимо
подтвердил замером две находки, до того имевшие только одно измерение (пик
памяти накопителя: 1002 МиБ против 780 на базе; единицы счётчика отброшенных),
и отдельно назвал семь мест, где существующее решение оказалось **лучше** его
собственного. Второе ценно не меньше первого: оно показывает, где проход
соглашается, а не только где спорит.
- **`quick`** — правка документов, конфигурации, сообщений; ничего, что меняет - **`quick`** — правка документов, конфигурации, сообщений; ничего, что меняет
хранимое. хранимое.
`reimpl` живёт за барьером `deep` и по тому же триггеру — новое правило слияния,
идентичности или разбора. Замеры окупаемости: единственный раз, когда триаж
назвал его отсутствие дырой покрытия, — задача с новым правилом слияния
сущностей. Второй замер (2026-08-03, словарь категориальных значений): триггер
сработал на новом правиле разбора и ключе реестра, проход **окупился** — он
независимо подтвердил замером две находки, до того имевшие только одно измерение
(пик памяти накопителя: 1002 МиБ против 780 на базе; единицы счётчика
отброшенных), и отдельно назвал семь мест, где существующее решение оказалось
**лучше** его собственного. Второе ценно не меньше первого: оно показывает, где
проход соглашается, а не только где спорит.
### Недоступно проверке ### Недоступно проверке
**Не проверит ни один проход.** Реальный профиль нагрузки: телефон шлёт молча и **Не проверит ни один проход.** Реальный профиль нагрузки: телефон шлёт молча и
+34 -32
View File
@@ -1,51 +1,53 @@
# Беклог # Беклог
Что **можно взять**. Одна задача = один файл `items/<slug>.md` Что **можно взять**. Одна задача = один файл `items/<slug>.md`
+ строка здесь. Целей тут нет — они в [PLAN.md](PLAN.md): беклог — то, что берут, + строка здесь. Целей тут нет — они в [ROADMAP.md](ROADMAP.md): беклог — то, что
план — то, подо что берут. Порядка внутри секции нет: «что делать берут, роадмап — то, подо что берут. Порядка внутри секции нет: «что делать
дальше» отвечает набор спринта. Ведётся скиллом `tasks`. дальше» отвечает набор спринта. Ведётся скиллом `tasks`.
Секции «блокеры» здесь нет и не заводится: блокер — это состояние Секции «блокеры» здесь нет и не заводится: блокер — это состояние
(спринт не может продолжаться ни одной задачей), оно живёт до ответа (спринт не может продолжаться ни одной задачей), оно живёт до ответа
человека, а его следы — вопросами в файлах задач. человека, а его следы — вопросами в файлах задач.
## ядро ## Ядро
- [[idea] Человеческие аннотации поверх выведенных схем](items/schema-annotations.md) — Выведенная схема говорит форму, но не смысл метрики — нужно ли описание сверху, зависит от стабильности формата - [[idea] Человеческие аннотации поверх выведенных схем](items/schema-annotations.md) — Выведенная схема говорит форму, но не смысл метрики — нужно ли описание сверху, зависит от стабильности формата
- [Проверка целостности собранной витрины перед подменой](items/integrity-before-swap.md) — Подмена файла необратима, а годность выхода подтверждена отпечатком на ещё открытом дескрипторе - [Проверять целостность собранной витрины до подмены](items/integrity-before-swap.md) — Подмена файла необратима, а годность выхода подтверждена отпечатком на ещё открытом дескрипторе
- [Цена слияния на широкой доставке](items/merge-cost-wide-delivery.md) — 63 МБ на одной координате держат транзакцию 5.15 с при busy_timeout 5 с — соседние доставки уходят в failed - [Снизить цену слияния на широкой доставке](items/merge-cost-wide-delivery.md) — 63 МБ на одной координате держат транзакцию 5.15 с при busy_timeout 5 с — соседние доставки уходят в failed
- [Сущность с id, но неразобранной меткой](items/entity-without-parsed-label.md) — Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым - [Не терять сущность с id и неразобранной меткой](items/entity-without-parsed-label.md) — Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым
- [Идентичность тренировок при импорте родного экспорта](items/workout-identity-on-import.md) — В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE - [Не задваивать тренировки при импорте родного экспорта](items/workout-identity-on-import.md) — В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE
- [Импорт родного экспорта Apple Health](items/apple-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут - [Импортировать родной экспорт Apple Health](items/apple-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
- [[idea] Месячный проход по ручным секциям](items/monthly-manual-sections-pass.md) — Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит - [[idea] Месячный проход по ручным секциям](items/monthly-manual-sections-pass.md) — Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит
- [[idea] NDJSON-поток для больших выборок Read API](items/ndjson-stream.md) — Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация - [[idea] NDJSON-поток для больших выборок Read API](items/ndjson-stream.md) — Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация
- [Data-миграции не отбирают строки по обрезаемым спискам](items/data-migration-row-selection.md) — Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону - [Не отбирать строки в data-миграциях по обрезаемым спискам](items/data-migration-row-selection.md) — Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону
- [[idea] Отказ от heartbeatSeries](items/drop-heartbeat-series.md) — 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе - [[idea] Отказ от heartbeatSeries](items/drop-heartbeat-series.md) — 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе
- [Пересборка держит весь журнал в памяти](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет - [Не держать весь журнал в памяти при пересборке](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет
- [[idea] Пересекающиеся источники одной метрики](items/overlapping-sources.md) — Сон пишут и часы, и стороннее приложение — сумма по обоим задвоит ночь - [[idea] Пересекающиеся источники одной метрики](items/overlapping-sources.md) — Сон пишут и часы, и стороннее приложение — сумма по обоим задвоит ночь
- [[idea] Порог sealed: с какого возраста час считается запечатанным](items/sealed-threshold.md) — WARN на изменение старого часа уже пишется, но порог не выбран — ставим по факту, когда накопится статистика досчёта - [[idea] Порог sealed: с какого возраста час считается запечатанным](items/sealed-threshold.md) — WARN на изменение старого часа уже пишется, но порог не выбран — ставим по факту, когда накопится статистика досчёта
- [Порядок журнала при конкурентных приёмах](items/journal-order-on-ingest.md) — Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой - [Держать порядок журнала при конкурентных приёмах](items/journal-order-on-ingest.md) — Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой
- [Предел на размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним - [Ограничить размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним
- [Пределы на размер сущности и потоковый расчёт формы](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе - [Ограничить размер сущности и считать форму потоково](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе
- [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом - [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом
- [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке - [Выводить схемы содержимого из данных](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
- [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено - [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено
- [Сверка живой витрины с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит - [Сверять живую витрину с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит
- [Устаревание нижнего слоя после экспорта](items/lower-layer-expiry.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен - [Помечать нижний слой устаревшим после экспорта](items/lower-layer-expiry.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
- [[idea] Выгрузка в parquet отдельной командой](items/parquet-export.md) — Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно - [[idea] Выгрузка в parquet отдельной командой](items/parquet-export.md) — Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно
- [Заголовки доставки в архиве рядом с телом](items/delivery-headers-in-archive.md) — Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке - [Класть заголовки доставки в архив рядом с телом](items/delivery-headers-in-archive.md) — Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке
- [Правило полноты против last wins](items/last-wins-over-completeness.md) — Полнота решает 1,2% спорных координат, и неизвестно, была ли более полная точка более поздней — от этого зависит, нужна ли она вообще - [Измерить, нужно ли правило полноты рядом с LWW](items/last-wins-over-completeness.md) — Полнота решает 1,2% спорных координат, и неизвестно, была ли более полная точка более поздней — от этого зависит, нужна ли она вообще
- [MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем - [Поднять MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
- [Рукописная OpenAPI-спека](items/openapi-spec.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате - [Написать OpenAPI-спеку руками](items/openapi-spec.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
- [Гейт против расхождения спеки с маршрутами](items/openapi-gate-check.md) — Рукописная спека — источник истины, а расходится она с кодом молча: без гейта решение о рукописной спеке не держится - [Ловить гейтом расхождение спеки с маршрутами](items/openapi-gate-check.md) — Рукописная спека — источник истины, а расходится она с кодом молча: без гейта решение о рукописной спеке не держится
- [Swagger UI без внешней сети](items/swagger-ui.md) — Контракт читается машиной, но человеку нечем ткнуть в живой сервис, а внешних CDN в локальной сети нет - [Поднять Swagger UI без внешней сети](items/swagger-ui.md) — Контракт читается машиной, но человеку нечем выполнить запрос к живому сервису из браузера, а внешних CDN в локальной сети нет
## инфра ## Инфра
- [Активный алерт «данных нет N часов»](items/stream-silence-alert.md) — Пропажу потока сейчас замечает человек, а не сервис
- [Деплой на rivendell](items/deploy-rivendell.md) — Сервис живёт на рабочей машине — телефон достаёт до него только дома - [Слать уведомление, когда данных нет N часов](items/stream-silence-alert.md) — Пропажу потока сейчас замечает человек, а не сервис
- [Счётчики слияния переживают ротацию логов](items/merge-counters-in-db.md) — единственный след несравнимых наборов — строка WARN в docker-логе с ротацией 3×10 МБ: событие может произойти и не оставить ничего - [Выложить сервис на rivendell](items/deploy-rivendell.md) — Сервис живёт на рабочей машине — телефон достаёт до него только дома
- [Остановка и миграция: раздельные бюджеты и следы в логе](items/shutdown-and-migration-traces.md) — Долгий запрос чтения съедает бюджет остановки, и WARN обвиняет воркер свёртки; миграция молчит и не прерывается SIGTERM - [Хранить счётчики слияния вне логов](items/merge-counters-in-db.md) — Единственный след несравнимых наборов — строка WARN в docker-логе с ротацией 3×10 МБ: событие может произойти и не оставить ничего
- [Чем откатывать релиз после наката миграции](items/release-rollback-after-migration.md) — Страж версии схемы делает возврат старого бинаря отказом, а понизить схему нечем — аварийный путь пришлось бы изобретать в аварии - [Развести бюджеты остановки и оставить следы миграции в логе](items/shutdown-and-migration-traces.md) — Долгий запрос чтения съедает бюджет остановки, и WARN обвиняет воркер свёртки; миграция молчит и не прерывается SIGTERM
- [Ретеншен сырого архива](items/raw-archive-retention.md) — Архив не подчищается вовсе — 14-дневный срок объявлен, но не работает - [Назвать механизм отката релиза после наката миграции](items/release-rollback-after-migration.md) — Страж версии схемы делает возврат старого бинаря отказом, а понизить схему нечем — аварийный путь пришлось бы изобретать в аварии
- [Наблюдаемость: /stats](items/stats-endpoint.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах - [Подчищать сырой архив до последнего проверенного экспорта](items/raw-archive-retention.md) — Архив не подчищается вовсе, а резать его раньше даты проверенного экспорта нельзя — в журнале останется дыра, которую нечем пересобрать
- [Умолчания конфига указывают на прежнюю раскладку](items/config-defaults-data-dir.md) — Запуск без конфига заведёт пустую базу в корне рядом с настоящей — тихая ловушка - [Отдавать состояние сервиса маршрутом /stats](items/stats-endpoint.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах
- [Управление токенами и секретами](items/token-and-secret-management.md) — Проверка токенов выключена, config.docker.toml коммитится — так нельзя выезжать наружу - [Свести умолчания конфига с рабочей раскладкой данных](items/config-defaults-data-dir.md) — Запуск без конфига заведёт пустую базу в корне рядом с настоящей — тихая ловушка
- [Развести токены контуров и убрать секреты из репозитория](items/token-and-secret-management.md) — Проверка токенов выключена, config.docker.toml коммитится — так нельзя выезжать наружу
-54
View File
@@ -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 поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы
+51
View File
@@ -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) — Сервис живёт на рабочей машине — телефон достаёт до него только дома, и оба контура открыты
+9 -8
View File
@@ -1,15 +1,16 @@
# Спринт # Спринт
- **Цель:** [[goal] Чтение данных клиентами](items/read-api.md) - **Цель:** [[goal] Клиенты читают данные через HTTP и MCP](items/read-api.md)
- **Начат:** 2026-08-04 - **Начат:** 2026-08-04
- **Спринт:** `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) — «Шаги за неделю по дням» — базовый запрос трекера и игры, и сегодня его нечем задать - [Отвечать 304 на повторный запрос точек](items/read-api-points-conditional.md) — Агент опрашивает по расписанию, а каждый повтор стоит полного чтения: на каталоге это 693 мс и +153 МиБ
- [Порог неполного ведра](items/read-api-partial-bucket.md) — Текущий час неполон всегда, и без порога свёртка отдаёт его наравне с полными — клиент видит провал вместо неизвестности - [Сворачивать точки по заданной сетке](items/read-api-points-bucket.md) — «Шаги за неделю по дням» — базовый запрос трекера и игры, и сегодня его нечем задать
- [Предел размера ответа](items/read-api-response-limit.md) — У маршрутов чтения нет ни одного потолка: множители «метрики × окно × точки × одновременные запросы» ничем не ограничены - [Отличать неполное ведро от полного](items/read-api-partial-bucket.md) — Текущий час неполон всегда, и без порога свёртка отдаёт его наравне с полными — клиент видит провал вместо неизвестности
- [Тренировки наружу](items/read-api-workouts.md) — Тренировки с маршрутами разобраны и лежат в витрине, а эндпоинтов нет — сценарий трекера не закрыт - [Ограничить размер ответа маршрутов чтения](items/read-api-response-limit.md) — У маршрутов чтения нет ни одного потолка: множители «метрики × окно × точки × одновременные запросы» ничем не ограничены
- [Записи наружу](items/read-api-records.md) — stateOfMind разобран и хранится, но наружу не отдаётся — а восстановить его нечем: в экспорте Apple его нет - [Отдавать тренировки вместе с маршрутом](items/read-api-workouts.md) — Тренировки с маршрутами разобраны и лежат в витрине, а маршрутов чтения нет — сценарий трекера не закрыт
- [Отдавать записи со своим id за период](items/read-api-records.md) — stateOfMind разобран и хранится, но наружу не отдаётся — а восстановить его нечем: в экспорте Apple его нет
+4 -2
View File
@@ -1,6 +1,6 @@
# Импорт родного экспорта Apple Health # Импортировать родной экспорт Apple Health
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут - **Зачем:** Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
- **Теги:** goal:native-export-import - **Теги:** goal:native-export-import
@@ -37,6 +37,8 @@
должен ничего менять; должен ничего менять;
- `export_cda.xml` игнорируем — это клинический формат тех же данных. - `export_cda.xml` игнорируем — это клинический формат тех же данных.
Двигает строку «Завершения» цели: «Слой `sample` наполнен историей с 2019 года, повторный импорт того же экспорта ничего не меняет».
## Импорт выставляет пометку покрытия ## Импорт выставляет пометку покрытия
Импорт — единственный, кто знает, какой период каким слоем обеспечен, поэтому Импорт — единственный, кто знает, какой период каким слоем обеспечен, поэтому
+4 -2
View File
@@ -1,6 +1,6 @@
# Умолчания конфига указывают на прежнюю раскладку # Свести умолчания конфига с рабочей раскладкой данных
- **Секция:** инфра - **Секция:** Инфра
- **Зачем:** Запуск без конфига заведёт пустую базу в корне рядом с настоящей — тихая ловушка - **Зачем:** Запуск без конфига заведёт пустую базу в корне рядом с настоящей — тихая ловушка
- **Теги:** goal:deploy - **Теги:** goal:deploy
@@ -16,3 +16,5 @@
Готово, когда запуск без конфига использует `./data` и не создаёт ничего в Готово, когда запуск без конфига использует `./data` и не создаёт ничего в
корне репозитория. Тогда же снимается предупреждение из `config.example.toml`. корне репозитория. Тогда же снимается предупреждение из `config.example.toml`.
Двигает строку «Завершения» цели: «Запуск без конфига не заводит базу мимо `./data`».
@@ -1,12 +1,14 @@
# Data-миграции не отбирают строки по обрезаемым спискам # Не отбирать строки в data-миграциях по обрезаемым спискам
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону - **Зачем:** Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону
- **Теги:** goal:journal-and-rebuild - **Теги:** goal:journal-and-rebuild
Остаток задачи «Дозакрыть находки ревью по слиянию сущностей» (архивный change Остаток задачи «Дозакрыть находки ревью по слиянию сущностей» (архивный change
`dozakryt-nahodki-sushchnostej`). `dozakryt-nahodki-sushchnostej`).
Двигает строку «Завершения» цели: «Data-миграции не наследуют слепые зоны обрезаемых списков».
## Оракул: механизм доказан, дефект пока пустой ## Оракул: механизм доказан, дефект пока пустой
Миграция `00007` переводит в `pending` доставки, у которых имя ставшей покрытой Миграция `00007` переводит в `pending` доставки, у которых имя ставшей покрытой
+1 -1
View File
@@ -1,6 +1,6 @@
# [idea] Что считать сутками при смене часового пояса # [idea] Что считать сутками при смене часового пояса
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено - **Зачем:** Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено
- **Теги:** goal:read-api - **Теги:** goal:read-api
+4 -2
View File
@@ -1,6 +1,6 @@
# Предел на размер и число заголовков доставки # Ограничить размер и число заголовков доставки
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним - **Зачем:** MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним
- **Теги:** goal:limits-and-load - **Теги:** goal:limits-and-load
@@ -27,3 +27,5 @@
не оставляя следа в базе, а обычная доставка проходит как раньше. не оставляя следа в базе, а обычная доставка проходит как раньше.
Связано: `internal/httpapi`, `internal/ingest`, `docs/architecture.md` → «Приём». Связано: `internal/httpapi`, `internal/ingest`, `docs/architecture.md` → «Приём».
Двигает строку «Завершения» цели: «У заголовков доставки есть названный предел».
@@ -1,6 +1,6 @@
# Заголовки доставки в архиве рядом с телом # Класть заголовки доставки в архив рядом с телом
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке - **Зачем:** Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке
- **Теги:** goal:journal-and-rebuild - **Теги:** goal:journal-and-rebuild
@@ -32,7 +32,7 @@ Prior art прямой: **WARC** (формат веб-архивов) храни
операциям, но появляется третья сущность. операциям, но появляется третья сущность.
Цена ошибки высокая: правится **путь приёма**, а доставка, не попавшая в архив, Цена ошибки высокая: правится **путь приёма**, а доставка, не попавшая в архив,
теряется навсегда. Значит профиль ревью — `deep`, и менять надо так, чтобы теряется навсегда. Значит менять надо так, чтобы
старые тела без заголовков продолжали читаться. старые тела без заголовков продолжали читаться.
Готово, когда пересборка на архиве, у которого рабочей базы нет вовсе, даёт то Готово, когда пересборка на архиве, у которого рабочей базы нет вовсе, даёт то
@@ -40,3 +40,5 @@ Prior art прямой: **WARC** (формат веб-архивов) храни
Связано: `docs/architecture.md` → «Сырой архив и восстановление состояния», Связано: `docs/architecture.md` → «Сырой архив и восстановление состояния»,
`internal/replay`. `internal/replay`.
Двигает строку «Завершения» цели: «Пересборка восстановима без базы: заголовки доставки лежат в архиве рядом с телом».
+4 -2
View File
@@ -1,6 +1,6 @@
# Деплой на rivendell # Выложить сервис на rivendell
- **Секция:** инфра - **Секция:** Инфра
- **Зачем:** Сервис живёт на рабочей машине — телефон достаёт до него только дома - **Зачем:** Сервис живёт на рабочей машине — телефон достаёт до него только дома
- **Теги:** goal:deploy - **Теги:** goal:deploy
@@ -30,3 +30,5 @@
Том стоит смонтировать так, чтобы серверный бекап забирал его без отдельной Том стоит смонтировать так, чтобы серверный бекап забирал его без отдельной
настройки, и снимать копию SQLite через `VACUUM INTO`, а не `cp`: телефон шлёт настройки, и снимать копию SQLite через `VACUUM INTO`, а не `cp`: телефон шлёт
непрерывно, и файл под записью копировать нельзя. непрерывно, и файл под записью копировать нельзя.
Двигает строку «Завершения» цели: «Телефон шлёт на публичный адрес из любой сети, агент читает по тому же домену».
+9 -9
View File
@@ -1,17 +1,17 @@
# [goal] Деплой # [goal] Сервис доступен телефону из любой сети
- **Секция:** порядок - **Секция:** Разработка
- **Зачем:** Сервис живёт на рабочей машине — телефон достаёт до него только дома, и оба контура открыты - **Зачем:** Сервис живёт на рабочей машине — телефон достаёт до него только дома, и оба контура открыты
- **Теги:** decomposed - **Теги:** decomposed
Сервис переезжает на rivendell и становится доступен телефону из любой сети. Сервис переезжает на rivendell и становится доступен телефону из любой сети.
Выведена из шага 11 плана.
Завершена, когда оба контура закрыты разными токенами, откат релиза имеет
названный механизм, а запуск без конфига не заводит базу мимо данных.
## Завершение ## Завершение
Оба контура закрыты разными токенами, откат релиза имеет названный механизм, - Телефон шлёт на публичный адрес из любой сети, агент читает по тому же домену
а запуск без конфига не заводит базу мимо данных. - Оба контура закрыты разными токенами, и без токенов сервис стартует только на
localhost
- Откат релиза после наката миграции имеет названный механизм
- Запуск без конфига не заводит базу мимо `./data`
- Остановка сервиса называет виновный этап честно, а накат миграций виден в логе
старта
+4 -2
View File
@@ -1,6 +1,6 @@
# Выведенные из данных схемы содержимого # Выводить схемы содержимого из данных
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке - **Зачем:** Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
- **Теги:** goal:self-description - **Теги:** goal:self-description
@@ -25,3 +25,5 @@
Связано: `docs/architecture.md` → «Самоописание». Форму конверта API описывает Связано: `docs/architecture.md` → «Самоописание». Форму конверта API описывает
не эта задача, а OpenAPI. не эта задача, а OpenAPI.
Двигает строку «Завершения» цели: «Формы содержимого метрик выведены из данных, а не описаны руками».
+1 -1
View File
@@ -1,6 +1,6 @@
# [idea] Отказ от heartbeatSeries # [idea] Отказ от heartbeatSeries
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе - **Зачем:** 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе
- **Теги:** goal:lower-layer-cleanup - **Теги:** goal:lower-layer-cleanup
+4 -2
View File
@@ -1,6 +1,6 @@
# Пределы на размер сущности и потоковый расчёт формы # Ограничить размер сущности и считать форму потоково
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе - **Зачем:** Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе
- **Теги:** goal:limits-and-load - **Теги:** goal:limits-and-load
@@ -10,6 +10,8 @@
структурное: **предела на размер одной сущности нет вовсе**, а форма и хеш структурное: **предела на размер одной сущности нет вовсе**, а форма и хеш
считаются материализацией значения целиком. считаются материализацией значения целиком.
Двигает строку «Завершения» цели: «У тела, сущности и секции доставки есть названный предел».
## Оракул: измерено ## Оракул: измерено
Оракулы жили в `tmp/adv/mem_test.go` и `tmp/adv/lock_test.go`; числа снимались Оракулы жили в `tmp/adv/mem_test.go` и `tmp/adv/lock_test.go`; числа снимались
@@ -1,6 +1,6 @@
# Сущность с id, но неразобранной меткой # Не терять сущность с id и неразобранной меткой
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым - **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым
- **Теги:** goal:parsing-completeness - **Теги:** goal:parsing-completeness
@@ -22,6 +22,8 @@
разбор кладёт сущность в `ts_utc`/`start_utc`, колонки `NOT NULL`, и сущность разбор кладёт сущность в `ts_utc`/`start_utc`, колонки `NOT NULL`, и сущность
с неразбираемой меткой по-прежнему пропускается целиком. с неразбираемой меткой по-прежнему пропускается целиком.
Двигает строку «Завершения» цели: «Сущность с `id` и неразобранной меткой не пропадает целиком».
## Что известно ## Что известно
- Оракул: `internal/hae/entity_test.go`, случаи «метка в ином формате», «метка - Оракул: `internal/hae/entity_test.go`, случаи «метка в ином формате», «метка
+4 -2
View File
@@ -1,6 +1,6 @@
# Проверка целостности собранной витрины перед подменой # Проверять целостность собранной витрины до подмены
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Подмена файла необратима, а годность выхода подтверждена отпечатком на ещё открытом дескрипторе - **Зачем:** Подмена файла необратима, а годность выхода подтверждена отпечатком на ещё открытом дескрипторе
- **Теги:** goal:journal-and-rebuild - **Теги:** goal:journal-and-rebuild
@@ -27,3 +27,5 @@
называть результат годным, а на здоровом — не замедляется заметно. называть результат годным, а на здоровом — не замедляется заметно.
Связано: `cmd/healthlog/reindex.go`, `docs/architecture.md` → «Пересборка». Связано: `cmd/healthlog/reindex.go`, `docs/architecture.md` → «Пересборка».
Двигает строку «Завершения» цели: «Годность собранной витрины подтверждена до подмены файла».
+16 -10
View File
@@ -1,20 +1,26 @@
# [goal] Журнал и пересборка # [goal] Расхождение витрины с журналом не молчит
- **Секция:** темы - **Секция:** Направления
- **Зачем:** Расхождение витрины с журналом сейчас молчит: reindex печатает оба отпечатка, а сравнивать их некому - **Зачем:** Расхождение витрины с журналом сейчас молчит: reindex печатает оба отпечатка, а сравнивать их некому
- **Теги:** decomposed - **Теги:** decomposed
Тема: инвариант «`import` + `replay` даёт то же состояние» и всё, что его Направление: инвариант «`import` + `replay` даёт то же состояние» и всё, что его
держит — архив, ретеншен, отпечаток витрины, расход памяти пересборки. держит — архив, ретеншен, отпечаток витрины, расход памяти пересборки.
В порядок не встаёт: работа приходит находками и растёт вместе с В «Запланировано» не встаёт: работа приходит находками и растёт вместе с
журналом. журналом.
Завершена не бывает: закрывается по мере того, как расхождение витрины с
журналом перестаёт быть молчащим.
## Завершение ## Завершение
Завершена не бывает — это тема. Закрывается по мере того, как расхождение витрины Завершена не бывает — это направление. Закрывается по мере того, как расхождение
с журналом перестаёт быть молчащим, а расход пересборки — расти вместе с витрины с журналом перестаёт быть молчащим, а расход пересборки — расти вместе с
журналом. журналом. Открыто сегодня:
- Расхождение живой витрины с пересборкой замечает сервис, а не человек
- Годность собранной витрины подтверждена до подмены файла
- Порядок журнала держится при конкурентных приёмах
- Пересборка восстановима без базы: заголовки доставки лежат в архиве рядом с
телом
- Сырой архив подчищается до последнего проверенного экспорта
- Расход пересборки не растёт вместе с журналом
- Data-миграции не наследуют слепые зоны обрезаемых списков
+4 -2
View File
@@ -1,6 +1,6 @@
# Порядок журнала при конкурентных приёмах # Держать порядок журнала при конкурентных приёмах
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой - **Зачем:** Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой
- **Теги:** goal:journal-and-rebuild, question - **Теги:** goal:journal-and-rebuild, question
@@ -13,6 +13,8 @@
Вынут ревью кода задачи «Разнести ответ приёма и свёртку доставки» (профиль Вынут ревью кода задачи «Разнести ответ приёма и свёртку доставки» (профиль
`deep`, враждебный проход, находка с построенным путём и прогоном). `deep`, враждебный проход, находка с построенным путём и прогоном).
Двигает строку «Завершения» цели: «Порядок журнала держится при конкурентных приёмах».
## Вопросы ## Вопросы
**Решение (в) порядок журнала не восстанавливает, а цена окна выросла.** **Решение (в) порядок журнала не восстанавливает, а цена окна выросла.**
+15 -12
View File
@@ -1,6 +1,6 @@
# Правило полноты против last wins # Измерить, нужно ли правило полноты рядом с LWW
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Полнота решает 1,2% спорных координат, и неизвестно, была ли более полная точка более поздней — от этого зависит, нужна ли она вообще - **Зачем:** Полнота решает 1,2% спорных координат, и неизвестно, была ли более полная точка более поздней — от этого зависит, нужна ли она вообще
- **Теги:** goal:merge-robustness, sprint:2026-08-03 - **Теги:** goal:merge-robustness, sprint:2026-08-03
@@ -8,17 +8,21 @@
зависит ровно там, где замер показал, что без этого теряются данные. Сегодня зависит ровно там, где замер показал, что без этого теряются данные. Сегодня
неизвестно, какой из двух случаев верен. неизвестно, какой из двух случаев верен.
Двигает строку «Завершения» цели: «Правило выбора между версиями измерено: полнота либо нужна, либо снята».
## Откуда задача ## Откуда задача
Владелец предложил 2026-08-04 держаться стратегии **last wins**: экспорт Apple Владелец предложил 2026-08-04 держаться стратегии **LWW** («выигрывает
последняя»): экспорт Apple
Health — база снапшота, новые доставки HAE затирают предыдущие. Это отменяет Health — база снапшота, новые доставки HAE затирают предыдущие. Это отменяет
`critical`-инвариант `CLAUDE.md` «при столкновении выигрывает более полная `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 случаях более полная точка была более поздней Вопрос ровно один: **в этих 1 022 случаях более полная точка была более поздней
или более ранней?** или более ранней?**
- **Всегда более поздней** — полнота ничего не решает сверх порядка, `last wins` - **Всегда более поздней** — полнота ничего не решает сверх порядка, LWW
строго проще и ничего не теряет. Ветка полноты удаляется, инвариант в строго проще и ничего не теряет. Ветка полноты удаляется, инвариант в
`CLAUDE.md` переписывается. `CLAUDE.md` переписывается.
- **Иногда более ранней** — значит HAE присылает обеднённые версии задним - **Иногда более ранней** — значит 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` → «Разрешение Связано: находки 10, 47, 49, 53; `docs/architecture.md` → «Разрешение
столкновений»; `docs/review.md`, запись 2026-08-04. столкновений»; `docs/review.md`, запись 2026-08-04.
+10 -9
View File
@@ -1,18 +1,19 @@
# [goal] Пределы и поведение под объёмом # [goal] У каждого входа есть названный предел
- **Секция:** темы - **Секция:** Направления
- **Зачем:** Предела на одну сущность нет вовсе: 63 МиБ держат блокировку 5.019 с, соседние доставки уходят в failed - **Зачем:** Предела на одну сущность нет вовсе: 63 МиБ держат блокировку 5.019 с, соседние доставки уходят в failed
- **Теги:** decomposed - **Теги:** decomposed
Тема: названные пределы на размер тела, сущности, заголовков и ответа плюс Направление: названные пределы на размер тела, сущности, заголовков и ответа плюс
поведение под удерживаемой блокировкой. поведение под удерживаемой блокировкой.
В порядок не встаёт: пределы всплывают замерами, а не планом. В «Запланировано» не встаёт: предел находит замер, а не очередь.
Завершена не бывает: закрывается по мере того, как каждый вход получает
названный предел вместо подразумеваемого.
## Завершение ## Завершение
Завершена не бывает — это тема. Закрывается по мере того, как каждый вход получает Завершена не бывает — это направление. Закрывается по мере того, как каждый вход
названный предел вместо подразумеваемого. получает названный предел вместо подразумеваемого. Открыто сегодня:
- У тела, сущности и секции доставки есть названный предел
- У заголовков доставки есть названный предел
- Занятость базы не выводит доставку из очереди
+7 -10
View File
@@ -1,18 +1,15 @@
# [goal] Устаревание нижнего слоя # [goal] Нижний слой чистится после проверенного экспорта
- **Секция:** порядок - **Секция:** Запланировано
- **Зачем:** Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен - **Зачем:** Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
- **Теги:** decomposed - **Теги:** decomposed
После проверенного экспорта нижний слой HAE избыточен и подлежит чистке. После проверенного экспорта нижний слой HAE избыточен, и его можно чистить.
Выведена из шага 9 плана. Нижний слой растёт на ~100 тысяч координат в сутки. Нижний слой растёт на ~100 тысяч координат в сутки.
Завершена, когда чистка идёт по правилу, а не по календарю, и решение о
удалении опирается на колонку, отличающую ноль от «не измерялось».
## Завершение ## Завершение
Чистка идёт по правилу «до следующего проверенного экспорта», а не по - Нижний слой помечен покрытым после проверенного экспорта
календарю, и решение об удалении опирается на колонку, отличающую ноль от - Чистка идёт по правилу «до следующего проверенного экспорта», а не по календарю
«не измерялось». - Решение об удалении опирается на колонку, отличающую ноль от «не измерялось»
+4 -2
View File
@@ -1,6 +1,6 @@
# Устаревание нижнего слоя после экспорта # Помечать нижний слой устаревшим после экспорта
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен - **Зачем:** Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
- **Теги:** goal:lower-layer-cleanup - **Теги:** goal:lower-layer-cleanup
@@ -17,6 +17,8 @@
- пометка ≠ удаление. Удаление включается только после того, как восстановление - пометка ≠ удаление. Удаление включается только после того, как восстановление
из экспорта отработает на живых данных хотя бы раз. из экспорта отработает на живых данных хотя бы раз.
Двигает строку «Завершения» цели: «Нижний слой помечен покрытым после проверенного экспорта».
## Чем помечать: разряд на диапазон, а не провенанс на точку ## Чем помечать: разряд на диапазон, а не провенанс на точку
Решено при постановке 2026-08-04. Пометка — **одна строка на диапазон**: Решено при постановке 2026-08-04. Пометка — **одна строка на диапазон**:
+5 -3
View File
@@ -1,6 +1,6 @@
# MCP-сервер поверх Read API # Поднять MCP-сервер поверх Read API
- **Секция:** ядро — набор ограничен HTTP-слоем чтения после дробления; адаптер берётся следующим спринтом по той же цели - **Секция:** Ядро — набор ограничен HTTP-слоем чтения после дробления; адаптер берётся следующим спринтом по той же цели
- **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем - **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
- **Теги:** goal:read-api - **Теги:** goal:read-api
@@ -8,7 +8,7 @@
на дату последнего ручного экспорта. на дату последнего ручного экспорта.
Транспорт — **Streamable HTTP**, не stdio: сервис живёт на VPS, агент ходит по Транспорт — **Streamable HTTP**, не stdio: сервис живёт на VPS, агент ходит по
сети. Отсюда: MCP — эндпоинт того же процесса и того же порта, аутентификация — сети. Отсюда: MCP — маршрут того же процесса и того же порта, аутентификация —
тот же токен чтения, что у Read API. Отдельного контура доступа не заводим: тот же токен чтения, что у Read API. Отдельного контура доступа не заводим:
MCP не даёт ничего, чего не даёт HTTP, и права обязаны совпадать. MCP не даёт ничего, чего не даёт HTTP, и права обязаны совпадать.
@@ -21,6 +21,8 @@ MCP не даёт ничего, чего не даёт HTTP, и права об
Готово, когда агент подключается по URL и отвечает на «как я спал на прошлой Готово, когда агент подключается по URL и отвечает на «как я спал на прошлой
неделе» без промежуточного кода. неделе» без промежуточного кода.
Двигает строку «Завершения» цели: «Агент-медик читает то же самое через MCP тем же токеном чтения».
## Критерии приёмки ## Критерии приёмки
- живой агент подключается по URL и отвечает на «как я спал на прошлой неделе» - живой агент подключается по URL и отвечает на «как я спал на прошлой неделе»
+4 -2
View File
@@ -1,12 +1,14 @@
# Цена слияния на широкой доставке # Снизить цену слияния на широкой доставке
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** 63 МБ на одной координате держат транзакцию 5.15 с при busy_timeout 5 с — соседние доставки уходят в failed - **Зачем:** 63 МБ на одной координате держат транзакцию 5.15 с при busy_timeout 5 с — соседние доставки уходят в failed
- **Теги:** goal:limits-and-load - **Теги:** goal:limits-and-load
Вынуто ревью кода задачи `pravilo-sliyaniya-tochek` (профиль `deep`, враждебный Вынуто ревью кода задачи `pravilo-sliyaniya-tochek` (профиль `deep`, враждебный
проход и независимая реализация — независимо друг от друга). проход и независимая реализация — независимо друг от друга).
Двигает строку «Завершения» цели: «Занятость базы не выводит доставку из очереди».
## Оракул: измерено ## Оракул: измерено
Тело 63 МБ (в запросе ~200 КБ gzip — предел приёма 64 МиБ), 119 точек на ОДНОЙ Тело 63 МБ (в запросе ~200 КБ gzip — предел приёма 64 МиБ), 119 точек на ОДНОЙ
+5 -3
View File
@@ -1,12 +1,14 @@
# Счётчики слияния переживают ротацию логов # Хранить счётчики слияния вне логов
- **Секция:** инфра - **Секция:** Инфра
- **Зачем:** единственный след несравнимых наборов — строка WARN в docker-логе с ротацией 3×10 МБ: событие может произойти и не оставить ничего - **Зачем:** Единственный след несравнимых наборов — строка WARN в docker-логе с ротацией 3×10 МБ: событие может произойти и не оставить ничего
- **Теги:** goal:observability - **Теги:** goal:observability
Вынуто ревью кода задачи `pravilo-sliyaniya-tochek` (профиль `deep`, проход Вынуто ревью кода задачи `pravilo-sliyaniya-tochek` (профиль `deep`, проход
негативного пространства, подтверждено эксплуатационным). негативного пространства, подтверждено эксплуатационным).
Двигает строку «Завершения» цели: «Счётчики слияния переживают ротацию логов».
## Что не так ## Что не так
Решение не реализовывать объединение полей при несравнимых наборах стоит на Решение не реализовывать объединение полей при несравнимых наборах стоит на
+10 -9
View File
@@ -1,17 +1,18 @@
# [goal] Прочность слияния и идентичности # [goal] Исход слияния не зависит от порядка элементов на проводе
- **Секция:** темы - **Секция:** Направления
- **Зачем:** Правила слияния держатся на замерах, и каждый новый замер находит зависимость от порядка на проводе - **Зачем:** Правила слияния держатся на замерах, и каждый новый замер находит зависимость от порядка на проводе
- **Теги:** decomposed - **Теги:** decomposed
Тема: правила, по которым две версии одних данных превращаются в одну. Направление: правила, по которым две версии одних данных превращаются в одну.
В порядок не встаёт — работа приходит находками ревью и замерами на В «Запланировано» не встаёт — очереди у направления нет: работа приходит находками ревью и замерами на
живом корпусе. живом корпусе.
Завершена не бывает: закрывается по мере того, как правила перестают зависеть
от порядка на проводе.
## Завершение ## Завершение
Завершена не бывает — это тема. Закрывается по мере того, как правила выбора между Завершена не бывает — это направление. Закрывается по мере того, как правила
версиями перестают зависеть от порядка элементов на проводе. выбора между версиями перестают зависеть от порядка элементов на проводе.
Открыто сегодня:
- Правило выбора между версиями измерено: полнота либо нужна, либо снята
- Порог `sealed` выбран по накопленной статистике досчёта
@@ -1,6 +1,6 @@
# [idea] Месячный проход по ручным секциям # [idea] Месячный проход по ручным секциям
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит - **Зачем:** Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит
- **Теги:** goal:parsing-completeness - **Теги:** goal:parsing-completeness
+6 -8
View File
@@ -1,19 +1,17 @@
# [goal] Импорт родного экспорта Apple # [goal] История из родного экспорта Apple лежит в хранилище
- **Секция:** порядок - **Секция:** Запланировано
- **Зачем:** Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут - **Зачем:** Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
- **Теги:** decomposed - **Теги:** decomposed
`healthlog import`: снапшот всей истории из родного экспорта Apple Health `healthlog import`: снапшот всей истории из родного экспорта Apple Health
ложится в хранилище перед проигрыванием хвоста доставок. ложится в хранилище перед проигрыванием хвоста доставок.
Выведена из шага 8 плана. Идёт перед устареванием нижнего слоя намеренно: пока Идёт перед чисткой нижнего слоя намеренно: пока
импорт экспорта не написан, помечать что-либо устаревшим не на основании чего. импорт экспорта не написан, помечать что-либо устаревшим не на основании чего.
Завершена, когда слой `sample` наполнен историей с 2019 года, а повторный
импорт того же экспорта ничего не меняет.
## Завершение ## Завершение
Слой `sample` наполнен историей с 2019 года, повторный импорт того же экспорта - Слой `sample` наполнен историей с 2019 года
ничего не меняет, а тренировки из экспорта не задваивают приехавшие от HAE. - Повторный импорт того же экспорта ничего не меняет
- Тренировки из экспорта не задваивают приехавшие от HAE
+1 -1
View File
@@ -1,6 +1,6 @@
# [idea] NDJSON-поток для больших выборок Read API # [idea] NDJSON-поток для больших выборок Read API
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация - **Зачем:** Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация
- **Теги:** goal:read-api - **Теги:** goal:read-api
+6 -9
View File
@@ -1,18 +1,15 @@
# [goal] Наблюдаемость # [goal] Приложение сообщает о своём состоянии
- **Секция:** порядок - **Секция:** Запланировано
- **Зачем:** Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах - **Зачем:** Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах
- **Теги:** decomposed - **Теги:** decomposed
Тихо сломавшаяся автоматизация — главный эксплуатационный риск: телефон шлёт Тихо сломавшаяся автоматизация — главный эксплуатационный риск: телефон шлёт
молча, и молчание неотличимо от нормы. молча, и молчание неотличимо от нормы.
Выведена из шага 10 плана.
Завершена, когда пропажа потока и расхождение витрины с журналом видны
владельцу без чтения логов.
## Завершение ## Завершение
Пропажа потока и расхождение витрины с журналом видны владельцу без чтения - Пропажа потока видна владельцу без чтения логов
логов и переживают ротацию логов. - Состояние сервиса — последняя доставка, счётчики, тишина — читается одним
запросом
- Счётчики слияния переживают ротацию логов
+8 -4
View File
@@ -1,6 +1,6 @@
# Гейт против расхождения спеки с маршрутами # Ловить гейтом расхождение спеки с маршрутами
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Рукописная спека — источник истины, а расходится она с кодом молча: без гейта решение о рукописной спеке не держится - **Зачем:** Рукописная спека — источник истины, а расходится она с кодом молча: без гейта решение о рукописной спеке не держится
- **Теги:** goal:read-api, sprint:2026-08-04 - **Теги:** goal:read-api, sprint:2026-08-04
@@ -14,14 +14,18 @@
Класс отказа тот же, что у остальных безусловных шагов гейта проекта: не виден Класс отказа тот же, что у остальных безусловных шагов гейта проекта: не виден
глазами и стоит дорого. Место ему там же. глазами и стоит дорого. Место ему там же.
Двигает строку «Завершения» цели: «Контракт чтения читается машиной».
## Критерии приёмки ## Критерии приёмки
- добавленный маршрут без правки спеки красит гейт — оракул: намеренно - добавленный маршрут без правки спеки красит гейт — оракул: намеренно
рассогласованный маршрут в прогоне гейта рассогласованный маршрут в прогоне гейта
- переименованное поле ответа красит гейт — оракул: намеренное переименование в - переименованное поле ответа красит гейт — оракул: намеренное переименование в
прогоне гейта прогоне гейта
- проверка не ходит в сеть и укладывается в бюджет гейта — оракул: замер шага по - проверка укладывается в бюджет гейта — оракул: замер шага по логу
логу `tmp/gate/` `tmp/gate/`
- проверка работает без внешней сети — оракул: прогон гейта в контейнере без
доступа наружу
## Рамки ## Рамки
+7 -5
View File
@@ -1,6 +1,6 @@
# Рукописная OpenAPI-спека # Написать OpenAPI-спеку руками
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате - **Зачем:** Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
- **Теги:** goal:read-api, sprint:2026-08-04 - **Теги:** goal:read-api, sprint:2026-08-04
@@ -18,13 +18,15 @@
Заодно снимает вопрос, чем быть «схеме контракта API» из раздела самоописания: Заодно снимает вопрос, чем быть «схеме контракта API» из раздела самоописания:
ею и будет OpenAPI-документ, а не собственный формат. ею и будет OpenAPI-документ, а не собственный формат.
Двигает строку «Завершения» цели: «Контракт чтения читается машиной».
## Критерии приёмки ## Критерии приёмки
- по спеке генерируется клиент, и он выполняет запрос к живому сервису — оракул: - по спеке генерируется клиент, и он выполняет запрос к живому сервису — оракул:
прогон генератора плюс запрос сгенерированным клиентом прогон генератора плюс запрос сгенерированным клиентом
- спека покрывает все существующие маршруты: приём, каталог, точки, тренировки, - спека покрывает все маршруты, которые сервис действительно регистрирует —
записи — оракул: сверка перечня путей спеки с таблицей маршрутов в оракул: сверка перечня путей спеки с обходом роутера поднятого сервиса
`docs/architecture.md` (`chi.Walk`)
- спека проходит валидатор OpenAPI 3.1 — оракул: прогон валидатора - спека проходит валидатор OpenAPI 3.1 — оракул: прогон валидатора
## Рамки ## Рамки
+1 -1
View File
@@ -1,6 +1,6 @@
# [idea] Пересекающиеся источники одной метрики # [idea] Пересекающиеся источники одной метрики
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Сон пишут и часы, и стороннее приложение — сумма по обоим задвоит ночь - **Зачем:** Сон пишут и часы, и стороннее приложение — сумма по обоим задвоит ночь
- **Теги:** goal:read-api - **Теги:** goal:read-api
+1 -1
View File
@@ -1,6 +1,6 @@
# [idea] Выгрузка в parquet отдельной командой # [idea] Выгрузка в parquet отдельной командой
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно - **Зачем:** Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно
- **Теги:** goal:read-api - **Теги:** goal:read-api
+13 -8
View File
@@ -1,9 +1,10 @@
# [goal] Полнота разбора потока # [goal] Новая форма от источника не теряется молча
- **Секция:** темы - **Секция:** Направления
- **Зачем:** Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы - **Зачем:** Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы
- **Теги:** decomposed
Тема: всё, что приезжает от источника, разобрано и доехало до витрины — не Направление: всё, что приезжает от источника, разобрано и доехало до витрины — не
только сегодня, но и после того, как источник изменится. только сегодня, но и после того, как источник изменится.
Выделена из цели «Разбор и хранилище», когда та достигла своего критерия Выделена из цели «Разбор и хранилище», когда та достигла своего критерия
@@ -12,11 +13,15 @@
вправе прислать форму, которой раньше не было, а часть секций заводится вправе прислать форму, которой раньше не было, а часть секций заводится
человеком задним числом. человеком задним числом.
В порядок не встаёт: работа приходит от потока, а не от плана. Первая встреча В «Запланировано» не встаёт: работа приходит от потока, а не от очереди. Первая встреча
новой секции наблюдаема (`healthlog uncovered` и `WARN` на свёртке) — тема новой секции наблюдаема (`healthlog uncovered` и `WARN` на свёртке) — работа
кормится этими событиями. направления приходит от этих событий.
## Завершение ## Завершение
Завершена не бывает — это тема. Закрывается по мере того, как каждая приезжающая Завершена не бывает — это направление. Закрывается по мере того, как каждая
форма доезжает до витрины, а не теряется между «принято» и «разобрано». приезжающая форма доезжает до витрины, а не теряется между «принято» и
«разобрано». Открыто сегодня:
- Сущность с `id` и неразобранной меткой не пропадает целиком
- Ручные секции, заведённые задним числом, доезжают до витрины
+5 -3
View File
@@ -1,7 +1,7 @@
# Ретеншен сырого архива # Подчищать сырой архив до последнего проверенного экспорта
- **Секция:** инфра - **Секция:** Инфра
- **Зачем:** Архив не подчищается вовсе — 14-дневный срок объявлен, но не работает - **Зачем:** Архив не подчищается вовсе, а резать его раньше даты проверенного экспорта нельзя — в журнале останется дыра, которую нечем пересобрать
- **Теги:** goal:journal-and-rebuild - **Теги:** goal:journal-and-rebuild
Срок жизни сырого архива объявлен (14 дней, `storage.raw_retention`), но Срок жизни сырого архива объявлен (14 дней, `storage.raw_retention`), но
@@ -28,6 +28,8 @@
экспорта, записи `stateOfMind` не трогаются вовсе, а `/stats` показывает экспорта, записи `stateOfMind` не трогаются вовсе, а `/stats` показывает
глубину архива и дату снапшота, до которой он подрезан. глубину архива и дату снапшота, до которой он подрезан.
Двигает строку «Завершения» цели: «Сырой архив подчищается до последнего проверенного экспорта».
## Предусловие снова открыто ## Предусловие снова открыто
Признак «доставка с непокрытой секцией» появился в change Признак «доставка с непокрытой секцией» появился в change
+13 -5
View File
@@ -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` — доля **Готовые решения задают порог противоположно.** Graphite `xFilesFactor` — доля
обязательно известных точек (умолчание 0.5 при роллапе и 0 при рендере: один обязательно известных точек (умолчание 0.5 при свёртке на записи и 0 при
параметр с двумя умолчаниями), RRDtool `xff` — доля допустимо неизвестных. Обе отдаче ответа: один параметр с двумя умолчаниями), RRDtool `xff` — доля допустимо неизвестных. Обе
величины выглядят как «0.5», означая разное. Полярность придётся назвать вслух, величины выглядят как «0.5», означая разное. Полярность придётся назвать вслух,
иначе через полгода два места кода поймут поле по-разному — и разойдутся молча. иначе через полгода два места кода поймут поле по-разному — и разойдутся молча.
Двигает строку «Завершения» цели: «Неполное ведро отличимо от полного, и полярность порога названа».
## Затрагивает
Форма ответа свёртки — признак неполного ведра рядом со значением. Конфиг и его
образцы — порог с названной полярностью. Раздел о свёртке в
`docs/architecture.md`. Схемы и формата на диске не трогает.
## Критерии приёмки ## Критерии приёмки
- полярность и умолчание порога названы в `docs/architecture.md` одной - полярность и умолчание порога названы в `docs/architecture.md` одной
+12 -4
View File
@@ -1,8 +1,8 @@
# Свёртка по сетке # Сворачивать точки по заданной сетке
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** «Шаги за неделю по дням» — базовый запрос трекера и игры, и сегодня его нечем задать - **Зачем:** «Шаги за неделю по дням» — базовый запрос трекера и игры, и сегодня его нечем задать
- **Теги:** goal:read-api, sprint:2026-08-04 - **Теги:** goal:read-api, sprint:2026-08-04, kind:feature
«Шаги за неделю по дням» отвечаются одним запросом `?from&to&bucket`, и род «Шаги за неделю по дням» отвечаются одним запросом `?from&to&bucket`, и род
свёртки берётся измеренным, а не угаданным. свёртки берётся измеренным, а не угаданным.
@@ -17,7 +17,15 @@
завышает втрое. завышает втрое.
Порог неполного ведра и предел размера ответа — соседние задачи; здесь они Порог неполного ведра и предел размера ответа — соседние задачи; здесь они
берутся в том виде, в каком есть на момент мерджа, и не проектируются. берутся в том виде, в каком есть на момент вливания, и не проектируются.
Двигает строку «Завершения» цели: «Точки сворачиваются по заданной сетке измеренным родом агрегации».
## Затрагивает
Маршрут `GET /api/v1/metrics/{name}` — параметр запроса `bucket`, поля
`bucket` и `aggregation` в конверте ответа, код отказа на метрике с неизвестным
родом. Схемы и формата на диске не трогает.
## Критерии приёмки ## Критерии приёмки
@@ -1,8 +1,8 @@
# Условный запрос по точкам # Отвечать 304 на повторный запрос точек
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Агент опрашивает по расписанию, а каждый повтор стоит полного чтения: на каталоге это 693 мс и +153 МиБ - **Зачем:** Агент опрашивает по расписанию, а каждый повтор стоит полного чтения: на каталоге это 693 мс и +153 МиБ
- **Теги:** goal:read-api, sprint:2026-08-04 - **Теги:** goal:read-api, sprint:2026-08-04, kind:feature
Повторный опрос точек с той же меткой стоит `304` вместо полного чтения, и метка Повторный опрос точек с той же меткой стоит `304` вместо полного чтения, и метка
не может ответить на другой набор данных. не может ответить на другой набор данных.
@@ -19,6 +19,15 @@
враждебном запросе. Агент опрашивает по расписанию, и без условного запроса враждебном запросе. Агент опрашивает по расписанию, и без условного запроса
каждый его повтор стоит полного чтения. каждый его повтор стоит полного чтения.
Двигает строку «Завершения» цели: «Повторный запрос тех же точек стоит `304`, а не полного чтения».
## Затрагивает
Маршрут `GET /api/v1/metrics/{name}` — заголовки `ETag` и `If-None-Match`,
код ответа `304`. Область действия метки: набор параметров запроса (метрика,
окно, слой) и версия витрины, из которых метка считается, и их каноническая
форма. Схемы и формата на диске не трогает.
## Критерии приёмки ## Критерии приёмки
- повторный запрос с `If-None-Match` при неизменной витрине даёт `304` — оракул: - повторный запрос с `If-None-Match` при неизменной витрине даёт `304` — оракул:
+12 -4
View File
@@ -1,13 +1,13 @@
# Записи наружу # Отдавать записи со своим id за период
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** stateOfMind разобран и хранится, но наружу не отдаётся — а восстановить его нечем: в экспорте Apple его нет - **Зачем:** stateOfMind разобран и хранится, но наружу не отдаётся — а восстановить его нечем: в экспорте Apple его нет
- **Теги:** goal:read-api, sprint:2026-08-04 - **Теги:** goal:read-api, sprint:2026-08-04, kind:feature
Записи со своим `id` — сегодня это `stateOfMind` — достаются за период через Записи со своим `id` — сегодня это `stateOfMind` — достаются за период через
`GET /records/{kind}`. `GET /records/{kind}`.
Разбор и хранение сделаны тем же change, что у тренировок; наружу не отдаётся Разбор и хранение сделаны тем же изменением, что у тренировок; наружу не отдаётся
ничего. У этих данных есть особенность, которой нет больше ни у чего в проекте: ничего. У этих данных есть особенность, которой нет больше ни у чего в проекте:
**`stateOfMind` нет в экспорте Apple**, он не восстанавливается пересборкой из **`stateOfMind` нет в экспорте Apple**, он не восстанавливается пересборкой из
снапшота, и единственный его источник — доставки HAE. Отдача наружу — не снапшота, и единственный его источник — доставки HAE. Отдача наружу — не
@@ -15,6 +15,14 @@
Конверт наследуется от точек; собственной формы у записей нет. Конверт наследуется от точек; собственной формы у записей нет.
Двигает строку «Завершения» цели: «Тренировки с маршрутом и записи со своим `id` отдаются за период».
## Затрагивает
Новый маршрут `GET /api/v1/records/{kind}` и код отказа на неизвестном `kind`.
Публичный тип провода в `internal/httpapi` — конверт записей. Чтение таблицы
записей; схемы и формата на диске не трогает.
## Критерии приёмки ## Критерии приёмки
- записи `stateOfMind` за период отдаются одним запросом — оракул: запрос к - записи `stateOfMind` за период отдаются одним запросом — оракул: запрос к
+18 -7
View File
@@ -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 МиБ. Множители «метрики × приём в том же процессе уже даёт пик 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`
и раздельными бюджетами остановки. и раздельными бюджетами остановки.
+14 -5
View File
@@ -1,14 +1,14 @@
# Тренировки наружу # Отдавать тренировки вместе с маршрутом
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Тренировки с маршрутами разобраны и лежат в витрине, а эндпоинтов нет — сценарий трекера не закрыт - **Зачем:** Тренировки с маршрутами разобраны и лежат в витрине, а маршрутов чтения нет — сценарий трекера не закрыт
- **Теги:** goal:read-api, sprint:2026-08-04 - **Теги:** goal:read-api, sprint:2026-08-04, kind:feature
Трекер забирает тренировку одним пакетом вместе с маршрутом: `GET /workouts` за Трекер забирает тренировку одним пакетом вместе с маршрутом: `GET /workouts` за
период и `GET /workouts/{id}` поштучно. период и `GET /workouts/{id}` поштучно.
Разбор и хранение тренировок сделаны (change `2026-08-02-trenirovki-i-zapisi`), Разбор и хранение тренировок сделаны (change `2026-08-02-trenirovki-i-zapisi`),
эндпоинтов нет: тренировка с маршрутом лежит в витрине и наружу не отдаётся. маршрутов чтения нет: тренировка с маршрутом лежит в витрине и наружу не отдаётся.
Второй сценарий паспорта — трекер тренировок — до тех пор не закрыт. Второй сценарий паспорта — трекер тренировок — до тех пор не закрыт.
**Это первый по-настоящему тяжёлый ответ проекта.** Маршрут лежит блобом внутри **Это первый по-настоящему тяжёлый ответ проекта.** Маршрут лежит блобом внутри
@@ -19,6 +19,15 @@
Конверт и форма провода наследуются, а не изобретаются. Конверт и форма провода наследуются, а не изобретаются.
Двигает строку «Завершения» цели: «Тренировки с маршрутом и записи со своим `id` отдаются за период».
## Затрагивает
Два новых маршрута: `GET /api/v1/workouts` за период и
`GET /api/v1/workouts/{id}` поштучно. Публичные типы провода в
`internal/httpapi` — конверт тренировки и элемент списка. Чтение таблицы
тренировок; схемы и формата на диске не трогает.
## Критерии приёмки ## Критерии приёмки
- тренировка отдаётся одним пакетом вместе с маршрутом — оракул: запрос к - тренировка отдаётся одним пакетом вместе с маршрутом — оракул: запрос к
+13 -7
View File
@@ -1,13 +1,13 @@
# [goal] Чтение данных клиентами # [goal] Клиенты читают данные через HTTP и MCP
- **Секция:** порядок - **Секция:** Запланировано
- **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может - **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может
- **Теги:** decomposed - **Теги:** decomposed
Потребители читают данные: точки с выбором слоя и свёрткой по сетке, тренировки Потребители читают данные: точки с выбором слоя и свёрткой по сетке, тренировки
и записи, машиночитаемый контракт — и всё то же самое через MCP. и записи, машиночитаемый контракт — и всё то же самое через MCP.
Выведена из шагов 5 и 7 плана. Идёт после каталога и рода агрегации намеренно: Идёт после каталога и рода агрегации намеренно:
без измеренного рода свёртка в ответе неотличима от угадывания, а ошибиться без измеренного рода свёртка в ответе неотличима от угадывания, а ошибиться
здесь дорого — просуммировать нижний слой значит завысить втрое. здесь дорого — просуммировать нижний слой значит завысить втрое.
@@ -20,7 +20,13 @@
## Завершение ## Завершение
Любой из трёх потребителей получает точки, тренировки и записи за период без - Точки метрики за период отдаются по HTTP без доступа к файлу базы
доступа к файлу базы; предел размера ответа объявлен, а не подразумевается; - Повторный запрос тех же точек стоит `304`, а не полного чтения
агент-медик читает то же самое через MCP тем же токеном чтения, и собственной - Точки сворачиваются по заданной сетке измеренным родом агрегации
логики адаптер не несёт. - Неполное ведро отличимо от полного, и полярность порога названа
- У ответа любого маршрута чтения есть объявленный предел размера
- Тренировки с маршрутом и записи со своим `id` отдаются за период
- Контракт чтения читается машиной: спека, гейт против её расхождения с
маршрутами, UI без внешней сети
- Агент-медик читает то же самое через MCP тем же токеном чтения, и собственной
логики адаптер не несёт
+5 -3
View File
@@ -1,6 +1,6 @@
# Сверка живой витрины с пересборкой # Сверять живую витрину с пересборкой
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит - **Зачем:** reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит
- **Теги:** goal:journal-and-rebuild - **Теги:** goal:journal-and-rebuild
@@ -10,7 +10,7 @@
только тем, что кто-то вручную запустил пересборку и посмотрел на два числа. только тем, что кто-то вручную запустил пересборку и посмотрел на два числа.
Между тем расхождение — не гипотеза. Известный путь к нему записан блокером Между тем расхождение — не гипотеза. Известный путь к нему записан блокером
[«Порядок журнала при конкурентных приёмах»](journal-order-on-ingest.md): [«Держать порядок журнала при конкурентных приёмах»](journal-order-on-ingest.md):
доставка, свёрнутая раньше своей предшественницы, уходит в `failed` навсегда, и доставка, свёрнутая раньше своей предшественницы, уходит в `failed` навсегда, и
живая витрина расходится с пересборкой молча. Пока тот предел не закрыт, сверка живая витрина расходится с пересборкой молча. Пока тот предел не закрыт, сверка
— единственный способ узнать, что он сработал. — единственный способ узнать, что он сработал.
@@ -30,3 +30,5 @@
Связано: `cmd/healthlog/reindex.go`, [наблюдаемость](stats-endpoint.md), Связано: `cmd/healthlog/reindex.go`, [наблюдаемость](stats-endpoint.md),
[деплой](deploy-rivendell.md). [деплой](deploy-rivendell.md).
Двигает строку «Завершения» цели: «Расхождение живой витрины с пересборкой замечает сервис, а не человек».
+4 -2
View File
@@ -1,6 +1,6 @@
# Пересборка держит весь журнал в памяти # Не держать весь журнал в памяти при пересборке
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет - **Зачем:** Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет
- **Теги:** goal:journal-and-rebuild - **Теги:** goal:journal-and-rebuild
@@ -26,3 +26,5 @@
памяти, не зависящим от его длины. памяти, не зависящим от его длины.
Связано: `internal/replay`, `cmd/healthlog/reindex.go`. Связано: `internal/replay`, `cmd/healthlog/reindex.go`.
Двигает строку «Завершения» цели: «Расход пересборки не растёт вместе с журналом».
@@ -1,6 +1,6 @@
# Чем откатывать релиз после наката миграции # Назвать механизм отката релиза после наката миграции
- **Секция:** инфра - **Секция:** Инфра
- **Зачем:** Страж версии схемы делает возврат старого бинаря отказом, а понизить схему нечем — аварийный путь пришлось бы изобретать в аварии - **Зачем:** Страж версии схемы делает возврат старого бинаря отказом, а понизить схему нечем — аварийный путь пришлось бы изобретать в аварии
- **Теги:** goal:deploy - **Теги:** goal:deploy
@@ -15,6 +15,8 @@
Вынуто ревью кода задачи «Дозакрыть находки ревью по слиянию сущностей» Вынуто ревью кода задачи «Дозакрыть находки ревью по слиянию сущностей»
(проходы `ops` и `negative`, профиль `deep`). (проходы `ops` и `negative`, профиль `deep`).
Двигает строку «Завершения» цели: «Откат релиза после наката миграции имеет названный механизм».
## Что именно решить ## Что именно решить
Та задача перенесла в `store.Open` стража версии схемы: база новее бинаря — Та задача перенесла в `store.Open` стража версии схемы: база новее бинаря —
+1 -1
View File
@@ -1,6 +1,6 @@
# [idea] Человеческие аннотации поверх выведенных схем # [idea] Человеческие аннотации поверх выведенных схем
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Выведенная схема говорит форму, но не смысл метрики — нужно ли описание сверху, зависит от стабильности формата - **Зачем:** Выведенная схема говорит форму, но не смысл метрики — нужно ли описание сверху, зависит от стабильности формата
- **Теги:** goal:self-description - **Теги:** goal:self-description
+1 -1
View File
@@ -1,6 +1,6 @@
# [idea] Порог sealed: с какого возраста час считается запечатанным # [idea] Порог sealed: с какого возраста час считается запечатанным
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** WARN на изменение старого часа уже пишется, но порог не выбран — ставим по факту, когда накопится статистика досчёта - **Зачем:** WARN на изменение старого часа уже пишется, но порог не выбран — ставим по факту, когда накопится статистика досчёта
- **Теги:** goal:merge-robustness - **Теги:** goal:merge-robustness
+4 -9
View File
@@ -1,17 +1,12 @@
# [goal] Самоописание # [goal] Клиент узнаёт форму данных из ответа
- **Секция:** порядок - **Секция:** Запланировано
- **Зачем:** Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке - **Зачем:** Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
- **Теги:** decomposed - **Теги:** decomposed
Клиент узнаёт форму данных из ответа сервиса, а не угадывает её по выборке. Клиент узнаёт форму данных из ответа сервиса, а не угадывает её по выборке.
Выведена из шага 6 плана.
Завершена, когда контракт читается машиной, а формы содержимого метрик
выведены из данных, а не описаны руками.
## Завершение ## Завершение
Контракт читается машиной, а формы содержимого метрик выведены из данных, а не - Формы содержимого метрик выведены из данных, а не описаны руками
описаны руками. - Клиент узнаёт форму одной метрики и всего хранилища одним запросом
@@ -1,6 +1,6 @@
# Остановка и миграция: раздельные бюджеты и следы в логе # Развести бюджеты остановки и оставить следы миграции в логе
- **Секция:** инфра - **Секция:** Инфра
- **Зачем:** Долгий запрос чтения съедает бюджет остановки, и WARN обвиняет воркер свёртки; миграция молчит и не прерывается SIGTERM - **Зачем:** Долгий запрос чтения съедает бюджет остановки, и WARN обвиняет воркер свёртки; миграция молчит и не прерывается SIGTERM
- **Теги:** goal:deploy - **Теги:** goal:deploy
@@ -49,3 +49,5 @@
Связано: `cmd/healthlog/serve.go`, `internal/store/store.go`, change Связано: `cmd/healthlog/serve.go`, `internal/store/store.go`, change
`2026-08-02-cena-chitayushchego-marshruta` (архив). `2026-08-02-cena-chitayushchego-marshruta` (архив).
Двигает строку «Завершения» цели: «Остановка сервиса называет виновный этап честно, а накат миграций виден в логе старта».
+4 -2
View File
@@ -1,6 +1,6 @@
# Наблюдаемость: /stats # Отдавать состояние сервиса маршрутом /stats
- **Секция:** инфра - **Секция:** Инфра
- **Зачем:** Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах - **Зачем:** Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах
- **Теги:** goal:observability - **Теги:** goal:observability
@@ -59,3 +59,5 @@
сколько страниц лежит и сколько перенесено) и — тем же полем — доля ответов сколько страниц лежит и сколько перенесено) и — тем же полем — доля ответов
чтения, которые удалось подписать `ETag`: механизм условного запроса может чтения, которые удалось подписать `ETag`: механизм условного запроса может
перестать окупаться под плотным потоком, и снаружи это неотличимо от нормы. перестать окупаться под плотным потоком, и снаружи это неотличимо от нормы.
Двигает строку «Завершения» цели: «Состояние сервиса — последняя доставка, счётчики, тишина — читается одним запросом».
+6 -4
View File
@@ -1,6 +1,6 @@
# Активный алерт «данных нет N часов» # Слать уведомление, когда данных нет N часов
- **Секция:** инфра - **Секция:** Инфра
- **Зачем:** Пропажу потока сейчас замечает человек, а не сервис - **Зачем:** Пропажу потока сейчас замечает человек, а не сервис
- **Теги:** goal:observability - **Теги:** goal:observability
@@ -18,10 +18,12 @@
После деплоя на rivendell поднимется. После деплоя на rivendell поднимется.
## Источник алерта не может жить внутри `serve` Двигает строку «Завершения» цели: «Пропажа потока видна владельцу без чтения логов».
## Источник уведомления не может жить внутри `serve`
Отказ стража версии схемы (база новее бинаря) останавливает процесс, а Отказ стража версии схемы (база новее бинаря) останавливает процесс, а
`restart: unless-stopped` даёт цикл перезапуска. Значит алерт «данных нет N `restart: unless-stopped` даёт цикл перезапуска. Значит уведомление «данных нет N
часов», живущий внутри сервиса, на эту причину остановки не сработает **по часов», живущий внутри сервиса, на эту причину остановки не сработает **по
построению** — он не поднимется вместе с ним. Обоснование стража («откат делает построению** — он не поднимется вместе с ним. Обоснование стража («откат делает
оператор, он в этот момент рядом») верно для ручного отката и не покрывает оператор, он в этот момент рядом») верно для ручного отката и не покрывает
+11 -7
View File
@@ -1,7 +1,7 @@
# Swagger UI без внешней сети # Поднять Swagger UI без внешней сети
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Контракт читается машиной, но человеку нечем ткнуть в живой сервис, а внешних CDN в локальной сети нет - **Зачем:** Контракт читается машиной, но человеку нечем выполнить запрос к живому сервису из браузера, а внешних CDN в локальной сети нет
- **Теги:** goal:read-api, sprint:2026-08-04 - **Теги:** goal:read-api, sprint:2026-08-04
Человек открывает UI на отдельном пути и выполняет запрос к живому сервису — не Человек открывает UI на отдельном пути и выполняет запрос к живому сервису — не
@@ -11,18 +11,22 @@
статика отдаётся самим сервисом и лежит в бинаре: внешние CDN здесь означают статика отдаётся самим сервисом и лежит в бинаре: внешние CDN здесь означают
«работает, пока работает чужой сайт». «работает, пока работает чужой сайт».
**UI — новый адресат недоверенного входа наизнанку:** он даёт человеку в один **UI — новый адресат недоверенного входа наизнанку:** он даёт человеку одним
клик дёрнуть любой описанный маршрут, включая приём. Права на запись из нажатием выполнить запрос к любому описанному маршруту, включая приём. Права
браузера не должны появляться сами собой — это ровно та граница, которую на запись из браузера не должны появляться сами собой — это ровно та граница, которую
описывает `docs/security.md`. описывает `docs/security.md`.
Двигает строку «Завершения» цели: «Контракт чтения читается машиной».
## Критерии приёмки ## Критерии приёмки
- UI открывается и выполняет запрос при отключённой внешней сети — оракул: - UI открывается и выполняет запрос при отключённой внешней сети — оракул:
запуск контейнера без доступа наружу запуск контейнера без доступа наружу
- бинарь работает без каталога со статикой рядом — оракул: запуск одного файла - бинарь работает без каталога со статикой рядом — оракул: запуск одного файла
из пустого каталога из пустого каталога
- маршрут приёма из UI не вызывается без явного токена приёма — оракул: тест - запрос к маршруту приёма без заголовка с токеном приёма отклоняется, откуда
бы он ни пришёл, а отдаваемая UI статика токена приёма в себе не держит —
оракул: тест на обработчике приёма плюс поиск токена в отдаваемой статике
## Рамки ## Рамки
@@ -1,6 +1,6 @@
# Управление токенами и секретами # Развести токены контуров и убрать секреты из репозитория
- **Секция:** инфра - **Секция:** Инфра
- **Зачем:** Проверка токенов выключена, config.docker.toml коммитится — так нельзя выезжать наружу - **Зачем:** Проверка токенов выключена, config.docker.toml коммитится — так нельзя выезжать наружу
- **Теги:** goal:deploy - **Теги:** goal:deploy
@@ -29,3 +29,5 @@
Готово, когда запуск без токенов возможен только на localhost, а на rivendell Готово, когда запуск без токенов возможен только на localhost, а на rivendell
оба контура закрыты разными токенами. оба контура закрыты разными токенами.
Двигает строку «Завершения» цели: «Оба контура закрыты разными токенами, и без токенов сервис стартует только на localhost».
@@ -1,6 +1,6 @@
# Идентичность тренировок при импорте родного экспорта # Не задваивать тренировки при импорте родного экспорта
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE - **Зачем:** В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE
- **Теги:** goal:native-export-import - **Теги:** goal:native-export-import
@@ -37,3 +37,5 @@ Prior art: `dogsheep/healthkit-to-sqlite` адресует тренировку
Связано: `docs/architecture.md` → «Тренировки и прочие секции», задача Связано: `docs/architecture.md` → «Тренировки и прочие секции», задача
`apple-export-import`. `apple-export-import`.
Двигает строку «Завершения» цели: «Тренировки из экспорта не задваивают приехавшие от HAE».
+1 -1
View File
@@ -1,6 +1,6 @@
# [idea] Разворачивание маршрутов тренировок в отдельную таблицу # [idea] Разворачивание маршрутов тренировок в отдельную таблицу
- **Секция:** ядро - **Секция:** Ядро
- **Зачем:** Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом - **Зачем:** Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом
- **Теги:** goal:read-api - **Теги:** goal:read-api
+1 -1
View File
@@ -65,7 +65,7 @@ func TestAcceptStoresBodyVerbatim(t *testing.T) {
} }
// Повтор того же тела пока принимается — отсев идентичных доставок отложен // Повтор того же тела пока принимается — отсев идентичных доставок отложен
// (docs/tasks/PLAN.md). Проверяем, что повтор не ломается и не затирает первую. // (docs/tasks/ROADMAP.md). Проверяем, что повтор не ломается и не затирает первую.
func TestAcceptAllowsRepeatedBody(t *testing.T) { func TestAcceptAllowsRepeatedBody(t *testing.T) {
svc, _, st := newService(t) svc, _, st := newService(t)
body := []byte(`{"data":{"metrics":[]}}`) body := []byte(`{"data":{"metrics":[]}}`)