# uncovered-sections Specification

## Purpose

Наблюдение за секциями тела Health Auto Export, которых разбор не покрывает:
первая по журналу встреча имени видна владельцу событием в логе, а перечень
накопленного отдаётся отдельной подкомандой. Разбор самих секций сюда не входит
— их формы никто не видел, и вслепую он не пишется.

## Requirements
### Requirement: Первая встреча непокрытой секции порождает событие

Свёртка SHALL писать запись уровня `WARN`, когда доставка принесла имя
непокрытой секции, которого **не было ни в одной доставке, стоящей в журнале
раньше**. Имена, признанные новыми, SHALL идти отдельным атрибутом записи —
всегда, независимо от выбранного уровня.

Уровень назван поимённо: `WARN` — «может стать проблемой, посмотри». `ERROR`
означал бы сбой, который надо разбирать, а приезд новой секции — штатное событие
внешнего мира.

Событие однократно за всю жизнь имени. Поэтому запись SHALL называть его
независимо от того, какие повторяющиеся события (перезаписи точек, запечатанный
час, расхождение слоя) случились в той же доставке: замаскировать однократное
рутинным значило бы потерять ровно то, ради чего наблюдение заведено.

Отдельной строки лога у события нет: чекпоинт свёртки остаётся единственным.

#### Scenario: Имя, которого журнал раньше не видел

- **GIVEN** ни одна прежняя доставка не содержала секции `symptoms`
- **WHEN** доставка с секцией `symptoms` свёрнута
- **THEN** запись чекпоинта свёртки имеет уровень `WARN`
- **AND** атрибут новых секций содержит ровно `symptoms`

#### Scenario: Новая секция не маскируется рутинным событием

- **GIVEN** ни одна прежняя доставка не содержала секции `ecg`
- **WHEN** доставка с секцией `ecg` одновременно перезаписывает точки уже
  сохранённого часа
- **THEN** запись чекпоинта называет новую секцию, а не перезапись точек
- **AND** счётчик перезаписей остаётся в атрибутах записи

### Requirement: Повторная встреча имени события не порождает

Система SHALL считать виденным всякое имя непокрытой секции, уже встречавшееся в
доставке, стоящей в журнале раньше текущей: в атрибут новых секций оно не
попадает и уровень записи не поднимает. Перечисление непокрытых секций при этом
не меняется: атрибут `uncovered` SHALL продолжать называть их все.

Отсюда следствие для приёмки: наблюдаемый признак события — **атрибут новых
секций**, а не присутствие имени в записи вообще. Имя непокрытой секции стоит в
записи каждой доставки, которая её принесла, и так было до этого изменения.

#### Scenario: Вторая доставка с тем же именем молчит

- **GIVEN** доставка с секцией `symptoms` уже свёрнута
- **WHEN** следующая доставка приносит ту же секцию `symptoms`
- **THEN** атрибут новых секций у второй записи пуст
- **AND** уровень второй записи рутинный
- **AND** секция `symptoms` остаётся в атрибуте непокрытых секций

#### Scenario: Новое имя рядом с уже виденным

- **GIVEN** доставка с секцией `symptoms` уже свёрнута
- **WHEN** следующая доставка приносит `symptoms` и `ecg`
- **THEN** атрибут новых секций содержит ровно `ecg`

### Requirement: Новизна выводится из журнала, а не из порядка свёртки

Признак новизны SHALL определяться сравнением с доставками, стоящими в журнале
**строго раньше** текущей — по паре `(received_at, id)`, как порядок журнала
определён capability пересборки. Собственная учётная запись доставки в сравнение
не входит при любом порядке записи исхода.

Отсюда следует, что признак идемпотентен: повторная свёртка той же доставки даёт
тот же исход, а проигрывание полного журнала пересборкой воспроизводит ровно те
же события — свёртка по журналу остаётся функцией журнала, а не числа прогонов.

Хранимого реестра встреченных имён это изменение заводить MUST NOT: факт уже
лежит в учётной записи доставки, и вторая его копия расходилась бы с первой при
пересборке молча. Это решение **этого** изменения, а не запрет навсегда:
потребителю, которому понадобится история непокрытых секций, переживающая
удаление тел (ретеншен архива), придётся его пересмотреть — цена названа в
требовании о честности перечня.

Запрос новизны SHALL выполняться **вне транзакции записи** свёртки: он идёт по
растущему журналу, а транзакция записи объектов не имеет права держать блокировку
дольше, чем позволено конкурирующему приёму.

