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

- каждая запись каталога задач получила тип вместо тега kind: и префикса
  заголовка; секция роадмапа «Разработка» стала «Сопровождением», порядок
  секций канонический
- поправлены протухшие факты: нереализованные маршруты Read API, MCP и
  `healthlog import`, словарь слоёв в инварианте, семантика гейта по покрытию
  диффа, периметр перестал дублировать security.md
- замер слияния переведён с находки 49 на находку 54, заполнены Purpose спек
  storage и parsing
This commit is contained in:
av
2026-08-05 19:09:35 +03:00
parent e4f62785d8
commit 3d24248075
66 changed files with 322 additions and 227 deletions
+12 -7
View File
@@ -13,7 +13,7 @@
Коллектор данных Apple Health. Принимает выгрузки из Health Auto Export и
родного экспорта Apple, хранит их и отдаёт другим моим проектам — через HTTP
API и через MCP. Это **хранилище, а не аналитика**: принять, дедуплицировать,
API и, в планах, через MCP. Это **хранилище, а не аналитика**: принять, дедуплицировать,
сохранить, отдать. Не переименовывать поля Apple, не интерпретировать
значения. Агрегат считается только в ответе на запрос и только там, где род
метрики измерен.
@@ -22,7 +22,7 @@ API и через MCP. Это **хранилище, а не аналитика**
Go, один статический бинарь (`CGO_ENABLED=0`). SQLite (`modernc.org/sqlite`,
чистый Go), `chi`, `sqlx`, `goose` (миграции), `pelletier/go-toml/v2`,
`log/slog`, ULID через `internal/ident`.
`log/slog`, ULID (`github.com/oklog/ulid/v2`) через `internal/ident`.
Module path — `git.vakhrushev.me/av/healthlog`.
@@ -65,8 +65,10 @@ Module path — `git.vakhrushev.me/av/healthlog`.
образ жизни» на языке телефона, а родной экспорт — коды HealthKit, и без
словаря эти два источника не сойтись.
- **Своей агрегации в хранении нет — есть слои.** `critical`, обратимо
пересборкой. Метрика лежит в той подробности, в какой пришла (`sample`/`raw`/`minute`/`hour`); слой выводится
из выравнивания меток, а не из заголовка HAE — тот врёт.
пересборкой. Метрика лежит в той подробности, в какой пришла
(`sample`/`raw`/`minute`/`hour`/`day`); слой выводится
из выравнивания меток, а не из заголовка HAE — тот врёт. Перечень слоёв один и
лежит в [docs/database.md](docs/database.md), таблица `bucket`.
- **Агрегация в ответе — только измеренная.** `critical`, обратимо: ответ не
хранится, но потребитель уже принял по нему решение. Род свёртки выводится
сверкой слоёв между собой (часовое = сумма минутных → накопительная, = среднее →
@@ -110,9 +112,12 @@ Module path — `git.vakhrushev.me/av/healthlog`.
- **Исходы:** 0 — зелёный; ненулевой — красный, и до его починки опиниативные
проходы ревью **не запускаются**.
- **Что красит безусловно:** любой файл из `./data` в индексе, любой токен в
индексе, непокрытая изменённая строка, миграция без правки `docs/database.md`.
Причина одна на все: это ровно те отказы, которые не видны глазами и стоят
необратимо.
индексе, миграция без правки `docs/database.md`. Причина одна на все: это
ровно те отказы, которые не видны глазами и стоят необратимо. Покрытие
изменённых строк задумано тем же классом, но сегодня гейт от него **не
краснеет**: `scripts/diff-coverage.py` всегда возвращает `0`, и шаг печатает
`OK` при любом покрытии — разбор непокрытых строк остаётся человеку или
проходу ревью. Запись 2026-08-04 в [docs/review.md](docs/review.md).
- **Чего в гейте намеренно нет и кто обязан это гонять:**
`task verify:archive` (минута прогона, данные есть только на этой машине) и
`task verify:busy` (около 50 секунд). Гоняет их **человек или оркестратор задачи**
+1 -1
View File
@@ -1,4 +1,4 @@
{
"canon": 3,
"canon": 4,
"migrations": "internal/store/migrations"
}
@@ -1,7 +1,7 @@
# Код HealthKit кладётся реестром рядом, а не полем внутри точки
- Дата: 2026-08-03
- Источник: openspec/changes/archive/2026-08-03-slovar-kategorialnyh-znachenij/design.md
- **Дата:** 2026-08-03
- **Источник:** openspec/changes/archive/2026-08-03-slovar-kategorialnyh-znachenij/design.md
## Решение
+1 -4
View File
@@ -28,10 +28,7 @@
## Записи
Новые сверху.
| Дата | Запись | Статус |
| --- | --- | --- |
Новые сверху. Все шесть активны — статуса поэтому ни у одной нет.
- [ADR-2026-08-04-otvet-tochek-nesyot-rod-i-ego-primenimost](ADR-2026-08-04-otvet-tochek-nesyot-rod-i-ego-primenimost.md)
— конверт точек несёт измеренный род, его **применимость к отданному ряду** и
+54 -32
View File
@@ -46,12 +46,14 @@ healthlog принимает выгрузки Apple Health из приложен
с переведённой строкой (см. «Категориальные значения»): он приписывается, а
не подменяет.
- **Своей агрегации в хранении нет — есть слои.** Метрика лежит в тех
разрезах подробности, в которых пришла (`sample`/`raw`/`minute`/`hour`);
переагрегирования при записи не происходит никогда.
- **Агрегация в ответе — только измеренная.** Read API умеет свести метрику к
запрошенной сетке, но род свёртки (сумма или среднее) выведен сверкой слоёв
между собой, а не проставлен вручную. Где род неизвестен, агрегация не
предлагается: отдаются значения как есть.
разрезах подробности, в которых пришла (перечень слоёв —
[database.md](database.md), таблица `bucket`); переагрегирования при записи
не происходит никогда.
- **Агрегация в ответе — только измеренная.** Род свёртки (сумма или среднее)
выведен сверкой слоёв между собой, а не проставлен вручную. Где род
неизвестен, агрегация не предлагается: отдаются значения как есть. Свёртка к
запрошенной сетке объявлена контрактом и **ещё не реализована** — параметр
`bucket` отвергается `400` (задача `read-api-points-bucket`).
- **Минимум компонентов** — один процесс, SQLite, файлы. Без очередей и
внешних зависимостей.
@@ -170,7 +172,7 @@ HRV); у накопительных — только `date`. Поэтому то
```
дыра моложе суток → закроется в течение часа
дыра моложе недели → закроется в течение суток
дыра старше недели → не закроется; лечится `healthlog import`
дыра старше недели → не закроется; лечится только `healthlog import` (ещё не написан)
```
Широкие проходы почти бесплатны именно из-за часовых объектов: глубокий проход
@@ -225,7 +227,7 @@ capability**, и здесь стоит ссылка, а не пересказ т
| `ident` | генерация и разбор ULID | — |
| `archive` | сырой архив: запись тела, чтение для reindex, ретеншен | [`storage`](../openspec/specs/storage/spec.md) |
| `hae` | разбор формата HAE, канонизация, хеш содержимого | [`parsing`](../openspec/specs/parsing/spec.md) |
| `ingest` | use-case приёма, общий для HTTP и CLI `import` | [`ingest`](../openspec/specs/ingest/spec.md) |
| `ingest` | use-case приёма, общий для HTTP и будущего CLI `import` | [`ingest`](../openspec/specs/ingest/spec.md) |
| `fold` | свёртка одной доставки в часовые объекты | [`storage`](../openspec/specs/storage/spec.md), [`uncovered-sections`](../openspec/specs/uncovered-sections/spec.md) |
| `replay` | проигрывание журнала в витрину: состав, порядок, отчёт | [`reindex`](../openspec/specs/reindex/spec.md) |
| `catalog` | каталог разрезов и измерение рода агрегации | [`catalog`](../openspec/specs/catalog/spec.md) |
@@ -235,6 +237,8 @@ capability**, и здесь стоит ссылка, а не пересказ т
## Приём
<!-- канон: поведение → openspec/specs/ingest -->
```
запрос → токен → лимит тела, gzip → проверка формы JSON
→ запись тела в архив → строка в delivery → 200
@@ -264,7 +268,8 @@ capability**, и здесь стоит ссылка, а не пересказ т
- **200** — тело сохранено в архив. Дальше даже полный провал разбора
(незнакомая метрика, новая форма точки) не меняет ответ: данные уже в
безопасности, исход разбора виден в логе, в `delivery.parse_status` и в
`/stats`, а доразобрать их можно командой `reindex`.
`/stats` (маршрут — задача `stats-endpoint`), а доразобрать их можно командой
`reindex`.
#### Очередь свёртки — таблица, а не структура в памяти
@@ -942,8 +947,11 @@ hour метки выровнены на час heart_rate 00:00:00
#### Разрешение столкновений
По одним координатам приезжают разные содержимые: 2 897 случаев из 444 256
координат, 0.65% (находка 49). Выигрывает **более полная** точка, и полнота
По одним координатам приезжают разные содержимые: спорных координат 80 129 из
460 995 (17,4%), и полнота отбрасывает кого-то лишь в 981 из них (1,2%)
остальное решает тай-брейк ([research/apple-health.md](research/apple-health.md),
находка 54; прежняя оценка «0,65%» из находки 49 считала ключ без слоя).
Выигрывает **более полная** точка, и полнота —
это сравнение **множеств** ключей с непустым значением, а не их числа.
Число сравнимо всегда и потому отвечает там, где ответа нет: точка
@@ -981,9 +989,9 @@ hour метки выровнены на час heart_rate 00:00:00
отдельно (см. ниже).
**Несравнимые множества не сливаются, а считаются.** Объединение полей — самая
дорогая часть правила — на живом потоке не потребовалось ни разу (0 из 2 897),
поэтому вместо реализации стоит счётчик и `WARN` с координатами объекта. Если
событие наступит, оно будет видно, а не додумано заранее.
дорогая часть правила — на живом корпусе наступило дважды на 155 доставок
(находка 54), поэтому вместо реализации стоит счётчик и `WARN` с координатами
объекта. Событие видно, а не додумано заранее.
**Тай-брейк при равной полноте — пришедшая доставка.** Порядок канонических
форм отвергнут замером: он берёт меньшее значение в 96% случаев (находка 49) и
@@ -1445,18 +1453,26 @@ MongoDB, и так просилось из слова «перезаписыва
## Read API
<!-- канон: поведение → openspec/specs/read-api -->
```
GET /api/v1/metrics каталог: имя, units, род, слои с диапазонами
GET /api/v1/metrics/{name}?from&to&layer точки метрики за период (bucket — соседняя задача, пока 400)
GET /api/v1/workouts?from&to заголовки тренировок
GET /api/v1/workouts/{id} тренировка целиком, с маршрутом
GET /api/v1/records/{kind}?from&to прочие секции
GET /api/v1/schema схемы всего, что есть в хранилище
GET /api/v1/metrics/{name}/schema схема и статистика одной метрики
GET /stats последняя доставка, счётчики, тишина по потоку
GET /healthz
```
**Целевая поверхность шире реализованной.** Маршрутов ниже в роутере ещё нет,
и запрос к ним получает `404`:
```
GET /api/v1/workouts?from&to заголовки тренировок → read-api-workouts
GET /api/v1/workouts/{id} тренировка целиком, с маршрутом → read-api-workouts
GET /api/v1/records/{kind}?from&to прочие секции → read-api-records
GET /api/v1/schema схемы всего, что есть в хранилище → цель self-description
GET /api/v1/metrics/{name}/schema схема и статистика одной метрики → цель self-description
GET /stats последняя доставка, счётчики, тишина → stats-endpoint
```
Хранение пачками на контракт не влияет: `GET /metrics/{name}` собирает ответ
из часовых объектов, попавших в диапазон, и отдаёт точки. Клиент про объекты
не знает — это деталь хранения, а не API.
@@ -1687,8 +1703,9 @@ Docker и go-kit; версионирование с конверсией у Kube
### MCP
Поверх Read API адаптер MCP, чтобы агент подключался без промежуточного
кода. Инструментов ровно два, по числу форм запроса выше, плюс каталог.
Поверх Read API **встанет** адаптер MCP, чтобы агент подключался без
промежуточного кода — кода адаптера сегодня нет, это задача `mcp-server` цели
`read-api`. Инструментов ровно два, по числу форм запроса выше, плюс каталог.
Собственной логики в адаптере нет: он переводит вызовы в те же обработчики.
**Транспорт — HTTP** (Streamable HTTP), не stdio: сервис живёт на VPS, и агент
@@ -1741,18 +1758,18 @@ Docker и go-kit; версионирование с конверсией у Kube
## Аутентификация
Статический токен в заголовке `Authorization: Bearer …`; список допустимых
токенов — в конфиге. HAE умеет слать произвольные заголовки, этого достаточно.
Токены **раздельные**: на запись (приём) и на чтение. Клиент, читающий
данные, не может писать. MCP пользуется токеном чтения — отдельного контура
у него нет, см. «MCP».
Наружу открыты два контура: приём (телефон) и чтение вместе с MCP (агенты и
приложения). Оба через Caddy с TLS, оба с разными токенами.
Периметр, модель угроз и разграничение контуров — [security.md](security.md),
разделы «Периметр» и «Что разграничивает доступ»; сегодняшний контур отличается
от целевого, и сказано это там. Здесь важно одно следствие для компоновки: MCP —
эндпоинт того же процесса и того же контура чтения, отдельного контура доступа у
него нет (см. «MCP»).
## Деплой
**Целевая** раскладка; сегодняшний контур — [security.md](security.md),
«Периметр», статус работ — [tasks/ROADMAP.md](tasks/ROADMAP.md),
«Сопровождение».
VPS **rivendell** (Timeweb), доступен всегда. Перед сервисом — **Caddy**, он
терминирует TLS; сам сервис слушает plain HTTP. Приём открыт наружу на
отдельном поддомене — телефон должен доставать до него из любой сети, иначе
@@ -1764,7 +1781,12 @@ VPS **rivendell** (Timeweb), доступен всегда. Перед серв
Тома: каталог сырого архива и файл SQLite — на постоянном хранении, конфиг
(с токенами) — отдельно, `0600`.
**Откат бинаря поверх новой схемы отказывает на старте.** Версия схемы базы выше
<!-- канон: поведение → openspec/specs/storage -->
**Откат бинаря поверх новой схемы отказывает на старте** — правило нормировано в
[`storage`](../openspec/specs/storage/spec.md), требование «Открытие базы
отказывает при схеме из будущего»; здесь только следствия для деплоя. Версия
схемы базы выше
версии, вшитой в бинарь, — отказ, а не повод мигрировать; в контейнере это
выглядит циклом перезапуска, и лечится возвратом бинаря вперёд. Версию читает
сам goose (`Provider.GetVersions`), а не собственный запрос: имя таблицы учёта и
+1
View File
@@ -240,5 +240,6 @@ Data-миграции у таблицы нет и быть не может: ко
| таймаут чтения запроса | 5 мин (`server.read_timeout`) | конфиг; щедро: экспорт истории по мобильной сети |
| таймаут отправки ответа | 30 с (`server.write_timeout`) | конфиг; маршрут приёма держит собственный бюджет |
| бюджет остановки | 30 с | `cmd/healthlog/serve.go`, `shutdownTimeout` |
| дедлайн свёртки одной доставки | 2 мин | `internal/replay/worker.go`, `foldTimeout`; обстоятельством не считается — не уложившаяся доставка уходит в `failed` |
| ретеншен сырого архива | до следующего проверенного экспорта (~2 ГБ за квартал) | правило, а не число; не реализован — задача `raw-archive-retention` |
| предела на одну сущность | **нет** | задача `entity-size-limits` |
+2 -2
View File
@@ -51,7 +51,7 @@ python3 tmp/research/hl.py workouts тренировки, ряд
## Записи
- [apple-health.md](apple-health.md) — 53 находки на живом потоке Health Auto
- [apple-health.md](apple-health.md) — 54 находки на живом потоке Health Auto
Export и на родном экспорте Apple.
Записи нумерованы сквозным номером внутри файла, и **на номер ссылаются
@@ -64,7 +64,7 @@ python3 tmp/research/hl.py workouts тренировки, ряд
| --- | --- |
| Форма точки, схемы, типы значений | 4, 21, 38, 39, 44 |
| Слой и гранулярность, режимы автоматизации | 5, 6, 13, 19, 20, 23, 33, 41 |
| Идентичность, столкновения, слияние, полнота | 11, 14, 36, 47, 49 |
| Идентичность, столкновения, слияние, полнота | 11, 14, 36, 47, 49, 54 |
| Досчёт задним числом и стабильность значений | 3, 10, 30, 48, 51 |
| Локализация и категориальные значения | 8, 24, 37, 43 |
| Секции потока и их состав | 9, 15, 16, 17, 22, 34, 50, 52 |
+37 -37
View File
@@ -11,43 +11,43 @@
## Ядро
- [[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-архиве и никуда не едут
- [[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 — следующая покрытая секция унаследует слепую зону
- [[idea] Отказ от heartbeatSeries](items/drop-heartbeat-series.md) — 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе
- [Не держать весь журнал в памяти при пересборке](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 с — предела на одну сущность нет вовсе
- [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.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 он избыточен
- [[idea] Выгрузка в parquet отдельной командой](items/parquet-export.md) — Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно
- [Класть заголовки доставки в архив рядом с телом](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 в локальной сети нет
- [✨ Проверять целостность собранной витрины до подмены](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-архиве и никуда не едут
- [🐞 Не отбирать строки в data-миграциях по обрезаемым спискам](items/data-migration-row-selection.md) — Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону
- [🧹 Не держать весь журнал в памяти при пересборке](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет
- [🐞 Держать порядок журнала при конкурентных приёмах](items/journal-order-on-ingest.md) — Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой
- [✨ Ограничить размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним
- [✨ Ограничить размер сущности и считать форму потоково](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе
- [✨ Выводить схемы содержимого из данных](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
- [✨ Сверять живую витрину с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит
- [✨ Помечать нижний слой устаревшим после экспорта](items/lower-layer-expiry.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
- [🐞 Класть заголовки доставки в архив рядом с телом](items/delivery-headers-in-archive.md) — Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке
- [✨ Поднять MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
- [✨ Написать OpenAPI-спеку руками](items/openapi-spec.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
- [🧹 Ловить гейтом расхождение спеки с маршрутами](items/openapi-gate-check.md) — Рукописная спека — источник истины, а расходится она с кодом молча: без гейта решение о рукописной спеке не держится
- [✨ Поднять Swagger UI без внешней сети](items/swagger-ui.md) — Контракт читается машиной, но человеку нечем выполнить запрос к живому сервису из браузера, а внешних CDN в локальной сети нет
- [🔬 Измерить, нужно ли правило полноты рядом с LWW](items/last-wins-over-completeness.md) — Полнота решает 1,2% спорных координат, и неизвестно, была ли более полная точка более поздней — от этого зависит, нужна ли она вообще
- [🔬 Человеческие аннотации поверх выведенных схем](items/schema-annotations.md) — Выведенная схема говорит форму, но не смысл метрики — нужно ли описание сверху, зависит от стабильности формата
- [🔬 Месячный проход по ручным секциям](items/monthly-manual-sections-pass.md) — Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит
- [🔬 NDJSON-поток для больших выборок Read API](items/ndjson-stream.md) — Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация
- [🔬 Отказ от heartbeatSeries](items/drop-heartbeat-series.md) — 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе
- [🔬 Пересекающиеся источники одной метрики](items/overlapping-sources.md) — Сон пишут и часы, и стороннее приложение — сумма по обоим задвоит ночь
- [🔬 Порог sealed: с какого возраста час считается запечатанным](items/sealed-threshold.md) — WARN на изменение старого часа уже пишется, но порог не выбран — ставим по факту, когда накопится статистика досчёта
- [🔬 Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом
- [🔬 Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено
- [🔬 Выгрузка в parquet отдельной командой](items/parquet-export.md) — Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно
## Инфра
- [Слать уведомление, когда данных нет 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 коммитится — так нельзя выезжать наружу
- [Слать уведомление, когда данных нет 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 -1
View File
@@ -8,7 +8,7 @@
- 2026-08-01 `bekap-dannyh` — Резервное копирование ./data. Причина: бекап обеспечивает готовый механизм на сервере пет-проектов — своего заводить не нужно, задача снимается деплоем. Была секция: высокий.
- 2026-08-01 `identichnost-epizodnyh-metrik` — Идентичность эпизодных метрик. Причина: решён измерением и prior art: ключ эпизода — метрика+слой+start+end (находка 47), вариант А; вернулся в scope razbor-metrik-v-obekty. Была секция: блокеры.
- 2026-08-01 `edinicy-metriki-v-razreze` — Единицы метрики: часть координаты или свойство объекта. Причина: измерено: на 99 доставках единицы не менялись ни у одной из 30 метрик (находка 49 → 48); реализованное правило «сохранённое побеждает + WARN + счётчик» делает событие наблюдаемым. Была секция: блокеры.
- 2026-08-04 `mcp`[goal] MCP. Причина: поглощена целью read-api («Чтение данных клиентами»): MCP — не направление, а последний шаг того же направления; адаптер переводит вызовы в те же обработчики и собственной логики не несёт. Очередь «Read API перед MCP» стала порядком задач внутри цели. Задача mcp-server жива и перевешена на read-api. Была секция: порядок.
- 2026-08-04 `mcp`🎯 MCP. Причина: поглощена целью read-api («Чтение данных клиентами»): MCP — не направление, а последний шаг того же направления; адаптер переводит вызовы в те же обработчики и собственной логики не несёт. Очередь «Read API перед MCP» стала порядком задач внутри цели. Задача mcp-server жива и перевешена на read-api. Была секция: порядок.
- 2026-08-04 `read-api-points` — Read API: точки, выбор слоя, свёртка по сетке. Причина: разложена на read-api-envelope-and-points (конверт, точки за период, форма провода, условный запрос), read-api-bucketing (свёртка по сетке, предел размера ответа, порог неполного ведра) и read-api-workouts-and-records (тренировки и записи наружу). Одним заходом не мерджилась: десяток критериев приёмки и три развилки в одном файле. Была секция: ядро.
- 2026-08-04 `read-api-envelope-and-points` — Конверт ответа и точки за период. Причина: разложена на read-api-wire-format (форма провода, мерджится первой и трогает только живой каталог), read-api-points-period (точки за период с конвертом) и read-api-points-conditional (условный запрос со scope-etag). Была секция: ядро.
- 2026-08-04 `read-api-bucketing` — Свёртка по сетке и предел размера ответа. Причина: разложена на read-api-points-bucket (свёртка по сетке), read-api-partial-bucket (порог неполного ведра и его полярность) и read-api-response-limit (предел размера ответа, общий для всех маршрутов чтения). Была секция: ядро.
+30 -27
View File
@@ -1,9 +1,37 @@
# Роадмап
Что приложение уже умеет и чего ещё не умеет. Цель — возможность приложения,
файл `[goal]` в `items/`; её задачи здесь **не перечисляются** — перечень даёт
файл типа `goal` в `items/`; её задачи здесь **не перечисляются** — перечень даёт
`tasks.py list --goal <слаг>`. Очередь значима только в «Запланировано» и
обосновывается прозой рядом.
обосновывается прозой рядом. В «Сопровождении» лежит то, чем держат проект —
выкладка, инструмент, эксплуатация; граница проходит по тому, кто наблюдает:
сообщает ли о состоянии приложение своему пользователю или дежурный смотрит на
сервис снаружи.
## Запланировано
Очередь держится на двух зависимостях. **`healthlog import` идёт перед чисткой
нижнего слоя:** пока импорт экспорта не написан, помечать что-либо устаревшим не
на основании чего. **MCP входит в чтение, а не идёт отдельной целью:** адаптер
собственной логики не несёт, он переводит вызовы в те же обработчики, и очередь
осталась порядком задач внутри цели.
- [🎯 Клиенты читают данные через HTTP и MCP](items/read-api.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может
- [🎯 Клиент узнаёт форму данных из ответа](items/self-description.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
- [🎯 История из родного экспорта Apple лежит в хранилище](items/native-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
- [🎯 Нижний слой чистится после проверенного экспорта](items/lower-layer-cleanup.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
- [🎯 Приложение сообщает о своём состоянии](items/observability.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах
## Направления
- [🎯 Исход слияния не зависит от порядка элементов на проводе](items/merge-robustness.md) — Правила слияния держатся на замерах, и каждый новый замер находит зависимость от порядка на проводе
- [🎯 Расхождение витрины с журналом не молчит](items/journal-and-rebuild.md) — Расхождение витрины с журналом сейчас молчит: reindex печатает оба отпечатка, а сравнивать их некому
- [🎯 У каждого входа есть названный предел](items/limits-and-load.md) — Предела на одну сущность нет вовсе: 63 МиБ держат блокировку 5.019 с, соседние доставки уходят в failed
- [🎯 Новая форма от источника не теряется молча](items/parsing-completeness.md) — Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы
## Сопровождение
- [🎯 Сервис доступен телефону из любой сети](items/deploy.md) — Сервис живёт на рабочей машине — телефон достаёт до него только дома, и оба контура открыты
## Готово
@@ -24,28 +52,3 @@
[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) — Сервис живёт на рабочей машине — телефон достаёт до него только дома, и оба контура открыты
+7 -7
View File
@@ -1,6 +1,6 @@
# Спринт
- **Цель:** [[goal] Клиенты читают данные через HTTP и MCP](items/read-api.md)
- **Цель:** [🎯 Клиенты читают данные через HTTP и MCP](items/read-api.md)
- **Начат:** 2026-08-04
- **Спринт:** `2026-08-04`
@@ -8,9 +8,9 @@
## Набор
- [Отвечать 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 его нет
- [Отвечать 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 его нет
+3 -2
View File
@@ -1,6 +1,7 @@
# Импортировать родной экспорт Apple Health
# Импортировать родной экспорт Apple Health
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
- **Теги:** goal:native-export-import
+3 -2
View File
@@ -1,6 +1,7 @@
# Свести умолчания конфига с рабочей раскладкой данных
# 🐞 Свести умолчания конфига с рабочей раскладкой данных
- **Секция:** Инфра
- **Тип:** fix
- **Категория:** Инфра
- **Зачем:** Запуск без конфига заведёт пустую базу в корне рядом с настоящей — тихая ловушка
- **Теги:** goal:deploy
@@ -1,6 +1,7 @@
# Не отбирать строки в data-миграциях по обрезаемым спискам
# 🐞 Не отбирать строки в data-миграциях по обрезаемым спискам
- **Секция:** Ядро
- **Тип:** fix
- **Категория:** Ядро
- **Зачем:** Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону
- **Теги:** goal:journal-and-rebuild
+3 -2
View File
@@ -1,6 +1,7 @@
# [idea] Что считать сутками при смене часового пояса
# 🔬 Что считать сутками при смене часового пояса
- **Секция:** Ядро
- **Тип:** research
- **Категория:** Ядро
- **Зачем:** Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено
- **Теги:** goal:read-api
+3 -2
View File
@@ -1,6 +1,7 @@
# Ограничить размер и число заголовков доставки
# Ограничить размер и число заголовков доставки
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним
- **Теги:** goal:limits-and-load
@@ -1,6 +1,7 @@
# Класть заголовки доставки в архив рядом с телом
# 🐞 Класть заголовки доставки в архив рядом с телом
- **Секция:** Ядро
- **Тип:** fix
- **Категория:** Ядро
- **Зачем:** Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке
- **Теги:** goal:journal-and-rebuild
+3 -2
View File
@@ -1,6 +1,7 @@
# Выложить сервис на rivendell
# Выложить сервис на rivendell
- **Секция:** Инфра
- **Тип:** feature
- **Категория:** Инфра
- **Зачем:** Сервис живёт на рабочей машине — телефон достаёт до него только дома
- **Теги:** goal:deploy
+3 -2
View File
@@ -1,6 +1,7 @@
# [goal] Сервис доступен телефону из любой сети
# 🎯 Сервис доступен телефону из любой сети
- **Секция:** Разработка
- **Тип:** goal
- **Секция:** Сопровождение
- **Зачем:** Сервис живёт на рабочей машине — телефон достаёт до него только дома, и оба контура открыты
- **Теги:** decomposed
+3 -2
View File
@@ -1,6 +1,7 @@
# Выводить схемы содержимого из данных
# Выводить схемы содержимого из данных
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
- **Теги:** goal:self-description
+3 -2
View File
@@ -1,6 +1,7 @@
# [idea] Отказ от heartbeatSeries
# 🔬 Отказ от heartbeatSeries
- **Секция:** Ядро
- **Тип:** research
- **Категория:** Ядро
- **Зачем:** 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе
- **Теги:** goal:lower-layer-cleanup
+3 -2
View File
@@ -1,6 +1,7 @@
# Ограничить размер сущности и считать форму потоково
# Ограничить размер сущности и считать форму потоково
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе
- **Теги:** goal:limits-and-load
@@ -1,6 +1,7 @@
# Не терять сущность с id и неразобранной меткой
# 🐞 Не терять сущность с id и неразобранной меткой
- **Секция:** Ядро
- **Тип:** fix
- **Категория:** Ядро
- **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым
- **Теги:** goal:parsing-completeness
+3 -2
View File
@@ -1,6 +1,7 @@
# Проверять целостность собранной витрины до подмены
# Проверять целостность собранной витрины до подмены
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Подмена файла необратима, а годность выхода подтверждена отпечатком на ещё открытом дескрипторе
- **Теги:** goal:journal-and-rebuild
+2 -1
View File
@@ -1,5 +1,6 @@
# [goal] Расхождение витрины с журналом не молчит
# 🎯 Расхождение витрины с журналом не молчит
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** Расхождение витрины с журналом сейчас молчит: reindex печатает оба отпечатка, а сравнивать их некому
- **Теги:** decomposed
+3 -2
View File
@@ -1,6 +1,7 @@
# Держать порядок журнала при конкурентных приёмах
# 🐞 Держать порядок журнала при конкурентных приёмах
- **Секция:** Ядро
- **Тип:** fix
- **Категория:** Ядро
- **Зачем:** Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой
- **Теги:** goal:journal-and-rebuild, question
@@ -1,6 +1,7 @@
# Измерить, нужно ли правило полноты рядом с LWW
# 🔬 Измерить, нужно ли правило полноты рядом с LWW
- **Секция:** Ядро
- **Тип:** research
- **Категория:** Ядро
- **Зачем:** Полнота решает 1,2% спорных координат, и неизвестно, была ли более полная точка более поздней — от этого зависит, нужна ли она вообще
- **Теги:** goal:merge-robustness, sprint:2026-08-03
+2 -1
View File
@@ -1,5 +1,6 @@
# [goal] У каждого входа есть названный предел
# 🎯 У каждого входа есть названный предел
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** Предела на одну сущность нет вовсе: 63 МиБ держат блокировку 5.019 с, соседние доставки уходят в failed
- **Теги:** decomposed
+2 -1
View File
@@ -1,5 +1,6 @@
# [goal] Нижний слой чистится после проверенного экспорта
# 🎯 Нижний слой чистится после проверенного экспорта
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
- **Теги:** decomposed
+3 -2
View File
@@ -1,6 +1,7 @@
# Помечать нижний слой устаревшим после экспорта
# Помечать нижний слой устаревшим после экспорта
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
- **Теги:** goal:lower-layer-cleanup
+3 -2
View File
@@ -1,6 +1,7 @@
# Поднять MCP-сервер поверх Read API
# Поднять MCP-сервер поверх Read API
- **Секция:** Ядро — набор ограничен HTTP-слоем чтения после дробления; адаптер берётся следующим спринтом по той же цели
- **Тип:** feature
- **Категория:** Ядро — набор ограничен HTTP-слоем чтения после дробления; адаптер берётся следующим спринтом по той же цели
- **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
- **Теги:** goal:read-api
+3 -2
View File
@@ -1,6 +1,7 @@
# Снизить цену слияния на широкой доставке
# 🐞 Снизить цену слияния на широкой доставке
- **Секция:** Ядро
- **Тип:** fix
- **Категория:** Ядро
- **Зачем:** 63 МБ на одной координате держат транзакцию 5.15 с при busy_timeout 5 с — соседние доставки уходят в failed
- **Теги:** goal:limits-and-load
+3 -2
View File
@@ -1,6 +1,7 @@
# Хранить счётчики слияния вне логов
# Хранить счётчики слияния вне логов
- **Секция:** Инфра
- **Тип:** feature
- **Категория:** Инфра
- **Зачем:** Единственный след несравнимых наборов — строка WARN в docker-логе с ротацией 3×10 МБ: событие может произойти и не оставить ничего
- **Теги:** goal:observability
+2 -1
View File
@@ -1,5 +1,6 @@
# [goal] Исход слияния не зависит от порядка элементов на проводе
# 🎯 Исход слияния не зависит от порядка элементов на проводе
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** Правила слияния держатся на замерах, и каждый новый замер находит зависимость от порядка на проводе
- **Теги:** decomposed
@@ -1,6 +1,7 @@
# [idea] Месячный проход по ручным секциям
# 🔬 Месячный проход по ручным секциям
- **Секция:** Ядро
- **Тип:** research
- **Категория:** Ядро
- **Зачем:** Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит
- **Теги:** goal:parsing-completeness
+2 -1
View File
@@ -1,5 +1,6 @@
# [goal] История из родного экспорта Apple лежит в хранилище
# 🎯 История из родного экспорта Apple лежит в хранилище
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
- **Теги:** decomposed
+3 -2
View File
@@ -1,6 +1,7 @@
# [idea] NDJSON-поток для больших выборок Read API
# 🔬 NDJSON-поток для больших выборок Read API
- **Секция:** Ядро
- **Тип:** research
- **Категория:** Ядро
- **Зачем:** Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация
- **Теги:** goal:read-api
+2 -1
View File
@@ -1,5 +1,6 @@
# [goal] Приложение сообщает о своём состоянии
# 🎯 Приложение сообщает о своём состоянии
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах
- **Теги:** decomposed
+3 -2
View File
@@ -1,6 +1,7 @@
# Ловить гейтом расхождение спеки с маршрутами
# 🧹 Ловить гейтом расхождение спеки с маршрутами
- **Секция:** Ядро
- **Тип:** chore
- **Категория:** Ядро
- **Зачем:** Рукописная спека — источник истины, а расходится она с кодом молча: без гейта решение о рукописной спеке не держится
- **Теги:** goal:read-api, sprint:2026-08-04
+3 -2
View File
@@ -1,6 +1,7 @@
# Написать OpenAPI-спеку руками
# Написать OpenAPI-спеку руками
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
- **Теги:** goal:read-api, sprint:2026-08-04
+3 -2
View File
@@ -1,6 +1,7 @@
# [idea] Пересекающиеся источники одной метрики
# 🔬 Пересекающиеся источники одной метрики
- **Секция:** Ядро
- **Тип:** research
- **Категория:** Ядро
- **Зачем:** Сон пишут и часы, и стороннее приложение — сумма по обоим задвоит ночь
- **Теги:** goal:read-api
+3 -2
View File
@@ -1,6 +1,7 @@
# [idea] Выгрузка в parquet отдельной командой
# 🔬 Выгрузка в parquet отдельной командой
- **Секция:** Ядро
- **Тип:** research
- **Категория:** Ядро
- **Зачем:** Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно
- **Теги:** goal:read-api
+2 -1
View File
@@ -1,5 +1,6 @@
# [goal] Новая форма от источника не теряется молча
# 🎯 Новая форма от источника не теряется молча
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы
- **Теги:** decomposed
+3 -2
View File
@@ -1,6 +1,7 @@
# Подчищать сырой архив до последнего проверенного экспорта
# Подчищать сырой архив до последнего проверенного экспорта
- **Секция:** Инфра
- **Тип:** feature
- **Категория:** Инфра
- **Зачем:** Архив не подчищается вовсе, а резать его раньше даты проверенного экспорта нельзя — в журнале останется дыра, которую нечем пересобрать
- **Теги:** goal:journal-and-rebuild
+4 -3
View File
@@ -1,8 +1,9 @@
# Отличать неполное ведро от полного
# Отличать неполное ведро от полного
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Текущий час неполон всегда, и без порога свёртка отдаёт его наравне с полными — клиент видит провал вместо неизвестности
- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature
- **Теги:** goal:read-api, sprint:2026-08-04
Ведро, в котором известна не вся сетка, отличимо от полного — а полярность
порога названа вслух, а не выводится читателем из умолчания.
+4 -3
View File
@@ -1,8 +1,9 @@
# Сворачивать точки по заданной сетке
# Сворачивать точки по заданной сетке
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** «Шаги за неделю по дням» — базовый запрос трекера и игры, и сегодня его нечем задать
- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature
- **Теги:** goal:read-api, sprint:2026-08-04
«Шаги за неделю по дням» отвечаются одним запросом `?from&to&bucket`, и род
свёртки берётся измеренным, а не угаданным.
@@ -1,8 +1,9 @@
# Отвечать 304 на повторный запрос точек
# Отвечать 304 на повторный запрос точек
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Агент опрашивает по расписанию, а каждый повтор стоит полного чтения: на каталоге это 693 мс и +153 МиБ
- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature
- **Теги:** goal:read-api, sprint:2026-08-04
Повторный опрос точек с той же меткой стоит `304` вместо полного чтения, и метка
не может ответить на другой набор данных.
+4 -3
View File
@@ -1,8 +1,9 @@
# Отдавать записи со своим id за период
# Отдавать записи со своим id за период
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** stateOfMind разобран и хранится, но наружу не отдаётся — а восстановить его нечем: в экспорте Apple его нет
- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature
- **Теги:** goal:read-api, sprint:2026-08-04
Записи со своим `id` — сегодня это `stateOfMind` — достаются за период через
`GET /records/{kind}`.
+4 -3
View File
@@ -1,8 +1,9 @@
# Ограничить размер ответа маршрутов чтения
# Ограничить размер ответа маршрутов чтения
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** У маршрутов чтения нет ни одного потолка: множители «метрики × окно × точки × одновременные запросы» ничем не ограничены
- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature
- **Теги:** goal:read-api, sprint:2026-08-04
У маршрутов чтения появляется названный потолок: сетка не задана и ответ не
влезает — сервер огрубляет её и **называет** в ответе; сетка задана явно и не
+4 -3
View File
@@ -1,8 +1,9 @@
# Отдавать тренировки вместе с маршрутом
# Отдавать тренировки вместе с маршрутом
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Тренировки с маршрутами разобраны и лежат в витрине, а маршрутов чтения нет — сценарий трекера не закрыт
- **Теги:** goal:read-api, sprint:2026-08-04, kind:feature
- **Теги:** goal:read-api, sprint:2026-08-04
Трекер забирает тренировку одним пакетом вместе с маршрутом: `GET /workouts` за
период и `GET /workouts/{id}` поштучно.
+2 -1
View File
@@ -1,5 +1,6 @@
# [goal] Клиенты читают данные через HTTP и MCP
# 🎯 Клиенты читают данные через HTTP и MCP
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может
- **Теги:** decomposed
+3 -2
View File
@@ -1,6 +1,7 @@
# Сверять живую витрину с пересборкой
# Сверять живую витрину с пересборкой
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит
- **Теги:** goal:journal-and-rebuild
+3 -2
View File
@@ -1,6 +1,7 @@
# Не держать весь журнал в памяти при пересборке
# 🧹 Не держать весь журнал в памяти при пересборке
- **Секция:** Ядро
- **Тип:** chore
- **Категория:** Ядро
- **Зачем:** Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет
- **Теги:** goal:journal-and-rebuild
@@ -1,6 +1,7 @@
# Назвать механизм отката релиза после наката миграции
# Назвать механизм отката релиза после наката миграции
- **Секция:** Инфра
- **Тип:** feature
- **Категория:** Инфра
- **Зачем:** Страж версии схемы делает возврат старого бинаря отказом, а понизить схему нечем — аварийный путь пришлось бы изобретать в аварии
- **Теги:** goal:deploy
+3 -2
View File
@@ -1,6 +1,7 @@
# [idea] Человеческие аннотации поверх выведенных схем
# 🔬 Человеческие аннотации поверх выведенных схем
- **Секция:** Ядро
- **Тип:** research
- **Категория:** Ядро
- **Зачем:** Выведенная схема говорит форму, но не смысл метрики — нужно ли описание сверху, зависит от стабильности формата
- **Теги:** goal:self-description
+3 -2
View File
@@ -1,6 +1,7 @@
# [idea] Порог sealed: с какого возраста час считается запечатанным
# 🔬 Порог sealed: с какого возраста час считается запечатанным
- **Секция:** Ядро
- **Тип:** research
- **Категория:** Ядро
- **Зачем:** WARN на изменение старого часа уже пишется, но порог не выбран — ставим по факту, когда накопится статистика досчёта
- **Теги:** goal:merge-robustness
+2 -1
View File
@@ -1,5 +1,6 @@
# [goal] Клиент узнаёт форму данных из ответа
# 🎯 Клиент узнаёт форму данных из ответа
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
- **Теги:** decomposed
@@ -1,6 +1,7 @@
# Развести бюджеты остановки и оставить следы миграции в логе
# 🐞 Развести бюджеты остановки и оставить следы миграции в логе
- **Секция:** Инфра
- **Тип:** fix
- **Категория:** Инфра
- **Зачем:** Долгий запрос чтения съедает бюджет остановки, и WARN обвиняет воркер свёртки; миграция молчит и не прерывается SIGTERM
- **Теги:** goal:deploy
+3 -2
View File
@@ -1,6 +1,7 @@
# Отдавать состояние сервиса маршрутом /stats
# Отдавать состояние сервиса маршрутом /stats
- **Секция:** Инфра
- **Тип:** feature
- **Категория:** Инфра
- **Зачем:** Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах
- **Теги:** goal:observability
+3 -2
View File
@@ -1,6 +1,7 @@
# Слать уведомление, когда данных нет N часов
# Слать уведомление, когда данных нет N часов
- **Секция:** Инфра
- **Тип:** feature
- **Категория:** Инфра
- **Зачем:** Пропажу потока сейчас замечает человек, а не сервис
- **Теги:** goal:observability
+3 -2
View File
@@ -1,6 +1,7 @@
# Поднять Swagger UI без внешней сети
# Поднять Swagger UI без внешней сети
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Контракт читается машиной, но человеку нечем выполнить запрос к живому сервису из браузера, а внешних CDN в локальной сети нет
- **Теги:** goal:read-api, sprint:2026-08-04
@@ -1,6 +1,7 @@
# Развести токены контуров и убрать секреты из репозитория
# Развести токены контуров и убрать секреты из репозитория
- **Секция:** Инфра
- **Тип:** feature
- **Категория:** Инфра
- **Зачем:** Проверка токенов выключена, config.docker.toml коммитится — так нельзя выезжать наружу
- **Теги:** goal:deploy
@@ -1,6 +1,7 @@
# Не задваивать тренировки при импорте родного экспорта
# Не задваивать тренировки при импорте родного экспорта
- **Секция:** Ядро
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE
- **Теги:** goal:native-export-import
+3 -2
View File
@@ -1,6 +1,7 @@
# [idea] Разворачивание маршрутов тренировок в отдельную таблицу
# 🔬 Разворачивание маршрутов тренировок в отдельную таблицу
- **Секция:** Ядро
- **Тип:** research
- **Категория:** Ядро
- **Зачем:** Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом
- **Теги:** goal:read-api
+6 -1
View File
@@ -1,7 +1,12 @@
# parsing Specification
## Purpose
TBD - created by archiving change razbor-metrik-v-obekty. Update Purpose after archive.
Разбор тела Health Auto Export: какие секции покрыты, как читается точка и её
метка времени, как выводится слой доставки и что происходит с непонятым.
Источник истины о формате — не документация HAE, а живые пакеты
(`docs/research/apple-health.md`).
## Requirements
### Requirement: Разбор секции метрик
+12 -3
View File
@@ -1,7 +1,13 @@
# storage Specification
## Purpose
TBD - created by archiving change razbor-metrik-v-obekty. Update Purpose after archive.
Витрина: как принятые точки и сущности ложатся, адресуются и выбираются между
версиями. Здесь живут идентичность точки по координатам, полнота и тай-брейк,
часовой объект, хранение сущностей с собственным `id`, отпечаток витрины и учёт
разобранности доставки. Правило одно на всё: содержимое хранится дословно, а
состояние остаётся свёрткой журнала в его порядке.
## Requirements
### Requirement: Идентичность точки по координатам
@@ -410,8 +416,11 @@ HTML-экранирования: `&`, `<` и `>` внутри точки обя
построчный разбор логов. То же относится к идентификатору сущности: он приходит
из чужого тела и ограничен по длине при разборе.
Частичный разбор уровня записи не повышает: `partial` — установившееся состояние
половины потока (53 доставки из 118), и постоянный `WARN` обесценил бы уровень.
Частичный разбор уровня записи не повышает: `partial` — установившееся
состояние, а не сигнал. До покрытия `workouts` и `stateOfMind` частичной
числилась половина потока (53 доставки из 118); после покрытия непокрытая
секция живым потоком не приносилась ни разу на том же корпусе. Постоянный
`WARN` обесценил бы уровень.
Повышает уровень другое, и оснований два:
- срабатывание границ списка: тело с сотнями секций или с именем длиннее предела