## 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-nablyudaemost.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` при разных байтах» исполнено **по канонической форме**: побайтовый счётчик срабатывал бы на измеренной норме потока (нестабильный порядок ключей, дребезг последнего разряда). - Тренировка с датой в **незнакомом формате** по-прежнему теряется целиком — мягкое чтение закрывает смену типа, а не формата строки. Пропуск виден в учётной записи; хранение сущности с неразобранной меткой вынесено остатком.