#### Scenario: Повторная свёртка той же доставки не меняет исход

- **GIVEN** доставка с новой секцией свёрнута и событие записано
- **WHEN** та же доставка свёрнута повторно
- **THEN** событие повторяется тем же составом имён

#### Scenario: Доставка, свёрнутая позже более новой, судится по журналу

- **GIVEN** доставка A стоит в журнале раньше доставки B, и обе содержат
  секцию `ecg`
- **WHEN** B свёрнута первой, а A — после неё
- **THEN** A признаёт `ecg` новым, потому что раньше неё в журнале этого имени
  не было

#### Scenario: Запрос новизны не удерживает блокировку записи

- **WHEN** доставка с непокрытыми секциями сворачивается
- **THEN** обращение к журналу за признаком новизны идёт вне транзакции записи
  объектов

### Requirement: Отказ свёртки не глотает событие

Список непокрытых секций переживает отказ разбора, поэтому проверка новизны SHALL
идти до ветвления на успех и отказ, а новые имена SHALL попадать в запись об
отказе — она уже выше рутинного уровня. Уровень такой записи определяет отказ;
событие опознаётся атрибутом новых секций.

Это относится к **каждому** исходу, который пишет список в учётную запись, а не
к одному только отказу разбора: отказ слияния тоже записывает имена, и терять
событие ему нельзя ровно по той же причине. Отказы, случившиеся до разбора
(нечитаемое тело, паника), список очищают и потому события не теряют.

Иначе событие теряется необратимо: имя записано в учётную запись отказавшей
доставки, и следующая доставка сочтёт его виденным.

Исход, отложенный по обстоятельствам (занятость базы, отмена снаружи), — другое
дело: он учётной записи не меняет вовсе, список непокрытых секций в базу не
попадает, и доставка вернётся следующим проходом. Новые имена в такой записи
система называть MUST NOT: событие не потеряно, а повторение его на каждом
проходе занятой базы превратило бы однократный признак в дребезг.

#### Scenario: Отказ слияния при новой секции

- **GIVEN** ни одна прежняя доставка не содержала секции `ecg`
- **WHEN** разбор доставки прошёл, а слияние отказало нетранзиентно
- **THEN** запись об отказе называет новую секцию `ecg`
- **AND** учётная запись доставки сохраняет её в списке непокрытых

#### Scenario: Доставка с новой секцией и невыводимым слоем

- **GIVEN** ни одна прежняя доставка не содержала секции `cycleTracking`
- **WHEN** доставка с этой секцией не свернулась, потому что слой не вывелся
- **THEN** запись об отказе называет новую секцию `cycleTracking`
- **AND** учётная запись доставки сохраняет её в списке непокрытых

#### Scenario: Отложенная доставка события не порождает

- **WHEN** свёртка доставки отложена по занятости базы
- **THEN** запись об отложенном исходе новых секций не называет
- **AND** доставка остаётся в очереди

### Requirement: Несостоявшаяся сверка не молчит

Отказ самого запроса новизны исходом доставки система объявлять MUST NOT: правила
классификации исходов свёртки это изменение не трогает, отменённый контекст и
занятая база остаются обстоятельствами, а не свойствами доставки.

Когда доставка дошла до записи исхода по прочим правилам, а сверка не удалась,
все её непокрытые имена SHALL считаться новыми, и запись SHALL нести отдельный
признак того, что сверка не состоялась, **вместе с причиной**: занятость базы
проходит сама, а испорченное содержимое колонки не пройдёт никогда и будет
поднимать признак на каждой доставке — по одному булеву эти случаи неразличимы.

Асимметрия названа: лишняя запись стоит внимания владельца один раз, а
промолчавшее событие не восстанавливается ничем, кроме ручного запроса в базу.

#### Scenario: Сверка не удалась, разбор прошёл

- **WHEN** запрос новизны отказал, а разбор доставки прошёл
- **THEN** свёртка доходит до конца и записывает исход
- **AND** все непокрытые имена доставки объявлены новыми
- **AND** запись несёт признак несостоявшейся сверки и причину

### Requirement: Перечень непокрытых секций отдаётся одной командой

Система SHALL отдавать перечень непокрытых секций, накопленных журналом,
отдельной подкомандой — без ручного SQL по рабочей базе. Строка перечня SHALL
называть имя секции, число доставок с ним, первую и последнюю встречу (метку
журнала и идентификатор доставки); идентификатор нужен, чтобы достать тело из
архива.

