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,118 @@
## 1. Правило слияния точек
- [x] 1.1 `candidate` получает флаг `incoming`; схлопывание совпавших
канонических форм поднимает флаг, а байты оставляет встреченные первыми
- [x] 1.2 `pointLess` — лексикографический порядок по паре
`(не incoming, каноническая форма)`; `pointDominates` не трогается
- [x] 1.3 `resolve` возвращает признак «победил сохранённый при наличии
пришедших»; `mergePoints` считает удержания
- [x] 1.4 `MergeStats.PointsHeld` и `PointsHeldAt`; проброс через `mergeResult`,
`SaveBuckets`, `fold.Stats`, `replay.Outcome`/`Report` и лог свёртки — без
значений точек
## 2. Порядок свёртки равен порядку журнала
- [x] 2.1 `Worker.Pass` прекращает проход на исходе `Deferred`; доставка,
оставшаяся `pending` без такого исхода, курсор двигает
- [x] 2.2 `Store.ParsedAfter` — есть ли доставка позже данной в статусе
`parsed` или `partial` (кортежный предикат; индекс — `delivery_received_at`,
цена измерена и названа в design.md)
- [x] 2.3 воркер пишет одну запись `WARN` на доставку о свёртке вне порядка
журнала; пересборка эту проверку не выполняет
## 3. Тесты
- [x] 3.1 пришедшая побеждает сохранённую при равной полноте
- [x] 3.2 столкновение внутри одной доставки решается порядком канонических
форм и не зависит от порядка элементов массива
- [x] 3.3 повторная присылка канонически совпавшего содержимого не переписывает
объект (хеш не двигается)
- [x] 3.4 полнота сильнее происхождения; счётчик удержаний растёт
- [x] 3.5 сходимость: живой путь через воркер с отложенной занятостью базы
доставкой даёт тот же отпечаток, что пересборка
- [x] 3.6 проход воркера не перешагивает отложенную доставку
- [x] 3.7 `WARN` о свёртке вне порядка журнала пишется, не содержит значений и
не пишется на `failed`-преемнице
- [x] 3.8 при разошедшихся значениях пустые ключи сохранённой точки её не
удерживают (сценарий, парный к 3.4)
## 4. Документы
- [x] 4.1 `docs/architecture.md` — «Разрешение столкновений» плюс **все**
упоминания правила: инвариант в шапке, «порядок прихода значения не имеет» в
разделе синхронизации; таблица двух правил равной полноты (точки против
сущностей) с критерием выбора
- [x] 4.2 `CLAUDE.md` — формулировка инварианта «ничего не теряем молча»:
«выигрывает более полная, при равной полноте — стоящая позже в журнале»
- [x] 4.3 `internal/store/entity.go` — комментарии, чей довод («порядок свёртки
журналу не равен») этим изменением сужен до окна видимости
- [x] 4.4 `docs/research/apple-health.md` — перемер 2026-08-04 с **методом**:
ключ со слоем, 155 доставок, 460 995 координат, 80 129 спорных, 981 полнотой,
79 148 тай-брейком, 75 494 меняют исход
- [x] 4.5 `docs/review.md` — запись о дефекте (посылка оракула умерла с ростом
корпуса) и о том, что конвейер её не ловил
- [x] 4.7 вопрос владельцу в `docs/tasks/items/journal-order-on-ingest.md`
(раздел «Вопросы» + тег `question`): решение (в) порядок журнала не
восстанавливает, а цена окна выросла с вывода слоя до значений точек
- [x] 4.6 `docs/conventions/` — промоут двух правил: «в проверке на живом
корпусе утверждается инвариант, число печатается» и «оракул сходимости
называет свою посылку рядом с собой»
## 5. Оракулы
- [x] 5.1 `task gate` зелёный
- [x] 5.2 `task verify:archive` — ноль противоречащих часов, `step_count`
накопительная, отпечаток изменился
- [x] 5.3 `task verify:archive` второй раз подряд — тот же отпечаток
- [x] 5.4 `task verify:busy` зелёный
- [x] 5.5 замер удержаний на живом архиве — число названо, не утверждено
## Приёмочные критерии из ревью предложения (профиль `design`, проход `rubric`)
Свойства, по которым судится узел рода «правило слияния версий в
хранилище-свёртке по журналу» плюс «упорядочивающий воркер очереди».
- [x] Р1 исход слияния — функция префикса журнала и только его; запрос воркера
«есть ли доставка позже» влияет на лог и MUST NOT влиять на содержимое
- [x] Р2 законные расхождения живого пути и пересборки перечислены поимённо, и
у оракула названа посылка
- [x] Р3 повторная свёртка идемпотентна на суффиксе журнала; свёртка более
ранней доставки после более поздней меняет состояние наблюдаемо
- [x] Р4 победитель — функция множества кандидатов и их происхождения, но не
порядка точек внутри доставки; отношение победы ациклично (проверка
перестановками ТРОЙКИ, не пары)
- [x] Р5 правило тотально на вырожденном входе: один кандидат, равные
канонические формы с разными байтами, не-JSON, несравнимые множества
- [x] Р6 победитель сохраняется дословно; цена лишних записей названа числом
- [x] Р7 правило структурно: не читает ни значение точки, ни род, ни витрину
- [x] Р8 барьер очереди не останавливает поток: названо, что держит очередь и
что её освобождает; канонизация и сжатие вне транзакции записи
- [x] Р9 наблюдаемость отличает «правило сработало» от «не сработало» и не
несёт значений здоровья; тест на утечку разбирает запись, а не ищет
подстроку в сыром буфере
- [x] Р10 ни одно утверждение теста не пришпилено к числу, производному от
размера корпуса: утверждается инвариант, число печатается
- [x] Р11 два правила равной полноты (точки и сущности) не расходятся молча:
различие механизмов и критерий выбора записаны
- [x] Р12 место снапшота Apple в порядке журнала — либо названо, либо явно
отложено до задачи импорта (сегодня стадия снапшота пуста)
## Критерии приёмки задачи
Приходят из `docs/tasks/items/tie-break-equal-completeness.md`, дословно. Числа
в них производны от размера корпуса — по каждому названо, чем именно измерено и
что при этом печатается, а не утверждается (`docs/review.md`, 2026-08-02).
- [x] ни одна метрика не теряет род из-за столкновения равной полноты;
`step_count` снова накопительная — оракул: `task verify:archive`, ноль
противоречащих часов
- [x] живая свёртка и пересборка дают **один** отпечаток витрины — оракул:
`healthlog reindex` против живого состояния; это же и есть страховка от
потерянной коммутативности
- [x] повторный прогон реплея даёт тот же отпечаток — оракул:
`task verify:archive`, второй прогон подряд
- [x] столкновение **внутри одной доставки** разрешается прежним байтовым
порядком — оракул: тест на двух точках одной доставки с равной полнотой
- [x] «пришедшая точка проиграла сохранённой» считается отдельно от общего
`MergeStats.Overwrites` — оракул: тест плюс прогон на живом архиве, число
сходится с замером 2026-08-04