блокеры разобраны замером: три стали задачами, один закрыт

- находки 48 и 49: единицы не менялись ни разу; настоящих столкновений 0.65%,
  несравнимых наборов полей нет, тай-брейк берёт меньшее в 96% случаев
- edinicy-metriki-v-razreze закрыт: гипотеза не подтвердилась замером
- тай-брейк отложен до каталога рода агрегации
This commit is contained in:
av
2026-08-01 19:33:47 +03:00
parent 37413bb551
commit 349a227ab1
8 changed files with 211 additions and 165 deletions
+48 -41
View File
@@ -1,19 +1,14 @@
# Правило выбора победителя при столкновении точек
# Полнота точки — по множеству ключей, а не по их числу
**Приоритет:** блокеры
**Приоритет:** высокий
Вынуто ревью кода задачи `razbor-metrik-v-obekty` (профиль `deep`, находка №7
триажа, severity major). Трогает записанный инвариант «при столкновении
выигрывает более полная точка».
Была блокером, вынутым ревью кода задачи `razbor-metrik-v-obekty` (профиль
`deep`, находка №7 триажа, severity major). **Решение принято замером**
(находка 49) — ниже задача на остаток.
## Что решить
## Что не так сегодня
Как выбирать победителя, когда по одним координатам приехали разные
содержимые. Сегодняшнее правило измеримо неверно в двух местах.
## Оракул: измерено
**Полнота — это счётчик ключей**, чьё значение не `null`, не `""` и не пусто.
Полнота — это **счётчик** ключей, чьё значение не `null`, не `""` и не пусто.
Поэтому `{}`, `[]`, `0` и `false` считаются содержательными:
```
@@ -24,42 +19,54 @@
Вторая точка — настоящее измерение — проигрывает первой и стирается
безвозвратно. Восстановить можно только из сырого архива, пока он жив.
**Тай-брейк при равной полноте** — лексикографический порядок канонических
форм, то есть исход зависит от самого значения. Для накопительной метрики,
которую досчитывают задним числом, это систематическая победа **меньшего**
числа: `qty:1` бьёт `qty:100`. Недосчёт, неотличимый от нормы.
## Что решено и на каком основании
Частота на живом потоке **не измерена** — стоит прогнать по архиву до решения.
Замер по всем 99 доставкам (находка 49): настоящих столкновений 2 897 из
444 256 координат (0.65%), и среди них **ноль несравнимых наборов полей**.
## Варианты и цена
Отсюда:
**(1) Полнота по множеству ключей: побеждает надмножество; несравнимые
множества — объединять поля, а не выбирать точку целиком.**
Цена: средняя — правка спеки, `resolve` и тестов. Самое честное прочтение
инварианта «ничего не теряем молча»: при несравнимых наборах не теряется
ничего вообще.
- **Полнота — сравнение множеств ключей, побеждает надмножество.** Ровно 981
случай из замера, и там правило уже работает верно — но работает случайно,
через счётчик, который на другом входе даст обратное.
- **Объединение полей при несравнимых наборах не нужно.** Самая дорогая часть
обсуждавшегося варианта (1) на живом потоке не срабатывает ни разу. Вместо
реализации — счётчик: несравнимый набор считается и логируется `WARN`. Если
событие когда-нибудь наступит, оно будет видно, а не додумано заранее.
- **Тай-брейк при равной полноте в эту задачу не входит.** Обоснование ниже.
**(2) Оставить счёт ключей, но не считать содержательными `{}`, `[]`, `0`,
`false`, `""`.**
Цена: малая. Правило остаётся играбельным (точку с лишними непустыми полями
никто не мешает прислать), и произвол тай-брейка не решён.
## Что делать
**(3) При равной полноте побеждает точка более поздней доставки по
`received_at`.**
Цена: малая, но спека должна признать зависимость от журнала, и нужен
детерминированный порядок **внутри** одной доставки — а именно там измерены
столкновения (31 координата в 21 доставке из 89).
1. `resolve` в `internal/store/bucket.go`: полнота — множество ключей с
непустым значением; побеждает строгое надмножество.
2. Несравнимые множества — счётчик в `MergeStats` плюс `WARN` с координатами
объекта (без значений). Точку выбирает тот же тай-брейк, что и при равной
полноте.
3. Дельта-спека `openspec/specs/storage` → «Разрешение столкновений по
полноте»: переформулировать с числа ключей на множество.
4. Тесты: «бедная точка с пятью пустыми полями не стирает измерение»
(регрессия на приведённой выше паре), «надмножество побеждает»,
«несравнимые наборы считаются».
5. `task verify:archive` обязан дать то же состояние — правило меняет исход
только там, где сегодня он неверен.
## Рекомендация
## Что отложено и почему
**(1).** Объединение полей при несравнимых наборах — единственный вариант, при
котором столкновение вообще перестаёт быть выбором «кого потерять».
**Тай-брейк при равной полноте.** Сегодня это лексикографический порядок
канонических форм. Измерено (находка 49): в 1 847 случаях из 1 912 он выбирает
**меньшее** значение — 96%. Четыре из шести затронутых метрик накопительные,
там это систематический недосчёт; но крупнейшая группа, `heart_rate` (985
случаев), мгновенная, и там «большее» не правильнее.
То есть правильный тай-брейк зависит от **рода метрики**, а род по замыслу
проекта измеряется сверкой слоёв между собой. Выбирать его сейчас — угадывать
ровно то, что станет известно точно.
Зависимость: [rod-agregacii-i-katalog](rod-agregacii-i-katalog.md). Тай-брейк
доделывается **после** неё, отдельной задачей.
## Что стоит без решения
Правило работает и теперь **наблюдаемо**: столкновение считается расхождением
канонических форм (а не байтов, как было), даёт `WARN` с координатами объекта
и счётчик `overwrites`. Если правило начнёт терять — это станет видно в логе,
а не через месяцы при сверке с экспортом Apple.
Связано: `openspec/specs/storage` → «Разрешение столкновений по полноте».
Ничего. Правило работает и **наблюдаемо**: столкновение считается расхождением
канонических форм (а не байтов), даёт `WARN` с координатами объекта и счётчик
`overwrites`.