# 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** символ выводится экранированной последовательностью, а не сырым байтом