tasks: разобраны вопросы и набран спринт 2026-08-03
- открытых вопросов не осталось: три решения владельца доведены до берущегося вида, по entity-without-parsed-label принято хранить с NULL-меткой после Read API - unseen-sections-check сжата до остатка — активная проверка появления секции; разбор невиденных секций из неё вынут, вслепую он не пишется - спринт под целью parsing-and-storage: categorical-value-dictionary и unseen-sections-check, обеим написаны критерии приёмки с оракулами
This commit is contained in:
@@ -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) — Запуск без конфига заведёт пустую базу в корне рядом с настоящей — тихая ловушка
|
||||
|
||||
@@ -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) — Момент появления новой секции фиксируется в базе, но заметить его может только тот, кто догадается заглянуть в колонку
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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): после него окно становится необратимым.
|
||||
Значит эти две задачи связаны порядком — ретеншен не включается раньше, чем
|
||||
сущность без метки начнёт храниться, либо включается с явной записью о том,
|
||||
что этот класс теряется.
|
||||
|
||||
@@ -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 — **до** записи
|
||||
тела в архив и до вставки строки учёта. Порядок, в котором строки становятся
|
||||
видимыми воркеру, порядку меток не подчиняется: между выпуском идентификатора и
|
||||
|
||||
@@ -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
|
||||
ведёт себя не так, как в постгресе). Зато откат становится операцией, а не
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Тай-брейк при равной полноте точек
|
||||
|
||||
- **Секция:** ядро
|
||||
- **Зачем:** Решено: брать бо́льшее значение. Порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт
|
||||
- **Теги:** goal:merge-robustness, question
|
||||
- **Зачем:** При равной полноте порядок канонических форм берёт меньшее значение в 96% случаев — у накопительных это систематический недосчёт
|
||||
- **Теги:** goal:merge-robustness
|
||||
|
||||
**Решение принято владельцем 2026-08-02: вариант (б) — брать бо́льшее значение
|
||||
точки.** Ниже — исходная постановка блокера, она же ТЗ; рекомендация в конце
|
||||
@@ -20,7 +20,8 @@
|
||||
**измениться** (иначе правило не сработало), а число столкновений с равной
|
||||
полнотой — остаться прежним.
|
||||
|
||||
## Вопросы
|
||||
## Что происходит
|
||||
|
||||
Какое правило выбирает победителя, когда по одним координатам приехали две точки
|
||||
с **равными** наборами содержательных полей и разными значениями. Структурная
|
||||
часть правила слияния закрыта (`pravilo-sliyaniya-tochek`); открыт только этот
|
||||
|
||||
@@ -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) ждёт
|
||||
тех же данных.
|
||||
|
||||
Reference in New Issue
Block a user