Files
healthlog/docs/backlog/dozakryt-nahodki-sushchnostej.md
T
av 51a5272c96 беклог: задача на дозакрытие находок ревью сущностей
- три пропущенных прохода дозапущены, триаж оставил семь причин
- обе развилки разобраны по уже записанным решениям проекта:
  второй разряд Covers как у Relate, мягкое чтение полей как у Duration
- уточнена формулировка в architecture.md про цену покрытия секции
2026-08-02 13:38:34 +03:00

126 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Дозакрыть находки ревью по слиянию сущностей
**Приоритет:** высокий
Задача «Тренировки и секции с собственными id» (`f8200f7`) прошла ревью не
полностью: проходы `adversary`, `ops` и архитектурный на коде не запускались.
Дозапуск принёс девять причин, триаж оставил семь. Оба заявленных `critical`
понижены до `major` с названной причиной — вход недостижим из штатного потока
HAE (корпус в 118 доставок такого не производил), — но остались в работе:
потеря маршрута необратима, а `reindex` проигрывает то же поражение.
Отчёт триажа с прогнанными оракулами — `tmp/triage-late.md`, оракулы —
`tmp/adv/*.go`. Каждый пункт ниже имеет падающий тест; правка считается
сделанной, когда соответствующий оракул зеленеет **и** переезжает из `tmp/` в
обычные тесты пакета.
## Что делать
**1. Скелет не затирает маршрут.** `canon.Fields.Covers` проверяет только
наличие ключа и длину верхнеуровневого массива, поэтому версия, где каждый
массив заменён массивом той же длины из `null`, а каждое не-массивное значение
— скаляром, признаётся равной настоящей и по тай-брейку журнала замещает её.
Оракул: `-run 'Скелет|Маршрут|ДвеВерсии'`.
Решение принято, новое не проектируем: применяем к сущностям **тот же
стандарт, что записан для точек** — второй разряд сравнения, как у
`canon.Relate` («иначе ключ с пустым значением исчезает по жребию»). Плюс
запрет вырождения формы: покрывающая версия не может подменить объект или
массив скаляром — проверка по верхнему уровню, стоимость O(ключей).
Поэлементная содержательность массивов **отвергнута ценой**: пункт 4 измерил
768 МиБ пика на канонизации, полный обход маршрута на каждое сравнение эту цену
умножит. Остающийся предел — порча *внутри* элемента ряда (точка маршрута без
`altitude`) — не закрывается ничем, кроме сверки с телом в архиве, и должен
быть записан в `architecture.md` рядом с описанием `Covers` так же прямо, как
он записан в комментарии кода.
Отдельно: `pickWithinDelivery` при равном содержании и **разных байтах** обязан
считать `differs=true` — сейчас две версии одного `id` в одном теле дают
`удержано=0` и молчащий счётчик.
**2. Одно поле не той формы не уносит сущность.** `entityHead` держит
`ID`/`Name`/`Date`/`Start`/`End` типизированными строками, поэтому смена типа
любого из пяти роняет `json.Unmarshal` целиком, а доставка при этом получает
`parsed` с пустым списком непокрытого. Достижимо из реального потока: дрейф
формата дат у HAE задокументирован. Оракул: `-run ОдноПоле`.
Читать пять полей через `json.RawMessage` и извлекать мягко — это буквально
принцип, уже записанный в коде для `Duration` («нечисловое значение — это
пропуск ОДНОГО поля, а не сломанная сущность»). Плюс пропуски обязаны быть
видны в **учётной записи** доставки, а не только в логе: ретеншен решает по
базе, и сегодня он получит ответ «терять нечего». Хранение сущности с
неразобранной меткой (NULL) в эту задачу не входит — см. остаток ниже.
**3. Откат бинаря не проходит молча.** `store.Open` мигрирует безусловно и не
сверяет версию схемы, поэтому старый бинарь успешно стартует поверх схемы 7,
молча игнорирует незнакомые секции и помечает доставки разобранными. Оракул
прогнан живьём: `-run СтарыйБинарь`. Перенести в `Open` страж из
`OpenForRead` — прецедент записан там же: «расхождение версий — отказ, а не
повод мигрировать».
**4. Канонизация — за транзакцию, по-настоящему.** Комментарий
`bucket.go:143-146` утверждает, что канонизация вынесена наружу; фактически
`analyze()` вызывается из `compareEntities` **внутри** `inTx`, который открывает
`immediate` и повторяет до пяти раз, а кеш `analyze()` пишется в **копию**
элемента среза и не переживает даже одной попытки. Измерено: тело 40 МиБ → пик
768.3 МиБ; 63 МиБ → блокировка удерживается 5.019 с при `busy_timeout` 5000, то
есть конкурентный `CreateDelivery` исчерпывает повторы и приём отвечает 500 по
доставке, тело которой уже на диске.
В этой задаче: вынести `analyze()` наружу по-настоящему, кешировать в срезе, а
не в копии, и различать в логе `delivery failed` занятость базы (`store.ErrBusy`
уже выделен доменной ошибкой) от прочих причин. Пределы на размер сущности и
потоковый расчёт хеша — остатком.
**5. Значения из тела не попадают в текст ошибки.** `fmt.Errorf("… встречено
%v", tok)` подставляет токен целиком: тело 8 МиБ даёт текст ошибки 8 МиБ,
который уходит атрибутом `error` на уровень `WARN`. Инвариант «тела запросов
только на `DEBUG` и с обрезкой» нарушен буквально. Называть тип токена и
`dec.InputOffset()`. Оракул: `-run Тело`. Дефект в базе диффа, не внесён
разбором сущностей.
**6. Победитель внутри доставки — функция множества, а не порядка.**
Попарная свёртка частичного порядка с тотальным тай-брейком нетранзитивна:
`[A,B,C]` даёт `C`, `[B,C,A]` даёт `A`. Стандарт «победитель — функция множества
точек, а не порядка» записан в `architecture.md` для точек и для сущностей
молча не применён. Собрать версии ключа, отбросить строго покрытые, среди
оставшихся взять минимум канонической формы. Оракул: `-run ПорядокВнутри`.
**7. Провенанс обновляется при равных хешах.** Совпал хеш — запись
пропускается вместе с провенансом, и в `delivery_id`/`delivery_received_at`
остаётся первая свёрнутая копия, а не победитель по журналу. Провенанс
устаревает на каждой из ~26 повторных присылок гарантированно; расхождение
живой витрины с `reindex` латентно (требует возврата содержимого к прежнему —
корпус такого не производил), но нарушает записанный инвариант детерминизма.
Сравнивать позиции в журнале и обновлять провенанс. Оракул: `-run Порядок`.
## Что уходит остатком
- хранение сущности с `id`, но неразобранной меткой (NULL-метка): требует схемы
и правил чтения, а после пункта 2 случай становится редким;
- пределы на размер одной сущности и суммарный размер секции, потоковый расчёт
канонической формы и хеша — заводится задачей вместе с условием из пункта 4;
- принцип «data-миграции не отбирают строки по спискам, которые где-то
обрезаются» (миграция `00007` отбирает по обрезаемому на 32
`uncovered_sections`; для неё дефект пустой — HAE шлёт одну секцию за
доставку, — но следующая покрытая секция унаследует слепую зону);
- длина очереди `pending` в `/stats` **и без WARN**: после миграции, переводящей
доставки в `pending`, отставание по конструкции не WARN-ится (`startupDone`),
и бэклог идёт молча при зелёном `/healthz` — строка уходит в
[наблюдаемость](stats-nablyudaemost.md).
## Кандидаты в конвенции
- текст ошибки разбора не содержит значений из тела — только тип токена и
смещение;
- тест перестановок правила слияния обязан включать версию с содержимым, равным
одной из уже присланных: тест трёх версий с разными хешами ветку равенства не
посещает ни разу.
Готово, когда все семь оракулов зелены, живут обычными тестами пакетов, а
`task verify:archive` сходится.
Связано: `internal/canon`, `internal/store/entity.go`, `internal/hae/entity.go`,
`docs/review-journal.md` (пропуск проходов на чекпоинте — отклонение процесса,
ему там место).