## MODIFIED Requirements ### Requirement: Разрешение столкновений по полноте Когда по одним координатам приходят разные содержимые, система SHALL оставлять **более полную** точку — ту, чьё множество ключей с непустым значением является **строгим надмножеством** множества другой, — а не последнюю пришедшую. Иначе бедная доставка стирает у богатой поля, которых сама не несёт. Полнота SHALL сравниваться множествами, а не их размером. Число сравнимо всегда, и потому счётчик даёт ответ там, где ответа нет: точка с пятью полями, не несущими содержания, побеждала бы настоящее измерение с двумя полями и стирала бы его безвозвратно. Измерено (находка 49): настоящих столкновений 2 897 из 444 256 координат (0.65%); из них 981 различаются набором полей — это и есть область правила полноты, — 1 916 несут равные наборы и разные значения, где исход решает тай-брейк, а несравнимых наборов ноль. Пустым значением MUST считаться `null`, пустая строка, число, равное нулю (в любой записи), пустой объект и пустой массив: поле без содержания не делает точку полнее точки, где этого поля нет вовсе. Пустота MUST определяться по разобранному значению, а не по байтам: `0`, `0.0`, `0e0`, `-0` и `{ }` — та же пустота, что `0` и `{}`. `false` пустотой MUST NOT считаться: для булева поля это одно из двух значений, а не отсутствие содержания (`isIndoor: false` — тренировка на улице). Поле `source` в множество не входит — оно нестабильно и переписывается задним числом (находка 36), так что о полноте измерения ничего не говорит. Содержимое, которое не разбирается как JSON-объект, SHALL нести **пустое множество** ключей: так оно проигрывает любой точке с содержанием и не загрязняет наблюдение о несравнимых наборах. Надмножество побеждает только тогда, когда оно **несёт то же содержание**: значения ключей, содержательных у обеих точек, MUST совпадать (с точностью до канонической формы). Иначе точки несут разные измерения, и надмножество имён о полноте не говорит ничего — такая пара MUST разрешаться как равнополная. Без этого условия точка `{date, qty:0.001, p1:null, p2:null}` вытесняла бы `{date, qty:72.5}`, то есть точка, где ни одно значение не измерение, стирала бы измерение — ровно то, ради отрицания чего правило переписано. Если множества ключей с непустым значением **равны и значения совпали**, система SHALL сравнить множества **всех** ключей, кроме `source`, и оставить строгое надмножество. Без этого разряда правило теряло бы поля там, где заведено их беречь: точка `{date, qty:10, Min:0, Max:0}` и точка `{date, qty:10}` несут одинаковое содержание, и `Min` с `Max` исчезли бы из витрины по жребию. Несравнимость на этом разряде исходом MUST NOT быть: лишние ключи там заведомо пусты, объединять в них нечего. Если равны и эти множества, а значения различаются, исход MUST быть детерминированным и не зависеть от порядка, в котором доставки дошли до хранилища: свёртка по журналу обязана давать то же состояние, что приём в реальном времени. Победитель MUST быть функцией **множества** точек координаты, а не порядка их поступления. Попарная свёртка этого не даёт: полнота — частичный порядок, тай-брейк — тотальный, и вместе они образуют нетранзитивное отношение победы (A превосходит B по полноте, B бьёт C тай-брейком, C бьёт A тай-брейком). При таком цикле повторная свёртка одной и той же доставки меняет содержимое объекта, и витрина перестаёт быть функцией журнала. Поэтому система SHALL отбросить кандидатов, превзойдённых по полноте кем-то другим, и выбрать победителя среди оставшихся по тотальному порядку — обе операции зависят только от состава множества. Сравнение по `received_at` для этого не годится: у сохранённой точки нет провенанса — ни времени приёма, ни идентификатора доставки, — и сравнивать не с чем. Детерминизм обеспечивается свойством самих значений (например, порядком канонических форм), а не порядком событий. Если множества **несравнимы** — каждое несёт ключ с непустым значением, которого нет у другого, — система SHALL выбрать победителя тем же детерминированным правилом, что и при равных множествах, и MUST оставить наблюдение: счётчик в итоге разбора доставки, координаты объекта и запись `WARN` без значений точки. Несравнимый набор — частный случай столкновения: счётчик перезаписей растёт вместе с ним, а координаты попадают в оба списка. Объединять поля двух точек система SHALL NOT: на живом потоке несравнимых наборов не встретилось ни разу (0 из 2 897 столкновений, при обоих определениях пустоты), и реализация правила, которое никогда не срабатывает, стоила бы больше, чем счётчик, который скажет, если оно наступит. Победителем SHALL оставаться одна из пришедших точек **дословно**: правило выбирает, а не конструирует. Каноническая форма существует только в момент сравнения — вернуть её вместо исходных байт значило бы сохранить округлённое число вместо присланного. #### Scenario: Бедная точка не стирает поля богатой - **WHEN** сохранена точка с `Avg`, `Min`, `Max` и `context` - **AND** по тем же координатам приезжает точка только с `Avg`, `Min` и `Max` - **THEN** сохранённая точка остаётся с `context` #### Scenario: Поля без содержания полноты не добавляют - **WHEN** сохранена точка с пятью полями, значения которых `0`, `{}` и `[]` - **AND** по тем же координатам приезжает точка с `date` и ненулевым `qty` - **THEN** остаётся точка с `date` и `qty` #### Scenario: При равном содержании поля не теряются - **WHEN** сохранена точка `{date, qty, Min:0, Max:0}` - **AND** по тем же координатам приезжает точка `{date, qty}` с другим `qty` - **THEN** остаётся точка с `Min` и `Max` #### Scenario: Одинаково полные точки с разными значениями - **WHEN** по одним координатам приходят две точки с одинаковыми множествами ключей и разными значениями - **THEN** исход определяется детерминированно и не зависит от порядка воспроизведения доставок #### Scenario: Несравнимые множества считаются, а не сливаются - **WHEN** по одним координатам приходят две точки, каждая из которых несёт ключ с непустым значением, которого нет у другой - **THEN** остаётся ровно одна точка, выбранная детерминированно - **AND** счётчик несравнимых наборов в итоге разбора доставки растёт - **AND** система пишет `WARN` с координатами объекта и без значений точки #### Scenario: Содержимое, которое не разбирается в объект - **WHEN** по координатам сталкиваются точка с непустыми полями и содержимое, не разбирающееся как JSON-объект - **THEN** остаётся точка с полями - **AND** счётчик несравнимых наборов не растёт Столкновением SHALL считаться расхождение **канонических форм**, а не байтов. Байты нестабильны — ради этого канонизация и заведена: из 81 952 повторно приехавших точек 67 534 различаются лишь порядком ключей, ещё 63% — последним разрядом double. Побайтовое сравнение давало бы тысячи ложных срабатываний на каждом глубоком проходе, и настоящий отказ правила стал бы неотличим от нормы. #### Scenario: Столкновение с различием содержимого оставляет след - **WHEN** по одним координатам сохраняется точка, каноническая форма которой отличается от уже сохранённой - **THEN** система пишет запись уровня `WARN` без значений точки - **AND** запись несёт координаты объекта: метрику, слой и час - **AND** увеличивает счётчик перезаписей в итоге разбора доставки #### Scenario: Дребезг сериализации столкновением не считается - **WHEN** та же точка приезжает с другим порядком ключей или отличаясь последним разрядом числа - **THEN** счётчик перезаписей не растёт и `WARN` не пишется Без этого следа допущение «меньше полей не значит новее» не получит ни одного наблюдения, а отказ правила будет неотличим от нормальной работы до сверки с экспортом Apple — то есть месяцами.