## ADDED Requirements ### Requirement: Учёт частично разобранной доставки Система SHALL отличать доставку, разобранную целиком, от доставки, в теле которой остались непокрытые разбором секции. Доставка с непустым списком непокрытых ключей MUST получать статус `partial`, а не `parsed`. Статусы разбора: ``` pending этим разбором ещё не смотрели parsed разобрано всё, что в теле было partial разобрано покрытое; в теле остались непокрытые секции failed разобрать не удалось, точек нет ``` Источник истины — список непокрытых ключей; статус производен от него и от факта отказа, в порядке `failed` → `partial` → `parsed`. Приоритет назван явно, чтобы читатели (ретеншен, статистика) спрашивали статус, а не сравнивали список со строкой. Список непокрытых ключей 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** доставка со статусом `partial` сворачивается повторно разбором, который эту секцию уже покрывает - **THEN** её статус становится `parsed` - **AND** сохранённый список непокрытых ключей пуст #### Scenario: Строки прежнего разбора переводятся в неразобранные - **WHEN** база содержит доставки со статусом `parsed`, свёрнутые до появления частичного статуса - **THEN** после миграции их статус равен `pending` - **AND** тела остаются в архиве, а повторная свёртка даёт то же состояние ## MODIFIED Requirements ### Requirement: Значения точек не попадают в логи Данные о здоровье чувствительнее токенов. Система MUST NOT писать значения точек и тела доставок в записи лога уровня выше `DEBUG`. Непокрытые секции называются в логе **именами ключей**: имя секции — это форма пакета, а не измерение. Содержимое секции в лог не попадает ни при каком уровне выше `DEBUG`. Имена идут структурным атрибутом, а не склейкой в текст сообщения: кодировщик экранирует управляющие символы, и имя из чужого тела не разрывает построчный разбор логов. Частичный разбор уровня записи не повышает: `partial` — установившееся состояние половины потока (48 доставок из 99), и постоянный `WARN` обесценил бы уровень. Повышает уровень другое — срабатывание границ списка: тело с сотнями секций или с именем длиннее предела на HAE не похоже вовсе. #### Scenario: Разбор доставки логируется без значений - **WHEN** доставка разобрана - **THEN** запись лога содержит счётчики (метрик, точек, объектов) и идентификатор доставки - **AND** не содержит ни значений точек, ни имён устройств #### Scenario: Непокрытые секции названы именами ключей - **WHEN** доставка содержит непокрытую секцию - **THEN** запись лога содержит имена непокрытых ключей отдельным атрибутом - **AND** не содержит ничего из содержимого этих секций - **AND** уровень записи из-за одной лишь частичности не повышается #### Scenario: Границы списка сработали - **WHEN** список непокрытых ключей усечён по числу имён или по длине имени - **THEN** запись лога имеет уровень `WARN` - **AND** содержит число отброшенных имён