## 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 — то есть месяцами.
