Files
av d79189be18 docs: документация переведена на канон av-dev-pm
- беклог и план переехали в 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 получил настройки с числовым значением
2026-08-03 17:14:53 +03:00

239 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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` при разных байтах» исполнено **по
канонической форме**: побайтовый счётчик срабатывал бы на измеренной норме
потока (нестабильный порядок ключей, дребезг последнего разряда).
- Тренировка с датой в **незнакомом формате** по-прежнему теряется целиком —
мягкое чтение закрывает смену типа, а не формата строки. Пропуск виден в
учётной записи; хранение сущности с неразобранной меткой вынесено остатком.