store: при равной полноте точек побеждает пришедшая доставка

- байтовый порядок канонических форм остался тай-брейком только внутри одной
  доставки: на живом корпусе он решал 98,8% спорных координат и системно хранил
  меньшее значение, из-за чего step_count терял род и verify:archive был красным
- правило перестало быть коммутативным осознанно, поэтому порядок свёртки
  приведён к журнальному: проход воркера прекращается на отложенной доставке,
  а свёртка вне порядка журнала пишет WARN
- заведены счётчики PointsHeld и PointsErased — удержание полнотой и
  единственное направление, в котором правило теряет содержание
This commit is contained in:
av
2026-08-04 11:16:24 +03:00
parent ae607f1ceb
commit b278501a6e
44 changed files with 3780 additions and 172 deletions
@@ -0,0 +1,73 @@
## Why
Тай-брейк при равной полноте точек сегодня — порядок канонических форм, и он
решает **98,8% спорных координат** (83 956 из 84 978 при 453 171 координате):
полнота отвечает лишь в 1,2% случаев. Лексикографический порядок системно берёт
меньшее значение (1 847 из 1 912, находка 49), то есть хранит **устаревшую**
версию точки там, где HAE досчитывает задним числом (находка 10).
У дефекта появился независимый оракул: `task verify:archive` покраснел на
`master` без единого коммита, с ростом корпуса. Час `2026-08-03T07:00Z` метрики
`step_count` получил четыре доставки с двумя различными значениями; победило
меньшее, оно же приехавшее первым, и сверка слоёв объявила метрику мгновенной
против 23 согласных часов. `step_count` ушёл в `unknown` — Read API больше не
имеет права суммировать шаги.
Решение владельца от 2026-08-04: **при равной полноте побеждает пришедшая
точка**, а не сохранённая. Значение при этом не интерпретируется — правило
остаётся структурным.
## What Changes
- **BREAKING (содержимое витрины):** при равной полноте побеждает точка,
пришедшая **этой** доставкой, а не сохранённая. Отпечаток витрины обязан
измениться; состояние восстанавливается пересборкой из архива.
- Порядок канонических форм остаётся тай-брейком **внутри одной доставки**, где
провенанс общий и различать нечем.
- **Правило слияния точек перестаёт быть функцией множества и становится явной
функцией порядка журнала.** Отсюда — плата, которую изменение обязано
внести целиком: порядок свёртки приводится к порядку журнала.
- Фоновый воркер **прекращает проход на первой отложенной доставке**, а не
перешагивает её. Иначе занятость базы переставляет доставки местами, и живая
витрина расходится с пересборкой молча.
- Свёртка доставки, у которой в журнале уже есть свёрнутая преемница, пишет
`WARN`: остаточное окно (конкурентный приём делает строку учёта видимой
не в порядке меток) закрыть без изменения приёма нельзя, но молчать о нём
нельзя тем более.
- Заводится счётчик «пришедшая точка проиграла сохранённой» — отдельно от
общего `MergeStats.Overwrites`, который считает столкновения в обе стороны и
различить их не даёт.
- Схема не трогается: колонок провенанса на точку не заводится, формат
`payload` не меняется, миграции нет.
## Capabilities
### New Capabilities
Новых нет.
### Modified Capabilities
- `storage`: тай-брейк при равной полноте точек — пришедшая побеждает
сохранённую; порядок канонических форм остаётся только внутри доставки.
Требование «победитель есть функция множества точек» заменяется на «функция
множества точек и позиции в журнале». Новый счётчик удержаний.
- `ingest`: проход воркера прекращается на первой отложенной доставке; свёртка
вне порядка журнала становится наблюдаемой.
- `reindex`: посылка сходимости («приём шёл последовательно») из оговорки
сценария становится названным условием требования — теперь от порядка свёртки
зависит не только вывод слоя, но и содержимое точек.
## Impact
- `internal/store/bucket.go``mergePoints`, `resolve`, `candidate`,
`pointLess`, `MergeStats`.
- `internal/store/delivery.go` — запрос «есть ли свёрнутая доставка позже этой».
- `internal/replay/worker.go` — барьер на отложенной доставке, `WARN` о свёртке
вне порядка журнала.
- `internal/fold/fold.go` — проброс нового счётчика в итог свёртки и в лог.
- Витрина в `./data`: отпечаток изменится. Пересборка — действие человека при
остановленном сервисе, этим изменением не выполняется.
- Оракулы: `task verify:archive` (ноль противоречащих часов, `step_count`
снова накопительная), тест сходимости «живой приём = пересборка» с отложенной
доставкой.