первая встреча непокрытой секции стала наблюдаемым событием
- свёртка спрашивает журнал, встречалось ли имя строго раньше по паре (received_at, id), и пишет WARN с атрибутом uncovered_new; повторные молчат. Признак выводится, а не хранится — реестр был бы второй копией факта - добавлена подкоманда `healthlog uncovered`: перечень накопленного, чтение только на чтение, экранированные имена и названные границы носителя - синк документации: ADR о выводе новизны из журнала, две записи в журнал дефектов, два правила промоутом в конвенции, терминал оператора назван адресатом недоверенного входа
This commit is contained in:
+63
@@ -0,0 +1,63 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Значения точек не попадают в логи
|
||||
|
||||
Данные о здоровье чувствительнее токенов. Система MUST NOT писать значения
|
||||
точек, содержимое сущностей и тела доставок в записи лога уровня выше `DEBUG`.
|
||||
|
||||
Содержимое сущности здесь не менее чувствительно, чем значение точки, а местами
|
||||
более: маршрут тренировки — это геотрек до дома, а `labels` и `associations`
|
||||
записи состояния разума — эмоциональные метки. Разрешены **координаты**:
|
||||
идентификатор и род сущности, метка времени, идентификатор доставки — они
|
||||
описывают, что случилось, а не что измерено.
|
||||
|
||||
Непокрытые секции называются в логе **именами ключей**: имя секции — это форма
|
||||
пакета, а не измерение. Содержимое секции в лог не попадает ни при каком уровне
|
||||
выше `DEBUG`. Имена идут структурным атрибутом, а не склейкой в текст сообщения:
|
||||
кодировщик экранирует управляющие символы, и имя из чужого тела не разрывает
|
||||
построчный разбор логов. То же относится к идентификатору сущности: он приходит
|
||||
из чужого тела и ограничен по длине при разборе.
|
||||
|
||||
Частичный разбор уровня записи не повышает: `partial` — установившееся состояние
|
||||
половины потока (53 доставки из 118), и постоянный `WARN` обесценил бы уровень.
|
||||
Повышает уровень другое, и оснований два:
|
||||
|
||||
- срабатывание границ списка: тело с сотнями секций или с именем длиннее предела
|
||||
на HAE не похоже вовсе;
|
||||
- **первая по журналу встреча имени непокрытой секции** — событие однократное за
|
||||
всю жизнь имени, и правила его живут в capability наблюдения за непокрытыми
|
||||
секциями. Здесь оно названо, чтобы перечень оснований оставался полным: иначе
|
||||
следующий читатель снимет ветвь как незаказанную.
|
||||
|
||||
#### Scenario: Разбор доставки логируется без значений
|
||||
|
||||
- **WHEN** доставка разобрана
|
||||
- **THEN** запись лога содержит счётчики (метрик, точек, объектов, сущностей) и
|
||||
идентификатор доставки
|
||||
- **AND** не содержит ни значений точек, ни имён устройств
|
||||
|
||||
#### Scenario: Удержанная обеднённая версия логируется координатами
|
||||
|
||||
- **WHEN** приехавшая версия сущности отклонена как теряющая содержание
|
||||
- **THEN** запись `WARN` содержит идентификатор и род сущности
|
||||
- **AND** не содержит ни точек маршрута, ни того, какие поля потерялись
|
||||
|
||||
#### Scenario: Непокрытые секции названы именами ключей
|
||||
|
||||
- **WHEN** доставка содержит непокрытую секцию
|
||||
- **THEN** запись лога содержит имена непокрытых ключей отдельным атрибутом
|
||||
- **AND** не содержит ничего из содержимого этих секций
|
||||
- **AND** уровень записи из-за одной лишь частичности не повышается
|
||||
|
||||
#### Scenario: Границы списка сработали
|
||||
|
||||
- **WHEN** список непокрытых ключей усечён по числу имён или по длине имени
|
||||
- **THEN** запись лога имеет уровень `WARN`
|
||||
- **AND** содержит число отброшенных имён
|
||||
|
||||
#### Scenario: Имя непокрытой секции встречено впервые
|
||||
|
||||
- **WHEN** доставка принесла имя непокрытой секции, которого не было ни в одной
|
||||
доставке раньше неё в журнале
|
||||
- **THEN** запись лога имеет уровень `WARN`
|
||||
- **AND** содержит имя отдельным атрибутом новых секций
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
## 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** символ выводится экранированной последовательностью, а не сырым
|
||||
байтом
|
||||
Reference in New Issue
Block a user