From 3df42afecade8ba9af7ad3d8b7a09a6f0f619574 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Mon, 3 Aug 2026 17:47:41 +0300 Subject: [PATCH] =?UTF-8?q?tasks:=20=D1=80=D0=B0=D0=B7=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B=20=D0=B2=D0=BE=D0=BF=D1=80=D0=BE=D1=81=D1=8B?= =?UTF-8?q?=20=D0=B8=20=D0=BD=D0=B0=D0=B1=D1=80=D0=B0=D0=BD=20=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=202026-08-03?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - открытых вопросов не осталось: три решения владельца доведены до берущегося вида, по entity-without-parsed-label принято хранить с NULL-меткой после Read API - unseen-sections-check сжата до остатка — активная проверка появления секции; разбор невиденных секций из неё вынут, вслепую он не пишется - спринт под целью parsing-and-storage: categorical-value-dictionary и unseen-sections-check, обеим написаны критерии приёмки с оракулами --- docs/tasks/BACKLOG.md | 10 ++- docs/tasks/SPRINT.md | 7 ++- .../items/categorical-value-dictionary.md | 32 +++++++++- .../items/entity-without-parsed-label.md | 37 ++++++----- docs/tasks/items/journal-order-on-ingest.md | 7 ++- .../items/release-rollback-after-migration.md | 7 ++- .../items/tie-break-equal-completeness.md | 7 ++- docs/tasks/items/unseen-sections-check.md | 61 +++++++++++++------ 8 files changed, 112 insertions(+), 56 deletions(-) diff --git a/docs/tasks/BACKLOG.md b/docs/tasks/BACKLOG.md index df8eeb9..abb30ca 100644 --- a/docs/tasks/BACKLOG.md +++ b/docs/tasks/BACKLOG.md @@ -13,7 +13,7 @@ - [[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) — Тренировка с меткой в неизвестном формате пропадает целиком — а её id и содержимое разобраны +- [Сущность с id, но неразобранной меткой](items/entity-without-parsed-label.md) — Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым - [Идентичность тренировок при импорте родного экспорта](items/workout-identity-on-import.md) — В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE - [Импорт родного экспорта Apple Health](items/apple-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут - [MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем @@ -25,17 +25,15 @@ - [Пересборка держит весь журнал в памяти](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет - [[idea] Пересекающиеся источники одной метрики](items/overlapping-sources.md) — Сон пишут и часы, и стороннее приложение — сумма по обоим задвоит ночь - [[idea] Порог sealed: с какого возраста час считается запечатанным](items/sealed-threshold.md) — WARN на изменение старого часа уже пишется, но порог не выбран — ставим по факту, когда накопится статистика досчёта -- [Порядок журнала при конкурентных приёмах](items/journal-order-on-ingest.md) — Решено: повторы, но после /stats. Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда +- [Порядок журнала при конкурентных приёмах](items/journal-order-on-ingest.md) — Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой - [Предел на размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним - [Пределы на размер сущности и потоковый расчёт формы](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе -- [Проверка секций, которых поток ещё не приносил](items/unseen-sections-check.md) — Симптомы, ЭКГ, лекарства, цикл и вес не виденны живьём — разбор писался вслепую - [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом - [Read API: точки, выбор слоя, свёртка по сетке](items/read-api-points.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может - [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке -- [Словарь категориальных значений → коды HealthKit](items/categorical-value-dictionary.md) — Фазы сна и типы тренировок приходят строками русской локали — с экспортом Apple их не сверить - [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено - [Сверка живой витрины с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит -- [Тай-брейк при равной полноте точек](items/tie-break-equal-completeness.md) — Решено: брать бо́льшее значение. Порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт +- [Тай-брейк при равной полноте точек](items/tie-break-equal-completeness.md) — При равной полноте порядок канонических форм берёт меньшее значение в 96% случаев — у накопительных это систематический недосчёт - [Устаревание нижнего слоя после экспорта](items/lower-layer-expiry.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен - [[idea] Выгрузка в parquet отдельной командой](items/parquet-export.md) — Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно - [Заголовки доставки в архиве рядом с телом](items/delivery-headers-in-archive.md) — Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке @@ -45,7 +43,7 @@ - [Деплой на 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/release-rollback-after-migration.md) — Страж версии схемы делает возврат старого бинаря отказом, а понизить схему нечем — аварийный путь пришлось бы изобретать в аварии - [Ретеншен сырого архива](items/raw-archive-retention.md) — Архив не подчищается вовсе — 14-дневный срок объявлен, но не работает - [Наблюдаемость: /stats](items/stats-endpoint.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах - [Умолчания конфига указывают на прежнюю раскладку](items/config-defaults-data-dir.md) — Запуск без конфига заведёт пустую базу в корне рядом с настоящей — тихая ловушка diff --git a/docs/tasks/SPRINT.md b/docs/tasks/SPRINT.md index 3638c73..09c6a10 100644 --- a/docs/tasks/SPRINT.md +++ b/docs/tasks/SPRINT.md @@ -1,6 +1,9 @@ # Спринт -Спринта нет. Цель называет человек, набор собирает агент: -`tasks.py sprint start --goal <слаг>`. +**Цель:** [[goal] Разбор и хранилище](items/parsing-and-storage.md) · **Начат:** 2026-08-03 · **Спринт:** `2026-08-03` + +Урожай спринта поднимается `tasks.py list --tag sprint:2026-08-03` — это первая порция переоценки на сессии. ## Набор +- [Словарь категориальных значений → коды HealthKit](items/categorical-value-dictionary.md) — Фазы сна и типы тренировок приходят строками русской локали — с экспортом Apple их не сверить +- [Активная проверка: поток принёс секцию, которой раньше не было](items/unseen-sections-check.md) — Момент появления новой секции фиксируется в базе, но заметить его может только тот, кто догадается заглянуть в колонку diff --git a/docs/tasks/items/categorical-value-dictionary.md b/docs/tasks/items/categorical-value-dictionary.md index 2a1dce5..32577a7 100644 --- a/docs/tasks/items/categorical-value-dictionary.md +++ b/docs/tasks/items/categorical-value-dictionary.md @@ -35,7 +35,33 @@ HAE отдаёт перечислимые значения строками ло расколется вторично — уже на «стабильной» стороне. Простейшее решение: хранить код как есть, а эквивалентность старых и новых имён держать отдельной таблицей синонимов. -Готово, когда фазы сна из потока и из экспорта Apple сравниваются напрямую, а -`/stats` показывает строки, для которых кода ещё нет. - `stateOfMind` в словаре не нуждается — он и так шлёт коды HealthKit. + +## Критерии приёмки + +- фаза сна из потока («БДГ») и из экспорта Apple + (`HKCategoryValueSleepAnalysisAsleepREM`) за один период сопоставляются + напрямую — оракул: запрос на живой базе за период с известным перекрытием, + ноль несопоставимых строк +- строка сохранена **дословно**, код лежит рядом отдельным полем — оракул: тест + разбора на реальном пакете HAE из `internal/hae/testdata` +- незнакомая строка даёт пустой код, разбор не падает, а событие попадает в + счётчик — оракул: тест на выдуманной фазе сна плюс проверка счётчика +- переименование кода самой Apple (`…Asleep` → `…AsleepUnspecified`, находка 43) + не раскалывает историю — оракул: тест на паре синонимов, обе формы сходятся + в один код +- повторный прогон живого архива даёт то же состояние — оракул: + `task verify:archive` + +Критерий «`/stats` показывает строки без кода» снят при переоценке 2026-08-03: +`/stats` ещё нет ([stats-endpoint](stats-endpoint.md)), и вешать приёмку на +несуществующий оракул значит либо блокировать задачу, либо принять её +непроверенной. Наблюдаемость закрывается счётчиком; показ в `/stats` — строка +задачи наблюдаемости, а не этой. + +## Рамки + +Схема трогается: у категориального значения появляется поле кода, плюс таблица +синонимов. Дословную строку не заменяем и не нормализуем — инвариант «точки +хранятся дословно». Пересборка обязана оставаться детерминированной, оракул +тот же `task verify:archive`. diff --git a/docs/tasks/items/entity-without-parsed-label.md b/docs/tasks/items/entity-without-parsed-label.md index 84a8ffe..977944c 100644 --- a/docs/tasks/items/entity-without-parsed-label.md +++ b/docs/tasks/items/entity-without-parsed-label.md @@ -1,8 +1,20 @@ # Сущность с id, но неразобранной меткой - **Секция:** ядро -- **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком — а её id и содержимое разобраны -- **Теги:** goal:parsing-and-storage, question +- **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым +- **Теги:** goal:parsing-and-storage + +**Решение принято владельцем 2026-08-03: вариант (1) — хранить с NULL-меткой.** +`start_utc`/`ts_utc` становятся NULLABLE, содержимое (включая маршрут) хранится, +метку восстановит пересборка, когда разбор научится читать формат. Вариант (3) +отвергнут при постановке: подстановка метки доставки — выдуманное измерение в +колонке, по которой идёт выборка. + +**Берётся после [Read API по точкам и сущностям](read-api-points.md).** Правило +чтения — что выборка «за период» делает со строками без метки — обязано +проектироваться вместе с читателем, иначе такие строки молча исчезнут из любого +ответа. Порядок тот же, что у [journal-order-on-ingest](journal-order-on-ingest.md) +после `/stats`: решение принято, момент взятия назван. Остаток задачи «Дозакрыть находки ревью по слиянию сущностей» (архивный change `dozakryt-nahodki-sushchnostej`). Та задача сделала мягким чтение заголовка: @@ -22,18 +34,11 @@ пропусков всех трёх классов. Дрейф формата дат у HAE при этом задокументирован (`docs/research/apple-health.md`), то есть вход не выдуман. -## Вопросы -Хранить ли сущность с разобранным `id` и неразобранной меткой. Цена: +## Чем платим за отсрочку -1. **Хранить с NULL-меткой** — правка схемы (`start_utc`/`ts_utc` становятся - NULLABLE) плюс правила чтения витрины: выборка «за период» обязана сказать, - что делает с такими строками, иначе они молча исчезнут из любого ответа. - Зато содержимое (маршрут!) сохраняется, а метку восстановит пересборка, - когда разбор научится читать формат. -2. **Не хранить** — как сейчас. Тело живёт в архиве до ретеншена, доставку - вернёт `reindex`. После включения ретеншена окно становится необратимым. -3. **Хранить, подставив метку доставки** — отвергается сразу: это выдуманное - измерение в колонке, по которой идёт выборка. - -Рекомендация — (1), но не раньше, чем появится Read API по сущностям: правило -чтения без читателя проектируется вслепую. +Вариант «не хранить» — то, чем живём сегодня: тело лежит в архиве, доставку +вернёт `reindex`. Отсрочка безопасна ровно до включения +[ретеншена](raw-archive-retention.md): после него окно становится необратимым. +Значит эти две задачи связаны порядком — ретеншен не включается раньше, чем +сущность без метки начнёт храниться, либо включается с явной записью о том, +что этот класс теряется. diff --git a/docs/tasks/items/journal-order-on-ingest.md b/docs/tasks/items/journal-order-on-ingest.md index f7a7566..f442d83 100644 --- a/docs/tasks/items/journal-order-on-ingest.md +++ b/docs/tasks/items/journal-order-on-ingest.md @@ -1,8 +1,8 @@ # Порядок журнала при конкурентных приёмах - **Секция:** ядро -- **Зачем:** Решено: повторы, но после /stats. Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда -- **Теги:** goal:journal-and-rebuild, question +- **Зачем:** Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда, и живая витрина молча расходится с пересборкой +- **Теги:** goal:journal-and-rebuild **Решение принято владельцем 2026-08-02: вариант (в), но не раньше `/stats`.** До появления наблюдаемости живём вариантом (г) с уже записанным в спеке @@ -13,7 +13,8 @@ Вынут ревью кода задачи «Разнести ответ приёма и свёртку доставки» (профиль `deep`, враждебный проход, находка с построенным путём и прогоном). -## Вопросы +## Что происходит + Метка `received_at` доставки фиксируется в момент выпуска ULID — **до** записи тела в архив и до вставки строки учёта. Порядок, в котором строки становятся видимыми воркеру, порядку меток не подчиняется: между выпуском идентификатора и diff --git a/docs/tasks/items/release-rollback-after-migration.md b/docs/tasks/items/release-rollback-after-migration.md index 96f3da0..f5e00a7 100644 --- a/docs/tasks/items/release-rollback-after-migration.md +++ b/docs/tasks/items/release-rollback-after-migration.md @@ -1,8 +1,8 @@ # Чем откатывать релиз после наката миграции - **Секция:** инфра -- **Зачем:** Решено: копия файла базы перед накатом. Страж версии схемы делает возврат бинаря отказом, а понизить схему нечем -- **Теги:** goal:deploy, question +- **Зачем:** Страж версии схемы делает возврат старого бинаря отказом, а понизить схему нечем — аварийный путь пришлось бы изобретать в аварии +- **Теги:** goal:deploy **Решение принято владельцем 2026-08-02: вариант (2) — копия файла базы перед накатом.** Entrypoint контейнера копирует файл базы рядом до старта бинаря, @@ -37,7 +37,8 @@ синхронизации; для `stateOfMind` не закрывается ничем — у него доставки HAE единственный источник. -## Вопросы +## Варианты и цена + 1. **Подкоманда `healthlog migrate --down-to N`.** Цена: новая поверхность CLI плюс тест на `Down` каждой миграции (сейчас их нет, и `DROP COLUMN` в SQLite ведёт себя не так, как в постгресе). Зато откат становится операцией, а не diff --git a/docs/tasks/items/tie-break-equal-completeness.md b/docs/tasks/items/tie-break-equal-completeness.md index 10d736a..a18eda2 100644 --- a/docs/tasks/items/tie-break-equal-completeness.md +++ b/docs/tasks/items/tie-break-equal-completeness.md @@ -1,8 +1,8 @@ # Тай-брейк при равной полноте точек - **Секция:** ядро -- **Зачем:** Решено: брать бо́льшее значение. Порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт -- **Теги:** goal:merge-robustness, question +- **Зачем:** При равной полноте порядок канонических форм берёт меньшее значение в 96% случаев — у накопительных это систематический недосчёт +- **Теги:** goal:merge-robustness **Решение принято владельцем 2026-08-02: вариант (б) — брать бо́льшее значение точки.** Ниже — исходная постановка блокера, она же ТЗ; рекомендация в конце @@ -20,7 +20,8 @@ **измениться** (иначе правило не сработало), а число столкновений с равной полнотой — остаться прежним. -## Вопросы +## Что происходит + Какое правило выбирает победителя, когда по одним координатам приехали две точки с **равными** наборами содержательных полей и разными значениями. Структурная часть правила слияния закрыта (`pravilo-sliyaniya-tochek`); открыт только этот diff --git a/docs/tasks/items/unseen-sections-check.md b/docs/tasks/items/unseen-sections-check.md index 1fbedef..c47dd8b 100644 --- a/docs/tasks/items/unseen-sections-check.md +++ b/docs/tasks/items/unseen-sections-check.md @@ -1,7 +1,7 @@ -# Проверка секций, которых поток ещё не приносил +# Активная проверка: поток принёс секцию, которой раньше не было - **Секция:** ядро -- **Зачем:** Симптомы, ЭКГ, лекарства, цикл и вес не виденны живьём — разбор писался вслепую +- **Зачем:** Момент появления новой секции фиксируется в базе, но заметить его может только тот, кто догадается заглянуть в колонку - **Теги:** goal:parsing-and-storage Разбор пишется по тем данным, что видел поток, а он приносил только `metrics`, @@ -10,11 +10,26 @@ агенту-медику нужен наверняка. Пользователь настраивает оставшиеся метрики на телефоне, так что данные -появятся сами. Задача — не пропустить момент: убедиться, что новые секции -разбираются, а не молча падают в `parse_status`. +появятся сами. **Задача — не пропустить момент.** Разбор самих секций сюда не +входит и входить не может: их формы никто не видел, и вслепую разбор +сознательно не пишется. Каждая приехавшая секция станет отдельной задачей — +тогда, когда её будет на чём проверить. + +## Что уже сделано + +Разбор перечисляет непокрытые секции и пишет их в `delivery.uncovered_sections` +(change `2026-08-01-nerazobrannye-sekcii-dostavki`). Событие **фиксируется** — +но ничем не наблюдается: узнать о нём можно только запросом в базу руками. + +Модель под секции с собственным `id` заложена (change +`2026-08-02-trenirovki-i-zapisi`): таблица `record` ключуется парой +`род + id`, и новая секция добавляется **одной строкой** в множество покрытых +имён разбора, а не миграцией. + +## Что известно про сами секции Часть вопроса закрыта разбором экспортов (находка 42): в Health эти данные -**есть** и в экспорте они присутствуют — `BodyMass` (1127 записей), +**есть** и в экспорте присутствуют — `BodyMass` (1127 записей), `BloodPressureSystolic`/`Diastolic` (по 18), `BodyTemperature` (11), `Headache` (36), `SexualActivity` (46), `Dietary*` (по 88). Значит вопрос не «есть ли данные», а «доедут ли они через HAE и в какой форме». @@ -27,21 +42,27 @@ экспорте точно, а вот как его отдаёт HAE, неизвестно. Это первое, на что смотреть, когда данные появятся. -Готово, когда каждая новая секция либо разобрана, либо явно описана в -`docs/research/apple-health.md` как не пришедшая, и ни одна не числится в ошибках -разбора. +## Критерии приёмки -## Что уже сделано +- имя секции, которого разбор раньше не встречал, порождает событие уровня выше + рутины — оракул: тест на доставке с выдуманной секцией, в логе ровно одна + строка с этим именем +- то же имя во второй доставке события больше не порождает — оракул: тот же + тест на двух доставках подряд, вторая молчит +- список всего, что поток когда-либо приносил и разбор не покрыл, достаётся + одной командой, без ручного SQL — оракул: прогон команды на живой базе, + вывод сходится с `SELECT DISTINCT` по `delivery.uncovered_sections` +- перечень не виденных живьём секций в `docs/research/apple-health.md` сходится + с множеством покрытых имён в `internal/hae` — оракул: глазами, сверка двух + списков поимённо +- повторный прогон живого архива даёт то же состояние — оракул: + `task verify:archive` -Разбор перечисляет непокрытые секции и пишет их в `delivery.uncovered_sections` -(change `2026-08-01-nerazobrannye-sekcii-dostavki`). Момент, когда поток принесёт -секцию, которой раньше не было, теперь **фиксируется** — остаётся научиться -замечать его активно: один `SELECT DISTINCT` по колонке даёт список всего, что -поток приносил, и сравнение с известным набором закрывает задачу. +## Рамки -Модель под секции с собственным `id` заложена (change -`2026-08-02-trenirovki-i-zapisi`): таблица `record` ключуется парой -`род + id`, и новая секция добавляется **одной строкой** в множество покрытых -имён разбора, а не миграцией. Покрыты `workouts` и `stateOfMind`; остались -`ecg`, `symptoms`, `cycleTracking`, `medications`, `heartRateNotifications` — -их формы никто не видел, и разбор вслепую сознательно не писался. +Схема не трогается: колонка `uncovered_sections` уже есть. Разбор новых секций +не пишется. Данные только читаются; перезапуск сервиса допустим. + +Связано: `docs/architecture.md` → «Неразобранные секции доставки», находки 42, +44; идея [monthly-manual-sections-pass](monthly-manual-sections-pass.md) ждёт +тех же данных.