## 1. Сравнение содержания сущностей (`internal/canon`)

- [x] 1.1 `arrayLen` → `arrayShape`: рядом с числом элементов считается число
      **содержательных** (по `isEmpty` литерала элемента, без разворачивания в
      дерево). Обход тот же, что и сегодня.
- [x] 1.2 `Covers` получает второй разряд (все ключи `g` есть у `f`), запрет
      вырождения формы (объект/массив нельзя подменить скаляром) и сравнение
      содержательных элементов массива. Условия 3 и 4 — только для ключей,
      содержательных у `g`.
- [x] 1.3 Комментарий `Covers` называет предел вслух: порча **внутри** элемента
      ряда не ловится ничем, кроме сверки с телом в архиве; и цену `isEmpty` на
      элементах (ряд настоящих нулей выглядит опустошённым).
- [x] 1.4 `FormAndHash(raw) (form []byte, hash string, err error)` — форма и хеш
      одним проходом через `io.MultiWriter`; `Form`, `Hash`, `HashAll`
      переписаны через общего писателя, чтобы реализация осталась одна.
- [x] 1.5 Тесты в `internal/canon/canon_test.go`: скелет не покрывает настоящую;
      массив из `null`/`{}` той же длины не покрывает содержательный; ключ с
      пустым значением не исчезает; форма не вырождается скаляром; покрытие
      остаётся транзитивным; `FormAndHash` совпадает с `Form`+`Hash`.

## 2. Каноническая форма один раз и вне транзакции (`internal/store`)

- [x] 2.1 `entityVersion` без ленивости: `newEntityVersion` зовёт `FormAndHash`
      один раз и держит `canon.Fields`. `analyze()` и флаг `parsed` удаляются.
- [x] 2.2 Сохранённая версия строится своим конструктором (её содержимое
      читается внутри транзакции и только при разошедшемся хеше).
- [x] 2.3 Комментарий `bucket.go` приводится в соответствие с тем, что код
      делает, — сегодня он утверждает обратное; остаточный предел (разбор
      сохранённой остаётся в транзакции) назван там же.

## 3. Победитель внутри доставки — функция множества (`internal/store`)

- [x] 3.1 Общий помощник выбора победителя: максимальные элементы частичного
      порядка плюс минимум по тотальному. `store.resolve` (точки) и
      `dedupeEntities` (сущности) становятся двумя его вызовами.
      `pickWithinDelivery` удаляется.
- [x] 3.2 Порядок тотален до конца: при совпавших канонических формах решает
      минимум исходных байтов. «Строго покрыта» = покрыта другой и сама её не
      покрывает.
- [x] 3.3 Счётчик различающихся версий считает кандидатов, чья каноническая
      форма отличается от формы победителя.
- [x] 3.4 Тесты: перестановки трёх версий одного `id` в одном теле; две версии
      разного содержания при равных множествах ключей и длинах (счётчик не
      молчит); две версии с равной канонической формой и разными байтами
      (счётчик молчит, байты в витрине одни при любой перестановке).

## 4. Провенанс при совпавшем хеше (`internal/store`)

- [x] 4.1 При совпавшем хеше сравниваются позиции журнала; сохранённая раньше
      приехавшей — обновляются **только** колонки провенанса. `updated_at` не
      двигается: иначе он становится меткой касания строки.
- [x] 4.2 Счётчик записанных сущностей от такого обновления не растёт;
      причина — комментарием. Обновление идемпотентно при равных позициях.
- [x] 4.3 Тест сходимости журнала `A → B → A` с отложенной доставкой: содержимое
      и отпечаток совпадают со свёрткой в порядке журнала.
- [x] 4.4 Тест: повторная присылка того же содержимого не двигает `updated_at`.

## 5. Страж версии схемы (`internal/store`)

- [x] 5.1 Чтение версии схемы — одной функцией и средствами goose
      (`Provider.GetVersions`), если это работает на соединении только для
      чтения; иначе ручной запрос с `sqlite_master` + `sql.NullInt64` и
      названной вслух причиной копии. Разбора текста ошибки драйвера быть не
      должно.
- [x] 5.2 `Open` отказывает, если версия базы **выше** версии бинаря, и не
      мигрирует. `OpenForRead` сохраняет строгое равенство — не ослабляется.
- [x] 5.3 Тесты: `Open` отказывает на базе с версией из будущего; новая база
      открывается и мигрирует; `OpenForRead` по-прежнему отказывает на базе
      старее бинаря.

## 6. Мягкий заголовок сущности (`internal/hae`)

- [x] 6.1 Тип `softString` с `UnmarshalJSON`, игнорирующим нестроковое значение;
      пять полей заголовка получают его. Теги остаются декларативными.
- [x] 6.2 Нестроковый `start` не откатывается на `date` — он считается
      неразбираемой меткой.
- [x] 6.3 Тесты: `name`/`end` не той формы не уносят тренировку; `id` не строкой
      и `id` длиннее предела пропускают сущность со счётчиком; метка не того
      формата и нестроковый `start` пропускают со счётчиком; элемент, который
      сам не объект, остаётся в **своём** счётчике.

