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:
av
2026-08-03 17:47:41 +03:00
parent b2bdb6383f
commit 3df42afeca
8 changed files with 112 additions and 56 deletions
@@ -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`.
+21 -16
View File
@@ -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): после него окно становится необратимым.
Значит эти две задачи связаны порядком — ретеншен не включается раньше, чем
сущность без метки начнёт храниться, либо включается с явной записью о том,
что этот класс теряется.
+4 -3
View File
@@ -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`); открыт только этот
+41 -20
View File
@@ -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) ждёт
тех же данных.