В перечень SHALL входить доставки **всех** статусов разбора: список непокрытых
секций сохраняется и при отказе, и молчать о таком имени значило бы терять как
раз подозрительное.

Порядок строк SHALL быть детерминированным — по имени секции.

Вывод SHALL иметь объявленный предел числа строк, а остаток называться числом:
предел разбора в 32 имени действует на **одну доставку**, а различных имён
журнал способен накопить сколько угодно.

База SHALL открываться только на чтение: команда диагностическая, и запуск её при
живом сервисе не должен ни мигрировать схему, ни писать. Отказ открытия (базы
нет, версия схемы не та) SHALL давать ненулевой код возврата и внятное
сообщение — молчаливый пустой перечень неотличим от «ничего не приезжало».

#### Scenario: Перечень на базе с непокрытыми секциями

- **GIVEN** журнал содержит доставки с секциями `symptoms` и `ecg`
- **WHEN** оператор запускает подкоманду перечня
- **THEN** вывод содержит обе секции с числом доставок и границами встреч
- **AND** порядок строк детерминирован

#### Scenario: Перечень на базе без непокрытых секций

- **WHEN** ни одна доставка непокрытых секций не приносила
- **THEN** команда завершается нулевым кодом и говорит, что перечень пуст

#### Scenario: Базы по указанному пути нет

- **WHEN** оператор запускает подкоманду с конфигом, указывающим на
  несуществующую базу
- **THEN** команда завершается ненулевым кодом и называет причину

#### Scenario: Имён больше предела вывода

- **WHEN** различных имён в журнале больше объявленного предела
- **THEN** вывод содержит предел строк и называет число оставшихся имён

### Requirement: Перечень честен относительно своего носителя

Система SHALL называть границы перечня в спеке и в выводе команды, а не обещать
«всё, что поток когда-либо приносил»: перечень и признак новизны производны от
`delivery.uncovered_sections` — колонки, которая обрезается разбором и
обнуляется пересборкой.

Границы SHALL называться и в **выводе команды**, а не только в спеке: пустой
перечень без них читается как «поток ничего не приносил» — обещание, которого
носитель не даёт.

- Имя, вытесненное границей списка (не больше 32 имён на доставку), в колонку не
  попадает вовсе — ни события, ни строки перечня оно не даст; наблюдаемым
  остаётся счётчик отброшенных имён, который уже поднимает уровень записи.
- Пересборка обнуляет производные от разбора поля и заполняет их заново только по
  сохранившимся телам: доставка, тело которой удалено ретеншеном, свой список
  теряет, поэтому «те же события» пересборка воспроизводит **при полном архиве**.
- Имя, секцию которого разбор научился покрывать, уходит из колонки при
  пересвёртке — перечень отвечает о текущем состоянии покрытия, а не об истории.
- Поэтому пересвёртка ранее частично разобранных доставок (правило «покрыли
  секцию — пересверните») может дать событие о новизне повторно: журнал тот же, а
  колонка заполняется заново.

#### Scenario: Пустой перечень не говорит за весь поток

- **WHEN** в учётных записях журнала непокрытых секций нет
- **THEN** вывод команды говорит именно это, а не «журнал такого не приносил»
- **AND** называет границы носителя

#### Scenario: Учётная запись с неразбираемым списком не роняет ответ

- **GIVEN** в колонке одной доставки лежит значение, не разбираемое как JSON
- **WHEN** выполняется сверка новизны или собирается перечень
- **THEN** ответ строится по остальным записям, а не отказывает целиком

#### Scenario: Имя вытеснено границей списка

- **GIVEN** тело доставки содержит 32 незнакомых ключа перед секцией `ecg`
- **WHEN** доставка свёрнута
- **THEN** события о новизне `ecg` нет, потому что имя в учётную запись не попало
- **AND** запись несёт счётчик отброшенных имён и уровень `WARN`

### Requirement: Вывод перечня не доверяет содержимому тела

Печатаемое имя секции SHALL быть экранировано, чтобы управляющие
последовательности из тела не влияли на терминал оператора: имя приходит
верхнеуровневым ключом чужого тела, длина его ограничена разбором, содержимое —
ничем.

Вывод SHALL оставаться свободным от значений точек, имён устройств и любых
других данных о здоровье: имя секции — структурный ключ, а не измерение.

#### Scenario: Имя секции с управляющими символами

- **GIVEN** учётная запись доставки содержит имя секции с управляющим символом
- **WHEN** оператор запускает подкоманду перечня
- **THEN** символ выводится экранированной последовательностью, а не сырым
  байтом
