Дозакрыты находки ревью по слиянию сущностей
- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
## 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` при разных байтах» исполнено **по
|
||||
канонической форме**: побайтовый счётчик срабатывал бы на измеренной норме
|
||||
потока (нестабильный порядок ключей, дребезг последнего разряда).
|
||||
- Тренировка с датой в **незнакомом формате** по-прежнему теряется целиком —
|
||||
мягкое чтение закрывает смену типа, а не формата строки. Пропуск виден в
|
||||
учётной записи; хранение сущности с неразобранной меткой вынесено остатком.
|
||||
Reference in New Issue
Block a user