- фазы сна, контекст пульса и имена тренировок попадают в реестр `category_value` (миграция 00010): строка хранится дословно, выведенный код лежит рядом отдельной записью, а не полем внутри точки - словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в сыром архиве нет - наблюдение входит в отпечаток витрины, выведенный код — нет: он производная от словаря, а не от журнала
518 lines
41 KiB
Markdown
518 lines
41 KiB
Markdown
# reindex Specification
|
||
|
||
## Purpose
|
||
|
||
Пересборка витрины проигрыванием журнала: `import(снапшот) + replay(доставки)`.
|
||
Витрина производна, источник истины — сырой архив тел; значит любое повреждение,
|
||
включая ошибку нашего же разбора любой давности, лечится пересборкой, а не
|
||
восстановлением из бекапа. Здесь живут состав журнала, порядок проигрывания,
|
||
детерминированность, оракул сходимости и граница «что не восстанавливается».
|
||
## Requirements
|
||
### Requirement: Пересборка витрины проигрыванием журнала
|
||
|
||
Система SHALL уметь собрать витрину заново, проиграв журнал целиком:
|
||
`import(снапшот) + replay(доставки)`. Стадия снапшота в этой дельте пуста —
|
||
пересборка из архива есть вырожденный случай с пустым снапшотом, — и отдельной
|
||
операции «пересборка из архива» рядом с импортом экспорта заводить MUST NOT.
|
||
|
||
Проигрываться SHALL **все** доставки журнала, а не только те, чей
|
||
`parse_status` говорит о неразобранности. Отбор по учётному статусу сделал бы
|
||
результат функцией предыдущего прогона, а не журнала; дешевизну повторного
|
||
проигрывания обеспечивает хеш-детектор объекта, а не пропуск доставок.
|
||
|
||
Пересборка собственного разбора и собственного слияния иметь MUST NOT: она
|
||
зовёт тот же код, что и приём, по идентификатору доставки, и тело читает из
|
||
архива тем же путём, с тем же пределом размера распакованного тела. Второй путь
|
||
разбора разошёлся бы с первым молча, а другой предел означал бы, что тело,
|
||
принятое со `200`, вечно отказывает на каждой пересборке.
|
||
|
||
#### Scenario: Пересобранная витрина совпадает с накопленной приёмом
|
||
|
||
- **GIVEN** рабочая витрина накоплена тем же разбором, приём во время
|
||
накопления шёл последовательно, и за время пересборки новых доставок не
|
||
приезжало
|
||
- **WHEN** журнал проигрывается заново с пустой витрины
|
||
- **THEN** отпечаток пересобранной витрины совпадает с отпечатком накопленной
|
||
|
||
#### Scenario: Повторная пересборка ничего не меняет
|
||
|
||
- **WHEN** пересборка того же журнала выполняется второй раз
|
||
- **THEN** отпечаток витрины не меняется
|
||
|
||
#### Scenario: Разобранная доставка проигрывается наравне с неразобранной
|
||
|
||
- **WHEN** в журнале есть доставки со статусом `parsed` и со статусом `pending`
|
||
- **THEN** проигрываются обе
|
||
|
||
### Requirement: Порядок проигрывания задаётся журналом
|
||
|
||
Система SHALL проигрывать доставки строго в порядке `(received_at, id)`, а не
|
||
в порядке обхода каталога архива.
|
||
|
||
Порядок входит в результат: слой доставки без плотных метрик наследуется от
|
||
**предшествующей** доставки той же автоматизации, то есть слой есть функция
|
||
префикса журнала. Обход каталога совпадает с хронологией только по датам
|
||
каталогов и внутри суток не упорядочивает ничего.
|
||
|
||
Второй ключ обязателен, а не для красоты: `received_at` хранится с секундной
|
||
точностью, и доставки одной секунды без него шли бы в неопределённом порядке —
|
||
а значит два прогона одного журнала могли бы разойтись.
|
||
|
||
Проигрывание SHALL быть последовательным. Распараллеливать его MUST NOT: слой
|
||
есть функция префикса журнала, а запись часового объекта — чтение, слияние и
|
||
запись обратно.
|
||
|
||
#### Scenario: Порядок не зависит от раскладки файлов в архиве
|
||
|
||
- **WHEN** тела одного журнала лежат в архиве так, что порядок обхода каталога
|
||
не совпадает с хронологией приёма
|
||
- **THEN** доставка без плотных метрик получает слой предшествующей ей по
|
||
`received_at` доставки той же автоматизации, а не слой соседа по каталогу
|
||
|
||
#### Scenario: Доставки одной секунды упорядочены идентификатором
|
||
|
||
- **WHEN** две доставки имеют одинаковый `received_at`
|
||
- **THEN** порядок между ними задаётся идентификатором и одинаков в каждом
|
||
прогоне
|
||
|
||
### Requirement: Журналом считается архив, а не таблица доставок
|
||
|
||
Система SHALL брать состав журнала из **сырого архива**, сверяя его с учётом
|
||
доставок, а не перечислять только строки `delivery`. Иначе витрина
|
||
пересобиралась бы из витрины.
|
||
|
||
Тело, у которого учётной записи нет, SHALL заводиться заново и проигрываться
|
||
наравне с остальными: приём кладёт тело на диск раньше строки в базе — обратный
|
||
порядок дал бы учтённую доставку без данных, — поэтому отказ на вставке строки
|
||
оставляет тело в архиве без учёта.
|
||
|
||
Для такого тела идентификатор берётся из имени файла, метка приёма — из времени
|
||
создания в ULID, приведённого к **UTC**. Дата каталога меткой служить MUST NOT:
|
||
она задаёт сутки, а порядок нужен внутри суток. Размер и хеш пересчитываются по
|
||
**распакованному** телу — так же, как их считает приём; иначе в тех же колонках
|
||
появились бы значения другой природы.
|
||
|
||
Заголовки доставки при этом восстановлены быть не могут — **в архиве их нет**.
|
||
Подобранное тело SHALL проигрываться без них, с честной деградацией вывода
|
||
слоя: наследовать не от чего и подтверждать нечем.
|
||
|
||
Строка учёта, у которой тела в архиве нет, отказом быть MUST NOT: она станет
|
||
штатной, когда появится ретеншен архива. Такая строка SHALL считаться и
|
||
называться в отчёте.
|
||
|
||
Файл архива, не подходящий под форму тела (чужое расширение, остаток `*.tmp`
|
||
от прерванной записи, имя не разбирается как ULID), SHALL пропускаться и
|
||
учитываться счётчиком. Молчаливый пропуск недопустим: тело есть, а в отчёте его
|
||
нет. Тело, чей идентификатор уже встретился в другом каталоге, SHALL
|
||
пропускаться тем же порядком: вторая запись журнала с тем же ключом сорвала бы
|
||
весь прогон, то есть один посторонний файл лишал бы пересборки всё остальное.
|
||
|
||
Каждый пропуск, повтор и неудачный подбор SHALL оставлять запись в логе с путём
|
||
файла — путь в архиве это дата и идентификатор, измерений в нём нет. Без неё
|
||
счётчик в отчёте не на что раскрыть, а отчёт при этом отсылает человека
|
||
разбираться по логу.
|
||
|
||
Ошибка **чтения** каталога архива, включая отсутствие самого корня, SHALL быть
|
||
отказом всей пересборки, а не пустым журналом. Нечитаемый каталог означает
|
||
«неизвестно, есть ли там тела», а не «тел нет». Каталог архива пересборка
|
||
создавать MUST NOT — создание превратило бы запуск не из того каталога в
|
||
успешный прогон по пустому журналу.
|
||
|
||
#### Scenario: Тело без учётной записи подбирается
|
||
|
||
- **WHEN** в архиве лежит тело, для которого строки `delivery` нет
|
||
- **THEN** доставка заводится заново с идентификатором из имени файла и меткой
|
||
приёма из ULID в UTC
|
||
- **AND** её размер и хеш посчитаны по распакованному телу
|
||
- **AND** её точки попадают в витрину
|
||
|
||
#### Scenario: Учётная запись без тела не роняет пересборку
|
||
|
||
- **WHEN** у строки `delivery` нет тела в архиве
|
||
- **THEN** пересборка продолжается
|
||
- **AND** факт учитывается счётчиком в отчёте
|
||
- **AND** сама запись переносится в базу назначения, но не сворачивается
|
||
|
||
#### Scenario: Файл, не являющийся телом, считается отдельно
|
||
|
||
- **WHEN** в архиве лежит файл, чьё имя не разбирается как ULID либо чьё
|
||
расширение не соответствует форме тела
|
||
- **THEN** он пропускается и учитывается счётчиком, а пересборка продолжается
|
||
|
||
#### Scenario: Повтор идентификатора не срывает прогон
|
||
|
||
- **WHEN** одно и то же имя тела встречается в двух каталогах суток
|
||
- **THEN** проигрывается первое, второе учитывается счётчиком
|
||
- **AND** пересборка доходит до конца
|
||
|
||
#### Scenario: Каталог архива не читается
|
||
|
||
- **WHEN** корня архива нет либо подкаталог не читается
|
||
- **THEN** команда завершается ошибкой и витрину не собирает
|
||
|
||
### Requirement: Пересборка не трогает рабочую базу
|
||
|
||
Система SHALL собирать витрину в **отдельный файл базы** и MUST NOT записывать
|
||
в рабочую базу ничего — ни объектов, ни строк учёта, ни миграций схемы.
|
||
|
||
Пересборка обязана начинаться с пустой витрины: точки из объекта не удаляются
|
||
никогда, поэтому проигрывание поверх накопленного оставило бы результат
|
||
прежнего, неверного разбора. Но очистка рабочей витрины необратима и наступает
|
||
**до** того, как известно, что пересборка удалась: отказ на середине (битое
|
||
тело, отменённый контекст, кончившееся место) оставил бы витрину пустой
|
||
наполовину в состоянии, неотличимом от нормального.
|
||
|
||
Рабочая база SHALL открываться **только для чтения и без наката миграций**.
|
||
Обычное открытие накатывает миграции безусловно, а миграции меняют и данные (та,
|
||
что ввела частичный разбор, переписала `parse_status` у всех строк) — то есть
|
||
штатный путь чтения нарушал бы запрет выше. Хуже: свежий бинарь мигрировал бы
|
||
схему под работающим старым сервисом.
|
||
|
||
Расхождение версии схемы рабочей базы с версией, которую знает бинарь, SHALL
|
||
быть отказом с указанием обеих версий, а не поводом мигрировать.
|
||
|
||
#### Scenario: Рабочая база остаётся нетронутой
|
||
|
||
- **WHEN** пересборка отработала успешно
|
||
- **THEN** отпечаток рабочей витрины не изменился
|
||
- **AND** учёт доставок в рабочей базе не изменился
|
||
- **AND** собранная витрина лежит в отдельном файле
|
||
|
||
#### Scenario: Отказ посреди пересборки не портит рабочую базу
|
||
|
||
- **WHEN** пересборка прерывается на середине журнала
|
||
- **THEN** рабочая витрина и учёт доставок в рабочей базе остаются такими же,
|
||
какими были
|
||
|
||
#### Scenario: Схема рабочей базы старше бинаря
|
||
|
||
- **WHEN** версия схемы рабочей базы не совпадает с версией бинаря
|
||
- **THEN** команда завершается ошибкой, называя обе версии
|
||
- **AND** не пишет в рабочую базу ни одной строки
|
||
|
||
### Requirement: Файл назначения и его жизненный цикл
|
||
|
||
Файл назначения по умолчанию SHALL быть соседом рабочей базы — так подмена
|
||
остаётся переименованием внутри одной файловой системы.
|
||
|
||
Тождество файла назначения с рабочей базой SHALL определяться **по файлу, а не
|
||
по строке пути**: путь через `..`, симлинк или другой префикс монтирования
|
||
дают то же тождество при разных строках. Совпадение MUST быть отказом: иначе
|
||
проигрывание пошло бы прямо в живую рабочую базу — ровно то, что запрещено выше.
|
||
|
||
Сборка SHALL идти под временным именем, а переименование в файл назначения быть
|
||
**последним шагом успешного прогона**. При любом ином исходе — отказ, отмена,
|
||
падение — файла по пути назначения появляться MUST NOT, а временный SHALL
|
||
убираться вместе со спутниками журнала SQLite.
|
||
|
||
Полусобранная база выглядит как обычная: это ровно то состояние, ради отрицания
|
||
которого отвергнута очистка рабочей витрины. Обломок по пути назначения ещё и
|
||
приучил бы обходить защиту от перезаписи флагом принудительности.
|
||
|
||
Существующий файл назначения перезаписываться молча MUST NOT: это отказ, если
|
||
человек явно не потребовал перезаписи. Затребованная перезапись SHALL давать ту
|
||
же витрину, что и сборка в отсутствующий файл, — сборка всегда начинается с
|
||
пустой витрины, а не дописывается в чужое содержимое.
|
||
|
||
#### Scenario: Файл назначения совпадает с рабочей базой
|
||
|
||
- **WHEN** файл назначения — тот же файл, что рабочая база, пусть и по другому
|
||
пути
|
||
- **THEN** команда завершается ошибкой и не пишет ничего
|
||
|
||
#### Scenario: Файл назначения уже существует
|
||
|
||
- **WHEN** файл назначения существует, а перезапись не затребована явно
|
||
- **THEN** команда завершается ошибкой и существующий файл не трогает
|
||
|
||
#### Scenario: Затребованная перезапись даёт ту же витрину
|
||
|
||
- **WHEN** пересборка выполняется поверх существующего файла назначения с
|
||
явно затребованной перезаписью
|
||
- **THEN** отпечаток собранной витрины совпадает с отпечатком сборки того же
|
||
журнала в отсутствующий файл
|
||
|
||
#### Scenario: Прерванная пересборка не оставляет файла назначения
|
||
|
||
- **WHEN** пересборка прерывается на середине журнала
|
||
- **THEN** файла по пути назначения не существует
|
||
|
||
### Requirement: База назначения пригодна к подмене
|
||
|
||
База назначения SHALL нести полноценный учёт доставок, а не только объекты
|
||
витрины: подменяется файл базы **целиком**, а не одна таблица.
|
||
|
||
Состав переноса нормируется явно, потому что колонки `delivery` двух разных
|
||
родов:
|
||
|
||
```
|
||
факты журнала id, received_at, automation_name, automation_id, aggregation,
|
||
period, session_id, bytes, sha256, raw_path, headers
|
||
← переносятся дословно
|
||
производные parse_status, points, derived_layer, uncovered_sections,
|
||
skipped_entities ← начинаются пустыми
|
||
```
|
||
|
||
Перечень производных полей SHALL пополняться **тем же изменением**, которое
|
||
заводит новое поле: он единственное место, где сказано, чему нельзя пережить
|
||
пересборку, и следующий автор решает по нему. Поле, не внесённое в перечень,
|
||
однажды перенесут «для полноты учёта».
|
||
|
||
Факты журнала SHALL переноситься дословно, включая записи, тела которых в
|
||
архиве уже нет. Заголовки восстановлению не подлежат ничем — в архиве их нет, —
|
||
и неполный перенос уничтожил бы их первой же подменой, а с ними и вывод слоя
|
||
для **всех** доставок, не только подобранных. Запись без тела при этом не
|
||
сворачивается и в наследовании слоя не участвует: выведенного слоя у неё нет.
|
||
|
||
Производные от разбора поля MUST начинаться пустыми. Перенос `derived_layer`
|
||
особенно опасен и незаметен: доставка, чей повторный разбор отказал (штатный
|
||
исход, когда слой не выводится), сохранила бы слой **прежнего** разбора, и
|
||
следующая доставка той же автоматизации унаследовала бы его. Витрина снова стала
|
||
бы функцией предыдущего прогона, а не журнала, причём оба прогона были бы
|
||
самосогласованы — проверка «повторная пересборка ничего не меняет» этого не
|
||
ловит. Для числа пропущенных сущностей цена та же и хуже: пустота у него значит
|
||
«не измерялось», и перенесённое число выдавало бы измерение прежнего разбора за
|
||
измерение текущего — а по нему принимается необратимое решение об удалении тела.
|
||
|
||
#### Scenario: Учёт переносится полностью
|
||
|
||
- **WHEN** пересборка завершилась
|
||
- **THEN** число строк учёта в базе назначения равно числу строк рабочей базы
|
||
плюс число подобранных тел
|
||
- **AND** заголовки перенесённых доставок совпадают с рабочей базой дословно
|
||
|
||
#### Scenario: Слой прошлого разбора в наследование не попадает
|
||
|
||
- **WHEN** в рабочей базе у доставок проставлен `derived_layer`
|
||
- **THEN** отпечаток пересобранной витрины совпадает с отпечатком пересборки
|
||
того же журнала из учёта без проставленных слоёв
|
||
|
||
#### Scenario: Число пропущенных сущностей не переносится из журнала
|
||
|
||
- **WHEN** в рабочей базе у доставки проставлено число пропущенных сущностей, а
|
||
тела этой доставки в архиве уже нет
|
||
- **THEN** в базе назначения её число пропущенных сущностей отсутствует
|
||
|
||
### Requirement: Подмену рабочей базы делает человек
|
||
|
||
Система SHALL оставлять замену рабочей базы пересобранной человеку и
|
||
выполнять её сама MUST NOT.
|
||
|
||
Файл базы держит открытым процесс сервиса, а переименование не касается уже
|
||
открытого дескриптора: процесс продолжит писать в отвязанный inode, читатели
|
||
увидят новый файл, и данные разойдутся молча. Документация SQLite называет
|
||
переименование используемого файла прямой причиной порчи базы. Безопасная
|
||
подмена требует остановленного сервиса, а остановить его команда не может:
|
||
сервисом управляет окружение снаружи.
|
||
|
||
Отчёт SHALL печатать процедуру подмены буквально — команды, а не намёк, — и
|
||
только тогда, когда прогон признан успешным (см. «Отчёт, оракул и исход
|
||
команды»).
|
||
|
||
#### Scenario: Отчёт называет процедуру подмены
|
||
|
||
- **WHEN** прогон признан успешным
|
||
- **THEN** отчёт содержит путь собранного файла и команды подмены
|
||
|
||
### Requirement: Отчёт, оракул и исход команды
|
||
|
||
Система SHALL завершать пересборку отчётом, который несёт счётчики
|
||
(проиграно, свёрнуто, отказов по классам, тел без учётной записи, строк без
|
||
тела, пропущенных файлов, повторов, объектов **до и после**) и **два
|
||
отпечатка** — рабочей витрины и пересобранной, — с прямым ответом, совпали они
|
||
или нет.
|
||
|
||
Счётчики «до и после» SHALL покрывать **каждую единицу хранения витрины**:
|
||
часовые объекты, тренировки, записи и строки реестра категориальных значений.
|
||
Отпечаток отвечает «да/нет» за витрину целиком, поэтому единица, которой нет в
|
||
счётчиках, делает расхождение безадресным: человек увидит «не совпало» при
|
||
неизменившемся числе объектов и не отличит появление двадцати семи тренировок от
|
||
пропажи двух. Перечень пополняется **тем же изменением**, которое заводит
|
||
единицу хранения: список закрытый, и единица, не внесённая в него, молчит ровно
|
||
там, где расхождение впервые становится заметным.
|
||
|
||
Отказы SHALL считаться **по классам**: слой не выводится, содержимое не
|
||
разбирается, работа отложена по обстоятельствам, всё прочее. Невыведенный слой
|
||
есть в каждом журнале и штатен; общий счётчик отправлял бы человека искать
|
||
дефект там, где его нет. Отдельно называть человеку следует только нештатные
|
||
отказы.
|
||
|
||
Отложенная доставка (занятость базы, отмена работы снаружи) SHALL считаться
|
||
нештатной **для пересборки**, хотя для фоновой свёртки она штатна: пересборка
|
||
идёт в свежий файл при единственном писателе, и такая доставка в собранной
|
||
витрине просто отсутствует — вместе с теми, кто наследовал от неё слой. Классы
|
||
при этом общие с фоновой свёрткой: второй классификатор разошёлся бы с первым
|
||
молча.
|
||
|
||
Число объектов «было и стало» SHALL печататься рядом с отпечатками: отпечатки
|
||
отвечают «да/нет», а решение о подмене необратимо, и по «да/нет» нельзя
|
||
судить о **направлении** расхождения. Именно пара чисел — 1737 против 1742 —
|
||
поймала прошлый дефект наследования слоя.
|
||
|
||
Отпечаток здесь оракул, а не украшение: число объектов к правилу разрешения
|
||
столкновений нечувствительно — на координате всегда ровно одна точка, и правило
|
||
выбирает, какая, а не сколько. «Объектов столько же» совпало бы и при заведомо
|
||
сломанном правиле.
|
||
|
||
Отпечаток рабочей витрины SHALL сниматься **до** начала проигрывания, а число
|
||
доставок в рабочей базе — до и после. Ненулевая разница SHALL называться в
|
||
отчёте, и при ней процедура подмены печататься MUST NOT: доставки, приехавшие за
|
||
время прогона, есть в рабочей базе и в архиве, но не в собранном файле, и
|
||
подмена стёрла бы их учёт вместе с заголовками, которых в архиве нет.
|
||
|
||
Величины, которые не снимались, отчёт печатать MUST NOT. При отмене отпечаток
|
||
пересобранной витрины и число доставок после прогона не измеряются вовсе —
|
||
печатать их сравнение значило бы выдать неизмеренное за измеренное, причём в
|
||
единственном оракуле задачи. Ожидаемые классы расхождения (новые доставки за время прогона,
|
||
непереносимый признак запечатанного часа, исправленный разбор, **покрытая
|
||
разбором новая секция**, **появившаяся единица хранения**) SHALL называться
|
||
отдельно от самого факта расхождения.
|
||
|
||
Класс «покрыта новая секция» назван потому, что первый прогон после такого
|
||
изменения расходится **гарантированно** и штатно: витрина обзаводится единицами
|
||
хранения, которых в рабочей базе нет по построению. Не назвав его, отчёт
|
||
приучает человека игнорировать расхождение отпечатков — то есть обесценивает
|
||
оракул ровно тогда, когда по нему принимается необратимое решение. Класс
|
||
«появилась единица хранения» — та же причина в общем виде: реестр категориальных
|
||
значений пуст у витрины, свёрнутой прежним бинарём, и первая сверка после
|
||
выкатки расходится по нему одному, при неизменившихся объектах и сущностях.
|
||
|
||
**Исход команды.** Расхождение отпечатков отказом быть MUST NOT: после
|
||
исправления разбора оно ожидаемо и есть сам смысл пересборки. Отказ отдельной
|
||
доставки отказом команды тоже MUST NOT быть: доставка, слой которой не
|
||
выводится, — штатный исход.
|
||
|
||
Отказом команды SHALL быть: пустой журнал, отсутствие хотя бы одной свёрнутой
|
||
доставки, отмена и любая ошибка окружения. Пустая витрина совпадает по
|
||
отпечатку с пустой витриной, поэтому прогон по пустому журналу выглядит
|
||
идеальной сходимостью — а все умолчания подыгрывают такому запуску: конфига
|
||
может не быть вовсе, и тогда пути указывают в рабочий каталог процесса. Человек,
|
||
выполнивший напечатанную процедуру, заменил бы витрину пустой.
|
||
|
||
Отчёт значений точек, имён метрик, имён устройств и содержимого тел содержать
|
||
MUST NOT: отпечаток берёт содержимое хешем. Строки реестра при этом счётом, а не
|
||
перечислением: наблюдённое значение — данные о здоровье наравне со значением
|
||
точки. Ограничение относится к отчёту в
|
||
стандартном выводе; лог свёртки живёт по правилам спеки хранения, где координаты
|
||
столкновения (метрика, слой, час) разрешены явно.
|
||
|
||
Отчёт идёт в стандартный вывод человеческим текстом. Прогресс длинного прогона
|
||
SHALL идти в поток ошибок, а не смешиваться с отчётом: прогон на полном архиве
|
||
молчит минутами, и зависший неотличим от идущего.
|
||
|
||
#### Scenario: Отчёт сравнивает отпечатки
|
||
|
||
- **WHEN** пересборка завершилась
|
||
- **THEN** отчёт содержит отпечаток рабочей витрины и отпечаток пересобранной
|
||
- **AND** прямо называет, совпали они или нет
|
||
- **AND** называет, изменилось ли число доставок в рабочей базе за время прогона
|
||
|
||
#### Scenario: Счётчики покрывают все единицы хранения
|
||
|
||
- **WHEN** пересборка завершилась
|
||
- **THEN** отчёт печатает «до и после» отдельно для часовых объектов,
|
||
тренировок, записей и строк реестра категориальных значений
|
||
|
||
#### Scenario: Пустой реестр рабочей витрины назван ожидаемым классом
|
||
|
||
- **WHEN** рабочая витрина свёрнута прежним бинарём и строк реестра не имеет, а
|
||
пересобранная их получила
|
||
- **THEN** отчёт называет «появилась единица хранения» ожидаемым классом
|
||
расхождения
|
||
- **AND** печатает число строк реестра «до и после»
|
||
|
||
#### Scenario: Расхождение отпечатков не является отказом
|
||
|
||
- **WHEN** отпечаток пересобранной витрины отличается от рабочей, и при этом
|
||
хотя бы одна доставка свёрнута
|
||
- **THEN** команда завершается успешно, а расхождение названо в отчёте
|
||
|
||
#### Scenario: Пустой журнал — отказ, а не идеальная сходимость
|
||
|
||
- **WHEN** в архиве не нашлось ни одного тела
|
||
- **THEN** команда завершается ненулевым кодом
|
||
- **AND** процедуры подмены не печатает
|
||
|
||
#### Scenario: Ни одна доставка не свернулась
|
||
|
||
- **WHEN** журнал непуст, но свернуть не удалось ни одной доставки
|
||
- **THEN** команда завершается ненулевым кодом
|
||
- **AND** процедуры подмены не печатает
|
||
|
||
#### Scenario: Приезд доставок за время прогона отменяет подмену
|
||
|
||
- **WHEN** число доставок в рабочей базе за время прогона изменилось
|
||
- **THEN** отчёт называет разницу
|
||
- **AND** процедуры подмены не печатает
|
||
|
||
#### Scenario: Отчёт после отмены не сравнивает неизмеренного
|
||
|
||
- **WHEN** прогон отменён
|
||
- **THEN** отчёт не содержит ни ответа о совпадении отпечатков, ни разницы
|
||
числа доставок
|
||
|
||
#### Scenario: Рабочей базы нет вовсе
|
||
|
||
- **WHEN** файла рабочей базы не существует
|
||
- **THEN** пересборка идёт по одним подобранным телам
|
||
- **AND** отчёт называет, что сверять не с чем и что заголовки доставок не
|
||
восстанавливаются
|
||
|
||
#### Scenario: Отчёт не раскрывает данных о здоровье
|
||
|
||
- **WHEN** отчёт напечатан
|
||
- **THEN** он не содержит ни значений точек, ни имён метрик, ни имён устройств,
|
||
ни наблюдённых категориальных строк
|
||
|
||
### Requirement: Отказ на одной доставке не останавливает пересборку
|
||
|
||
Система SHALL продолжать проигрывание, когда отдельная доставка не сворачивается
|
||
(тело не читается, тело больше предела, слой не выводится, содержимое не
|
||
разбирается), и учитывать такие доставки счётчиком отказов.
|
||
|
||
Останавливаться на первой нельзя: журнал заведомо содержит доставки, слой
|
||
которых не выводится, — это штатный исход, а не поломка, и он не должен лишать
|
||
пересборки остальные тела.
|
||
|
||
Отмена, наоборот, останавливать проигрывание SHALL: это требование прекратить
|
||
работу, а не свойство доставки. Источник отмены SHALL быть назван: команду
|
||
прерывает человек, и без перевода сигнала прерывания в отмену контекста
|
||
требование к поведению по отмене недостижимо в эксплуатации — процесс умирает
|
||
мимо всей логики. По отмене команда SHALL напечатать частичный отчёт и
|
||
завершиться ненулевым кодом.
|
||
|
||
#### Scenario: Битое тело не срывает прогон
|
||
|
||
- **WHEN** одно из тел архива не распаковывается
|
||
- **THEN** остальные доставки проигрываются
|
||
- **AND** отказ учитывается счётчиком в отчёте
|
||
|
||
#### Scenario: Отмена прекращает проигрывание
|
||
|
||
- **WHEN** сигнал прерывания приходит посреди журнала
|
||
- **THEN** проигрывание прекращается, печатается частичный отчёт
|
||
- **AND** команда завершается ненулевым кодом
|
||
- **AND** файла по пути назначения не остаётся
|
||
|
||
### Requirement: Отчёт пересборки показывает удержанные версии сущностей
|
||
|
||
Отчёт пересборки SHALL называть число версий сущностей, удержанных правилом «не
|
||
теряем содержания», — тем же счётчиком, что ведёт свёртка.
|
||
|
||
Без него правило слияния сущностей проверить нечем. Сходимость отпечатка его не
|
||
проверяет **по построению**: живой приём и пересборка пользуются одним правилом
|
||
и одинаково сойдутся на одинаково удержанной версии. То есть слишком строгое
|
||
правило — например, замораживающее тренировку на старой версии из-за исчезнувшего
|
||
ключа с пустым значением — выглядело бы как идеальная сходимость. Счётчик
|
||
несравнимых наборов точек выведен в отчёт по ровно той же причине и тем же
|
||
рассуждением.
|
||
|
||
Число SHALL печататься всегда, а не только при ненулевом значении: ноль здесь
|
||
утверждение, а не отсутствие новостей.
|
||
|
||
#### Scenario: Удержанная версия видна в отчёте пересборки
|
||
|
||
- **WHEN** журнал содержит доставку, приехавшая версия сущности в которой
|
||
теряет содержание сохранённой
|
||
- **THEN** отчёт пересборки называет число удержанных версий больше нуля
|