- свёртка спрашивает журнал, встречалось ли имя строго раньше по паре (received_at, id), и пишет WARN с атрибутом uncovered_new; повторные молчат. Признак выводится, а не хранится — реестр был бы второй копией факта - добавлена подкоманда `healthlog uncovered`: перечень накопленного, чтение только на чтение, экранированные имена и названные границы носителя - синк документации: ADR о выводе новизны из журнала, две записи в журнал дефектов, два правила промоутом в конвенции, терминал оператора назван адресатом недоверенного входа
270 lines
21 KiB
Markdown
270 lines
21 KiB
Markdown
## 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** символ выводится экранированной последовательностью, а не сырым
|
||
байтом
|