## 7. Пропуски сущностей в учёте доставки

- [x] 7.1 Миграция `00008`: колонка `skipped_entities INTEGER` **без
      умолчания** — отсутствие значения означает «не измерялось» и отличимо от
      нуля.
- [x] 7.2 `ParseOutcome` несёт число пропущенных сущностей; свёртка заполняет
      его в обоих исходах (успех и отказ, если разбор успел досчитать), замещая
      прежнее значение целиком.
- [x] 7.3 `docs/database.md` обновлён тем же изменением.
- [x] 7.4 Колонка внесена в реестр производных полей: комментарий
      `ListDeliveries` и дельта `specs/reindex/`. Пересборка её не переносит.
- [x] 7.5 Тесты: доставка с пропущенной сущностью получает ненулевой счётчик;
      пересвёртка без пропусков его обнуляет; доставка, свёрнутая до появления
      счётчика, отличима от нулевой.
- [x] 7.6 **Замер сделан до утверждения формулировок**: живой архив, 118 тел,
      пропусков `noID=0 noTime=0 malformed=0` — data-миграции не нужно, и это
      сказано числом в комментарии миграции.

## 8. Диагностика разбора без значений из тела (`internal/hae`)

- [x] 8.1 Четыре места `fmt.Errorf("… встречено %v", tok)` называют тип токена
      словарём JSON (`object`/`array`/`string`/`number`/`bool`/`null`,
      делимитер — значением) и смещение **начала** токена: `InputOffset()`
      снимается ДО `Token()`.
- [x] 8.2 Тест: тело в мегабайты даёт текст ошибки постоянной длины.

## 9. Занятость базы в логе приёма (`internal/ingest`)

- [x] 9.1 Отказ учёта доставки различает именно `store.ErrBusy`, а не
      «обстоятельства вообще»: отмена на этом пути невозможна по построению.
      Уровень остаётся `ERROR`.
- [x] 9.2 Тест на форму записи лога.

## 10. Наблюдаемость нового правила

- [x] 10.1 Счётчик удержанных версий сущностей выведен в отчёт пересборки
      (`internal/replay`) и печатается в `task verify:archive` — сходимость
      отпечатка это правило не проверяет по построению.
- [x] 10.2 Замер: сколько удержаний даёт живой архив с новым правилом.

## 11. Документация и беклог

- [x] 11.1 `docs/architecture.md`: правило покрытия (четыре условия + пустота
      элемента), предел «порча внутри элемента ряда», провенанс при совпавшем
      хеше и его асимметрия с провенансом объекта, победитель внутри доставки
      как функция множества, страж версии схемы с эксплуатационной ценой,
      счётчик пропущенных сущностей, пункт 5 как единственная точка, где витрина
      не функция множества доставок.
- [x] 11.2 `docs/conventions.md`: текст ошибки разбора без значений из тела;
      тест перестановок обязан включать версию с содержимым, равным одной из
      присланных, и пару «равная форма, разные байты»; колонка необратимого
      решения отличает ноль от «не измерялось»; метка изменения меняется только
      при изменении содержимого.
- [x] 11.3 `docs/review-journal.md`: запись о чекпоинте кода без трёх проходов.
- [x] 11.4 Остатки заведены задачами беклога: NULL-метка; пределы размера
      сущности и секции с потоковым расчётом; принцип отбора data-миграций;
      строка про очередь `pending` — в `stats-endpoint.md`.

## 12. Приёмка

- [x] 12.1 Оракулы `tmp/adv/` переписаны под нормированные ожидания и живут
      обычными тестами пакетов. Из семи подслучаев `ОдноПоле` зеленеют два
      (`name` числом, `end` числом); пять (`id` числом, `id` длиннее предела,
      метка иного формата, метка Unix-эпохой, метки нет) остаются пропусками со
      счётчиком **по замыслу** и проверяются как пропуски.
- [x] 12.2 `task gate` зелёный.
- [x] 12.3 `task verify:archive` сходится на живом архиве (база: 2049 объектов,
      отпечаток `799dc2b7…`, тренировок 2, записей 2).

## Приёмочные критерии (рубрика ревью предложения)

- [x] A1 Любая перестановка порядка свёртки в пределах одного журнала даёт один
      отпечаток витрины **при нулевом счётчике несравнимых версий**; при
      ненулевом расхождение допустимо и обязано сопровождаться этим счётчиком.
- [x] A2 Ни одно правило слияния не зависит от порядка элементов в JSON-массиве,
      включая случай равных канонических форм.
- [x] A3 Внутри транзакции не считается ни одна каноническая форма приехавшей
      версии; пик кучи на большом теле измерен до и после.
- [x] A4 Правило слияния тотально: для каждой пары версий назван ровно один
      исход, ветки «по умолчанию побеждает приехавшая» нет.
- [x] A5 Каждый исход, при котором содержимое отброшено или сохранённая
      удержана, даёт счётчик; счётчик удержаний виден в отчёте пересборки.
- [x] A6 Одно поле не того типа стоит одного поля; исключения (`id`, метка)
      названы поимённо и обоснованы.
