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