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

21 KiB
Raw Blame History

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