Files
av bd5d17b079 первая встреча непокрытой секции стала наблюдаемым событием
- свёртка спрашивает журнал, встречалось ли имя строго раньше по паре
  (received_at, id), и пишет WARN с атрибутом uncovered_new; повторные молчат.
  Признак выводится, а не хранится — реестр был бы второй копией факта
- добавлена подкоманда `healthlog uncovered`: перечень накопленного, чтение
  только на чтение, экранированные имена и названные границы носителя
- синк документации: ADR о выводе новизны из журнала, две записи в журнал
  дефектов, два правила промоутом в конвенции, терминал оператора назван
  адресатом недоверенного входа
2026-08-04 13:39:48 +03:00

278 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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** символ выводится экранированной последовательностью, а не сырым
байтом