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

- половина потока (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,84 @@
## 1. Разбор: перечисление непокрытых ключей
- [x] 1.1 `decodeMetrics``decodeEnvelope`: один `json.Decoder` идёт по
верхнему уровню, имя члена читается `Token()`, значение покрытого ключа
декодируется на месте, значение непокрытого проглатывается декодированием в
выбрасываемый `json.RawMessage` (ограничитель вложенности stdlib при этом
работает, в отличие от ручного счёта глубины)
- [x] 1.2 Покрытость — функция рядом с разбором, а не изменяемая пакетная карта;
`Result.Uncovered` отдаёт непокрытые отсортированными и без повторов
- [x] 1.3 Границы: не больше 32 имён (`Result.UncoveredDropped` считает
отброшенные), имя длиннее 64 байт декодированного имени обрезано по границе
рун, маркер `…` приписывается сверх предела
- [x] 1.4 Повтор ключа `metrics` даёт объединение секций; повтор непокрытого
ключа даёт одно имя. Тело без `data`, `data` не объект, `data` пустой,
`metrics` неверного типа — прежнее поведение (ошибка ровно там, где была)
- [x] 1.5 Ошибка после разобранной секции `metrics` точек не отдаёт
## 2. Хранилище: статус и список
- [x] 2.1 Константа `store.ParsePartial`; док-комментарий `ParsePending`
переписан на «этим разбором ещё не смотрели»
- [x] 2.2 Миграция: колонка `uncovered_sections TEXT NOT NULL DEFAULT '[]'` и
перевод существующих `parsed` в `pending`; в комментарии миграции сказано, что
по данным она односторонняя — `Down` снимает колонку, прежние статусы не
восстанавливает
- [x] 2.3 Исход разбора пишется структурой (`store.ParseOutcome`), а не растущим
списком позиционных параметров; список замещает прежнее значение целиком,
включая замещение пустым; `nil` и пустой срез записываются как `[]`
## 3. Свёртка: исход и лог
- [x] 3.1 `Stats.Uncovered` и `Stats.UncoveredDropped`; статус `partial` при
непустом списке, `failed` сильнее; при отказе список сохраняется, если разбор
успел его собрать
- [x] 3.2 Атрибуты `uncovered` (структурным `[]string`) и `uncovered_dropped` в
единственном логирующем чекпоинте; уровень из-за одной лишь частичности не
растёт, срабатывание границ даёт `WARN`
## 4. Тесты (приёмочные критерии)
- [x] 4.1 Фикстура `uncovered_sections.json`: точки метрик сохранены, ключи
`workouts` и `stateOfMind` в списке, статус `partial`
- [x] 4.2 Доставка из одной непокрытой секции: разбор без ошибки, ноль точек,
ключ в списке, статус `partial`
- [x] 4.3 Доставка из одних метрик: список пуст, статус `parsed`
- [x] 4.4 Детерминизм: тот же набор секций в разном порядке и с повтором ключа
даёт тот же список
- [x] 4.5 Границы: 40 ключей → 32 имени и счётчик отброшенных; длинное имя →
обрезка с маркером
- [x] 4.6 Удержание кучи: тело в десятки мегабайт, состоящее преимущественно из
непокрытой секции, удерживает не больше четырёх тел; тело из вложенных скобок
отвергается, а не съедает память
- [x] 4.7 Отказ всё или ничего: тело оборвано после секции метрик — ошибка, ноль
точек, `failed`
- [x] 4.8 Лог свёртки: имена ключей есть, содержимого секций нет; уровень при
обычной частичности не повышен
- [x] 4.9 Миграция: строка со статусом `parsed` становится `pending`, колонка
получает `[]`
- [x] 4.10 Пересвёртка: доставка `partial`, у которой список опустел, становится
`parsed` с пустым списком
- [x] 4.11 Прогон живого архива (`task verify:archive`): доставки без метрик
получают `partial` с непустым списком, повторный прогон состояния не меняет
- [x] 4.12 Сверка с состоянием ДО изменения: витрина, собранная из живого архива
новым кодом, совпадает по объектам, точкам и метрикам со снимком, снятым до
изменения
## 5. Документация
- [x] 5.1 `docs/database.md`: колонка, полный набор статусов, новый смысл
`pending`
- [x] 5.2 `docs/architecture.md`: частичный разбор в разделе приёма
- [x] 5.3 `docs/local-research.md`: находка о наборах секций в живом потоке
(99 доставок: 51 `metrics`, 24 `workouts`, 24 `stateOfMind`, секции не
смешиваются, пустых нет)
## 6. Замеры после реализации
- [x] 6.1 Живой архив (104 доставки): частично разобрано **50** — 25 `stateOfMind`
и 25 `workouts`. Отпечаток витрины `a59b38ea…` совпал с прогоном ДО изменения
на том же архиве: переписанный разбор конверта — строгий no-op для витрины
- [x] 6.2 Удержание кучи: тело 40 МиБ, из которых почти всё — непокрытая
секция, удерживает **0 МиБ**; тело из 100 000 уровней вложенности отвергается
- [x] 6.3 Живой сервис разобрал пришедшие с телефона доставки (5 штук) —
заодно закрыт пункт 6.3a задачи `razbor-metrik-v-obekty`