непокрытые секции доставки видны в статусе разбора

- половина потока (50 доставок из 104) не несёт metrics вовсе и до сих пор
  числилась parsed: ретеншен, поверив статусу, срезал бы тела stateOfMind,
  которых в экспорте Apple нет
- разбор перечисляет верхнеуровневые ключи data, непокрытые проглатываются
  декодированием: тело 40 МиБ из непокрытой секции удерживает 0 МиБ
- статус partial и колонка delivery.uncovered_sections; миграция переводит
  прежние parsed в pending — им верить нельзя
- витрина не изменилась: отпечаток совпал с прогоном до изменения
This commit is contained in:
av
2026-08-01 21:25:59 +03:00
parent 7a7594e3e7
commit 34e5109b6d
26 changed files with 1808 additions and 89 deletions
-1
View File
@@ -20,7 +20,6 @@
- [Read API: точки, выбор слоя, свёртка по сетке](read-api-tochki.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
- [OpenAPI-спека и Swagger UI](openapi-swagger.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
- [MCP-сервер поверх Read API](mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
- [Непокрытые секции доставки видны в статусе разбора](nerazobrannye-sekcii-dostavki.md) — тело с одним stateOfMind числится parsed, а ретеншен снесёт его как разобранное — и в экспорте Apple его нет
- [Разнести ответ приёма и свёртку доставки](otvet-i-svyortka.md) — синхронная свёртка не помещается в write_timeout: широкие проходы получают обрыв вместо 200
## средний
@@ -1,60 +0,0 @@
# Непокрытые секции доставки видны в статусе разбора
**Приоритет:** высокий
Была блокером, вынутым ревью кода задачи `razbor-metrik-v-obekty` (профиль
`deep`, проход негативного пространства). **Решение принято** — ниже задача.
## Что не так сегодня
Разбор читает только `data.metrics`. Доставка, состоящая из `workouts`,
`stateOfMind`, `symptoms` или `ecg`, помечается `parse_status=parsed` с нулём
точек — неотличимо от доставки с пустой секцией метрик.
Само по себе некритично: секции пока не разбираются сознательно, тела лежат в
архиве. Опасность в **сцепке с ретеншеном**. Ретеншен по замыслу срезает архив
до следующего проверенного экспорта. Если он будет ориентироваться на
`parse_status`, он снесёт тела, которые числятся разобранными, — а для
`stateOfMind` это необратимо: **в экспорте Apple его нет** (находка 46),
доставки HAE для него единственный источник.
Цена ошибки здесь не «придётся пересобрать», а «истории состояния разума больше
не существует».
## Что решено
Вариант (1): **разбор возвращает список верхнеуровневых ключей `data`, которые
он не покрыл; статус доставки — `partial`.**
Почему он, а не альтернативы:
- Считать `parsed` только полностью разобранную доставку, а остальные держать
в `pending` — дёшево, но `pending` перестаёт означать «ещё не смотрели», и
подбор зависших доставок теряет свой признак. А подбор `pending` как раз
появляется задачей [otvet-i-svyortka](otvet-i-svyortka.md).
- Запретить ретеншену смотреть на `parse_status` — нулевая цена сейчас, но
ретеншен становится тупым и не защищает от «тело разобрано неверно, а мы его
уже срезали».
Список непокрытых ключей — дешёвая честность, и он же закрывает задачу «не
пропустить момент, когда поедет новая секция».
## Что делать
1. `hae.Parse`: перечислить верхнеуровневые ключи `data` и вернуть те, что
разбор не покрыл. Один `json.Decoder` по верхнему уровню, **без чтения
содержимого** — секции доходят до десятков МиБ.
2. Статус `partial` рядом с `parsed`/`failed`; миграция, если статус хранится
ограниченным набором.
3. Свёртка пишет непокрытые ключи в исход доставки и логирует их один раз —
именами ключей, без содержимого.
4. Тест на фикстуре с секцией, которой разбор не знает: статус `partial`,
ключ в списке, точки метрик при этом сохранены.
## Связано
- [retenshen-syrogo-arhiva](retenshen-syrogo-arhiva.md) — решить **до** неё.
- [proverka-novyh-sekcij](proverka-novyh-sekcij.md) — тот же признак закрывает
и её: момент появления новой секции становится событием в логе.
- [trenirovki-i-zapisi](trenirovki-i-zapisi.md) — по мере разбора секций список
непокрытых сокращается сам.
+7
View File
@@ -29,3 +29,10 @@
`docs/local-research.md` как не пришедшая, и ни одна не числится в ошибках
разбора.
## Что уже сделано
Разбор перечисляет непокрытые секции и пишет их в `delivery.uncovered_sections`
(change `2026-08-01-nerazobrannye-sekcii-dostavki`). Момент, когда поток принесёт
секцию, которой раньше не было, теперь **фиксируется** — остаётся научиться
замечать его активно: один `SELECT DISTINCT` по колонке даёт список всего, что
поток приносил, и сравнение с известным набором закрывает задачу.
+11
View File
@@ -26,3 +26,14 @@
экспорта, записи `stateOfMind` не трогаются вовсе, а `/stats` показывает
глубину архива и дату снапшота, до которой он подрезан.
## Предусловие снято
Признак, без которого ретеншен был опасен, готов: доставка с непокрытой секцией
имеет статус `partial` и список непокрытых ключей
(change `2026-08-01-nerazobrannye-sekcii-dostavki`). Ретеншен обязан спрашивать
статус, а не считать `parsed` разрешением: тело `stateOfMind` восстановить
неоткуда — в экспорте Apple секции нет.
Вместе с этим действует правило: задача, которая начинает разбирать секцию, тем
же изменением переводит `partial`-строки с этим ключом в `pending`. Ретеншену
позволено смотреть на `partial` только пока правило соблюдается.