- беклог и план переехали в docs/tasks (38 задач, 11 целей), слаги переименованы с транслита на английские, 85 ссылок поправлены - conventions.md разобран в docs/conventions/, local-research.md — в docs/research/, review-journal.md — в docs/review.md с разделом настройки конвейера; заведены security.md, adr/ и .pm.json - шаг docs.py check добавлен в task gate; поведение в architecture.md помечено девятью маркерами долга, database.md получил настройки с числовым значением
22 KiB
22 KiB
1. Сравнение содержания сущностей (internal/canon)
- 1.1
arrayLen→arrayShape: рядом с числом элементов считается число содержательных (поisEmptyлитерала элемента, без разворачивания в дерево). Обход тот же, что и сегодня. - 1.2
Coversполучает второй разряд (все ключиgесть уf), запрет вырождения формы (объект/массив нельзя подменить скаляром) и сравнение содержательных элементов массива. Условия 3 и 4 — только для ключей, содержательных уg. - 1.3 Комментарий
Coversназывает предел вслух: порча внутри элемента ряда не ловится ничем, кроме сверки с телом в архиве; и ценуisEmptyна элементах (ряд настоящих нулей выглядит опустошённым). - 1.4
FormAndHash(raw) (form []byte, hash string, err error)— форма и хеш одним проходом черезio.MultiWriter;Form,Hash,HashAllпереписаны через общего писателя, чтобы реализация осталась одна. - 1.5 Тесты в
internal/canon/canon_test.go: скелет не покрывает настоящую; массив изnull/{}той же длины не покрывает содержательный; ключ с пустым значением не исчезает; форма не вырождается скаляром; покрытие остаётся транзитивным;FormAndHashсовпадает сForm+Hash.
2. Каноническая форма один раз и вне транзакции (internal/store)
- 2.1
entityVersionбез ленивости:newEntityVersionзовётFormAndHashодин раз и держитcanon.Fields.analyze()и флагparsedудаляются. - 2.2 Сохранённая версия строится своим конструктором (её содержимое читается внутри транзакции и только при разошедшемся хеше).
- 2.3 Комментарий
bucket.goприводится в соответствие с тем, что код делает, — сегодня он утверждает обратное; остаточный предел (разбор сохранённой остаётся в транзакции) назван там же.
3. Победитель внутри доставки — функция множества (internal/store)
- 3.1 Общий помощник выбора победителя: максимальные элементы частичного
порядка плюс минимум по тотальному.
store.resolve(точки) иdedupeEntities(сущности) становятся двумя его вызовами.pickWithinDeliveryудаляется. - 3.2 Порядок тотален до конца: при совпавших канонических формах решает минимум исходных байтов. «Строго покрыта» = покрыта другой и сама её не покрывает.
- 3.3 Счётчик различающихся версий считает кандидатов, чья каноническая форма отличается от формы победителя.
- 3.4 Тесты: перестановки трёх версий одного
idв одном теле; две версии разного содержания при равных множествах ключей и длинах (счётчик не молчит); две версии с равной канонической формой и разными байтами (счётчик молчит, байты в витрине одни при любой перестановке).
4. Провенанс при совпавшем хеше (internal/store)
- 4.1 При совпавшем хеше сравниваются позиции журнала; сохранённая раньше
приехавшей — обновляются только колонки провенанса.
updated_atне двигается: иначе он становится меткой касания строки. - 4.2 Счётчик записанных сущностей от такого обновления не растёт; причина — комментарием. Обновление идемпотентно при равных позициях.
- 4.3 Тест сходимости журнала
A → B → Aс отложенной доставкой: содержимое и отпечаток совпадают со свёрткой в порядке журнала. - 4.4 Тест: повторная присылка того же содержимого не двигает
updated_at.
5. Страж версии схемы (internal/store)
- 5.1 Чтение версии схемы — одной функцией и средствами goose
(
Provider.GetVersions), если это работает на соединении только для чтения; иначе ручной запрос сsqlite_master+sql.NullInt64и названной вслух причиной копии. Разбора текста ошибки драйвера быть не должно. - 5.2
Openотказывает, если версия базы выше версии бинаря, и не мигрирует.OpenForReadсохраняет строгое равенство — не ослабляется. - 5.3 Тесты:
Openотказывает на базе с версией из будущего; новая база открывается и мигрирует;OpenForReadпо-прежнему отказывает на базе старее бинаря.
6. Мягкий заголовок сущности (internal/hae)
- 6.1 Тип
softStringсUnmarshalJSON, игнорирующим нестроковое значение; пять полей заголовка получают его. Теги остаются декларативными. - 6.2 Нестроковый
startне откатывается наdate— он считается неразбираемой меткой. - 6.3 Тесты:
name/endне той формы не уносят тренировку;idне строкой иidдлиннее предела пропускают сущность со счётчиком; метка не того формата и нестроковыйstartпропускают со счётчиком; элемент, который сам не объект, остаётся в своём счётчике.
7. Пропуски сущностей в учёте доставки
- 7.1 Миграция
00008: колонкаskipped_entities INTEGERбез умолчания — отсутствие значения означает «не измерялось» и отличимо от нуля. - 7.2
ParseOutcomeнесёт число пропущенных сущностей; свёртка заполняет его в обоих исходах (успех и отказ, если разбор успел досчитать), замещая прежнее значение целиком. - 7.3
docs/database.mdобновлён тем же изменением. - 7.4 Колонка внесена в реестр производных полей: комментарий
ListDeliveriesи дельтаspecs/reindex/. Пересборка её не переносит. - 7.5 Тесты: доставка с пропущенной сущностью получает ненулевой счётчик; пересвёртка без пропусков его обнуляет; доставка, свёрнутая до появления счётчика, отличима от нулевой.
- 7.6 Замер сделан до утверждения формулировок: живой архив, 118 тел,
пропусков
noID=0 noTime=0 malformed=0— data-миграции не нужно, и это сказано числом в комментарии миграции.
8. Диагностика разбора без значений из тела (internal/hae)
- 8.1 Четыре места
fmt.Errorf("… встречено %v", tok)называют тип токена словарём JSON (object/array/string/number/bool/null, делимитер — значением) и смещение начала токена:InputOffset()снимается ДОToken(). - 8.2 Тест: тело в мегабайты даёт текст ошибки постоянной длины.
9. Занятость базы в логе приёма (internal/ingest)
- 9.1 Отказ учёта доставки различает именно
store.ErrBusy, а не «обстоятельства вообще»: отмена на этом пути невозможна по построению. Уровень остаётсяERROR. - 9.2 Тест на форму записи лога.
10. Наблюдаемость нового правила
- 10.1 Счётчик удержанных версий сущностей выведен в отчёт пересборки
(
internal/replay) и печатается вtask verify:archive— сходимость отпечатка это правило не проверяет по построению. - 10.2 Замер: сколько удержаний даёт живой архив с новым правилом.
11. Документация и беклог
- 11.1
docs/architecture.md: правило покрытия (четыре условия + пустота элемента), предел «порча внутри элемента ряда», провенанс при совпавшем хеше и его асимметрия с провенансом объекта, победитель внутри доставки как функция множества, страж версии схемы с эксплуатационной ценой, счётчик пропущенных сущностей, пункт 5 как единственная точка, где витрина не функция множества доставок. - 11.2
docs/conventions.md: текст ошибки разбора без значений из тела; тест перестановок обязан включать версию с содержимым, равным одной из присланных, и пару «равная форма, разные байты»; колонка необратимого решения отличает ноль от «не измерялось»; метка изменения меняется только при изменении содержимого. - 11.3
docs/review-journal.md: запись о чекпоинте кода без трёх проходов. - 11.4 Остатки заведены задачами беклога: NULL-метка; пределы размера
сущности и секции с потоковым расчётом; принцип отбора data-миграций;
строка про очередь
pending— вstats-endpoint.md.
12. Приёмка
- 12.1 Оракулы
tmp/adv/переписаны под нормированные ожидания и живут обычными тестами пакетов. Из семи подслучаевОдноПолезеленеют два (nameчислом,endчислом); пять (idчислом,idдлиннее предела, метка иного формата, метка Unix-эпохой, метки нет) остаются пропусками со счётчиком по замыслу и проверяются как пропуски. - 12.2
task gateзелёный. - 12.3
task verify:archiveсходится на живом архиве (база: 2049 объектов, отпечаток799dc2b7…, тренировок 2, записей 2).
Приёмочные критерии (рубрика ревью предложения)
- A1 Любая перестановка порядка свёртки в пределах одного журнала даёт один отпечаток витрины при нулевом счётчике несравнимых версий; при ненулевом расхождение допустимо и обязано сопровождаться этим счётчиком.
- A2 Ни одно правило слияния не зависит от порядка элементов в JSON-массиве, включая случай равных канонических форм.
- A3 Внутри транзакции не считается ни одна каноническая форма приехавшей версии; пик кучи на большом теле измерен до и после.
- A4 Правило слияния тотально: для каждой пары версий назван ровно один исход, ветки «по умолчанию побеждает приехавшая» нет.
- A5 Каждый исход, при котором содержимое отброшено или сохранённая удержана, даёт счётчик; счётчик удержаний виден в отчёте пересборки.
- A6 Одно поле не того типа стоит одного поля; исключения (
id, метка) названы поимённо и обоснованы. - A7 Ни одно сообщение об ошибке и ни одна запись лога выше
DEBUGне несут значений из тела доставки; длина текста ошибки от длины значения не зависит. - A8 Повторная присылка того же содержимого не меняет ни витрину, ни отпечаток, ни счётчики записи, ни метку изменения содержимого.
- A9 Чтение хеша и провенанса сохранённой версии идёт в той же транзакции, в которой пишется результат.
- A10 Всё, по чему принимается необратимое решение (число пропусков), отличает «ноль» от «не измерялось».
13. Дозакрыто по ревью кода (профиль deep, девять проходов)
Обе регрессии, найденные враждебным проходом, подтверждены триажем прогоном против базы и исправлены.
- 13.1 Второй разряд
Coversсделан условным — как уRelateи как велела постановка. Безусловный вариант был регрессией: версия сtotalEnergy: nullи без маршрута запирала законный досчёт навсегда (оракул: база писала маршрут, новый код удерживал пустышку). - 13.2 Выбор победителя внутри доставки перестал быть квадратичным по
числу присланных версий: совпавшие канонические формы схлопываются до
отбора, отбор видит отмену. Было: n=4000 — 3.9 с против 31 мс базы; тело в
1.6 МБ перекрывало дедлайн свёртки, 64 МиБ — порядка 59 часов работы
единственного воркера при зелёном
/healthz. Стало: 2000 копий — 0.06 с. - 13.3 Отказ разбора больше не пишет «измеренный ноль» пропусков.
ParseOutcome.SkippedEntitiesстал указателем, колонка не трогается той же идиомой, что и выведенный слой. - 13.4 Счётчики разведены:
EntitiesHeld(удержания против сохранённой) иEntitiesDiverging(версии одного ключа в одном теле). Отдельные атрибуты лога, отдельные ветви WARN, две цифры в отчёте пересборки. - 13.5
canon.Formперестал считать и выбрасывать SHA-256 на каждой точке (общий низ без хеша);arrayShapeперестал аллоцировать копию каждого элемента;shapeKeptсмотрит признак «это массив» у обеих сторон. - 13.6 Разбор сохранённой версии внутри транзакции удешевлён: только множества ключей, форма — лениво (замер: 4.5 мс / 2.3 МБ против 1.3 мс / 174 КБ).
- 13.7
softStringзадаёт поле целиком и различает «ключа не было» от «ключ был»:start: nullбольше не уводит тренировку на момент изdate, повтор ключа решается последним значением. - 13.8 Элемент секции, не являющийся объектом (включая
null), уходит в свой счётчик, а не в «нетid». - 13.9 Чужая причина ошибки обрезается на границе
haeи на проверке формы конверта в приёме:UnmarshalTypeErrorкладёт в текст литерал, и тело из миллиона цифр давало мегабайт в логе. - 13.10
OpenForReadотвергает базу без журнала миграций сразу и структурно, а не тремя секундами попыток записи через goose. - 13.11
touchEntityProvenanceпроверяетRowsAffected; ключевые аргументы живут одной функцией рядом с текстомWHERE; ветка составного ключа покрыта тестом. - 13.12 Названы вслух: предел «байты от первой свёрнутой доставки при
совпавшей форме», зависимость исхода несравнимости от порядка свёртки,
провенанс вне отпечатка, исключение
source, удержание памяти на всё время транзакции, отсутствие пути понижения схемы. - 13.13 Заведён блокер «Чем откатывать релиз после наката миграции»;
пополнены задачи-остатки (потолок числа версий, чтение
NULLретеншеном, источник алерта тишины).
14. Границы, оставленные сознательно
- Исход при несравнимых версиях остаётся функцией порядка свёртки, а не
журнала: в витрине лежит победитель прошлых слияний, а не все кандидаты
истории. Единственная точка, где витрина не функция множества доставок;
названа в коде и в
architecture.md, наблюдается счётчиком удержаний. - При совпавшей канонической форме в витрине остаются байты первой свёрнутой доставки. Содержания не теряется, отпечаток не различает, переписывать мегабайтный маршрут ради выбора между эквивалентными литералами не стали.
- Требование постановки «
differs=trueпри разных байтах» исполнено по канонической форме: побайтовый счётчик срабатывал бы на измеренной норме потока (нестабильный порядок ключей, дребезг последнего разряда). - Тренировка с датой в незнакомом формате по-прежнему теряется целиком — мягкое чтение закрывает смену типа, а не формата строки. Пропуск виден в учётной записи; хранение сущности с неразобранной меткой вынесено остатком.