добавлен словарь категориальных значений HAE → коды HealthKit
- фазы сна, контекст пульса и имена тренировок попадают в реестр `category_value` (миграция 00010): строка хранится дословно, выведенный код лежит рядом отдельной записью, а не полем внутри точки - словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в сыром архиве нет - наблюдение входит в отпечаток витрины, выведенный код — нет: он производная от словаря, а не от журнала
This commit is contained in:
@@ -704,3 +704,254 @@ SHALL называть **тип** встреченного токена и см
|
||||
- **AND** текст называет тип токена словарём JSON и смещение перед токеном
|
||||
- **AND** смещение не меняется, если то же значение сделать длиннее
|
||||
|
||||
### Requirement: Категориальные значения получают стабильный код
|
||||
|
||||
Разбор SHALL выделять из тела **категориальные значения** — строки объявленных
|
||||
полей, у которых значение перечислимо, — и выводить для каждого стабильный код
|
||||
HealthKit по словарю `(локаль, строка) → код`.
|
||||
|
||||
Объявленных полей три, и все три измерены на живом потоке (находка 37):
|
||||
|
||||
```
|
||||
sleep_analysis value «БДГ», «Основная», «Глубокий», «Во сне», «В кровати», «Бодрствование»
|
||||
heart_rate context «Не задано», «Сидячий образ жизни», «Активен»
|
||||
workouts name «В помещении Ходьба», «На улице Ходьба»
|
||||
```
|
||||
|
||||
Список полей SHALL быть объявлен явно, а не выведен из формы значения. Строк в
|
||||
точке много (`date`, `start`, `source`), и «всякая строка категориальна» завело
|
||||
бы в реестр метки времени и имена устройств.
|
||||
|
||||
Именем в наблюдении SHALL быть **то же** имя метрики или секции, которым
|
||||
адресуется единица хранения: `sleep_analysis_summary` после разделения схем,
|
||||
`workouts` для тренировок. Второе имя для того же понятия развело бы наблюдение
|
||||
и объект по разным ключам.
|
||||
|
||||
Исходная строка MUST оставаться в содержимом точки или сущности **дословно и
|
||||
нетронутой**: код добавляется рядом, отдельной единицей, и обратное
|
||||
преобразование остаётся возможным всегда. Содержимое точки MUST NOT
|
||||
пересериализовываться ради кода.
|
||||
|
||||
Значение SHALL читаться из исходных байтов и приниматься только как непустая
|
||||
JSON-строка. Значение другого рода (число, объект, `null`), отсутствие поля и
|
||||
пустая строка категориальным значением не считаются; точка при этом MUST
|
||||
разбираться как обычно — новых путей потери точки требование не заводит. Пустая
|
||||
строка исключена намеренно: сказать о данных ей нечего, а в счётчике строк без
|
||||
кода она сидела бы вечно.
|
||||
|
||||
Результат разбора SHALL нести множество различных наблюдённых категориальных
|
||||
значений — по одному элементу на тройку `(метрика или секция, поле, строка)`, в
|
||||
детерминированном порядке. Повтор одной строки в тысяче точек даёт один элемент.
|
||||
Ключ наблюдения совпадает с ключом реестра: два разных ключа на одно понятие
|
||||
разошлись бы при первом же поле с одинаковым именем у двух метрик.
|
||||
|
||||
#### Scenario: Фаза сна из реального пакета получает код
|
||||
|
||||
- **WHEN** разбирается пакет HAE из `testdata`, где `sleep_analysis.value`
|
||||
равно «Во сне», а локаль доставки — `ru`
|
||||
- **THEN** содержимое точки в результате разбора совпадает с исходными байтами
|
||||
тела, включая строку «Во сне»
|
||||
- **AND** результат разбора несёт наблюдение
|
||||
`sleep_analysis / value / «Во сне»` с кодом
|
||||
`HKCategoryValueSleepAnalysisAsleepUnspecified`
|
||||
|
||||
#### Scenario: Имя тренировки попадает в наблюдённые значения
|
||||
|
||||
- **WHEN** разбирается пакет с тренировкой «В помещении Ходьба»
|
||||
- **THEN** результат разбора несёт наблюдение `workouts / name / «В помещении
|
||||
Ходьба»`
|
||||
- **AND** содержимое тренировки не изменено
|
||||
|
||||
#### Scenario: Повтор строки не задваивает наблюдение
|
||||
|
||||
- **WHEN** в доставке тысяча точек `heart_rate` с одинаковым `context`
|
||||
- **THEN** в результате разбора это одно наблюдение
|
||||
|
||||
#### Scenario: Значение не-строка точку не роняет
|
||||
|
||||
- **WHEN** у точки `sleep_analysis` поле `value` пришло числом
|
||||
- **THEN** точка разобрана как обычно и её содержимое сохранено дословно
|
||||
- **AND** наблюдения из неё не выводится
|
||||
|
||||
#### Scenario: Тренировка без имени наблюдения не даёт
|
||||
|
||||
- **WHEN** у тренировки нет поля `name` либо оно пришло пустой строкой
|
||||
- **THEN** наблюдения из неё не выводится
|
||||
- **AND** счётчик значений без кода не растёт
|
||||
|
||||
### Requirement: Незнакомая строка даёт пустой код и попадает в счётчик
|
||||
|
||||
Разбор MUST NOT угадывать код: строка, которой словарь не знает, SHALL получать
|
||||
**пустой** код. Пустота честнее догадки — по коду сверяются с экспортом Apple, и
|
||||
неверный код неотличим от верного до тех пор, пока по нему не примут решение.
|
||||
|
||||
Разбор SHALL считать различные наблюдения, для которых код не выведен, и
|
||||
отдавать счётчик вызывающему. Считаются **различные** наблюдения, а не их
|
||||
вхождения: вопрос, на который счётчик отвечает, — «сколько строк ждёт словаря»,
|
||||
а не «сколько точек их несло».
|
||||
|
||||
Счётчик ненулевой в установившемся режиме — словарь покрывает только фазы сна, а
|
||||
контекст пульса и имена тренировок объявлены категориальными заранее. Сигналом
|
||||
«появилось новое» служит поэтому **новая строка в реестре**, а не ненулевой
|
||||
счётчик; требование называет это, чтобы счётчик не читали как тревогу.
|
||||
|
||||
Незнакомая строка MUST NOT влиять на исход разбора: доставка разбирается,
|
||||
точки сохраняются, статус не меняется.
|
||||
|
||||
#### Scenario: Выдуманная фаза сна не роняет разбор
|
||||
|
||||
- **WHEN** в теле встречается `sleep_analysis.value` со строкой, которой в
|
||||
словаре нет
|
||||
- **THEN** разбор завершается без ошибки и точка сохраняется дословно
|
||||
- **AND** её наблюдение имеет пустой код
|
||||
- **AND** счётчик значений без кода равен единице
|
||||
|
||||
#### Scenario: Известная и неизвестная строки в одной доставке
|
||||
|
||||
- **WHEN** доставка несёт и «Во сне», и незнакомую строку в том же поле
|
||||
- **THEN** у первой код выведен, у второй пуст
|
||||
- **AND** счётчик значений без кода равен единице
|
||||
|
||||
### Requirement: Локаль сужает поиск, но её отсутствие не отменяет вывода
|
||||
|
||||
Локаль доставки SHALL браться из заголовка `Accept-Language` и нормализоваться
|
||||
по правилу lookup RFC 4647: берётся первый тег списка, подтеги отсекаются,
|
||||
регистр свёрнут (`RU-ru,ru;q=0.9` → `ru`). Свёртка регистра обязательна: теги
|
||||
BCP 47 регистронезависимы, и без неё `RU` и `ru` были бы разными языками.
|
||||
|
||||
Вывод кода SHALL идти тремя разрядами:
|
||||
|
||||
1. пара `(локаль, строка)` есть в словаре — её код;
|
||||
2. локали нет либо пары нет, но строка известна в других локалях и **все** они
|
||||
дают один код — этот код;
|
||||
3. иначе — пустой код.
|
||||
|
||||
Второй разряд обязателен, а не удобен. Заголовки запроса в сыром архиве не
|
||||
лежат: они были заголовками запроса, а не телом. Доставка, восстановленная из
|
||||
осиротевшего тела, приезжает без `Accept-Language`, и правило «нет локали — нет
|
||||
кода» сделало бы состояние функцией от того, уцелела ли строка учёта, — то есть
|
||||
сломало бы `import + replay`.
|
||||
|
||||
Локаль MUST NOT входить в ключ наблюдения. Ключ, содержащий локаль, оставляет ту
|
||||
же зависимость этажом ниже: усыновлённая доставка положила бы вторую строку с
|
||||
пустой локалью, и раздел реестра в отпечатке разошёлся бы вне всякого названного
|
||||
класса. На каком языке приехала строка, восстанавливается по доставке
|
||||
провенанса — тем же приёмом, каким устроен провенанс часового объекта.
|
||||
|
||||
Расхождение локалей MUST разрешаться пустым кодом, а не выбором: если одна и та
|
||||
же строка в разных локалях означает разные коды, выбирать не из чего.
|
||||
|
||||
#### Scenario: Локаль с подтегом, весами и в верхнем регистре приводится к базовому тегу
|
||||
|
||||
- **WHEN** доставка пришла с `Accept-Language: RU-ru,ru;q=0.9,en;q=0.8`
|
||||
- **THEN** локалью доставки считается `ru`
|
||||
- **AND** фаза сна «Во сне» получает свой код
|
||||
|
||||
#### Scenario: Доставка без заголовка локали код всё равно получает
|
||||
|
||||
- **WHEN** у доставки заголовка `Accept-Language` нет вовсе
|
||||
- **THEN** «Во сне» получает тот же код, что и при локали `ru`
|
||||
- **AND** наблюдение имеет тот же ключ, что и при локали `ru`
|
||||
|
||||
#### Scenario: Незнакомая локаль знакомой строки код не отменяет
|
||||
|
||||
- **WHEN** доставка пришла с локалью, которой в словаре нет, а строка известна в
|
||||
единственной локали
|
||||
- **THEN** код выведен
|
||||
|
||||
#### Scenario: Мусор в заголовке разбор не роняет
|
||||
|
||||
- **WHEN** заголовок пуст, состоит из `*`, из пробелов или из сотни тегов
|
||||
- **THEN** разбор завершается без паники, локаль либо выведена, либо пуста
|
||||
- **AND** коды выводятся вторым разрядом
|
||||
|
||||
### Requirement: Переименование кода самой Apple не раскалывает историю
|
||||
|
||||
Система SHALL держать таблицу синонимов кодов HealthKit — соответствие
|
||||
устаревшего имени каноническому — и приводить к каноническому имени **всякий**
|
||||
выведенный код, а не только код, пришедший из экспорта Apple.
|
||||
|
||||
Основание измерено (находка 43): те же 338 записей сна экспортированы как
|
||||
`HKCategoryValueSleepAnalysisAsleep` в 2021 году и как
|
||||
`HKCategoryValueSleepAnalysisAsleepUnspecified` в 2026-м — Apple переименовала
|
||||
значение и переписывает историю при выгрузке. Код устойчивее локализованной
|
||||
строки, но не абсолютен.
|
||||
|
||||
Приведение SHALL применяться и к словарю, поэтому «две формы сходятся в один
|
||||
код» верно по построению. Таблица SHALL быть **плоской**: ни одно её значение не
|
||||
является ключом, поэтому алиас разрешается ровно за один шаг. Инвариант
|
||||
проверяется тестом по таблице целиком; рантайм-обхода цепочек и ограничения
|
||||
глубины у системы быть MUST NOT — у них нет ни одного достижимого сценария, зато
|
||||
есть собственный вырожденный случай.
|
||||
|
||||
#### Scenario: Устаревшее и новое имя дают один код
|
||||
|
||||
- **WHEN** канонизируются `HKCategoryValueSleepAnalysisAsleep` и
|
||||
`HKCategoryValueSleepAnalysisAsleepUnspecified`
|
||||
- **THEN** обе формы дают `HKCategoryValueSleepAnalysisAsleepUnspecified`
|
||||
|
||||
#### Scenario: Код без синонима остаётся собой
|
||||
|
||||
- **WHEN** канонизируется `HKCategoryValueSleepAnalysisAsleepREM`
|
||||
- **THEN** результат равен исходному коду
|
||||
|
||||
#### Scenario: Таблица синонимов плоская
|
||||
|
||||
- **WHEN** проверяется таблица синонимов целиком
|
||||
- **THEN** ни одно её значение не встречается среди её ключей
|
||||
|
||||
### Requirement: Границы наблюдённых категориальных значений
|
||||
|
||||
Число различных наблюдений одной доставки MUST быть ограничено **64**, а длина
|
||||
значения — **128 байт**; отброшенное SHALL считаться. Тело контролирует
|
||||
отправитель целиком: без границы одна доставка кладёт в витрину сколько угодно
|
||||
строк.
|
||||
|
||||
Граница числа SHALL применяться **при накоплении, а не при выдаче**. Накопитель
|
||||
без границы растёт по числу различных строк тела, а их контролирует
|
||||
отправитель: измерено — тело в 60 МиБ из миллиона различных значений (предел
|
||||
приёма 64 МиБ) поднимало пик процесса с 780 до 1002 МиБ. Лимита памяти у
|
||||
контейнера нет, отказ по памяти в фоновой горутине свёртки не перехватывается,
|
||||
а перезапуск берёт ту же доставку из архива: приём стоит, телефон доставку не
|
||||
перешлёт.
|
||||
|
||||
Счётчик отброшенного считает **вхождения**, а не различные значения, и единица
|
||||
MUST быть названа. Различные здесь не считаются не по недосмотру: отброшенный
|
||||
ключ нигде не запоминается, а запомнить его значило бы вернуть тот самый
|
||||
неограниченный рост. Счётчик отвечает на «сколько раз сработала граница»; на
|
||||
«какие строки ждут словаря» отвечает реестр.
|
||||
|
||||
Числа названы измерением, а не аналогией: на живом потоке различных значений по
|
||||
всем трём полям около одиннадцати, самое длинное — «Сидячий образ жизни»,
|
||||
36 байт UTF-8. Предел в 32 байта, взятый по аналогии с именами непокрытых
|
||||
секций, отбросил бы две из трёх измеренных строк контекста пульса: там имена
|
||||
короткие и латинские, здесь — русские фразы.
|
||||
|
||||
Значение длиннее предела SHALL **отбрасываться со счётчиком, а не обрезаться**.
|
||||
Обрезанная строка неотличима от настоящей и попала бы в реестр самостоятельным
|
||||
значением; настоящая строка при этом остаётся в точке целиком — теряется
|
||||
наблюдение, а не данные.
|
||||
|
||||
При переполнении границы числа уцелевший набор SHALL быть **функцией множества
|
||||
наблюдений, а не порядка элементов на проводе**: наблюдения упорядочиваются по
|
||||
ключу и берутся первые. Иначе то же содержимое, переприсланное в другом порядке
|
||||
ключей (порядок у HAE нестабилен, находка 2), давало бы другой реестр.
|
||||
|
||||
#### Scenario: Слишком много различных строк
|
||||
|
||||
- **WHEN** доставка несёт различных наблюдений больше предела
|
||||
- **THEN** в результате разбора их ровно предел
|
||||
- **AND** счётчик отброшенных наблюдений положителен
|
||||
- **AND** все точки доставки сохранены
|
||||
|
||||
#### Scenario: Уцелевший набор не зависит от порядка точек в теле
|
||||
|
||||
- **WHEN** два тела несут одно и то же множество категориальных строк в разном
|
||||
порядке, и обоим не хватает предела
|
||||
- **THEN** уцелевшие наблюдения у них совпадают
|
||||
|
||||
#### Scenario: Непомерно длинное значение отброшено целиком
|
||||
|
||||
- **WHEN** `sleep_analysis.value` длиннее предела длины
|
||||
- **THEN** наблюдения из него не выводится, счётчик отброшенных положителен
|
||||
- **AND** содержимое точки сохранено дословно и целиком
|
||||
|
||||
@@ -323,10 +323,13 @@
|
||||
или нет.
|
||||
|
||||
Счётчики «до и после» SHALL покрывать **каждую единицу хранения витрины**:
|
||||
часовые объекты, тренировки и записи. Отпечаток отвечает «да/нет» за витрину
|
||||
целиком, поэтому единица, которой нет в счётчиках, делает расхождение
|
||||
безадресным: человек увидит «не совпало» при неизменившемся числе объектов и не
|
||||
отличит появление двадцати семи тренировок от пропажи двух.
|
||||
часовые объекты, тренировки, записи и строки реестра категориальных значений.
|
||||
Отпечаток отвечает «да/нет» за витрину целиком, поэтому единица, которой нет в
|
||||
счётчиках, делает расхождение безадресным: человек увидит «не совпало» при
|
||||
неизменившемся числе объектов и не отличит появление двадцати семи тренировок от
|
||||
пропажи двух. Перечень пополняется **тем же изменением**, которое заводит
|
||||
единицу хранения: список закрытый, и единица, не внесённая в него, молчит ровно
|
||||
там, где расхождение впервые становится заметным.
|
||||
|
||||
Отказы SHALL считаться **по классам**: слой не выводится, содержимое не
|
||||
разбирается, работа отложена по обстоятельствам, всё прочее. Невыведенный слой
|
||||
@@ -362,13 +365,17 @@
|
||||
печатать их сравнение значило бы выдать неизмеренное за измеренное, причём в
|
||||
единственном оракуле задачи. Ожидаемые классы расхождения (новые доставки за время прогона,
|
||||
непереносимый признак запечатанного часа, исправленный разбор, **покрытая
|
||||
разбором новая секция**) SHALL называться отдельно от самого факта расхождения.
|
||||
разбором новая секция**, **появившаяся единица хранения**) SHALL называться
|
||||
отдельно от самого факта расхождения.
|
||||
|
||||
Класс «покрыта новая секция» назван потому, что первый прогон после такого
|
||||
изменения расходится **гарантированно** и штатно: витрина обзаводится единицами
|
||||
хранения, которых в рабочей базе нет по построению. Не назвав его, отчёт
|
||||
приучает человека игнорировать расхождение отпечатков — то есть обесценивает
|
||||
оракул ровно тогда, когда по нему принимается необратимое решение.
|
||||
оракул ровно тогда, когда по нему принимается необратимое решение. Класс
|
||||
«появилась единица хранения» — та же причина в общем виде: реестр категориальных
|
||||
значений пуст у витрины, свёрнутой прежним бинарём, и первая сверка после
|
||||
выкатки расходится по нему одному, при неизменившихся объектах и сущностях.
|
||||
|
||||
**Исход команды.** Расхождение отпечатков отказом быть MUST NOT: после
|
||||
исправления разбора оно ожидаемо и есть сам смысл пересборки. Отказ отдельной
|
||||
@@ -383,7 +390,9 @@
|
||||
выполнивший напечатанную процедуру, заменил бы витрину пустой.
|
||||
|
||||
Отчёт значений точек, имён метрик, имён устройств и содержимого тел содержать
|
||||
MUST NOT: отпечаток берёт содержимое хешем. Ограничение относится к отчёту в
|
||||
MUST NOT: отпечаток берёт содержимое хешем. Строки реестра при этом счётом, а не
|
||||
перечислением: наблюдённое значение — данные о здоровье наравне со значением
|
||||
точки. Ограничение относится к отчёту в
|
||||
стандартном выводе; лог свёртки живёт по правилам спеки хранения, где координаты
|
||||
столкновения (метрика, слой, час) разрешены явно.
|
||||
|
||||
@@ -402,7 +411,15 @@ SHALL идти в поток ошибок, а не смешиваться с о
|
||||
|
||||
- **WHEN** пересборка завершилась
|
||||
- **THEN** отчёт печатает «до и после» отдельно для часовых объектов,
|
||||
тренировок и записей
|
||||
тренировок, записей и строк реестра категориальных значений
|
||||
|
||||
#### Scenario: Пустой реестр рабочей витрины назван ожидаемым классом
|
||||
|
||||
- **WHEN** рабочая витрина свёрнута прежним бинарём и строк реестра не имеет, а
|
||||
пересобранная их получила
|
||||
- **THEN** отчёт называет «появилась единица хранения» ожидаемым классом
|
||||
расхождения
|
||||
- **AND** печатает число строк реестра «до и после»
|
||||
|
||||
#### Scenario: Расхождение отпечатков не является отказом
|
||||
|
||||
@@ -444,7 +461,8 @@ SHALL идти в поток ошибок, а не смешиваться с о
|
||||
#### Scenario: Отчёт не раскрывает данных о здоровье
|
||||
|
||||
- **WHEN** отчёт напечатан
|
||||
- **THEN** он не содержит ни значений точек, ни имён метрик, ни имён устройств
|
||||
- **THEN** он не содержит ни значений точек, ни имён метрик, ни имён устройств,
|
||||
ни наблюдённых категориальных строк
|
||||
|
||||
### Requirement: Отказ на одной доставке не останавливает пересборку
|
||||
|
||||
@@ -497,4 +515,3 @@ SHALL идти в поток ошибок, а не смешиваться с о
|
||||
- **WHEN** журнал содержит доставку, приехавшая версия сущности в которой
|
||||
теряет содержание сохранённой
|
||||
- **THEN** отчёт пересборки называет число удержанных версий больше нуля
|
||||
|
||||
|
||||
@@ -1335,3 +1335,162 @@ MUST NOT, а закрывать базу по выходу одной из дв
|
||||
- **THEN** база не закрывается, а запись лога называет этап, не указывая
|
||||
виновной горутины
|
||||
|
||||
### Requirement: Реестр наблюдённых категориальных значений
|
||||
|
||||
Хранилище SHALL держать реестр категориальных значений, которые приносил поток:
|
||||
одна строка на тройку `(метрика или секция, поле, значение)`, с выведенным кодом
|
||||
HealthKit рядом. Значение хранится **дословно**, тем же текстом, каким пришло.
|
||||
|
||||
Локаль в ключ MUST NOT входить. Она живёт в заголовке доставки, которого нет в
|
||||
сыром архиве, — ключ с локалью сделал бы состояние функцией от того, уцелела ли
|
||||
строка учёта: усыновлённая доставка положила бы вторую строку с пустой локалью.
|
||||
Язык, на котором приехала строка, восстанавливается по доставке провенанса.
|
||||
|
||||
Реестр SHALL нести выведенный код и провенанс **первой** встречи — метку приёма
|
||||
и идентификатор доставки, в которой строка появилась впервые по порядку журнала.
|
||||
Первая встреча отвечает на вопрос «когда сменился язык телефона»; счётчик встреч
|
||||
не заводится вовсе — он не идемпотентен при повторной свёртке той же доставки, а
|
||||
значит сделал бы состояние зависящим от числа прогонов.
|
||||
|
||||
Пустой код — законное состояние строки: он означает «словарь этой строки не
|
||||
знает», и перечень таких строк есть заявка на пополнение словаря.
|
||||
|
||||
Словарь, по которому выводится код, SHALL жить в бинаре, а не в таблице базы и
|
||||
не в строках миграции. Таблица, наполняемая руками, стала бы входом, которого
|
||||
нет в журнале, и `import + replay` перестал бы задавать состояние однозначно;
|
||||
данные, засеянные миграцией, живут в двух местах сразу и расходятся с бинарём
|
||||
молча после первой же правки словаря.
|
||||
|
||||
Строки реестра SHALL писаться **той же транзакцией**, что и точки доставки.
|
||||
Доставка — единица свёртки; частичное состояние «точки легли, реестр нет»
|
||||
повторная свёртка не чинит: хеш содержимого сойдётся, и объекты переписываться
|
||||
не станут.
|
||||
|
||||
Обслуживания у реестра нет: строки не удаляются. Строка, единственные доставки
|
||||
которой выпали из журнала подрезкой архива, переживёт их в рабочей витрине и не
|
||||
появится в пересобранной. Это тот же класс, что «верхние слои за периоды с
|
||||
удалёнными доставками», и он MUST называться, а не досчитываться.
|
||||
|
||||
#### Scenario: Фаза сна попадает в реестр с кодом
|
||||
|
||||
- **WHEN** свёрнута доставка с фазой сна «Во сне» и локалью `ru`
|
||||
- **THEN** в реестре есть строка `sleep_analysis / value / «Во сне»` с кодом
|
||||
`HKCategoryValueSleepAnalysisAsleepUnspecified`
|
||||
- **AND** содержимое точки в часовом объекте не изменилось
|
||||
|
||||
#### Scenario: Та же строка без заголовка локали даёт ту же строку реестра
|
||||
|
||||
- **WHEN** та же строка приехала доставкой без `Accept-Language`
|
||||
- **THEN** второй строки в реестре не появляется
|
||||
|
||||
#### Scenario: Строка без кода в реестре видна
|
||||
|
||||
- **WHEN** свёрнута доставка с `heart_rate.context`, которого словарь не знает
|
||||
- **THEN** в реестре есть строка с этим значением и пустым кодом
|
||||
|
||||
#### Scenario: Отказ слияния не оставляет строк реестра
|
||||
|
||||
- **WHEN** слияние доставки отказало
|
||||
- **THEN** реестр не содержит значений этой доставки
|
||||
|
||||
### Requirement: Реестр детерминирован по журналу
|
||||
|
||||
Повторная свёртка той же доставки MUST оставлять реестр без изменений, а
|
||||
провенанс первой встречи MUST быть **минимумом** по порядку журнала
|
||||
(`received_at`, затем идентификатор доставки), а не значением последней записи.
|
||||
|
||||
Проигрывание журнала в любом порядке доставок, дающем тот же префикс, SHALL
|
||||
приводить реестр к тому же состоянию. Реестр — свёртка по журналу, как и всё
|
||||
остальное в витрине.
|
||||
|
||||
Выведенный код SHALL обновляться при каждой встрече строки: словарь живёт в
|
||||
бинаре, и пересборка обязана давать код по текущему словарю, а не по тому,
|
||||
который действовал при первой свёртке. Строка, переставшая приезжать, держит код
|
||||
прежнего словаря до пересборки — это осознанная цена материализации, и на
|
||||
сходимость она не влияет, потому что код в отпечаток не входит.
|
||||
|
||||
#### Scenario: Повторная свёртка ничего не меняет
|
||||
|
||||
- **WHEN** одна и та же доставка свёрнута дважды
|
||||
- **THEN** строки реестра и их провенанс совпадают с состоянием после первой
|
||||
свёртки
|
||||
|
||||
#### Scenario: Провенанс — самая ранняя доставка
|
||||
|
||||
- **WHEN** одна строка приехала сперва поздней доставкой, затем ранней
|
||||
- **THEN** провенансом остаётся ранняя по журналу
|
||||
|
||||
#### Scenario: Порядок свёртки на реестр не влияет
|
||||
|
||||
- **WHEN** три доставки свёрнуты во всех шести порядках
|
||||
- **THEN** реестр и его раздел отпечатка совпадают у всех шести прогонов
|
||||
|
||||
#### Scenario: Пересборка даёт тот же реестр
|
||||
|
||||
- **WHEN** журнал проигран в свежую витрину
|
||||
- **THEN** реестр пересобранной витрины совпадает с реестром исходной
|
||||
|
||||
### Requirement: Отпечаток покрывает наблюдение реестра, но не выведенный код
|
||||
|
||||
Отпечаток витрины SHALL покрывать реестр категориальных значений отдельным
|
||||
разделом: ключ и провенанс первой встречи. Иначе недетерминированная запись
|
||||
реестра прошла бы мимо единственного оракула сходимости.
|
||||
|
||||
Выведенный код в отпечаток входить MUST NOT. Ключ и провенанс — функция журнала;
|
||||
код — функция журнала **и версии словаря в бинаре**. Включённый в отпечаток, он
|
||||
заставил бы всякое пополнение словаря давать расхождение при побайтно совпавшем
|
||||
журнале, а пополнение объявлено рабочим циклом. Человек, принимающий по
|
||||
отпечатку необратимое решение о подмене базы, читал бы это как дефект.
|
||||
Правильность вывода кода проверяется тестами словаря — это другой вопрос, и
|
||||
смешение обесценило бы оракул.
|
||||
|
||||
Раздел SHALL быть отличим от прочих признаком впереди строки, а поля переменной
|
||||
длины SHALL идти с длиной впереди: два разных состояния витрины не имеют права
|
||||
дать один отпечаток.
|
||||
|
||||
Цена названа вслух: у витрины, свёрнутой до этого изменения, реестр пуст, и
|
||||
первая же сверка отпечатков после выкатки покажет расхождение. Это законное
|
||||
расхождение, а не дефект; отчёт пересборки обязан назвать его ожидаемым классом
|
||||
и напечатать счётчик строк реестра.
|
||||
|
||||
#### Scenario: Разошедшееся наблюдение меняет отпечаток
|
||||
|
||||
- **WHEN** у двух витрин совпадают объекты и сущности, но в реестре одной есть
|
||||
строка, которой нет в другой
|
||||
- **THEN** отпечатки различны
|
||||
|
||||
#### Scenario: Расхождение только по коду отпечаток не двигает
|
||||
|
||||
- **WHEN** две витрины несут те же строки реестра с тем же провенансом, но
|
||||
разными кодами
|
||||
- **THEN** отпечатки совпадают
|
||||
|
||||
#### Scenario: Значение с разделителем границу поля не подделывает
|
||||
|
||||
- **WHEN** значение содержит признак раздела, цифры и нулевой байт
|
||||
- **THEN** отпечаток отличается от отпечатка витрины с другим разбиением тех же
|
||||
байтов по полям
|
||||
|
||||
#### Scenario: Пустой реестр отпечаток не ломает
|
||||
|
||||
- **WHEN** в витрине нет ни одной строки реестра
|
||||
- **THEN** отпечаток считается и совпадает с отпечатком такой же витрины без
|
||||
реестра
|
||||
|
||||
### Requirement: Значения категориальных строк не попадают в логи
|
||||
|
||||
Сами наблюдённые строки и выведенные коды MUST NOT попадать в записи лога выше
|
||||
`DEBUG`; в журнал SHALL уходить только счётчики — сколько значений наблюдалось и
|
||||
сколько осталось без кода. Основание: строки категориальных значений — данные о
|
||||
здоровье, контекст пульса и фаза сна описывают человека не меньше, чем число.
|
||||
|
||||
Проверка отсутствия строки в логе SHALL разбирать запись и сравнивать значения
|
||||
полей, а не искать подстроку в сыром буфере: служебная метка времени содержит
|
||||
произвольные цифры, и поиск по буферу делает тест флаки по построению.
|
||||
|
||||
#### Scenario: Свёртка доставки со сном не пишет строк в лог
|
||||
|
||||
- **WHEN** свёрнута доставка с фазами сна и контекстом пульса
|
||||
- **THEN** в разобранных записях лога нет ни одной наблюдённой строки и ни
|
||||
одного кода
|
||||
- **AND** счётчики значений без кода в записи присутствуют
|
||||
|
||||
Reference in New Issue
Block a user