## MODIFIED Requirements ### Requirement: Учёт частично разобранной доставки Система SHALL отличать доставку, разобранную целиком, от доставки, в теле которой остались непокрытые разбором секции. Доставка с непустым списком непокрытых ключей MUST получать статус `partial`, а не `parsed`. Статусы разбора: ``` pending этим разбором ещё не смотрели — или смотрели, но работа не сделана по обстоятельствам (см. ниже) parsed разобрано всё, что в теле было partial разобрано покрытое; в теле остались непокрытые секции failed разобрать не удалось, точек нет ``` Источник истины — список непокрытых ключей; статус производен от него и от факта отказа, в порядке `failed` → `partial` → `parsed`. Приоритет назван явно, чтобы читатели (ретеншен, статистика) спрашивали статус, а не сравнивали список со строкой. **Отказ обстоятельств статуса не меняет вовсе.** Отмена работы снаружи и занятость базы дольше повторов транзакции означают «не сделано», а не «не выходит»: доставка остаётся `pending` и будет свёрнута снова. Правило появилось не из аккуратности — фоновая свёртка `failed` не подбирает никогда, и без этого различения занятость базы (а с фоновой свёрткой конкуренция за неё штатная) выводила бы доставку из очереди навсегда. Различение живёт **в одном месте**: тот, кто пишет исход, и тот, кто классифицирует его в счётчики, спрашивают один предикат. Дедлайн самой свёртки к обстоятельствам MUST NOT относиться: доставка, не уложившаяся в бюджет, не уложится в него и в следующий раз, а бесконечный повтор заведомо безнадёжного — это очередь, которая не движется. Отказы, случившиеся **до** чтения тела (учётной записи нет, соседний запрос не прошёл), и отказ самой записи исхода статуса не меняют по другой причине — записать его нечем. Доставка остаётся `pending`, что честно: этим разбором её не досмотрели. Список непокрытых ключей SHALL сохраняться рядом с доставкой — именами ключей, без содержимого секций. Он же ответ на вопрос «что останется потерянным, если тело удалить»: для `stateOfMind` доставки HAE единственный источник, в экспорте Apple его нет (находка 46). Поэтому список MUST сохраняться и при отказе разбора, если разбор успел его собрать: `failed` с непустым списком — законное состояние. Запись списка MUST замещать прежнее значение целиком, включая замещение пустым: иначе доставка, все секции которой стали покрытыми, осталась бы `partial` навсегда. Список — снимок покрытия **на момент свёртки**. Задача, которая начинает разбирать секцию, тем же изменением SHALL переводить `partial`-строки с этим ключом в `pending`; ретеншену позволено смотреть на `partial` только при соблюдении этого правила. Статусы, поставленные разбором, который частичного исхода не различал, доверия не заслуживают: под `parsed` у них лежат и полностью разобранные доставки, и доставки без метрик вовсе. Такие строки MUST переводиться в `pending` — «этим разбором ещё не смотрели». Число точек у них до пересвёртки остаётся прежним: оно производно от объектов витрины, которые никуда не делись. #### Scenario: Доставка с непокрытой секцией отмечается частичной - **WHEN** разбор доставки вернул непустой список непокрытых ключей - **THEN** `parse_status` доставки равен `partial` - **AND** список непокрытых ключей сохранён вместе с доставкой - **AND** точки покрытой секции сохранены как обычно #### Scenario: Доставка без непокрытых секций остаётся `parsed` - **WHEN** разбор доставки не дал непокрытых ключей - **THEN** `parse_status` равен `parsed` - **AND** сохранённый список непокрытых ключей пуст #### Scenario: Отказ разбора сильнее частичности - **WHEN** разбор доставки завершился ошибкой в самом разборе или в записи точек - **THEN** `parse_status` равен `failed` - **AND** список непокрытых ключей сохранён, если разбор успел его собрать #### Scenario: Занятая база доставку из очереди не выводит - **WHEN** разбор не состоялся из-за занятости базы или отмены работы снаружи - **THEN** `parse_status` остаётся `pending` #### Scenario: Пересвёртка после того, как секция стала покрытой - **WHEN** доставка со статусом `partial` сворачивается повторно разбором, который эту секцию покрывает - **THEN** `parse_status` становится `parsed` - **AND** сохранённый список непокрытых ключей пуст