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

- половина потока (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
@@ -0,0 +1,58 @@
## Why
Разбор читает только `data.metrics`. Доставка, целиком состоящая из другой
секции, получает `parse_status=parsed` с нулём точек — неотличимо от доставки с
пустой секцией метрик. Измерено на живом архиве: из 99 доставок 48 не содержат
`metrics` вовсе (24 `workouts`, 24 `stateOfMind`), то есть почти половина потока
сейчас числится разобранной, не будучи разобранной.
Само по себе это некритично — тела лежат в архиве. Опасность в сцепке с
ретеншеном: он по замыслу срезает архив до следующего проверенного экспорта, и
если станет опираться на `parse_status`, снесёт тела, которые числятся
разобранными. Для `stateOfMind` это необратимо — в экспорте Apple его нет
(находка 46), доставки HAE единственный его источник. Ретеншен — следующая
задача, поэтому признак нужен до неё.
## What Changes
- `hae.Parse` перечисляет верхнеуровневые ключи `data` и возвращает те, что
разбор не покрыл. Перечисление идёт **без чтения содержимого секций**: тела
доходят до 42 МиБ, и удержание кучи здесь — часть контракта.
- Появляется статус доставки `partial` рядом с `parsed`/`failed`: тело
разобрано в той части, которую разбор покрывает, и в нём остались
непокрытые секции.
- Свёртка сохраняет список непокрытых ключей в исход доставки и называет их
**именами ключей** в своём единственном логирующем чекпоинте. Содержимого
секций в логе нет и быть не может.
- Число ключей и длина имени в записи ограничены: ключи приходят из тела,
которым отправитель управляет целиком; усечение видно счётчиком и маркером.
- **Миграция переписывает `parse_status` у всех накопленных доставок:**
существующие `parsed` становятся `pending` — «этим разбором ещё не смотрели».
Статус, поставленный кодом, который частичного разбора не различал, ничего не
доказывает, а ретеншен собирается на него опираться. Цена: 99 строк живой базы
меняют статус на первом старте нового бинаря, и до появления подбора `pending`
(задача `otvet-i-svyortka`) никто их не пересвернёт; тела при этом остаются в
архиве, объекты витрины — на месте, и `Down` прежние статусы не восстановит.
## Capabilities
### New Capabilities
Новых нет.
### Modified Capabilities
- `parsing`: разбор обязан перечислять непокрытые верхнеуровневые ключи `data`,
не читая их содержимого, и отдавать их вызывающему.
- `storage`: у доставки появляется статус `partial` и список непокрытых секций;
учёт обязан отличать «разобрано целиком» от «разобрано частично».
## Impact
- `internal/hae` — перечисление ключей в том же проходе, что и разбор метрик.
- `internal/store` — константа статуса, колонка `uncovered_sections`, миграция.
- `internal/fold` — исход свёртки, статус и атрибут лога.
- `docs/database.md`, `docs/architecture.md`, `docs/local-research.md`
схема, статусы и находка о наборах секций в живом потоке.
- Ретеншен сырого архива (задача `retenshen-syrogo-arhiva`) получает признак,
на который ему можно опираться.