- [x] A7 Ни одно сообщение об ошибке и ни одна запись лога выше `DEBUG` не несут
      значений из тела доставки; длина текста ошибки от длины значения не
      зависит.
- [x] A8 Повторная присылка того же содержимого не меняет ни витрину, ни
      отпечаток, ни счётчики записи, ни метку изменения содержимого.
- [x] A9 Чтение хеша и провенанса сохранённой версии идёт в той же транзакции,
      в которой пишется результат.
- [x] A10 Всё, по чему принимается необратимое решение (число пропусков),
      отличает «ноль» от «не измерялось».

## 13. Дозакрыто по ревью кода (профиль `deep`, девять проходов)

Обе регрессии, найденные враждебным проходом, подтверждены триажем прогоном
против базы и исправлены.

- [x] 13.1 **Второй разряд `Covers` сделан условным** — как у `Relate` и как
      велела постановка. Безусловный вариант был регрессией: версия с
      `totalEnergy: null` и без маршрута запирала законный досчёт навсегда
      (оракул: база писала маршрут, новый код удерживал пустышку).
- [x] 13.2 **Выбор победителя внутри доставки перестал быть квадратичным по
      числу присланных версий**: совпавшие канонические формы схлопываются до
      отбора, отбор видит отмену. Было: n=4000 — 3.9 с против 31 мс базы; тело в
      1.6 МБ перекрывало дедлайн свёртки, 64 МиБ — порядка 59 часов работы
      единственного воркера при зелёном `/healthz`. Стало: 2000 копий — 0.06 с.
- [x] 13.3 **Отказ разбора больше не пишет «измеренный ноль» пропусков.**
      `ParseOutcome.SkippedEntities` стал указателем, колонка не трогается той
      же идиомой, что и выведенный слой.
- [x] 13.4 Счётчики разведены: `EntitiesHeld` (удержания против сохранённой) и
      `EntitiesDiverging` (версии одного ключа в одном теле). Отдельные атрибуты
      лога, отдельные ветви WARN, две цифры в отчёте пересборки.
- [x] 13.5 `canon.Form` перестал считать и выбрасывать SHA-256 на каждой точке
      (общий низ без хеша); `arrayShape` перестал аллоцировать копию каждого
      элемента; `shapeKept` смотрит признак «это массив» у обеих сторон.
- [x] 13.6 Разбор сохранённой версии внутри транзакции удешевлён: только
      множества ключей, форма — лениво (замер: 4.5 мс / 2.3 МБ против 1.3 мс /
      174 КБ).
- [x] 13.7 `softString` задаёт поле целиком и различает «ключа не было» от
      «ключ был»: `start: null` больше не уводит тренировку на момент из `date`,
      повтор ключа решается последним значением.
- [x] 13.8 Элемент секции, не являющийся объектом (включая `null`), уходит в
      свой счётчик, а не в «нет `id`».
- [x] 13.9 Чужая причина ошибки обрезается на границе `hae` и на проверке формы
      конверта в приёме: `UnmarshalTypeError` кладёт в текст литерал, и тело из
      миллиона цифр давало мегабайт в логе.
- [x] 13.10 `OpenForRead` отвергает базу без журнала миграций сразу и
      структурно, а не тремя секундами попыток записи через goose.
- [x] 13.11 `touchEntityProvenance` проверяет `RowsAffected`; ключевые аргументы
      живут одной функцией рядом с текстом `WHERE`; ветка составного ключа
      покрыта тестом.
- [x] 13.12 Названы вслух: предел «байты от первой свёрнутой доставки при
      совпавшей форме», зависимость исхода несравнимости от порядка свёртки,
      провенанс вне отпечатка, исключение `source`, удержание памяти на всё
      время транзакции, отсутствие пути понижения схемы.
- [x] 13.13 Заведён блокер «Чем откатывать релиз после наката миграции»;
      пополнены задачи-остатки (потолок числа версий, чтение `NULL` ретеншеном,
      источник алерта тишины).

## 14. Границы, оставленные сознательно

- Исход при **несравнимых** версиях остаётся функцией порядка свёртки, а не
  журнала: в витрине лежит победитель прошлых слияний, а не все кандидаты
  истории. Единственная точка, где витрина не функция множества доставок;
  названа в коде и в `architecture.md`, наблюдается счётчиком удержаний.
- При совпавшей канонической форме в витрине остаются **байты** первой
  свёрнутой доставки. Содержания не теряется, отпечаток не различает,
  переписывать мегабайтный маршрут ради выбора между эквивалентными литералами
  не стали.
- Требование постановки «`differs=true` при разных байтах» исполнено **по
  канонической форме**: побайтовый счётчик срабатывал бы на измеренной норме
  потока (нестабильный порядок ключей, дребезг последнего разряда).
- Тренировка с датой в **незнакомом формате** по-прежнему теряется целиком —
  мягкое чтение закрывает смену типа, а не формата строки. Пропуск виден в
  учётной записи; хранение сущности с неразобранной меткой вынесено остатком.
