- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
239 lines
22 KiB
Markdown
239 lines
22 KiB
Markdown
## 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` при разных байтах» исполнено **по
|
||
канонической форме**: побайтовый счётчик срабатывал бы на измеренной норме
|
||
потока (нестабильный порядок ключей, дребезг последнего разряда).
|
||
- Тренировка с датой в **незнакомом формате** по-прежнему теряется целиком —
|
||
мягкое чтение закрывает смену типа, а не формата строки. Пропуск виден в
|
||
учётной записи; хранение сущности с неразобранной меткой вынесено остатком.
|